diff --git a/cli/src/codex/shared/frontend.integration.test.ts b/cli/src/codex/shared/frontend.integration.test.ts index 4f161553..fb510b58 100644 --- a/cli/src/codex/shared/frontend.integration.test.ts +++ b/cli/src/codex/shared/frontend.integration.test.ts @@ -318,6 +318,16 @@ describe.skipIf(process.env.HAPI_RUN_SHARED_CODEX_TESTS !== '1' || process.platf await client.request('hapi/stopSession', { sessionId }); await api(`/sessions/${webId}/archive`, {}); await eventually(webDetail, value => record(value.session).active === false, 'end independent Web session'); + const reopened = await api(`/sessions/${webId}/reopen`, {}); + expect(reopened.sessionId).toBe(webId); + const reopenedSession = record((await webDetail()).session); + expect(record(reopenedSession.metadata).codexSessionId).toBe(record(record(independent.session).metadata).codexSessionId); + runtimePids.add(Number(record(reopenedSession.metadata).hostPid)); + await api(`/sessions/${webId}/messages`, { text: 'AFTER_ARCHIVE_REOPEN', localId: randomUUID() }); + await eventually(() => api(`/sessions/${webId}/messages`), value => hasReply(value, 'AFTER_ARCHIVE_REOPEN'), 'Web reply after reopening archived native thread'); + expect(hasReply(await api(`/sessions/${webId}/messages`), 'HELLO_WEB_CREATED')).toBe(true); + await api(`/sessions/${webId}/archive`, {}); + await eventually(webDetail, value => record(value.session).active === false, 'end reopened Web session'); } catch (error) { await writeFile('/tmp/hapi-shared-e2e-terminal.log', output + '\nRUNNER:\n' + runnerOutput); throw error; diff --git a/cli/src/codex/shared/runtime.ts b/cli/src/codex/shared/runtime.ts index 53dfcf20..3a054ce2 100644 --- a/cli/src/codex/shared/runtime.ts +++ b/cli/src/codex/shared/runtime.ts @@ -356,7 +356,19 @@ export async function runSharedRuntime(options: SharedLaunchOptions, onReady?: ( const root = await prepare(launch.cwd, existing); await reserveRecord(root, threadId); return root; }); - const response = record(await root.client.request('thread/resume', root.config({ ...launch.threadParams, threadId }))); + const params = root.config({ ...launch.threadParams, threadId }); + let response: Record; + try { + response = record(await root.client.request('thread/resume', params)); + } catch (error) { + // Reopening a HAPI binding also restores its native archive. + // Only retry an explicit archived rejection, never a transport + // failure whose resume outcome may be unknown. + if (!options.existingSessionId || !(error instanceof Error) + || !error.message.startsWith(`session ${threadId} is archived.`)) throw error; + await root.client.request('thread/unarchive', { threadId }); + response = record(await root.client.request('thread/resume', params)); + } await bind(root, response, false, options); } else root = await create('thread/start', { ...launch.threadParams, cwd: launch.cwd }, undefined, options); assertRunning(); diff --git a/cli/src/runner/run.ts b/cli/src/runner/run.ts index 6a9d3db5..4d0186fe 100644 --- a/cli/src/runner/run.ts +++ b/cli/src/runner/run.ts @@ -1493,19 +1493,8 @@ export async function startRunner(options: { workspaceRoots?: string[] } = {}): // Heartbeat try { const updatedState: RunnerLocallyPersistedState = { - pid: process.pid, - httpPort: controlPort, - startTime: fileState.startTime, - startedWithCliVersion: packageJson.version, - startedWithCliMtimeMs, - startedWithApiUrl: fileState.startedWithApiUrl, - startedWithMachineId: fileState.startedWithMachineId, - startedWithCliApiTokenHash: fileState.startedWithCliApiTokenHash, - startedWithExtraHeadersHash: fileState.startedWithExtraHeadersHash, - startedWithArgv, - startedWithVersionHandoffDisabled, - lastHeartbeat: new Date().toLocaleString(), - runnerLogPath: fileState.runnerLogPath + ...fileState, + lastHeartbeat: new Date().toLocaleString() }; writeRunnerState(updatedState); if (process.env.DEBUG) { diff --git a/cli/src/runner/runner.integration.test.ts b/cli/src/runner/runner.integration.test.ts index ca124af7..d24aaed7 100644 --- a/cli/src/runner/runner.integration.test.ts +++ b/cli/src/runner/runner.integration.test.ts @@ -174,6 +174,17 @@ describe.skipIf(!await isServerHealthy())('Runner Integration Tests', { timeout: expect(sessions).toEqual([]); }); + it('preserves shared Codex support after a runner heartbeat', async () => { + const initial = await readRunnerState(); + expect(initial?.sharedCodexRuntime).toBe(true); + + await waitFor(async () => Boolean((await readRunnerState())?.lastHeartbeat), 35_000); + + const afterHeartbeat = await readRunnerState(); + expect(afterHeartbeat?.pid).toBe(initial?.pid); + expect(afterHeartbeat?.sharedCodexRuntime).toBe(true); + }, 40_000); + it('should track session-started webhook from terminal session', async () => { // Simulate a terminal-started session reporting to runner const mockMetadata: Metadata = {