@@ -172,6 +172,9 @@ interface FakeCaptures {
172172 followUpFailure : boolean ;
173173 inputRecoveryPending : boolean ;
174174 inputAdmissionBusy : boolean ;
175+ correlatedPromptLifecycleAdmissionBlocked : boolean ;
176+ correlatedPromptLifecycleAdmissionBlockAfterReads : number | undefined ;
177+ correlatedPromptLifecycleAdmissionReads : number ;
175178 correlatedPromptLifecycleAvailable : boolean ;
176179 readonly correlatedPromptSubmissions : Array < {
177180 readonly text : string ;
@@ -327,6 +330,9 @@ interface FakeCaptures {
327330 rlmQuiescenceFailure : boolean ;
328331 rlmConnectionGeneration : number ;
329332 rlmContinuityValid : boolean ;
333+ correlatedRecoveryProofCurrent : boolean ;
334+ correlatedRecoveryProofEpoch : number ;
335+ reconnectSnapshotResolutionAccepted : boolean ;
330336 readonly reconnectResolutions : Array < {
331337 readonly generation : number ;
332338 readonly reconciled : boolean ;
@@ -356,6 +362,9 @@ function makeCaptures(): FakeCaptures {
356362 followUpFailure : false ,
357363 inputRecoveryPending : false ,
358364 inputAdmissionBusy : false ,
365+ correlatedPromptLifecycleAdmissionBlocked : false ,
366+ correlatedPromptLifecycleAdmissionBlockAfterReads : undefined ,
367+ correlatedPromptLifecycleAdmissionReads : 0 ,
359368 correlatedPromptLifecycleAvailable : false ,
360369 correlatedPromptSubmissions : [ ] ,
361370 correlatedPromptCancellations : [ ] ,
@@ -477,6 +486,9 @@ function makeCaptures(): FakeCaptures {
477486 rlmQuiescenceFailure : false ,
478487 rlmConnectionGeneration : 0 ,
479488 rlmContinuityValid : true ,
489+ correlatedRecoveryProofCurrent : true ,
490+ correlatedRecoveryProofEpoch : 1 ,
491+ reconnectSnapshotResolutionAccepted : true ,
480492 reconnectResolutions : [ ] ,
481493 retryWorkerRecoverySnapshots : false ,
482494 retryWorkerRecoverySnapshotCalls : [ ] ,
@@ -836,7 +848,14 @@ function fakeRuntimeFactory(
836848 const resolution = { generation, reconciled, terminalResponseObserved } ;
837849 captures . reconnectResolutions . push ( resolution ) ;
838850 captures . reconnectSnapshotResolutionObserved ?.( resolution ) ;
839- if ( generation !== captures . rlmConnectionGeneration ) return false ;
851+ if (
852+ generation !== captures . rlmConnectionGeneration ||
853+ ! captures . reconnectSnapshotResolutionAccepted ||
854+ ( captures . correlatedPromptLifecycleAvailable &&
855+ ! captures . correlatedRecoveryProofCurrent )
856+ ) {
857+ return false ;
858+ }
840859 captures . rlmContinuityValid = reconciled ;
841860 return true ;
842861 } ,
@@ -848,8 +867,12 @@ function fakeRuntimeFactory(
848867 noteWorkerRecoveryTerminalResponse : ( ) => {
849868 captures . workerRecoveryTerminalResponseObserved ?.( ) ;
850869 } ,
851- isConnectionGenerationCurrent : ( generation ) =>
852- generation === captures . rlmConnectionGeneration ,
870+ isConnectionGenerationCurrent : ( generation , proofEpoch ) =>
871+ generation === captures . rlmConnectionGeneration &&
872+ ( ! captures . correlatedPromptLifecycleAvailable ||
873+ ( captures . correlatedRecoveryProofCurrent &&
874+ ( proofEpoch ?? captures . correlatedRecoveryProofEpoch ) ===
875+ captures . correlatedRecoveryProofEpoch ) ) ,
853876 get correlatedPromptLifecycleAvailable ( ) {
854877 return captures . correlatedPromptLifecycleAvailable ;
855878 } ,
@@ -897,6 +920,15 @@ function fakeRuntimeFactory(
897920 }
898921 ) ;
899922 } ) ,
923+ get correlatedPromptLifecycleAdmissionBlocked ( ) {
924+ captures . correlatedPromptLifecycleAdmissionReads += 1 ;
925+ return (
926+ captures . correlatedPromptLifecycleAdmissionBlocked ||
927+ ( captures . correlatedPromptLifecycleAdmissionBlockAfterReads !== undefined &&
928+ captures . correlatedPromptLifecycleAdmissionReads >=
929+ captures . correlatedPromptLifecycleAdmissionBlockAfterReads )
930+ ) ;
931+ } ,
900932 get inputAdmissionBusy ( ) {
901933 return captures . inputAdmissionBusy ;
902934 } ,
@@ -1121,6 +1153,32 @@ function offer(captures: FakeCaptures, event: PrimeDaemonEvent) {
11211153}
11221154
11231155describe ( "PrimeAgentDaemonAdapter" , ( ) => {
1156+ it . effect ( "rechecks correlated recovery before committing a new strict turn" , ( ) =>
1157+ Effect . scoped (
1158+ Effect . gen ( function * ( ) {
1159+ const captures = makeCaptures ( ) ;
1160+ captures . correlatedPromptLifecycleAvailable = true ;
1161+ captures . correlatedPromptLifecycleAdmissionBlockAfterReads = 2 ;
1162+ const adapter = yield * makePrimeAgentDaemonAdapter ( decodeSettings ( { } ) , manager , {
1163+ instanceId,
1164+ runtimeFactory : fakeRuntimeFactory ( captures ) ,
1165+ } ) ;
1166+ yield * adapter . startSession ( { threadId, cwd : process . cwd ( ) , runtimeMode : "full-access" } ) ;
1167+
1168+ const error = yield * adapter
1169+ . sendTurn ( { threadId, input : "must not cross pending resync" } )
1170+ . pipe ( Effect . flip ) ;
1171+
1172+ expect ( error ) . toMatchObject ( {
1173+ _tag : "ProviderAdapterValidationError" ,
1174+ reason : "busy" ,
1175+ } ) ;
1176+ expect ( captures . correlatedPromptLifecycleAdmissionReads ) . toBeGreaterThanOrEqual ( 2 ) ;
1177+ expect ( captures . correlatedPromptSubmissions ) . toEqual ( [ ] ) ;
1178+ } ) ,
1179+ ) . pipe ( Effect . provide ( testLayer ) ) ,
1180+ ) ;
1181+
11241182 it . effect ( "settles only the delivered correlated owner and uses terminal lifecycle usage" , ( ) =>
11251183 Effect . scoped (
11261184 Effect . gen ( function * ( ) {
@@ -1376,6 +1434,8 @@ describe("PrimeAgentDaemonAdapter", () => {
13761434 ...initialSnapshot ( ) ,
13771435 lastEventSequence : 2 ,
13781436 replayContinuity : "complete" ,
1437+ connectionGeneration : 0 ,
1438+ correlatedProofEpoch : 1 ,
13791439 promptLifecycles : {
13801440 records : [ lifecycleSnapshot ( correlationId , "failed" , 2 , { usage } ) ] ,
13811441 expired : [ ] ,
@@ -1428,6 +1488,8 @@ describe("PrimeAgentDaemonAdapter", () => {
14281488 ...initialSnapshot ( ) ,
14291489 lastEventSequence : 2 ,
14301490 replayContinuity : "complete" ,
1491+ connectionGeneration : 0 ,
1492+ correlatedProofEpoch : 1 ,
14311493 children : [ { id : "background-child" , label : "background" , status : "running" } ] ,
14321494 promptLifecycles : {
14331495 records : [ lifecycleSnapshot ( correlationId , "queued" , 2 ) ] ,
@@ -1931,6 +1993,69 @@ describe("PrimeAgentDaemonAdapter", () => {
19311993 ) . pipe ( Effect . provide ( testLayer ) ) ,
19321994 ) ;
19331995
1996+ it . effect ( "does not apply a terminal correlated snapshot when proof settlement is rejected" , ( ) =>
1997+ Effect . scoped (
1998+ Effect . gen ( function * ( ) {
1999+ const captures = makeCaptures ( ) ;
2000+ captures . correlatedPromptLifecycleAvailable = true ;
2001+ captures . correlatedPromptObserved = yield * Queue . unbounded < string > ( ) ;
2002+ captures . rlmConnectionGeneration = 1 ;
2003+ captures . rlmContinuityValid = false ;
2004+ captures . reconnectSnapshotResolutionAccepted = false ;
2005+ const adapter = yield * makePrimeAgentDaemonAdapter ( decodeSettings ( { } ) , manager , {
2006+ instanceId,
2007+ runtimeFactory : fakeRuntimeFactory ( captures ) ,
2008+ } ) ;
2009+ const subscription = yield * subscribe ( adapter ) ;
2010+ yield * adapter . startSession ( { threadId, cwd : process . cwd ( ) , runtimeMode : "full-access" } ) ;
2011+ const turnFiber = yield * adapter
2012+ . sendTurn ( { threadId, input : "retired proof must not settle" } )
2013+ . pipe ( Effect . forkChild ) ;
2014+ const correlationId = yield * Queue . take ( captures . correlatedPromptObserved ) ;
2015+ yield * offer ( captures , {
2016+ _tag : "PromptLifecycleUpdated" ,
2017+ lifecycle : lifecycleSnapshot ( correlationId , "delivered" , 2 ) ,
2018+ } ) ;
2019+ const answer = assistantMessage ( "stale terminal answer" ) ;
2020+ yield * offer ( captures , {
2021+ _tag : "MessageCompleted" ,
2022+ message : answer ,
2023+ attribution : { scope : "prompt" , correlationId } ,
2024+ } ) ;
2025+
2026+ yield * offer ( captures , {
2027+ ...initialSnapshot ( ) ,
2028+ state : { ...initialSnapshot ( ) . state , messageCount : 1 } ,
2029+ messages : [ answer ] ,
2030+ replayContinuity : "complete" ,
2031+ connectionGeneration : 1 ,
2032+ correlatedProofEpoch : 1 ,
2033+ promptLifecycles : {
2034+ records : [ lifecycleSnapshot ( correlationId , "completed" , 3 , { usage } ) ] ,
2035+ expired : [ ] ,
2036+ } ,
2037+ } ) ;
2038+ yield * offer ( captures , {
2039+ _tag : "SessionClosed" ,
2040+ error : "Prime Agent correlated prompt capability proof was lost during recovery." ,
2041+ } ) ;
2042+
2043+ const result = yield * Fiber . join ( turnFiber ) ;
2044+ expect ( captures . reconnectResolutions ) . toContainEqual ( {
2045+ generation : 1 ,
2046+ reconciled : true ,
2047+ terminalResponseObserved : false ,
2048+ } ) ;
2049+ const terminal = subscription . events . findLast (
2050+ ( event ) => event . turnId === result . turnId && event . type === "turn.completed" ,
2051+ ) ;
2052+ expect ( terminal ) . toMatchObject ( { payload : { state : "failed" } } ) ;
2053+ expect ( terminal ?. payload ) . not . toHaveProperty ( "usage" ) ;
2054+ expect ( terminal ?. payload ) . not . toHaveProperty ( "totalCostUsd" ) ;
2055+ } ) ,
2056+ ) . pipe ( Effect . provide ( testLayer ) ) ,
2057+ ) ;
2058+
19342059 it . effect ( "keeps control mismatch busy and scopes pre-delivery cancellation" , ( ) =>
19352060 Effect . scoped (
19362061 Effect . gen ( function * ( ) {
0 commit comments