@@ -909,7 +909,11 @@ describe("AcpAdapterV2", () => {
909909 flavor : {
910910 driver : ACP_TEST_DRIVER ,
911911 capabilities : AcpProviderCapabilitiesV2 ,
912- makeRuntime : makeMockRuntime ( { childProcessSpawner, mockAgentPath, protocolEvents } ) ,
912+ makeRuntime : makeMockRuntime ( {
913+ childProcessSpawner,
914+ mockAgentPath,
915+ protocolEvents,
916+ } ) ,
913917 } ,
914918 selfInvocation,
915919 } ) ;
@@ -2406,6 +2410,99 @@ describe("AcpAdapterV2", () => {
24062410 } ) . pipe ( Effect . provide ( layerTest ) , Effect . scoped ) ,
24072411 ) ;
24082412
2413+ it . live ( "creates a fresh ACP identity when recovering an exhausted target" , ( ) =>
2414+ Effect . gen ( function * ( ) {
2415+ const childProcessSpawner = yield * ChildProcessSpawner . ChildProcessSpawner ;
2416+ const path = yield * Path . Path ;
2417+ const selfInvocation = yield * resolveSelfInvocation ( ) ;
2418+ const mockAgentPath = yield * path . fromFileUrl (
2419+ new URL ( "../../../scripts/acp-mock-agent.ts" , import . meta. url ) ,
2420+ ) ;
2421+ const protocolEvents = yield * Queue . unbounded < EffectAcpProtocol . AcpProtocolLogEvent > ( ) ;
2422+ const instanceId = ProviderInstanceId . make ( "acp-test-exhausted-target" ) ;
2423+ const adapter = yield * makeAcpAdapterV2 ( {
2424+ instanceId,
2425+ flavor : {
2426+ driver : ACP_TEST_DRIVER ,
2427+ capabilities : AcpProviderCapabilitiesV2 ,
2428+ makeRuntime : makeMockRuntime ( {
2429+ childProcessSpawner,
2430+ mockAgentPath,
2431+ protocolEvents,
2432+ wrapRuntime : ( runtime , ordinal ) => ( {
2433+ ...runtime ,
2434+ start : ( ) =>
2435+ runtime . start ( ) . pipe (
2436+ Effect . map ( ( started ) => ( {
2437+ ...started ,
2438+ sessionId : `mock-recovery-session-${ ordinal } ` ,
2439+ } ) ) ,
2440+ ) ,
2441+ } ) ,
2442+ } ) ,
2443+ } ,
2444+ selfInvocation,
2445+ } ) ;
2446+ const threadId = ThreadId . make ( "thread-acp-exhausted-target" ) ;
2447+ const runtimePolicy = ProviderAdapter . ProviderAdapterV2RuntimePolicy . make ( {
2448+ runtimeMode : "full-access" ,
2449+ interactionMode : "default" ,
2450+ cwd : process . cwd ( ) ,
2451+ } ) ;
2452+ const modelSelection = { instanceId, model : "default" } as const ;
2453+ const runtime = yield * adapter . openSession ( {
2454+ threadId,
2455+ providerSessionId : ProviderSessionId . make ( "provider-session-acp-exhausted-target" ) ,
2456+ modelSelection,
2457+ runtimePolicy,
2458+ } ) ;
2459+ const original = yield * runtime . ensureThread ( { threadId, modelSelection, runtimePolicy } ) ;
2460+ yield * runtime . startTurn (
2461+ makeTurnInput ( {
2462+ threadId,
2463+ providerThread : original ,
2464+ instanceId,
2465+ runtimePolicy,
2466+ now : yield * DateTime . now ,
2467+ ordinal : 1 ,
2468+ } ) ,
2469+ ) ;
2470+ yield * runtime . events . pipe (
2471+ Stream . takeUntil ( ( event ) => event . type === "turn.terminal" ) ,
2472+ Stream . runDrain ,
2473+ ) ;
2474+ const replacement = yield * runtime . ensureThread ( {
2475+ threadId,
2476+ modelSelection,
2477+ runtimePolicy,
2478+ existingProviderThread : { ...original , nativeThreadRef : null } ,
2479+ forceNewNativeThread : true ,
2480+ } ) ;
2481+ assert . notEqual ( replacement . nativeThreadRef ?. nativeId , original . nativeThreadRef ?. nativeId ) ;
2482+ assert . equal ( replacement . id , original . id ) ;
2483+ assert . equal ( replacement . nativeMetadata ?. itemIdentityVersion , 2 ) ;
2484+ yield * runtime . startTurn (
2485+ makeTurnInput ( {
2486+ threadId,
2487+ providerThread : replacement ,
2488+ instanceId,
2489+ runtimePolicy,
2490+ now : yield * DateTime . now ,
2491+ ordinal : 2 ,
2492+ } ) ,
2493+ ) ;
2494+ const events = yield * runtime . events . pipe (
2495+ Stream . takeUntil ( ( event ) => event . type === "turn.terminal" ) ,
2496+ Stream . runCollect ,
2497+ ) ;
2498+ assert . isTrue (
2499+ events . some ( ( event ) => event . type === "turn.terminal" && event . status === "completed" ) ,
2500+ ) ;
2501+ const methods = yield * pollProtocolMethods ( protocolEvents ) ;
2502+ assert . equal ( methods . filter ( ( method ) => method === "session/new" ) . length , 2 ) ;
2503+ } ) . pipe ( Effect . provide ( layerTest ) , Effect . scoped ) ,
2504+ ) ;
2505+
24092506 it . live ( "preserves new-session fallback when an eager ACP session load is stale" , ( ) =>
24102507 Effect . gen ( function * ( ) {
24112508 const childProcessSpawner = yield * ChildProcessSpawner . ChildProcessSpawner ;
@@ -5240,6 +5337,19 @@ describe("AcpAdapterV2", () => {
52405337 yield * hasPendingBackgroundWork ,
52415338 "carryover live subagent must pin hasPendingBackgroundWork after root settle" ,
52425339 ) ;
5340+ const refusedReplacement = yield * runtime
5341+ . ensureThread ( {
5342+ threadId,
5343+ modelSelection,
5344+ runtimePolicy,
5345+ existingProviderThread : { ...providerThread , nativeThreadRef : null } ,
5346+ forceNewNativeThread : true ,
5347+ } )
5348+ . pipe ( Effect . flip ) ;
5349+ assert . equal ( refusedReplacement . _tag , "ProviderAdapterEnsureThreadError" ) ;
5350+ const preserved = yield * runtime . ensureThread ( { threadId, modelSelection, runtimePolicy } ) ;
5351+ assert . equal ( preserved . nativeThreadRef ?. nativeId , providerThread . nativeThreadRef ?. nativeId ) ;
5352+ assert . isTrue ( yield * hasPendingBackgroundWork ) ;
52435353 assert . isDefined ( sessionUpdateHandler , "session update handler must be wired" ) ;
52445354
52455355 subagentPhase = "complete" ;
0 commit comments