@@ -120,6 +120,22 @@ export function agentRebuildFailure(err: unknown): Error {
120120 : new Error ( String ( err ) ) ;
121121}
122122
123+ /**
124+ * Hard-stop interrupt: bump delivery generation, then enqueue the agent rebuild.
125+ * Overlay stays open across interrupt; the bump must happen before enqueue so a
126+ * later accept/decline cannot late-bind into the rebuilt agent.
127+ */
128+ export function startInterruptRebuild ( args : {
129+ deliveryGeneration : { bump : ( ) => void } ;
130+ markSendAborted : ( ) => void ;
131+ enqueue : ( op : ( ) => Promise < void > ) => unknown ;
132+ rebuild : ( ) => Promise < void > ;
133+ } ) : void {
134+ args . deliveryGeneration . bump ( ) ;
135+ args . markSendAborted ( ) ;
136+ void args . enqueue ( args . rebuild ) ;
137+ }
138+
123139export function clearsActiveRun ( kind : SnapshotKind ) : boolean {
124140 return kind === "run-end" ;
125141}
@@ -401,36 +417,39 @@ export async function createRunLifecycle(
401417 // Close it, drain the old stream, and rebuild a fresh agent so the next send
402418 // works.
403419 const interrupt = ( ) : void => {
404- // Overlay stays open across interrupt; bump so a later accept/decline
405- // cannot late-bind into the rebuilt agent.
406- services . deliveryGeneration . bump ( ) ;
407- state . sendAborted = true ;
408- void enqueueOp ( async ( ) => {
409- try {
410- // close() tears down stream consumers before the aborted cycle's
411- // inference.error is delivered, so the recorder never sees a terminal
412- // event for the dead cycle — dispose closes it against stray deltas
413- // and salvages the buffer before that teardown, so it is never lost
414- // or misattributed to the rebuilt agent's next cycle.
415- await services . cycleRecorder . dispose ( "interrupted" ) ;
416- const closedCleanly = await closeAgentForRebuild ( liveAgent ( state ) , "interrupt" ) ;
417- await state . streamPromise ?. catch ( ( err : unknown ) => {
418- tuiLogger . debug ( "stream drain during interrupt teardown failed: {error}" , {
419- error : err instanceof Error ? err . message : String ( err ) ,
420+ startInterruptRebuild ( {
421+ deliveryGeneration : services . deliveryGeneration ,
422+ markSendAborted : ( ) => {
423+ state . sendAborted = true ;
424+ } ,
425+ enqueue : enqueueOp ,
426+ rebuild : async ( ) => {
427+ try {
428+ // close() tears down stream consumers before the aborted cycle's
429+ // inference.error is delivered, so the recorder never sees a terminal
430+ // event for the dead cycle — dispose closes it against stray deltas
431+ // and salvages the buffer before that teardown, so it is never lost
432+ // or misattributed to the rebuilt agent's next cycle.
433+ await services . cycleRecorder . dispose ( "interrupted" ) ;
434+ const closedCleanly = await closeAgentForRebuild ( liveAgent ( state ) , "interrupt" ) ;
435+ await state . streamPromise ?. catch ( ( err : unknown ) => {
436+ tuiLogger . debug ( "stream drain during interrupt teardown failed: {error}" , {
437+ error : err instanceof Error ? err . message : String ( err ) ,
438+ } ) ;
420439 } ) ;
421- } ) ;
422- if ( ! closedCleanly ) {
423- throw new AgentContextLockError ( state . workdir ) ;
440+ if ( ! closedCleanly ) {
441+ throw new AgentContextLockError ( state . workdir ) ;
442+ }
443+ state . currentAgent = await services . buildAgent ( ) ;
444+ services . cycleRecorder . reset ( ) ;
445+ state . streamPromise = consumeStream ( liveAgent ( state ) . stream ( ) , streamSink ) ;
446+ services . workflowController . reattach ( ) ;
447+ state . fatalBuildError = null ;
448+ } catch ( err ) {
449+ recordRunError ( state , err ) ;
450+ state . fatalBuildError = agentRebuildFailure ( err ) ;
424451 }
425- state . currentAgent = await services . buildAgent ( ) ;
426- services . cycleRecorder . reset ( ) ;
427- state . streamPromise = consumeStream ( liveAgent ( state ) . stream ( ) , streamSink ) ;
428- services . workflowController . reattach ( ) ;
429- state . fatalBuildError = null ;
430- } catch ( err ) {
431- recordRunError ( state , err ) ;
432- state . fatalBuildError = agentRebuildFailure ( err ) ;
433- }
452+ } ,
434453 } ) ;
435454 } ;
436455 state . interrupt = interrupt ;
0 commit comments