Stop active Codex child agents on abort (#615)

* fix(cli): stop active codex child agents

* chore: refresh bun lockfile for deploy

* fix(web): enable stop for active codex child agents

* test(cli): cover aborting active codex child agents
This commit is contained in:
SmallSpider
2026-05-12 23:25:41 +08:00
committed by GitHub
parent af3491e046
commit 088a712f1e
5 changed files with 183 additions and 28 deletions
+53 -21
View File
@@ -84,6 +84,7 @@ class CodexRemoteLauncher extends RemoteLauncherBase {
private abortController: AbortController = new AbortController();
private currentThreadId: string | null = null;
private currentTurnId: string | null = null;
private readonly activeChildTurns = new Map<string, string>();
constructor(session: CodexSession) {
super(process.env.DEBUG ? session.logPath : undefined);
@@ -95,19 +96,50 @@ class CodexRemoteLauncher extends RemoteLauncherBase {
return React.createElement(CodexDisplay, context);
}
private async interruptActiveTurns(reason: string): Promise<void> {
const turnsToInterrupt = [
...(this.currentThreadId && this.currentTurnId
? [{ threadId: this.currentThreadId, turnId: this.currentTurnId, role: 'parent' as const }]
: []),
...Array.from(this.activeChildTurns, ([threadId, turnId]) => ({
threadId,
turnId,
role: 'child' as const
}))
];
if (turnsToInterrupt.length === 0) {
return;
}
const results = await Promise.allSettled(
turnsToInterrupt.map((target) => this.appServerClient.interruptTurn({
threadId: target.threadId,
turnId: target.turnId
}))
);
results.forEach((result, index) => {
const target = turnsToInterrupt[index];
if (result.status === 'fulfilled') {
if (target.role === 'child') {
this.activeChildTurns.delete(target.threadId);
}
return;
}
logger.debug(
`[Codex] Error interrupting ${target.role} app-server turn ` +
`for ${reason}; threadId=${target.threadId} turnId=${target.turnId}:`,
result.reason
);
});
}
private async handleAbort(): Promise<void> {
logger.debug('[Codex] Abort requested - stopping current task');
try {
if (this.currentThreadId && this.currentTurnId) {
try {
await this.appServerClient.interruptTurn({
threadId: this.currentThreadId,
turnId: this.currentTurnId
});
} catch (error) {
logger.debug('[Codex] Error interrupting app-server turn:', error);
}
}
await this.interruptActiveTurns('abort');
this.currentTurnId = null;
this.abortController.abort();
@@ -1635,6 +1667,15 @@ class CodexRemoteLauncher extends RemoteLauncherBase {
`[Codex] Routing event from non-active thread into agent trace; ` +
`type=${msgType}, eventThreadId=${eventThreadId}, activeThread=${this.currentThreadId}`
);
if (msgType === 'task_started') {
if (eventTurnId) {
this.activeChildTurns.set(eventThreadId, eventTurnId);
} else {
logger.debug(`[Codex] Child task_started missing turn id; threadId=${eventThreadId}`);
}
} else if (isTerminalEvent) {
this.activeChildTurns.delete(eventThreadId);
}
handleChildCodexEvent(eventThreadId, msg);
return;
}
@@ -2176,17 +2217,7 @@ class CodexRemoteLauncher extends RemoteLauncherBase {
};
const interruptActiveTurn = async () => {
const threadId = this.currentThreadId;
const turnId = this.currentTurnId;
if (!threadId || !turnId) {
return;
}
try {
await appServerClient.interruptTurn({ threadId, turnId });
} catch (error) {
logger.debug('[Codex] Error interrupting app-server turn for slash command:', error);
}
await this.interruptActiveTurns('slash command');
};
const resumeExistingThreadForCompact = async (mode: EnhancedMode): Promise<string | null> => {
@@ -2475,6 +2506,7 @@ class CodexRemoteLauncher extends RemoteLauncherBase {
this.permissionHandler = null;
this.reasoningProcessor = null;
this.diffProcessor = null;
this.activeChildTurns.clear();
logger.debug('[codex-remote]: cleanup done');
}