@@ -30,6 +30,7 @@ import {
3030 hydrateSessionJsonl ,
3131} from "../adapters/claude/session/jsonl-hydration" ;
3232import type { GatewayEnv } from "../adapters/claude/session/options" ;
33+ import { hasCodexThreadState } from "../adapters/codex-app-server/thread-state" ;
3334import {
3435 type AgentErrorClassification ,
3536 classifyAgentError ,
@@ -672,8 +673,6 @@ export class AgentServer {
672673 cwd : string ,
673674 permissionMode : PermissionMode ,
674675 ) : Promise < { sessionId : string ; warm : boolean } | null > {
675- if ( runtimeAdapter !== "claude" ) return null ;
676-
677676 const resumeRunId = this . getResumeRunId ( preTaskRun ) ;
678677 if ( ! resumeRunId ) return null ;
679678
@@ -689,6 +688,22 @@ export class AgentServer {
689688 return null ;
690689 }
691690
691+ if ( runtimeAdapter === "codex" ) {
692+ // Codex owns thread persistence in CODEX_HOME (the ACP sessionId is the
693+ // codex thread id). The rollout only survives a snapshot restart — there
694+ // is no cold hydration equivalent, so a fresh sandbox keeps the summary
695+ // fallback while a warm one resumes the thread natively via thread/resume.
696+ if ( ! ( await hasCodexThreadState ( priorSessionId ) ) ) {
697+ this . logger . debug (
698+ "No codex thread state on disk; using summary resume fallback" ,
699+ { resumeRunId, priorSessionId } ,
700+ ) ;
701+ return null ;
702+ }
703+ this . logger . debug ( "Native codex resume prepared" , { priorSessionId } ) ;
704+ return { sessionId : priorSessionId , warm : true } ;
705+ }
706+
692707 let warm = false ;
693708 try {
694709 await access ( getSessionJsonlPath ( priorSessionId , cwd ) ) ;
@@ -1309,22 +1324,32 @@ export class AgentServer {
13091324 initialPermissionMode ,
13101325 ) ;
13111326
1312- let acpSessionId : string ;
1327+ let acpSessionId : string | null = null ;
13131328 if ( nativeResume ) {
1314- await clientConnection . resumeSession ( {
1315- sessionId : nativeResume . sessionId ,
1316- cwd : sessionCwd ,
1317- mcpServers : this . config . mcpServers ?? [ ] ,
1318- _meta : { ...sessionMeta , sessionId : nativeResume . sessionId } ,
1319- } ) ;
1320- acpSessionId = nativeResume . sessionId ;
1321- this . nativeResume = nativeResume ;
1322- this . logger . debug ( "ACP session resumed" , {
1323- acpSessionId,
1324- runId : payload . run_id ,
1325- warm : nativeResume . warm ,
1326- } ) ;
1327- } else {
1329+ try {
1330+ await clientConnection . resumeSession ( {
1331+ sessionId : nativeResume . sessionId ,
1332+ cwd : sessionCwd ,
1333+ mcpServers : this . config . mcpServers ?? [ ] ,
1334+ _meta : { ...sessionMeta , sessionId : nativeResume . sessionId } ,
1335+ } ) ;
1336+ acpSessionId = nativeResume . sessionId ;
1337+ this . nativeResume = nativeResume ;
1338+ this . logger . debug ( "ACP session resumed" , {
1339+ acpSessionId,
1340+ runId : payload . run_id ,
1341+ warm : nativeResume . warm ,
1342+ } ) ;
1343+ } catch ( error ) {
1344+ // resumeState is still loaded, so the summary resume path takes over
1345+ // on the fresh session below.
1346+ this . logger . warn ( "Native resume failed; starting a fresh session" , {
1347+ sessionId : nativeResume . sessionId ,
1348+ error : error instanceof Error ? error . message : String ( error ) ,
1349+ } ) ;
1350+ }
1351+ }
1352+ if ( ! acpSessionId ) {
13281353 const sessionResponse = await clientConnection . newSession ( {
13291354 cwd : sessionCwd ,
13301355 mcpServers : this . config . mcpServers ?? [ ] ,
0 commit comments