mirror of
https://github.com/wu736139669/hapi.git
synced 2026-11-04 00:01:41 +00:00
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:
@@ -34,6 +34,9 @@ const harness = vi.hoisted(() => ({
|
||||
emitParentSpawnStartWithoutEnd: false,
|
||||
emitParentSendInputFailure: false,
|
||||
emitParentResumeSuccess: false,
|
||||
emitRunningChildTurnBeforeSuppressedParent: false,
|
||||
emitCompletedChildTurnBeforeSuppressedParent: false,
|
||||
emitTurnAbortedOnInterrupt: false,
|
||||
bridgeOptions: [] as unknown[]
|
||||
}));
|
||||
|
||||
@@ -109,6 +112,33 @@ vi.mock('./codexAppServerClient', () => {
|
||||
return { turn: { id: turnId } };
|
||||
}
|
||||
|
||||
if (
|
||||
harness.emitRunningChildTurnBeforeSuppressedParent
|
||||
|| harness.emitCompletedChildTurnBeforeSuppressedParent
|
||||
) {
|
||||
const childStarted = {
|
||||
msg: {
|
||||
type: 'task_started',
|
||||
thread_id: 'child-thread',
|
||||
turn_id: 'child-turn'
|
||||
}
|
||||
};
|
||||
harness.notifications.push({ method: 'codex/event/task_started', params: childStarted });
|
||||
this.notificationHandler?.('codex/event/task_started', childStarted);
|
||||
|
||||
if (harness.emitCompletedChildTurnBeforeSuppressedParent) {
|
||||
const childCompleted = {
|
||||
msg: {
|
||||
type: 'task_complete',
|
||||
thread_id: 'child-thread',
|
||||
turn_id: 'child-turn'
|
||||
}
|
||||
};
|
||||
harness.notifications.push({ method: 'codex/event/task_complete', params: childCompleted });
|
||||
this.notificationHandler?.('codex/event/task_complete', childCompleted);
|
||||
}
|
||||
}
|
||||
|
||||
if (harness.suppressTurnCompletion) {
|
||||
return { turn: { id: turnId } };
|
||||
}
|
||||
@@ -565,10 +595,19 @@ vi.mock('./codexAppServerClient', () => {
|
||||
}
|
||||
|
||||
async interruptTurn(params?: { threadId?: string; turnId?: string }): Promise<Record<string, never>> {
|
||||
harness.interruptedTurns.push({
|
||||
threadId: params?.threadId ?? 'thread-unknown',
|
||||
turnId: params?.turnId ?? 'turn-unknown'
|
||||
});
|
||||
const threadId = params?.threadId ?? 'thread-unknown';
|
||||
const turnId = params?.turnId ?? 'turn-unknown';
|
||||
harness.interruptedTurns.push({ threadId, turnId });
|
||||
if (harness.emitTurnAbortedOnInterrupt) {
|
||||
const interrupted = {
|
||||
threadId,
|
||||
turnId,
|
||||
status: 'interrupted',
|
||||
turn: { id: turnId }
|
||||
};
|
||||
harness.notifications.push({ method: 'turn/completed', params: interrupted });
|
||||
this.notificationHandler?.('turn/completed', interrupted);
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
@@ -737,6 +776,9 @@ describe('codexRemoteLauncher', () => {
|
||||
harness.emitParentSpawnStartWithoutEnd = false;
|
||||
harness.emitParentSendInputFailure = false;
|
||||
harness.emitParentResumeSuccess = false;
|
||||
harness.emitRunningChildTurnBeforeSuppressedParent = false;
|
||||
harness.emitCompletedChildTurnBeforeSuppressedParent = false;
|
||||
harness.emitTurnAbortedOnInterrupt = false;
|
||||
harness.bridgeOptions = [];
|
||||
});
|
||||
|
||||
@@ -1431,6 +1473,60 @@ describe('codexRemoteLauncher', () => {
|
||||
expect(session.thinking).toBe(false);
|
||||
});
|
||||
|
||||
it('interrupts active child agent turns before clearing codex thread state', async () => {
|
||||
harness.suppressTurnCompletion = true;
|
||||
harness.emitRunningChildTurnBeforeSuppressedParent = true;
|
||||
const { session, resetThreadCalls } = createSessionStub(['first message', '/clear']);
|
||||
|
||||
const exitReason = await codexRemoteLauncher(session as never);
|
||||
|
||||
expect(exitReason).toBe('exit');
|
||||
expect(harness.interruptedTurns).toEqual([
|
||||
{ threadId: 'thread-1', turnId: 'turn-1' },
|
||||
{ threadId: 'child-thread', turnId: 'child-turn' }
|
||||
]);
|
||||
expect(resetThreadCalls).toEqual(['thread-1']);
|
||||
expect(session.thinking).toBe(false);
|
||||
});
|
||||
|
||||
it('interrupts active child agent turns when the abort RPC is invoked', async () => {
|
||||
harness.suppressTurnCompletion = true;
|
||||
harness.emitRunningChildTurnBeforeSuppressedParent = true;
|
||||
harness.emitTurnAbortedOnInterrupt = true;
|
||||
const { session, rpcHandlers } = createSessionStub(['first message']);
|
||||
|
||||
const running = codexRemoteLauncher(session as never);
|
||||
await vi.waitFor(() => {
|
||||
expect(harness.startTurnThreadIds).toEqual(['thread-1']);
|
||||
expect(rpcHandlers.has('abort')).toBe(true);
|
||||
});
|
||||
|
||||
await rpcHandlers.get('abort')?.({});
|
||||
const exitReason = await running;
|
||||
|
||||
expect(exitReason).toBe('exit');
|
||||
expect(harness.interruptedTurns).toEqual([
|
||||
{ threadId: 'thread-1', turnId: 'turn-1' },
|
||||
{ threadId: 'child-thread', turnId: 'child-turn' }
|
||||
]);
|
||||
expect(session.thinking).toBe(false);
|
||||
});
|
||||
|
||||
it('does not interrupt completed child agent turns when clearing codex thread state', async () => {
|
||||
harness.suppressTurnCompletion = true;
|
||||
harness.emitCompletedChildTurnBeforeSuppressedParent = true;
|
||||
const { session, resetThreadCalls } = createSessionStub(['first message', '/clear']);
|
||||
|
||||
const exitReason = await codexRemoteLauncher(session as never);
|
||||
|
||||
expect(exitReason).toBe('exit');
|
||||
expect(harness.interruptedTurns).toEqual([
|
||||
{ threadId: 'thread-1', turnId: 'turn-1' }
|
||||
]);
|
||||
expect(resetThreadCalls).toEqual(['thread-1']);
|
||||
expect(session.thinking).toBe(false);
|
||||
});
|
||||
|
||||
it('compacts the current thread without starting a turn', async () => {
|
||||
const { session, sessionEvents } = createSessionStub(['first message', '/compact']);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user