fix: keep the firstmate probe pane alive across its own read (#761) - #763
Merged
Conversation
The execution-context gate could never admit herdr, in any environment.
herdr destroys a pane the instant its command returns, and a read against a
destroyed pane answers `pane_not_found`. `RUNNER_PROBE_SCRIPT` prints one
fingerprint line and exits, so the probe pane lost the race with its own reader:
[session-runner] execution-context gate: probe pane w3:p4 emitted no
fingerprint within 15000ms
The gate then reported "no probe fingerprint obtained" and degraded to tmux β
correct-looking output for the wrong reason, and a permanent refusal wearing the
shape of a proof obligation.
Measured live, with a keep-alive appended so the pane outlives the read:
caller: host=928eeb5cbca5 docker=yes worktree=yes
probe: host=928eeb5cbca5 docker=yes worktree=yes
Identical. The comparison the gate exists to make would have passed.
WHY IT SURVIVED THE ORIGINAL BUILD
Two reasons, and both are worth recording.
The live path failed earlier and correctly: while the operator config bind
(#756) was open, the probe pane was a HOST shell that stayed alive long enough
to read, and the gate refused herdr on a genuine fingerprint mismatch. US-010's
smoke measured exactly that path. The race is only reachable once the
environment matches, so closing #756 is what exposed it.
The unit-test stub was MORE FORGIVING than herdr: it replayed pane output
regardless of whether the pane could still exist. That is why 46 tests passed
against a gate that could not work live. The stub now models real pane
lifetime β a pane is readable only if its start invocation carried a keep-alive
β so removing the keep-alive fails five tests, three of which predate this
change.
THE FIX
A keep-alive suffix rides the pane invocation only. It is never folded into
`RUNNER_PROBE_SCRIPT`, because that snippet also runs in-process for the caller
side, and "the same snippet runs in the probe pane and locally" is what makes
the comparison true by construction rather than by convention; a sleeping local
fingerprint would also stall every caller. The budget derives from the single
`RUNNER_PROBE_TIMEOUT_MS` source so the two cannot drift, and it is an upper
bound rather than a cost β the gate closes the pane as soon as the read
completes, on the match and the mismatch path alike.
VERIFIED BY REJECTION
`session-runner-ladder.sh` gains five assertions. Each was shown to FAIL against
a deliberately broken copy, not merely to pass against the good one:
- the pane runs the bare snippet instead of the composed one
- the keep-alive constant is deleted
- the suffix is dropped from the composition
- a keep-alive leaks into the shared `RUNNER_PROBE_SCRIPT`
- the budget stops deriving from `RUNNER_PROBE_TIMEOUT_MS`
One assertion initially rejected the fix itself: `code_only` drops `printf`
lines so that prose naming a command cannot satisfy a check that the command
runs. The single-`printf` helper was restructured to assign then print, rather
than weakening the filter.
The header's MIGRATION TRIGGER is discharged and replaced with the observed
result. Its prediction β that the gate would "stop rejecting herdr by itself"
once #756 closed β was wrong, and the note now says so, with the lesson: a gate
failing for one reason can hide a second reason it would fail anyway. Removing
the cause does not prove the gate works; only re-probing does.
The gate itself is unchanged in intent and stays after this fix. It is a
build-correctness guard whose job is to PROVE environment identity rather than
assume it.
588 tests pass, 102 probes run with 99 PASS and 3 pre-existing SKIPPED.
Closes #761. Refs #746, #748, #756.
Submitted-by: Claude
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
5 tasks
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.
Closes #761. Refs #746, #748, #756.
The defect
The firstmate execution-context gate could never admit herdr, in any
environment β including a correct one.
herdr destroys a pane the instant its command returns, and a read against a
destroyed pane answers
pane_not_found.RUNNER_PROBE_SCRIPTprints onefingerprint line and exits, so the probe pane lost the race with its own reader:
The ladder degraded to tmux β correct-looking output for the wrong reason, and a
permanent refusal wearing the shape of a proof obligation.
Measured live with a keep-alive appended so the pane outlives the read:
/.dockerenv928eeb5cbca5928eeb5cbca5Identical. The comparison the gate exists to make would have passed.
Why it survived the original build
Two reasons, both worth recording.
The live path failed earlier, and correctly. While the operator config bind
(#756) was open, the probe pane was a HOST shell that stayed alive long enough to
read, and the gate refused herdr on a genuine host-vs-container fingerprint
mismatch. #748's US-010 smoke measured exactly that path. The race is only
reachable once the environment matches, so closing #756 is what exposed it.
The unit-test stub was more forgiving than herdr. It replayed pane output
regardless of whether the pane could still exist. That is why 46 tests passed
against a gate that could not work live. The stub now models real pane lifetime β
a pane is readable only if its start invocation carried a keep-alive β so
removing the keep-alive fails five tests, three of which predate this change.
The fix
A keep-alive suffix rides the pane invocation only. It is never folded into
RUNNER_PROBE_SCRIPT, because that snippet also runs in-process for the callerside, and the same snippet runs in the probe pane and locally is what makes the
comparison true by construction rather than by convention. A sleeping local
fingerprint would also stall every caller.
The budget derives from the single
RUNNER_PROBE_TIMEOUT_MSsource so the twocannot drift, and it is an upper bound rather than a cost: the gate closes the
pane as soon as the read completes, on the match and the mismatch path alike.
The gate is unchanged in intent and stays. It is a build-correctness guard
whose job is to prove environment identity rather than assume it.
Verified by rejection
session-runner-ladder.shgains five assertions. Each was shown to FAIL againsta deliberately broken copy, not merely to pass against the good one:
RUNNER_PROBE_SCRIPTRUNNER_PROBE_TIMEOUT_MSOne assertion initially rejected the fix itself.
code_onlydropsprintflinesso prose naming a command cannot satisfy a check that the command runs, and the
new helper's whole body was a single
printf. The helper was restructured toassign then print β the probe's discipline was right, so the code moved, not the
filter.
Migration note discharged
session-runner.sh's header carried a MIGRATION TRIGGER: re-verify with a liveprobe pane once #756 closed, and record the result. Done. Its prediction β that
the gate would "stop rejecting herdr by itself" β was wrong, and the note now
says so, with the lesson: a gate failing for one reason can hide a second reason
it would fail anyway. Removing the cause does not prove the gate works; only
re-probing does.
Gates
bash -nclean.oh/evals/RESULTS.mdregenerated against this branch's baseSubmitted-by: Claude
π€ Generated with Claude Code