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
This commit is contained in:
SSU-WEI HUANG
2026-07-16 12:35:04 +08:00
committed by GitHub
parent 520c3f511a
commit 6f5ecde2c4
3 changed files with 26 additions and 5 deletions
+2
View File
@@ -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;
}
+15 -3
View File
@@ -14,7 +14,9 @@ const harness = vi.hoisted(() => ({
collaborationModeResponse: { data: [{ mode: 'default' }, { mode: 'plan' }] } as unknown,
failListCollaborationModes: false,
startThreadIds: [] as string[],
startThreadParams: [] as Array<Record<string, unknown>>,
resumeThreadIds: [] as string[],
resumeThreadParams: [] as Array<Record<string, unknown>>,
startTurnThreadIds: [] as string[],
startTurnParams: [] as Array<Record<string, unknown>>,
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<string, unknown>): 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<string, unknown>): 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');
+9 -2
View File
@@ -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);