Browse Source

feat: add extractSessionId utility, fix reversed-priority session ID bug

Michael Henke 1 month ago
parent
commit
fca8f3429d

+ 2 - 1
src/hooks/foreground-fallback/index.ts

@@ -16,6 +16,7 @@
 
 import type { PluginInput } from '@opencode-ai/plugin';
 import { ALL_AGENT_NAMES } from '../../config/constants';
+import { extractSessionId } from '../../utils';
 import { log } from '../../utils/logger';
 import {
   abortSessionWithTimeout,
@@ -233,7 +234,7 @@ export class ForegroundFallbackManager {
         const props = event.properties as
           | { sessionID?: string; info?: { id?: string } }
           | undefined;
-        const id = props?.info?.id ?? props?.sessionID;
+        const id = extractSessionId(props?.info, props?.sessionID);
         if (id) {
           this.sessionModel.delete(id);
           this.sessionAgent.delete(id);

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

@@ -7,6 +7,7 @@
  */
 
 import { PHASE_REMINDER } from '../../config/constants';
+import { extractSessionId } from '../../utils';
 
 interface ToolExecuteAfterInput {
   tool: string;
@@ -73,8 +74,10 @@ export function createPostFileToolNudgeHook(
       };
     }): Promise<void> => {
       if (input.event.type !== 'session.deleted') return;
-      const sid =
-        input.event.properties?.sessionID ?? input.event.properties?.info?.id;
+      const sid = extractSessionId(
+        input.event.properties?.info,
+        input.event.properties?.sessionID,
+      );
       if (sid) {
         pendingSessionIds.delete(sid);
         everPendingSessionIds.delete(sid);

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

@@ -3,6 +3,7 @@ import {
   BackgroundJobBoard,
   type BackgroundJobRecord,
   deriveTaskSessionLabel,
+  extractSessionId,
   parseTaskIdFromTaskOutput,
   parseTaskLaunchOutput,
   parseTaskStatusOutput,
@@ -578,8 +579,10 @@ export function createTaskSessionManagerHook(
           (input.event.properties as { status?: { type?: string } } | undefined)
             ?.status?.type === 'idle')
       ) {
-        const sessionId =
-          input.event.properties?.info?.id ?? input.event.properties?.sessionID;
+        const sessionId = extractSessionId(
+          input.event.properties?.info,
+          input.event.properties?.sessionID,
+        );
         const job = sessionId ? backgroundJobBoard.get(sessionId) : undefined;
         log('[task-session-manager] idle/status idle observed', {
           sessionID: sessionId,
@@ -631,8 +634,10 @@ export function createTaskSessionManagerHook(
       }
 
       if (input.event.type === 'session.error') {
-        const sessionId =
-          input.event.properties?.info?.id ?? input.event.properties?.sessionID;
+        const sessionId = extractSessionId(
+          input.event.properties?.info,
+          input.event.properties?.sessionID,
+        );
         if (sessionId && options.shouldManageSession(sessionId)) {
           // Only clear injected terminal jobs for fatal errors.
           // Rate-limit errors are recovered by ForegroundFallbackManager
@@ -655,8 +660,10 @@ export function createTaskSessionManagerHook(
         (input.event.properties as { status?: { type?: string } } | undefined)
           ?.status?.type === 'busy'
       ) {
-        const sessionId =
-          input.event.properties?.info?.id ?? input.event.properties?.sessionID;
+        const sessionId = extractSessionId(
+          input.event.properties?.info,
+          input.event.properties?.sessionID,
+        );
         const before = sessionId
           ? backgroundJobBoard.get(sessionId)
           : undefined;
@@ -689,8 +696,10 @@ export function createTaskSessionManagerHook(
       }
 
       if (input.event.type !== 'session.deleted') return;
-      const sessionId =
-        input.event.properties?.info?.id ?? input.event.properties?.sessionID;
+      const sessionId = extractSessionId(
+        input.event.properties?.info,
+        input.event.properties?.sessionID,
+      );
       if (!sessionId) return;
 
       log(

+ 3 - 9
src/index.ts

@@ -54,6 +54,7 @@ import { recordTuiAgentModel, recordTuiAgentModels } from './tui-state';
 import {
   BackgroundJobBoard,
   createDisplayNameMentionRewriter,
+  extractSessionId,
   resolveRuntimeAgentName,
 } from './utils';
 import { isPluginDisabledByEnv } from './utils/env';
@@ -886,16 +887,9 @@ const OhMyOpenCodeLite: Plugin = async (ctx) => {
         const props = input.event.properties as
           | { info?: { id?: string }; sessionID?: string }
           | undefined;
-        const sessionID = props?.info?.id ?? props?.sessionID;
-        companionManager.onSessionDeleted(sessionID);
-      }
-
-      if (input.event.type === 'session.deleted') {
-        const props = input.event.properties as
-          | { info?: { id?: string }; sessionID?: string }
-          | undefined;
-        const sessionID = props?.info?.id ?? props?.sessionID;
+        const sessionID = extractSessionId(props?.info, props?.sessionID);
 
+        companionManager.onSessionDeleted(sessionID);
         if (depthTracker && sessionID) {
           depthTracker.cleanup(sessionID);
         }

+ 5 - 1
src/multiplexer/session-manager.ts

@@ -6,6 +6,7 @@ import {
   isServerRunning,
   type Multiplexer,
 } from '../multiplexer';
+import { extractSessionId } from '../utils';
 import type {
   BackgroundJobBoard,
   BackgroundJobState,
@@ -607,7 +608,10 @@ export class MultiplexerSessionManager {
   }
 
   private getSessionId(event: SessionEvent): string | undefined {
-    return event.properties?.info?.id ?? event.properties?.sessionID;
+    return extractSessionId(
+      event.properties?.info,
+      event.properties?.sessionID,
+    );
   }
 
   private backgroundJobState(

+ 23 - 0
src/utils/extract-session-id.test.ts

@@ -0,0 +1,23 @@
+import { describe, expect, test } from 'bun:test';
+import { extractSessionId } from './extract-session-id';
+
+describe('extractSessionId', () => {
+  test('prefers info.id over sessionID', () => {
+    expect(extractSessionId({ id: 'i' }, 's')).toBe('i');
+  });
+
+  test('falls back to sessionID when info.id missing', () => {
+    expect(extractSessionId({}, 's')).toBe('s');
+    expect(extractSessionId({ id: undefined }, 's')).toBe('s');
+  });
+
+  test('returns undefined when both missing', () => {
+    expect(extractSessionId(undefined, undefined)).toBeUndefined();
+    expect(extractSessionId(null, null)).toBeUndefined();
+    expect(extractSessionId({}, undefined)).toBeUndefined();
+  });
+
+  test('handles null info', () => {
+    expect(extractSessionId(null, 's')).toBe('s');
+  });
+});

+ 7 - 0
src/utils/extract-session-id.ts

@@ -0,0 +1,7 @@
+export function extractSessionId(
+  info: { id?: string } | undefined | null,
+  sessionID: string | undefined | null,
+): string | undefined {
+  // ponytail: ?? undefined converts null to undefined for TS strict mode
+  return info?.id ?? sessionID ?? undefined;
+}

+ 1 - 0
src/utils/index.ts

@@ -1,5 +1,6 @@
 export * from './agent-variant';
 export * from './background-job-board';
+export * from './extract-session-id';
 export * from './internal-initiator';
 export { getLogDir, initLogger, log } from './logger';
 export * from './polling';