Repository navigation
fix(update): fan-out downloads get full bytes; stuck updates explain themselves - #435
Merged
Merged
Conversation
…themselves The lazy-release fetch shared one in-flight promise whose single stream was already spent for every joiner but the first, so "Update all" handed all but one node an empty download. fetchArtifact now resolves only once the bytes are complete and verified on disk, hands out a handle that opens its own reader per call, and a digest mismatch fails the fetch before any byte is served. The not-supervised refusal composes from the runtime report (live or held), naming whether a definition exists and what a launchd GUI-domain refusal means. On the node, kickstart exit 125 carries the signed-in-at-the-display hint, and a refused restart after the swap reports the installed binary, the manager's reason, and the two ways forward.
…ownload temps, honest docs
A failed fetch no longer drags its joiners down with it: a waiter that
inherits a rejection retries once onto whatever newer attempt is running
(or starts one), so one poisoned digest poisons one caller, not the
fan-out. downloadVerified names its temp per download (pid plus a uuid),
which is what two parallel cache:false fetches of one stale-shelf target
need to stop interleaving writers and cross-deleting each other, and
drainedFile opens its reader on first pull so a materialization that
throws still sweeps the temp instead of leaking it.
security.md still told the pre-fix story ("streamed through and hashed on
the way past") and cited line anchors that had moved; both now describe
the shape that shipped. Two regression tests cover the new behavior: a
joiner's retry refetches, and side-by-side cache:false fetches both drain
full bytes and leave the operator's file and no .download- residue.
…comment anchors, the GUI hint narrows Round 2 of the fan-out review found no MAJOR: the comments were the only survivors. downloads.route.ts's cache:false note, node-artifact-serve's rule 3 and broken-pair note, the shelf-serve test, and one fetchArtifact note still described bytes streaming through without landing on disk; they now say what happens, which is a uniquely-named temp copy the response removes. The response-body docblock moves off fileReader onto drainedFile, the function it describes, and the joiner comment stops overstating the pre-fix harm: without the retry, it was the downloads already coalesced onto the failed attempt that died, not every download for the TTL. The launchctl kickstart hint keys on the exact sentence its story is true of (Domain does not support specified action) instead of the bare word domain, so a find-service-in-domain failure keeps its status pointer; a new near-miss test pins the split.
…eam word One test comment still said the operator's bytes are streamed PAST, and the air-gapped arm said a refusal beats streaming to a node. The first names a mechanism the fan-out fix removed (those bytes now serve from a temp copy); the second never had a stream to name (that arm fetches nothing). Comments only.
…nchors go, vacuous temp assertions made honest downloadVerified's temp name is unique per attempt now, which is what the fan-out needs and what a SIGKILL turns into a new orphan every crash: no in-process path can clean what the kill interrupted, so the disk was one crash-loop away from accumulating artifact-sized strays with no reclaim path. Each download now sweeps matching strays older than an hour before it writes (the mtime of a live download stays fresh, so the age bound keeps a concurrent fetch's copy out of reach), and a new test plants an old stray beside a young one and expects only the corpse to go. Two comment anchors still described the retired serve-through shape in the present tense (the ledger docblock, the uncoalesced arm's label); they now match what the code does. Four assertions in the downloadVerified suite hunted the old pid-only temp name, which nothing writes anymore, and so could no longer fail; they scan for prefix strays instead, which is the honest form. The digest-failure phrase is a named constant so the artifact-log branch cannot silently drift from what downloadVerified throws, and the fan-out test's title counts its own tokens.
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 subscribe to this conversation on GitHub.
Already have an account?
Sign in.
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
Prompts typed into a freshly launched agent pane sometimes sat in the input box without submitting. Claude Code paints its full UI within half a second of spawn but mounts its stdin reader seconds later; anything typed in that window is consumed as one bulk chunk on mount, which the harness eats as a paste: the text lands, the trailing Enter is stripped, nothing submits. Waiting for a non-blank screen cannot see this: the screen is fully painted while the pane still swallows input.
How
Delivery now holds the Enter until the typed text comes back on the screen, which proves the harness is reading its input. The check counts non-overlapping occurrences of the prompt's normalized head (48 chars) and waits for one MORE than the baseline frame, so a short prompt hiding inside banner text and spinner repaints both hold the gate instead of releasing it early. A pane that never echoes (masked input, say) still gets its Enter when the gate budget (10s, on top of the 15s settle) runs out, which is the old blind behavior, never a worse one.
The policy lives in one shared module,
deliverPromptIntoin@internal/pane-runtime, now used by both delivery sites (the control plane'sLocalLauncher.deliverPromptand the node'sprompt_deliverexecutor) instead of two mirrors that drift. Theprompt_deliverwire frame is unchanged, so an old server with a new node just gets the new gate; no protocol bump owed (worst case 25s, inside the remote RPC's 45s budget).Tests
prompt-delivery.test.tscases (fake pane + owned clock: order, swallowed newlines, ANSI, banner collisions, spinner repaints, budget fallbacks, error paths) plus a real-tmux regression: a booting TUI whose mount-time read strips CRs and only submits a later Enter chunk.commands-prompt-tail.test.tsupdated for the delegated executor.Changeset:
@internal/serverpatch +@internal/nodepatch.