From 6f5ecde2c42be6ed03d356aa73ded22916f9215e Mon Sep 17 00:00:00 2001 From: SSU-WEI HUANG Date: Thu, 16 Jul 2026 12:35:04 +0800 Subject: [PATCH] fix(codex): expose HAPI threads to Codex Desktop (#1022) * test(codex): reproduce desktop thread classification gap * fix(codex): classify HAPI threads as user sessions * test(codex): cover thread source creation paths --- cli/src/codex/appServerTypes.ts | 2 ++ cli/src/codex/codexRemoteLauncher.test.ts | 18 +++++++++++++++--- cli/src/codex/codexRemoteLauncher.ts | 11 +++++++++-- 3 files changed, 26 insertions(+), 5 deletions(-) diff --git a/cli/src/codex/appServerTypes.ts b/cli/src/codex/appServerTypes.ts index 2978aab4..bb06f221 100644 --- a/cli/src/codex/appServerTypes.ts +++ b/cli/src/codex/appServerTypes.ts @@ -93,6 +93,8 @@ export interface ThreadStartParams { baseInstructions?: string; developerInstructions?: string; personality?: string; + /** Client-supplied analytics classification persisted with the thread. */ + threadSource?: string; ephemeral?: boolean; experimentalRawEvents?: boolean; } diff --git a/cli/src/codex/codexRemoteLauncher.test.ts b/cli/src/codex/codexRemoteLauncher.test.ts index a179bd53..bb245c63 100644 --- a/cli/src/codex/codexRemoteLauncher.test.ts +++ b/cli/src/codex/codexRemoteLauncher.test.ts @@ -14,7 +14,9 @@ const harness = vi.hoisted(() => ({ collaborationModeResponse: { data: [{ mode: 'default' }, { mode: 'plan' }] } as unknown, failListCollaborationModes: false, startThreadIds: [] as string[], + startThreadParams: [] as Array>, resumeThreadIds: [] as string[], + resumeThreadParams: [] as Array>, startTurnThreadIds: [] as string[], startTurnParams: [] as Array>, startTurnErrors: [] as Error[], @@ -110,15 +112,17 @@ vi.mock('./codexAppServerClient', () => { harness.requestHandlers.set(method, handler); } - async startThread(): Promise<{ thread: { id: string }; model: string }> { + async startThread(params?: Record): Promise<{ thread: { id: string }; model: string }> { const id = `thread-${harness.startThreadIds.length + 1}`; harness.startThreadIds.push(id); + harness.startThreadParams.push(params ?? {}); return { thread: { id }, model: 'gpt-5.4' }; } - async resumeThread(params?: { threadId?: string }): Promise<{ thread: { id: string }; model: string }> { - const id = params?.threadId ?? 'thread-resumed'; + async resumeThread(params?: Record): Promise<{ thread: { id: string }; model: string }> { + const id = typeof params?.threadId === 'string' ? params.threadId : 'thread-resumed'; harness.resumeThreadIds.push(id); + harness.resumeThreadParams.push(params ?? {}); if (harness.failResumeThreadIds.includes(id)) { throw new Error('resume failed'); } @@ -1051,7 +1055,9 @@ describe('codexRemoteLauncher', () => { harness.collaborationModeResponse = { data: [{ mode: 'default' }, { mode: 'plan' }] }; harness.failListCollaborationModes = false; harness.startThreadIds = []; + harness.startThreadParams = []; harness.resumeThreadIds = []; + harness.resumeThreadParams = []; harness.startTurnThreadIds = []; harness.startTurnParams = []; harness.startTurnErrors = []; @@ -1119,6 +1125,8 @@ describe('codexRemoteLauncher', () => { expect(exitReason).toBe('exit'); expect(foundSessionIds).toContain('thread-1'); expect(getModel()).toBe('gpt-5.4'); + expect(harness.startThreadParams).toHaveLength(1); + expect(harness.startThreadParams[0]?.threadSource).toBe('user'); expect(harness.initializeCalls).toEqual([{ clientInfo: { name: 'hapi-codex-client', @@ -1344,6 +1352,8 @@ describe('codexRemoteLauncher', () => { expect(exitReason).toBe('exit'); expect(foundSessionIds).toEqual(['thread-1']); + expect(harness.startThreadParams).toHaveLength(1); + expect(harness.startThreadParams[0]?.threadSource).toBe('user'); expect(harness.startTurnParams).toHaveLength(0); expect(harness.goalSetCalls).toEqual([{ threadId: 'thread-1', @@ -1967,6 +1977,8 @@ describe('codexRemoteLauncher', () => { expect(exitReason).toBe('exit'); expect(harness.resumeThreadIds).toEqual(['thread-old']); + expect(harness.resumeThreadParams).toHaveLength(1); + expect(harness.resumeThreadParams[0]?.threadSource).toBeUndefined(); expect(harness.startThreadIds).toEqual([]); expect(harness.startTurnThreadIds).toEqual([]); expect(session.sessionId).toBe('thread-old'); diff --git a/cli/src/codex/codexRemoteLauncher.ts b/cli/src/codex/codexRemoteLauncher.ts index d39b5650..c5ff2af7 100644 --- a/cli/src/codex/codexRemoteLauncher.ts +++ b/cli/src/codex/codexRemoteLauncher.ts @@ -98,6 +98,7 @@ const TRUSTED_ACCESS_FOR_CYBER_URL = 'https://chatgpt.com/cyber'; const CYBER_POLICY_TRUSTED_ACCESS_URL = 'https://openai.com/form/enterprise-trusted-access-for-cyber/'; const CODEX_GOALS_UNSUPPORTED_MESSAGE = 'Codex goals are not supported by this Codex runtime. Upgrade Codex or enable features.goals.'; const MAX_CODEX_GOAL_OBJECTIVE_CHARS = 4_000; +const HAPI_TOP_LEVEL_THREAD_SOURCE = 'user'; type GoalForwardSignature = { objective: string | null; @@ -3338,7 +3339,10 @@ class CodexRemoteLauncher extends RemoteLauncherBase { mcpServers, cliOverrides: session.codexCliOverrides }); - const threadResponse = await appServerClient.startThread(threadParams, { + const threadResponse = await appServerClient.startThread({ + ...threadParams, + threadSource: HAPI_TOP_LEVEL_THREAD_SOURCE + }, { signal: this.abortController.signal }); const threadRecord = asRecord(threadResponse); @@ -3597,7 +3601,10 @@ class CodexRemoteLauncher extends RemoteLauncherBase { } if (!threadId) { - const threadResponse = await appServerClient.startThread(threadParams, { + const threadResponse = await appServerClient.startThread({ + ...threadParams, + threadSource: HAPI_TOP_LEVEL_THREAD_SOURCE + }, { signal: this.abortController.signal }); const threadRecord = asRecord(threadResponse);