Browse Source

fix(scheduler): document 2s idle reconciliation window, export constant, add post-delay test

Michael Henke 1 month ago
parent
commit
dbb193b9e4

+ 41 - 2
src/hooks/task-session-manager/index.test.ts

@@ -1,10 +1,12 @@
 import { describe, expect, mock, test } from 'bun:test';
 import { BackgroundJobBoard } from '../../utils';
-import { createTaskSessionManagerHook } from './index';
+import { createTaskSessionManagerHook, IDLE_RECONCILE_DELAY_MS } from './index';
 
 /** Wait for the idle reconciliation delay (2s + margin) to flush. */
 function flushIdleReconcileDelay() {
-  return new Promise((resolve) => setTimeout(resolve, 2_100));
+  return new Promise((resolve) =>
+    setTimeout(resolve, IDLE_RECONCILE_DELAY_MS + 100),
+  );
 }
 
 function createHook(options?: {
@@ -1257,6 +1259,43 @@ describe('task-session-manager hook', () => {
     });
   });
 
+  test('completion arriving after idle reconciliation delay is still dropped', async () => {
+    const board = new BackgroundJobBoard();
+    const { hook } = createHook({ backgroundJobBoard: board });
+
+    setupCompletedJob(board);
+
+    const messages = createMessages('parent-1', 'continue');
+    await hook['experimental.chat.messages.transform']({}, messages);
+
+    // Fire idle event (starts 2s reconciliation timer)
+    await hook.event({
+      event: {
+        type: 'session.status',
+        properties: { sessionID: 'parent-1', status: { type: 'idle' } },
+      },
+    });
+
+    // Wait for reconciliation to complete
+    await flushIdleReconcileDelay();
+
+    // Job is now reconciled
+    expect(board.get('child-1')).toMatchObject({
+      state: 'reconciled',
+    });
+
+    // Late completion arrives after reconciliation — should be silently dropped
+    const lateUpdate = board.updateStatus({
+      taskID: 'child-1',
+      state: 'error',
+      resultSummary: 'late completion after reconciliation',
+    });
+
+    // updateStatus returns existing record without modification
+    expect(lateUpdate).toBeDefined();
+    expect(lateUpdate?.state).toBe('reconciled');
+  });
+
   test('does not reconcile terminal jobs before they are injected into a prompt', async () => {
     const board = new BackgroundJobBoard();
     const { hook } = createHook({ backgroundJobBoard: board });

+ 8 - 2
src/hooks/task-session-manager/index.ts

@@ -36,8 +36,14 @@ const BACKGROUND_COMPLETION_FAILED = /^Background task failed: /;
 const MAX_PROCESSED_INJECTED_COMPLETIONS = 500;
 const RAW_SESSION_ID_PATTERN = /^ses_[A-Za-z0-9_-]+$/;
 
-/** Delay before reconciling idle sessions — gives late injected completions time to arrive. */
-const IDLE_RECONCILE_DELAY_MS = 2_000;
+/**
+ * Delay before reconciling idle sessions.
+ * Gives late injected completions time to arrive within this window.
+ * Completions arriving after the window are still dropped (the race is reduced, not eliminated).
+ * ponytail: fixed timeout — event-driven confirmation would fully close the race but adds
+ * significant complexity for a case that rarely exceeds this window in practice.
+ */
+export const IDLE_RECONCILE_DELAY_MS = 2_000;
 
 function djb2Hash(str: string): string {
   let hash = 5381;