Close the two finished-session races the E2E stories surfaced - #1528
Merged
Conversation
Both product findings from #1527 are now fixes, and the stories that found them act inside the old race windows on purpose, as regression tests. 1) Acting on a session the moment its meta flips done raced teardown. The child writes `status: done` and exits; teardown then archives the history, commits the bookkeeping to the run branch, and retires the worktree. A Push/Open-PR/Remove/Delete/Resume fired inside that window ran its own git against the same checkout: the loser reported "could not commit the work this session left uncommitted", and a teardown that lost stranded a worktree it should have removed. Every mutator of one run's checkout now serializes on a per-run lock (run-locks.ts) — teardown, the handoff RPCs' commit step, the worktree removal RPCs, and continuation reuse. Both actors live in the daemon process by design, so the in-process lock is the whole fix. 2) A live event tail whose journal was retired went silent without the final events. Teardown copies events.jsonl verbatim into the archive and removes the worktree; when fs.watch missed the last appends, the poll backstop found the file gone and delivered nothing again — including the run's `end`. tailRunEvents (events-tail.ts) re-resolves the journal's home when the tailed file disappears and retargets the same tailer, carrying its read offset, so exactly the missed lines arrive, once. The first pull after a retarget adopts the copy's mtime so the #567 same-length-rewrite check cannot misread the move as a truncation and replay the feed. Wired into the dashboard's onEvents (with a guard against relocating onto the project-root journal, which after a Delete is another run's feed), the device relay tail, and the E2E harness — which drops its private archive-swap workaround for the product seam. The publish and resume stories now fire at the instant the run flips done; the harness's close() drains in-flight teardowns by acquiring each run's lock before deleting the fixture repos, which is what the stranded-worktree noise in stress runs turned out to be. Verified: full suite 1713 tests green; the two race stories plus every other story file stressed 6-15x each with zero failures and zero stranded-worktree warnings. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XKihCTfcazhDgt4dhUmfM9
brillout
pushed a commit
that referenced
this pull request
Aug 10, 2026
sdd.md scopes spec.md files to business logic and explicitly excludes test code, and specs are plain-language product docs — the e2e spec.md was both misplaced and written as technical harness documentation. - src/e2e/spec.md becomes src/e2e/README.md (developer doc, where telefunction names, env vars, and file references belong); the five source-comment references follow the rename. - The two product behaviors #1528 shipped move to the specs that own them, in spec language: acting on a session the instant it finishes is safe (product spec, teardown) and a live stream follows the log into the archive without dropping the ending (dashboard-rpc spec). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XKihCTfcazhDgt4dhUmfM9
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.
What
The two product findings documented in #1527 (
src/e2e/spec.md) are now fixes, and the stories that found them act inside the old race windows on purpose — they are the regression tests.1. Acting on a session the moment it flips
doneno longer races teardownThe child writes
status: doneand exits; the daemon's teardown then archives the history, commits the bookkeeping to the run branch, and retires the worktree. A Push / Open-PR / Remove / Delete / Resume fired inside that window ran its own git against the same checkout: the loser reported "could not commit the work this session left uncommitted", and a teardown that lost stranded a worktree it should have removed.Fix: every mutator of one run's checkout serializes on a per-run lock (
src/run-locks.ts, keyed by resolved checkout path):tearDownWorktreeandcontinueWorkspace(daemon-runtime),sendPushBranch/sendOpenPullRequest,sendRemoveWorktree/sendDeleteSession.Both actors live in the daemon process by design (RPCs are served in-process; only the daemon writes to the project checkout), so an in-process lock is the whole fix. Whichever side wins commits the entire pending state (
add -A); the other finds a clean tree — or no checkout, whichcommitSessionWorkalready reads as "the branch is authoritative".2. The live event feed follows the journal into the archive
Teardown copies
events.jsonlverbatim into the archive and removes the worktree. When fs.watch missed the final appends, the 1s poll found the file gone and the tail never delivered anything again — including theendthe transcript needs.Fix:
tailRunEvents(events-tail.ts) re-resolves the journal's home when the tailed file disappears after having existed, and retargets the same tailer, carrying its read offset — the copy is content-identical, so exactly the missed lines arrive, once, with no replay. Details that matter:JsonlTailer.retargetadopts the new home's mtime on its first pull, so the The dashboard's event tail silently drops a same-length rewrite #567 same-length-rewrite check can't misread the move as a truncation and replay the whole feed;onEventsrefuses to relocate onto the project-root journal (after a Delete that's all the resolver has left, and it is another run's feed, Run-scoped events subscribe for an ended run tails the project ROOT journal #1472) while keeping the permissive initial attach a non-git fallback run needs;onEvents, the device relay tail (tailRelayEvents), and the E2E harness — which drops its private archive-swap workaround for the product seam.Test changes
doneand asserts the push succeeds and teardown still retires the worktree; the resume story fires its continuation atdonethe same way.close()drains in-flight teardowns by acquiring each run's lock before deleting fixture repos — the stranded-worktree noise in stress runs turned out to be the harness deleting repos under mid-flight teardowns, not a product fault.withRunLock(ordering, isolation, failure hygiene, key normalization), 3 fortailRunEvents(missed lines exactly once across the move; no replay of a fully-consumed journal; idling while the resolver has no better answer).Verification
dist/shipsrun-locks.js;src/e2estays excluded from the publish build. Changeset included (patch).Generated by Claude Code