@@ -44,7 +44,9 @@ import {
4444import { makeAcpNativeLoggerFactory } from "../acp/AcpNativeLogging.ts" ;
4545import {
4646 makePrimeAgentAcpRuntime ,
47+ parsePrimeAgentAcpTerminalUpdate ,
4748 primeAgentLaunchArgsIssue ,
49+ type PrimeAgentAcpTerminalUpdate ,
4850} from "../acp/PrimeAgentAcpSupport.ts" ;
4951import type { PrimeAgentAdapterShape } from "../Services/PrimeAgentAdapter.ts" ;
5052import { canonicalPrimeToolItemId } from "../prime/PrimeAgentDaemonRuntimeEvents.ts" ;
@@ -75,11 +77,19 @@ export interface PrimeAgentAdapterLiveOptions {
7577 readonly startupWarning ?: string ;
7678}
7779
80+ type PrimeAgentAcpTerminalSettlement =
81+ | { readonly state : "settled" ; readonly outcome : "result" | "error" }
82+ | { readonly state : "invalid" } ;
83+
7884interface PrimeAgentActiveTurn {
7985 readonly id : TurnId ;
8086 readonly cancellation : Deferred . Deferred < void > ;
87+ readonly terminalQuiescence : Deferred . Deferred < PrimeAgentAcpTerminalSettlement > ;
8188 cancellationRequested : boolean ;
8289 hasPublicAssistantTextAfterLatestToolBoundary : boolean ;
90+ nativePromptTurnId : number | undefined ;
91+ lastNativeEventSequence : number | undefined ;
92+ terminalQuiescenceExpected : boolean ;
8393}
8494
8595interface PrimeAgentSessionContext {
@@ -95,6 +105,55 @@ interface PrimeAgentSessionContext {
95105 stopped : boolean ;
96106}
97107
108+ function observePrimeAgentAcpTerminalUpdate (
109+ ctx : PrimeAgentSessionContext | undefined ,
110+ update : PrimeAgentAcpTerminalUpdate ,
111+ ) : Effect . Effect < void > {
112+ const activeTurn = ctx ?. activeTurn ;
113+ if ( activeTurn === undefined ) return Effect . void ;
114+ if ( update . phase === "invalid" ) {
115+ activeTurn . terminalQuiescenceExpected = true ;
116+ return Deferred . succeed ( activeTurn . terminalQuiescence , { state : "invalid" } ) . pipe (
117+ Effect . asVoid ,
118+ ) ;
119+ }
120+ if (
121+ activeTurn . lastNativeEventSequence !== undefined &&
122+ update . eventSequence <= activeTurn . lastNativeEventSequence
123+ ) {
124+ return Effect . void ;
125+ }
126+ activeTurn . lastNativeEventSequence = update . eventSequence ;
127+
128+ if ( update . phase === "responseBoundary" ) {
129+ if ( activeTurn . nativePromptTurnId !== undefined ) {
130+ activeTurn . terminalQuiescenceExpected = true ;
131+ return Deferred . succeed ( activeTurn . terminalQuiescence , { state : "invalid" } ) . pipe (
132+ Effect . asVoid ,
133+ ) ;
134+ }
135+ activeTurn . nativePromptTurnId = update . promptTurnId ;
136+ activeTurn . terminalQuiescenceExpected = update . terminalQuiescenceExpected ;
137+ return Effect . void ;
138+ }
139+
140+ if (
141+ activeTurn . nativePromptTurnId === undefined ||
142+ activeTurn . nativePromptTurnId !== update . promptTurnId ||
143+ ! activeTurn . terminalQuiescenceExpected
144+ ) {
145+ activeTurn . terminalQuiescenceExpected = true ;
146+ return Deferred . succeed ( activeTurn . terminalQuiescence , { state : "invalid" } ) . pipe (
147+ Effect . asVoid ,
148+ ) ;
149+ }
150+ activeTurn . terminalQuiescenceExpected = true ;
151+ return Deferred . succeed ( activeTurn . terminalQuiescence , {
152+ state : "settled" ,
153+ outcome : update . outcome ,
154+ } ) . pipe ( Effect . asVoid ) ;
155+ }
156+
98157export function parsePrimeAgentResumeMarker ( raw : unknown ) : boolean {
99158 return isPrimeAgentCompatibleResumeCursor ( raw ) ;
100159}
@@ -449,6 +508,7 @@ export function makePrimeAgentAdapter(
449508 ) ;
450509
451510 const sessionScope = yield * Scope . make ( "sequential" ) ;
511+ let sessionContext : PrimeAgentSessionContext | undefined ;
452512 let sessionScopeTransferred = false ;
453513 yield * Effect . addFinalizer ( ( ) =>
454514 sessionScopeTransferred ? Effect . void : Scope . close ( sessionScope , Exit . void ) ,
@@ -467,6 +527,12 @@ export function makePrimeAgentAdapter(
467527 continueSession : parsePrimeAgentResumeMarker ( input . resumeCursor ) ,
468528 model,
469529 clientInfo : { name : "pylon" , version : "0.0.0" } ,
530+ observeSessionUpdate : ( notification ) => {
531+ const update = parsePrimeAgentAcpTerminalUpdate ( notification ) ;
532+ return update === undefined
533+ ? Effect . void
534+ : observePrimeAgentAcpTerminalUpdate ( sessionContext , update ) ;
535+ } ,
470536 ...acpNativeLoggers ,
471537 } ) . pipe (
472538 Effect . provideService ( Crypto . Crypto , crypto ) ,
@@ -524,6 +590,7 @@ export function makePrimeAgentAdapter(
524590 stopRequested : false ,
525591 stopped : false ,
526592 } ;
593+ sessionContext = ctx ;
527594
528595 const notificationFiber = yield * Stream . runDrain (
529596 Stream . mapEffect ( acp . getEvents ( ) , ( event ) =>
@@ -777,8 +844,12 @@ export function makePrimeAgentAdapter(
777844 const activeTurn : PrimeAgentActiveTurn = {
778845 id : turnId ,
779846 cancellation : yield * Deferred . make < void > ( ) ,
847+ terminalQuiescence : yield * Deferred . make < PrimeAgentAcpTerminalSettlement > ( ) ,
780848 cancellationRequested : false ,
781849 hasPublicAssistantTextAfterLatestToolBoundary : false ,
850+ nativePromptTurnId : undefined ,
851+ lastNativeEventSequence : undefined ,
852+ terminalQuiescenceExpected : false ,
782853 } ;
783854 ctx . activeTurn = activeTurn ;
784855 ctx . lastPlanFingerprint = undefined ;
@@ -802,7 +873,7 @@ export function makePrimeAgentAdapter(
802873 turnId,
803874 payload : { model : ctx . session . model ?? "default" } ,
804875 } ) ;
805- const result = yield * Effect . raceFirst (
876+ const promptExit = yield * Effect . raceFirst (
806877 ctx . acp . prompt ( { prompt } ) ,
807878 Deferred . await ( activeTurn . cancellation ) . pipe (
808879 Effect . as ( { stopReason : "cancelled" as const } ) ,
@@ -811,13 +882,52 @@ export function makePrimeAgentAdapter(
811882 Effect . mapError ( ( error ) =>
812883 mapAcpToAdapterError ( PROVIDER , input . threadId , "session/prompt" , error ) ,
813884 ) ,
885+ Effect . exit ,
814886 ) ;
887+ // Prime Agent 0.8 publishes a response boundary before the ACP response and
888+ // an authoritative terminal-quiescence envelope after descendant work settles.
889+ // Older releases publish neither, so their prompt response remains terminal.
890+ // A stopped session has already shut down its notification consumer, so it
891+ // must not enqueue a barrier that can no longer be acknowledged.
892+ const promptCancelled =
893+ Exit . isSuccess ( promptExit ) && promptExit . value . stopReason === "cancelled" ;
894+ if ( ! promptCancelled && ! activeTurn . cancellationRequested && ! ctx . stopRequested ) {
895+ yield * ctx . acp . drainEvents ;
896+ }
897+ const terminal =
898+ activeTurn . terminalQuiescenceExpected && ! promptCancelled
899+ ? yield * Effect . raceFirst (
900+ Deferred . await ( activeTurn . terminalQuiescence ) ,
901+ Deferred . await ( activeTurn . cancellation ) . pipe (
902+ Effect . as ( { state : "cancelled" as const } ) ,
903+ ) ,
904+ )
905+ : undefined ;
906+ if ( terminal ?. state === "invalid" ) {
907+ return yield * new ProviderAdapterRequestError ( {
908+ provider : PROVIDER ,
909+ method : "session/prompt" ,
910+ detail : "Prime Agent returned invalid terminal-quiescence metadata." ,
911+ } ) ;
912+ }
913+ if ( Exit . isFailure ( promptExit ) && terminal ?. state !== "cancelled" ) {
914+ return yield * Effect . failCause ( promptExit . cause ) ;
915+ }
916+ const result = Exit . isSuccess ( promptExit )
917+ ? promptExit . value
918+ : ( { stopReason : "cancelled" } as const ) ;
815919 const settled = yield * settleActiveTurn (
816920 ctx ,
817921 turnId ,
818- result . stopReason === "cancelled"
819- ? { state : "cancelled" }
820- : { state : "completed" , stopReason : result . stopReason ?? null } ,
922+ terminal ?. state === "settled" && terminal . outcome === "error"
923+ ? {
924+ state : "failed" ,
925+ errorMessage : PRIME_AGENT_TURN_FAILED ,
926+ terminalFailure : true ,
927+ }
928+ : terminal ?. state === "cancelled" || result . stopReason === "cancelled"
929+ ? { state : "cancelled" }
930+ : { state : "completed" , stopReason : result . stopReason ?? null } ,
821931 true ,
822932 ) ;
823933 if ( ! settled && ! activeTurn . cancellationRequested && ! ctx . stopRequested ) {
0 commit comments