@@ -198,11 +198,11 @@ function createStartedThreadHarness(
198198 async notify ( notification : CodexServerNotification ) {
199199 await notify ( notification ) ;
200200 } ,
201- async completeTurn ( status : "completed" | "failed" = "completed" ) {
201+ async completeTurn ( status : "completed" | "failed" = "completed" , threadId = "thread-1" ) {
202202 await notify ( {
203203 method : "turn/completed" ,
204204 params : {
205- threadId : "thread-1" ,
205+ threadId,
206206 turnId : "turn-1" ,
207207 turn : {
208208 id : "turn-1" ,
@@ -545,6 +545,201 @@ describe("runCodexAppServerAttempt context-engine lifecycle", () => {
545545 await secondRun ;
546546 } ) ;
547547
548+ it . each ( [
549+ [
550+ "token" ,
551+ `${ JSON . stringify ( {
552+ payload : {
553+ type : "token_count" ,
554+ info : {
555+ last_token_usage : {
556+ total_tokens : 300_000 ,
557+ } ,
558+ } ,
559+ } ,
560+ } ) } \n`,
561+ "1mb" ,
562+ ] ,
563+ [ "byte" , "x" . repeat ( 2_000 ) , 1_000 ] ,
564+ ] as const ) (
565+ "resumes a matching thread-bootstrap binding even when the bootstrap turn exceeded the native %s guard" ,
566+ async ( _guard , rolloutContent , maxActiveTranscriptBytes ) => {
567+ const sessionFile = path . join ( tempDir , "session.jsonl" ) ;
568+ const workspaceDir = path . join ( tempDir , "workspace" ) ;
569+ const agentDir = path . join ( tempDir , "agent" ) ;
570+ await writeCodexAppServerBinding ( sessionFile , {
571+ threadId : "thread-bootstrapped" ,
572+ cwd : workspaceDir ,
573+ dynamicToolsFingerprint : "[]" ,
574+ contextEngine : {
575+ schemaVersion : 1 ,
576+ engineId : "lossless-claw" ,
577+ policyFingerprint :
578+ '{"schemaVersion":1,"engineId":"lossless-claw","ownsCompaction":true,"projectionMaxChars":24000}' ,
579+ projection : {
580+ schemaVersion : 1 ,
581+ mode : "thread_bootstrap" ,
582+ epoch : "epoch-1" ,
583+ } ,
584+ } ,
585+ } ) ;
586+ await fs . writeFile (
587+ path . join ( path . dirname ( sessionFile ) , "sessions.json" ) ,
588+ JSON . stringify ( {
589+ "agent:main:session-1" : {
590+ sessionFile,
591+ totalTokens : 12_000 ,
592+ } ,
593+ } ) ,
594+ ) ;
595+ const rolloutDir = path . join ( agentDir , "codex-home" , "sessions" ) ;
596+ await fs . mkdir ( rolloutDir , { recursive : true } ) ;
597+ await fs . writeFile (
598+ path . join ( rolloutDir , "rollout-thread-bootstrapped.jsonl" ) ,
599+ rolloutContent ,
600+ ) ;
601+ const contextEngine = createContextEngine ( {
602+ assemble : vi . fn ( async ( { prompt } ) => ( {
603+ messages : [
604+ assistantMessage ( "already bootstrapped context" , 10 ) ,
605+ userMessage ( prompt ?? "" , 11 ) ,
606+ ] ,
607+ estimatedTokens : 42 ,
608+ systemPromptAddition : "context-engine system" ,
609+ contextProjection : { mode : "thread_bootstrap" as const , epoch : "epoch-1" } ,
610+ } ) ) ,
611+ } ) ;
612+ const harness = createStartedThreadHarness ( async ( method ) => {
613+ if ( method === "thread/resume" ) {
614+ return threadStartResult ( "thread-bootstrapped" ) ;
615+ }
616+ if ( method === "thread/start" ) {
617+ return threadStartResult ( "thread-fresh" ) ;
618+ }
619+ return undefined ;
620+ } ) ;
621+ const params = createParams ( sessionFile , workspaceDir ) ;
622+ params . agentDir = agentDir ;
623+ params . contextEngine = contextEngine ;
624+ params . config = {
625+ agents : {
626+ defaults : {
627+ compaction : {
628+ truncateAfterCompaction : true ,
629+ maxActiveTranscriptBytes,
630+ } ,
631+ } ,
632+ } ,
633+ } as EmbeddedRunAttemptParams [ "config" ] ;
634+
635+ const run = runCodexAppServerAttempt ( params ) ;
636+ await harness . waitForMethod ( "turn/start" ) ;
637+
638+ expect ( harness . requests . map ( ( request ) => request . method ) ) . toEqual ( [
639+ "thread/resume" ,
640+ "turn/start" ,
641+ ] ) ;
642+ const inputText = getRequestInputText ( harness ) ;
643+ expect ( inputText ) . not . toContain ( "OpenClaw assembled context for this turn:" ) ;
644+ expect ( inputText ) . not . toContain ( "already bootstrapped context" ) ;
645+ expect ( inputText ) . toBe ( "hello" ) ;
646+
647+ await harness . completeTurn ( "completed" , "thread-bootstrapped" ) ;
648+ await run ;
649+ } ,
650+ ) ;
651+
652+ it ( "projects mirrored history when an oversized thread-bootstrap binding has no active context engine" , async ( ) => {
653+ const sessionFile = path . join ( tempDir , "session.jsonl" ) ;
654+ const workspaceDir = path . join ( tempDir , "workspace" ) ;
655+ const agentDir = path . join ( tempDir , "agent" ) ;
656+ const sessionManager = SessionManager . open ( sessionFile ) ;
657+ sessionManager . appendMessage (
658+ userMessage ( "previous stale-bootstrap request" , Date . now ( ) ) as never ,
659+ ) ;
660+ sessionManager . appendMessage (
661+ assistantMessage ( "previous stale-bootstrap answer" , Date . now ( ) + 1 ) as never ,
662+ ) ;
663+ await writeCodexAppServerBinding ( sessionFile , {
664+ threadId : "thread-stale-bootstrap" ,
665+ cwd : workspaceDir ,
666+ dynamicToolsFingerprint : "[]" ,
667+ contextEngine : {
668+ schemaVersion : 1 ,
669+ engineId : "lossless-claw" ,
670+ policyFingerprint :
671+ '{"schemaVersion":1,"engineId":"lossless-claw","ownsCompaction":true,"projectionMaxChars":24000}' ,
672+ projection : {
673+ schemaVersion : 1 ,
674+ mode : "thread_bootstrap" ,
675+ epoch : "epoch-stale" ,
676+ } ,
677+ } ,
678+ } ) ;
679+ await fs . writeFile (
680+ path . join ( path . dirname ( sessionFile ) , "sessions.json" ) ,
681+ JSON . stringify ( {
682+ "agent:main:session-1" : {
683+ sessionFile,
684+ totalTokens : 12_000 ,
685+ } ,
686+ } ) ,
687+ ) ;
688+ const rolloutDir = path . join ( agentDir , "codex-home" , "sessions" ) ;
689+ await fs . mkdir ( rolloutDir , { recursive : true } ) ;
690+ await fs . writeFile (
691+ path . join ( rolloutDir , "rollout-thread-stale-bootstrap.jsonl" ) ,
692+ `${ JSON . stringify ( {
693+ payload : {
694+ type : "token_count" ,
695+ info : {
696+ last_token_usage : {
697+ total_tokens : 300_000 ,
698+ } ,
699+ } ,
700+ } ,
701+ } ) } \n`,
702+ ) ;
703+ const harness = createStartedThreadHarness ( async ( method ) => {
704+ if ( method === "thread/resume" ) {
705+ return threadStartResult ( "thread-stale-bootstrap" ) ;
706+ }
707+ if ( method === "thread/start" ) {
708+ return threadStartResult ( "thread-fresh" ) ;
709+ }
710+ return undefined ;
711+ } ) ;
712+ const params = createParams ( sessionFile , workspaceDir ) ;
713+ params . agentDir = agentDir ;
714+ params . config = {
715+ agents : {
716+ defaults : {
717+ compaction : {
718+ truncateAfterCompaction : true ,
719+ maxActiveTranscriptBytes : "1mb" ,
720+ } ,
721+ } ,
722+ } ,
723+ } as EmbeddedRunAttemptParams [ "config" ] ;
724+
725+ const run = runCodexAppServerAttempt ( params ) ;
726+ await harness . waitForMethod ( "turn/start" ) ;
727+
728+ expect ( harness . requests . map ( ( request ) => request . method ) ) . toEqual ( [
729+ "thread/start" ,
730+ "turn/start" ,
731+ ] ) ;
732+ const inputText = getRequestInputText ( harness ) ;
733+ expect ( inputText ) . toContain ( "OpenClaw assembled context for this turn:" ) ;
734+ expect ( inputText ) . toContain ( "previous stale-bootstrap request" ) ;
735+ expect ( inputText ) . toContain ( "previous stale-bootstrap answer" ) ;
736+ expect ( inputText ) . toContain ( "Current user request:" ) ;
737+ expect ( inputText ) . toContain ( "hello" ) ;
738+
739+ await harness . completeTurn ( "completed" , "thread-fresh" ) ;
740+ await run ;
741+ } ) ;
742+
548743 it ( "starts a fresh Codex thread and reprojects when context-engine epoch changes" , async ( ) => {
549744 const info = vi . spyOn ( embeddedAgentLog , "info" ) . mockImplementation ( ( ) => undefined ) ;
550745 const sessionFile = path . join ( tempDir , "session.jsonl" ) ;
0 commit comments