Tie active-run liveness to a single write instead of two independent ones - #404
Merged
TheGreatAxios merged 10 commits intoAug 9, 2026
Conversation
TheGreatAxios
force-pushed
the
cl-5675-runstatehandleactive-in-memory-and-runstatestatus-on-disk
branch
from
August 8, 2026 20:05
1771cde to
c435140
Compare
RunStateHandle carried its own active boolean alongside RunState.status on disk, set at two separate call sites in src/tui/runner.ts (finalizeOnCrash and the normal completion path). Both sites happened to null the handle immediately after flipping the flag, so the boolean was always true whenever the handle existed, making it a redundant copy of the same fact rather than independent state -- deleted in favor of "the handle is null" as the sole liveness signal (src/session/active-run.ts). Terminal RunState writes now go through finalizeRunState (src/session/state.ts), which persists the record and clears the active-run handle in one call instead of leaving each terminal call site to remember both.
Routing every non-"running" run.json write through finalizeRunState made the terminal status itself the signal that the run had ended, and so cleared the active-run handle. Session rotation (/clear, /new) breaks that equivalence: it persists a terminal "done" for the outgoing session while the process keeps running under a fresh session id. Clearing liveness there left getActiveRun() null for the rest of the process, so a crash after the first rotation never wrote a terminal record and the session read as "running" forever. Why a write happens is now explicit (SnapshotKind: progress, session-rotation, run-end) instead of inferred from what it writes. Only run-end clears the handle; persistRunSnapshot cannot request it, since everything routed there happens while the process is still alive. Also drops the deleted `active` field from the crash fixture, which sits outside tsconfig's include and so escaped typecheck.
The existing rotation tests covered clearsActiveRun as a pure function and confirmed a handle survives a rotation write, but nothing exercised a crash actually occurring afterward. Extend the crash fixture to optionally rotate the session first, through the same clearsActiveRun dispatch runner.ts uses, then crash and assert the crashed record lands under the post-rotation session.
The prior comment only noted that finalizeRunState's own clear becomes a no-op repeat, which reads as an argument for deleting the early call rather than keeping it. It is not redundant: index.ts installs its own crash listeners that read the handle directly and would otherwise race a competing write during the awaits here.
Document that this is a second terminal write path alongside finalizeRunState, kept apart because its only callers are the process-level crash and signal handlers in index.ts, which cannot afford to wait on saveState's per-session write chain during exit.
TheGreatAxios
force-pushed
the
cl-5675-runstatehandleactive-in-memory-and-runstatestatus-on-disk
branch
from
August 8, 2026 21:43
c435140 to
3084acc
Compare
finalizeRunState cleared the handle only after its saveState await resolved, so a signal or uncaught exception landing during that write still saw a live run and raced a competing "crashed" write against the terminal write in progress. That reopened the crash-after-rotation race on every ordinary run-end write, not just the crash path's own. Clearing before the await closes it: by the time anything else can observe the handle, it already reads as gone.
Deleted along with the standalone active-run clear when that call site was folded into finalizeRunState's write, but the dispose host has no on-disk write to piggyback on, so it never got a replacement. Every normal run was leaving the dispose host pointing at a torn-down closure that a later signal could still invoke. Restored to mirror finalizeOnCrash, matching active-host.ts's stated contract of clearing on either path.
RunStateHandle dropped its own active flag in favor of handle presence as the sole liveness signal, but this fixture still set it. tsconfig excludes tests/ from typecheck, so nothing caught the stale field and the setter silently ignored the excess property.
The existing crash-after-rotation test only covers a crash arriving before any terminal write starts, so it stayed green even with the run-end handle-clearing regression present. This test parks the run-end write mid-flight and fires an unrelated uncaughtException during that window, asserting the outcome is never "crashed" — pinning the fix that moved finalizeRunState's clear ahead of its own write.
saveCrashState requires markCrashed() first so chained saveState renames cannot clobber the terminal write. handleFatal already did this for uncaughtException; SIGINT/SIGTERM/SIGHUP called saveCrashState without the fence. Flip the flag before the signal write, and park stragglers in the signal fixture so the integration test pins it.
Collaborator
Author
|
Merged. Signal-path finalize now mirrors the crash path: on SIGTERM/SIGINT/SIGHUP the process marks the active run crashed before exit, and the signal fixture gates straggler writes so a late write cannot last-write-win over terminal
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
active: booleanfield fromRunStateHandle(src/session/active-run.ts:12) -- both write sites cleared the handle immediately after setting it false, so the boolean never disagreed with "handle is present"; it was a copy of the same fact, not independent state.finalizeRunState(src/session/state.ts) as the single write path for a terminalRunState: it persists the record and clears the active-run handle in one call, used by both terminal call sites in src/tui/runner.ts (the crash-finalize path and the normal completion path) instead of each remembering to update both facts separately.Test plan
finalizeRunStatewrite leaves both the persisted status and the active-run handle in agreementbun run typecheckbun run buildbun run test(4172 pass, 0 fail)