@@ -174,7 +174,7 @@ import {
174174import { createRunSink } from "../session/run-sink.js" ;
175175import { generateSessionId , initSessionDir , renameSession , sessionContextDir , sessionDir } from "../session/index.js" ;
176176import { resolveSessionLabel , truncateSessionLabel } from "../session/session-label.js" ;
177- import { loadState , saveState , type ConnectedMcpServer , type RunState } from "../session/state.js" ;
177+ import { finalizeRunState , loadState , saveState , type ConnectedMcpServer , type RunState } from "../session/state.js" ;
178178import { setActiveRun , clearActiveRun , type RunStateHandle } from "../session/active-run.js" ;
179179import { openInBrowser } from "../auth/oauth/browser.js" ;
180180import { pickSession } from "./pick-session.js" ;
@@ -503,7 +503,6 @@ export async function runTUI(initialConfig: Config): Promise<number> {
503503 const activeRunHandle : RunStateHandle = {
504504 sessionId,
505505 cwd : config . cwd ,
506- active : true ,
507506 task : runTaskTitle . trim ( ) . length > 0 ? runTaskTitle . trim ( ) : "(conversation)" ,
508507 startedAt,
509508 model : `${ config . providerName } :${ config . model } ` ,
@@ -536,7 +535,12 @@ export async function runTUI(initialConfig: Config): Promise<number> {
536535 const finalizeOnCrash = async ( err : unknown ) : Promise < void > => {
537536 if ( finalized ) return ;
538537 finalized = true ;
539- activeRunHandle . active = false ;
538+ // Clear the active-run handle up front, before the awaits below, so a
539+ // second crash mid-flush can't see this run as still live and race the
540+ // finalize write issued here. finalizeRunState (state.ts) would otherwise
541+ // do this itself, but only after saveState resolves — too late for that
542+ // guard, so it's done here and finalizeRunState's own clear becomes a
543+ // no-op repeat of the same fact rather than a second independent write.
540544 clearActiveRun ( ) ;
541545 await flushPartialOnCrash ( ) . catch ( ( flushErr : unknown ) => {
542546 // Best-effort only — still attempt saveState below. Log so a flush
@@ -546,7 +550,7 @@ export async function runTUI(initialConfig: Config): Promise<number> {
546550 process . stderr . write ( `${ COMMAND_NAME } : crash finalize partial flush failed: ${ flushMessage } \n` ) ;
547551 } ) ;
548552 const message = err instanceof Error ? err . message : String ( err ) ;
549- await saveState ( config . cwd , sessionId , {
553+ await finalizeRunState ( config . cwd , sessionId , {
550554 status : "failed" ,
551555 turnsUsed : 0 ,
552556 task : runTaskTitle . trim ( ) . length > 0 ? runTaskTitle . trim ( ) : "(conversation)" ,
@@ -1445,15 +1449,24 @@ export async function runTUI(initialConfig: Config): Promise<number> {
14451449 activeRunHandle . task = task ;
14461450 activeRunHandle . startedAt = startedAt ;
14471451 activeRunHandle . model = model ;
1448- await saveState ( config . cwd , sessionId , {
1452+ const state : RunState = {
14491453 status,
14501454 turnsUsed : runSink . getTurnCount ( ) ,
14511455 task,
14521456 startedAt,
14531457 model,
14541458 mcpServers : connectedMcpServers ,
14551459 ...extra ,
1456- } ) ;
1460+ } ;
1461+ // "running" is the only non-terminal status writeRunSnapshot ever
1462+ // receives (progress snapshots); anything else closes the run out, so
1463+ // the active-run handle is cleared in the same call as the disk write
1464+ // rather than by a separate statement at each terminal call site.
1465+ if ( status === "running" ) {
1466+ await saveState ( config . cwd , sessionId , state ) ;
1467+ } else {
1468+ await finalizeRunState ( config . cwd , sessionId , state ) ;
1469+ }
14571470 } ;
14581471
14591472 // Progress snapshots are fired unsequenced (model switch, MCP connect, turn
@@ -2375,8 +2388,9 @@ export async function runTUI(initialConfig: Config): Promise<number> {
23752388 // finished run (finishedAt set) can be left reading as still in progress.
23762389 const persistedStatus : RunState [ "status" ] = summaryStatus ;
23772390 finalized = true ;
2378- activeRunHandle . active = false ;
2379- clearActiveRun ( ) ;
2391+ // writeRunSnapshot clears the active-run handle itself for a terminal
2392+ // status (via finalizeRunState in state.ts), pairing the on-disk write
2393+ // with the in-memory one instead of setting them at two call sites.
23802394 await writeRunSnapshot ( persistedStatus , {
23812395 finishedAt,
23822396 ...( sinkError !== undefined ? { error : sinkError } : { } ) ,
0 commit comments