Эх сурвалжийг харах

fix: keep completed task sessions terminal

Alvin Unreal 3 сар өмнө
parent
commit
f398a89a3a

+ 37 - 3
src/hooks/task-session-manager/index.test.ts

@@ -1024,7 +1024,7 @@ describe('task-session-manager hook', () => {
     );
   });
 
-  test('reopens stale cancelled child job when child session becomes busy', async () => {
+  test('does not reopen stale cancelled child job when child session becomes busy', async () => {
     const board = new BackgroundJobBoard();
     const { hook } = createHook({ backgroundJobBoard: board });
 
@@ -1045,10 +1045,10 @@ describe('task-session-manager hook', () => {
     });
 
     expect(board.get('child-1')).toMatchObject({
-      state: 'running',
+      state: 'reconciled',
       terminalUnreconciled: false,
+      terminalState: 'cancelled',
     });
-    expect(board.get('child-1')?.terminalState).toBeUndefined();
   });
 
   test('does not reconcile terminal jobs before they are injected into a prompt', async () => {
@@ -1416,6 +1416,40 @@ describe('task-session-manager hook', () => {
     expect(resume.args.task_id).toBe('ses_child');
   });
 
+  test('late child busy event does not reopen completed foreground XML task', async () => {
+    const board = new BackgroundJobBoard();
+    const { hook } = createHook({ backgroundJobBoard: board });
+    await hook['tool.execute.before'](
+      { tool: 'task', sessionID: 'parent-1', callID: 'call-1' },
+      { args: { subagent_type: 'fixer', description: 'reuse probe' } },
+    );
+    await hook['tool.execute.after'](
+      { tool: 'task', sessionID: 'parent-1', callID: 'call-1' },
+      {
+        output: [
+          '<task id="ses_child" state="completed">',
+          '<task_result>',
+          'done',
+          '</task_result>',
+          '</task>',
+        ].join('\n'),
+      },
+    );
+
+    await hook.event({
+      event: {
+        type: 'session.status',
+        properties: { sessionID: 'ses_child', status: { type: 'busy' } },
+      },
+    });
+
+    expect(board.get('ses_child')).toMatchObject({
+      state: 'completed',
+      terminalState: 'completed',
+      terminalUnreconciled: true,
+    });
+  });
+
   test('preserves explicit raw session ids when reusable board misses', async () => {
     const { hook } = createHook();
     const resume = {

+ 16 - 17
src/utils/background-job-board.test.ts

@@ -398,7 +398,7 @@ describe('BackgroundJobBoard', () => {
     });
   });
 
-  test('live busy session reopens stale cancelled jobs', () => {
+  test('live busy session does not reopen stale cancelled jobs', () => {
     const board = new BackgroundJobBoard();
     board.registerLaunch({
       taskID: 'ses_1',
@@ -415,14 +415,13 @@ describe('BackgroundJobBoard', () => {
     const updated = board.markRunningFromLiveSession('ses_1', 200);
 
     expect(updated).toMatchObject({
-      state: 'running',
-      terminalUnreconciled: false,
-      timedOut: false,
-      updatedAt: 200,
+      state: 'cancelled',
+      terminalUnreconciled: true,
+      lastLiveBusyAt: 200,
     });
-    expect(updated?.completedAt).toBeUndefined();
-    expect(updated?.terminalState).toBeUndefined();
-    expect(updated?.resultSummary).toBeUndefined();
+    expect(updated?.completedAt).toBeDefined();
+    expect(updated?.terminalState).toBe('cancelled');
+    expect(updated?.resultSummary).toBe('upstream cancelled during compaction');
   });
 
   test('live busy session does not reopen explicit cancel requests', () => {
@@ -443,7 +442,7 @@ describe('BackgroundJobBoard', () => {
     });
   });
 
-  test('live busy session reopens reconciled stale cancellations', () => {
+  test('live busy session does not reopen reconciled stale cancellations', () => {
     const board = new BackgroundJobBoard();
     board.registerLaunch({
       taskID: 'ses_1',
@@ -456,14 +455,14 @@ describe('BackgroundJobBoard', () => {
     const updated = board.markRunningFromLiveSession('ses_1', 200);
 
     expect(updated).toMatchObject({
-      state: 'running',
+      state: 'reconciled',
       terminalUnreconciled: false,
-      updatedAt: 200,
+      terminalState: 'cancelled',
+      lastLiveBusyAt: 200,
     });
-    expect(updated?.terminalState).toBeUndefined();
   });
 
-  test('live busy session reopens non-cancelled terminal jobs', () => {
+  test('live busy session does not reopen non-cancelled terminal jobs', () => {
     const board = new BackgroundJobBoard();
     board.registerLaunch({
       taskID: 'ses_1',
@@ -475,12 +474,12 @@ describe('BackgroundJobBoard', () => {
     const updated = board.markRunningFromLiveSession('ses_1', 200);
 
     expect(updated).toMatchObject({
-      state: 'running',
-      terminalUnreconciled: false,
-      completedAt: undefined,
-      terminalState: undefined,
+      state: 'completed',
+      terminalUnreconciled: true,
+      terminalState: 'completed',
       lastLiveBusyAt: 200,
     });
+    expect(updated?.completedAt).toBeDefined();
   });
 
   test('stale status updates cannot reopen already reconciled jobs', () => {

+ 1 - 14
src/utils/background-job-board.ts

@@ -196,13 +196,9 @@ export class BackgroundJobBoard {
     const existing = this.jobs.get(taskID);
     if (!existing) return undefined;
 
-    // OpenCode process-local task status can briefly disagree with the live
-    // session event stream. Trust live session.status=busy over stale terminal
-    // board state, except for explicit user cancellations where the next step is
-    // stronger cancellation/delete rather than reopening the lane.
     const isStaleTerminal =
       TERMINAL_STATES.has(existing.state) || existing.state === 'reconciled';
-    if (!isStaleTerminal || existing.cancellationRequested) {
+    if (isStaleTerminal) {
       const updated: BackgroundJobRecord = {
         ...existing,
         lastLiveBusyAt: now,
@@ -213,17 +209,8 @@ export class BackgroundJobBoard {
 
     const updated: BackgroundJobRecord = {
       ...existing,
-      state: 'running',
-      timedOut: false,
-      statusUncertain: false,
-      cancellationRequested: false,
-      terminalUnreconciled: false,
       updatedAt: now,
       lastLiveBusyAt: now,
-      completedAt: undefined,
-      terminalState: undefined,
-      resultSummary: undefined,
-      lastStatusError: undefined,
     };
 
     this.jobs.set(taskID, updated);