@@ -403,6 +403,13 @@ export interface AcpAdapterV2Flavor {
403403 readonly assertComplete ?: Effect . Effect < void , EffectAcpErrors . AcpError > ;
404404 /** Interrupt the local prompt fiber before `session/cancel` (Grok wedged prompts). */
405405 readonly interruptPromptOnCancel ?: boolean ;
406+ /**
407+ * Hold `session/cancel` until the agent sends its first prompt-scoped
408+ * `session/update`, or skip it when the prompt settles first. Kiro 2.27
409+ * drops a cancel that arrives before it has started the prompt and runs the
410+ * turn to completion.
411+ */
412+ readonly cancelAfterPromptStarts ?: boolean ;
406413 /**
407414 * Kill and respawn the ACP child process before the next `session/prompt` after a
408415 * user interrupt. Grok can keep `task_already_running` state until the process exits.
@@ -1228,6 +1235,10 @@ interface ActiveAcpTurn {
12281235 * complete this; settled-soft classification ORs it with `promptSettled`.
12291236 */
12301237 readonly promptWireSettled : Deferred . Deferred < void , never > ;
1238+ /** Completed once `session/prompt` is the runtime's active prompt. */
1239+ readonly promptDispatched : Deferred . Deferred < void , never > ;
1240+ /** Completed by the first prompt-scoped `session/update` after dispatch. */
1241+ readonly promptStarted : Deferred . Deferred < void , never > ;
12311242 backgroundFinalizeGeneration : number ;
12321243}
12331244
@@ -5431,10 +5442,27 @@ export function makeAcpAdapterV2(
54315442 }
54325443 } ) ;
54335444
5445+ // Session-scoped updates (late setup echoes) do not show that the
5446+ // agent has started the prompt.
5447+ const markPromptStarted = Effect . fnUntraced ( function * (
5448+ notification : EffectAcpSchema . SessionNotification ,
5449+ ) {
5450+ const context = yield * Ref . get ( activeTurn ) ;
5451+ if ( context === null || context . nativeThreadId !== notification . sessionId ) return ;
5452+ if ( ! ( yield * Deferred . isDone ( context . promptDispatched ) ) ) return ;
5453+ switch ( notification . update . sessionUpdate ) {
5454+ case "available_commands_update" :
5455+ case "config_option_update" :
5456+ case "current_mode_update" :
5457+ return ;
5458+ }
5459+ yield * Deferred . succeed ( context . promptStarted , undefined ) ;
5460+ } ) ;
54345461 const projectAcpRuntimeSessionUpdateEffect = (
54355462 rawNotification : EffectAcpSchema . SessionNotification ,
54365463 ) =>
54375464 Effect . gen ( function * ( ) {
5465+ yield * markPromptStarted ( rawNotification ) ;
54385466 if ( clientTerminals !== undefined ) {
54395467 const embedded = embeddedTerminalIdsFromSessionUpdate ( rawNotification ) ;
54405468 if ( embedded !== undefined ) {
@@ -6536,6 +6564,19 @@ export function makeAcpAdapterV2(
65366564 }
65376565 } ) ;
65386566
6567+ /**
6568+ * `cancelAfterPromptStarts`: waits until the agent has started the
6569+ * prompt. False when the prompt settled first, so there is nothing to
6570+ * cancel. A hold that outlives the interrupt timeout cancels anyway.
6571+ */
6572+ const promptStillRunsAfterHold = Effect . fnUntraced ( function * ( context : ActiveAcpTurn ) {
6573+ if ( flavor . cancelAfterPromptStarts !== true ) return true ;
6574+ const started = yield * Effect . raceFirst (
6575+ Deferred . await ( context . promptStarted ) . pipe ( Effect . as ( true ) ) ,
6576+ Deferred . await ( context . completed ) . pipe ( Effect . as ( false ) ) ,
6577+ ) . pipe ( Effect . timeoutOption ( "10 seconds" ) ) ;
6578+ return Option . getOrElse ( started , ( ) => true ) ;
6579+ } ) ;
65396580 const quarantineStoppedRun = Effect . fnUntraced ( function * ( ) {
65406581 yield * continuationPermit . withPermit (
65416582 Effect . gen ( function * ( ) {
@@ -6962,6 +7003,8 @@ export function makeAcpAdapterV2(
69627003 const providerTurnId = deriveProviderTurnId ( nativeTurnId ) ;
69637004 const completed = yield * Deferred . make < void , never > ( ) ;
69647005 const promptWireSettled = yield * Deferred . make < void , never > ( ) ;
7006+ const promptDispatched = yield * Deferred . make < void , never > ( ) ;
7007+ const promptStarted = yield * Deferred . make < void , never > ( ) ;
69657008 const rememberedContextUsage = ( yield * Ref . get ( contextUsageBySessionId ) ) . get (
69667009 requestedSessionId ,
69677010 ) ;
@@ -7010,6 +7053,8 @@ export function makeAcpAdapterV2(
70107053 promptSettled : false ,
70117054 promptSettledStatus : null ,
70127055 promptWireSettled,
7056+ promptDispatched,
7057+ promptStarted,
70137058 backgroundFinalizeGeneration : 0 ,
70147059 } ;
70157060 const carryover = yield * Ref . getAndSet ( carryoverSubagents , null ) ;
@@ -7140,7 +7185,8 @@ export function makeAcpAdapterV2(
71407185 return ;
71417186 }
71427187 const promptGeneration = yield * Ref . get ( runtimeCallbackGeneration ) ;
7143- yield * runtime . prompt ( { prompt : promptParts ! . prompt } ) . pipe (
7188+ const promptOptions = { dispatched : context . promptDispatched } ;
7189+ yield * runtime . prompt ( { prompt : promptParts ! . prompt } , promptOptions ) . pipe (
71447190 Effect . tap ( ( ) =>
71457191 Ref . update ( promptInstructionStates , ( current ) => {
71467192 if ( promptParts ?. instructionState === undefined ) return current ;
@@ -7692,7 +7738,7 @@ export function makeAcpAdapterV2(
76927738 // acknowledge and would only threaten still-running
76937739 // background subagents. Skip it and leave the runtime
76947740 // untouched for the replacement turn.
7695- if ( ! settledSoftInterrupt ) {
7741+ if ( ! settledSoftInterrupt && ( yield * promptStillRunsAfterHold ( context ) ) ) {
76967742 yield * runtime . cancel ;
76977743 }
76987744 if ( restartRuntime && flavor . restartRuntimeAfterInterrupt === true ) {
0 commit comments