Browse Source

Repair session mappings before reminders

DanMaly 2 weeks ago
parent
commit
1b1d2eb1ad

+ 5 - 1
src/hooks/post-file-tool-nudge/index.test.ts

@@ -63,7 +63,7 @@ describe('post-file-tool-nudge hook', () => {
     expect(reminderParts(freshMessage)).toHaveLength(1);
   });
 
-  test('shared session eligibility suppresses both reminders for a rejected turn', async () => {
+  test('shared session eligibility suppresses a rejected turn and retains its pending nudge', async () => {
     const coordinator = new SessionLifecycle(() => {});
     let isOrchestratorSession = false;
     const shouldInject = () => isOrchestratorSession;
@@ -84,6 +84,10 @@ describe('post-file-tool-nudge hook', () => {
 
     isOrchestratorSession = true;
     const eligibleMessage = orchestratorMessage();
+    await nudge['experimental.chat.messages.transform'](
+      {},
+      { messages: [eligibleMessage] },
+    );
     await phaseReminder['experimental.chat.messages.transform'](
       {},
       { messages: [eligibleMessage] },

+ 3 - 7
src/hooks/post-file-tool-nudge/index.ts

@@ -59,15 +59,11 @@ export function createPostFileToolNudgeHook(
       const { message, sessionID } = eligible;
 
       const hasReminder = message.parts.some(hasPhaseReminder);
-      if (!coordinator.consumePending(sessionID)) {
-        return;
-      }
-      if (
-        hasReminder ||
-        (options.shouldInject && !options.shouldInject(sessionID))
-      ) {
+      if (options.shouldInject && !options.shouldInject(sessionID)) {
         return;
       }
+      if (!coordinator.consumePending(sessionID)) return;
+      if (hasReminder) return;
       // This transform must run before phase-reminder so this metadata deduplicates.
       message.parts.push({
         type: 'text',

+ 42 - 0
src/hooks/task-session-manager/index.test.ts

@@ -5,6 +5,11 @@ import {
   createInternalAgentTextPart,
   SLIM_INTERNAL_INITIATOR_MARKER,
 } from '../../utils';
+import {
+  createPhaseReminderHook,
+  PHASE_REMINDER_METADATA_KEY,
+} from '../phase-reminder';
+import { createPostFileToolNudgeHook } from '../post-file-tool-nudge';
 import {
   BACKGROUND_JOB_BOARD_METADATA_KEY,
   createTaskSessionManagerHook,
@@ -2456,4 +2461,41 @@ describe('task-session-manager hook', () => {
     );
     expect(messages.messages[0].parts[0].text).toContain('child-transform-1');
   });
+
+  test('repairs session mapping before composed reminder transforms', async () => {
+    const agentMap = new Map<string, string>();
+    const coordinator = new SessionLifecycle(() => {});
+    const shouldInject = (sessionID: string) =>
+      agentMap.get(sessionID) === 'orchestrator';
+    const { hook: taskSessionManager } = createHook({
+      shouldManageSession: shouldInject,
+      registerSessionAsOrchestrator: (sessionID) => {
+        agentMap.set(sessionID, 'orchestrator');
+      },
+    });
+    const postFileNudge = createPostFileToolNudgeHook({
+      coordinator,
+      shouldInject,
+    });
+    const phaseReminder = createPhaseReminderHook({ shouldInject });
+    const messages = createMessages('orchestrator-1');
+
+    await postFileNudge['tool.execute.after'](
+      { tool: 'Read', sessionID: 'orchestrator-1' },
+      {},
+    );
+    await taskSessionManager['experimental.chat.messages.transform'](
+      {},
+      messages,
+    );
+    await postFileNudge['experimental.chat.messages.transform']({}, messages);
+    await phaseReminder['experimental.chat.messages.transform']({}, messages);
+
+    expect(agentMap.get('orchestrator-1')).toBe('orchestrator');
+    expect(
+      messages.messages[0].parts.filter(
+        (part) => part.metadata?.[PHASE_REMINDER_METADATA_KEY] === true,
+      ),
+    ).toHaveLength(1);
+  });
 });

+ 5 - 5
src/index.ts

@@ -1195,20 +1195,20 @@ const OhMyOpenCodeLite: Plugin = async (ctx) => {
         log,
       });
 
-      // Post-file metadata must be added before phase-reminder deduplicates it.
-      await postFileToolNudge['experimental.chat.messages.transform'](
+      // Repair session mappings before reminder gates; nudge metadata precedes phase dedup.
+      await taskSessionManagerHook['experimental.chat.messages.transform'](
         input as never,
         typedOutput as never,
       );
-      await phaseReminder['experimental.chat.messages.transform'](
+      await postFileToolNudge['experimental.chat.messages.transform'](
         input as never,
         typedOutput as never,
       );
-      await filterAvailableSkills['experimental.chat.messages.transform'](
+      await phaseReminder['experimental.chat.messages.transform'](
         input as never,
         typedOutput as never,
       );
-      await taskSessionManagerHook['experimental.chat.messages.transform'](
+      await filterAvailableSkills['experimental.chat.messages.transform'](
         input as never,
         typedOutput as never,
       );