Skip to content

Commit 8cd3e30

Browse files
committed
fix(claude): retire interrupted background tasks
1 parent 7e51a59 commit 8cd3e30

2 files changed

Lines changed: 18 additions & 1 deletion

File tree

server-agents/claude/src/agents/claude/__tests__/abort-timer.test.js

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -282,7 +282,8 @@ describe('ClaudeCliRuntime abort force-kill fallback', () => {
282282
it('settles cleanly when an interrupt ends a fenced background wait', async () => {
283283
const runtime = createRuntime();
284284
const ctrl = createControllableProc();
285-
spawnMock.mockReturnValue(ctrl.proc);
285+
const nextCtrl = createControllableProc();
286+
spawnMock.mockReturnValueOnce(ctrl.proc).mockReturnValueOnce(nextCtrl.proc);
286287
const failures = [];
287288
const finishes = [];
288289
runtime.onFailed((chatId, message) => failures.push({ chatId, message }));
@@ -321,10 +322,21 @@ describe('ClaudeCliRuntime abort force-kill fallback', () => {
321322

322323
ctrl.push(IDLE);
323324
await turn;
325+
await flush();
324326
expect(cleared).toContain(completionFallback.id);
325327
expect(ctrl.proc.killed).toBe(false);
328+
expect(ctrl.proc.ended).toBe(true);
326329
expect(failures).toEqual([]);
327330
expect(finishes).toEqual([{ chatId: 'chat-1', exitCode: 0 }]);
331+
332+
const nextTurn = runtime.runClaudeTurn(startOptions({ command: 'after stop' }));
333+
await flush();
334+
nextCtrl.push(INIT);
335+
nextCtrl.startLatestInput();
336+
nextCtrl.push({ type: 'assistant', content: [{ type: 'text', text: 'continued' }] });
337+
settleTurn(nextCtrl);
338+
await nextTurn;
339+
expect(spawnMock).toHaveBeenCalledTimes(2);
328340
});
329341

330342
it('cancels a queued submitted input when an internal turn is interrupted', async () => {

server-agents/claude/src/agents/claude/cli-session-state.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -124,11 +124,15 @@ export function handleClaudeProviderLifecycleMessage(
124124
);
125125
return true;
126126
}
127+
const retireAfterSettlement =
128+
protocol.backgroundContinuationPending && protocol.abortRequested;
129+
if (retireAfterSettlement) session.backgroundTaskCount = 0;
127130

128131
const failure = protocol.settlementFailureMessage();
129132
if (failure) {
130133
handlers.logger.warn('Claude CLI run settled with an error', details);
131134
handlers.fail(failure);
135+
if (retireAfterSettlement) handlers.retire();
132136
return true;
133137
}
134138
if (protocol.cleanAbortResultSeen) {
@@ -137,5 +141,6 @@ export function handleClaudeProviderLifecycleMessage(
137141
handlers.logger.info('Claude CLI run settled at provider idle', details);
138142
}
139143
handlers.finish();
144+
if (retireAfterSettlement) handlers.retire();
140145
return true;
141146
}

0 commit comments

Comments
 (0)