Skip to content

fix: keep the firstmate probe pane alive across its own read (#761) - #763

Merged
ryaneggz merged 1 commit into
developmentfrom
fix/761-gate-probe-lifetime
Aug 13, 2026
Merged

fix: keep the firstmate probe pane alive across its own read (#761)#763
ryaneggz merged 1 commit into
developmentfrom
fix/761-gate-probe-lifetime

Conversation

@ryaneggz

Copy link
Copy Markdown
Collaborator

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_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
[session-runner] ladder: herdr ineligible β€” execution-context gate: no probe fingerprint
                 obtained (caller[host=928eeb5cbca5 docker=yes worktree=yes])

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:

host /.dockerenv worktree resolves
caller 928eeb5cbca5 yes yes
probe pane 928eeb5cbca5 yes yes

Identical. 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 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.

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.sh gains five assertions. Each was shown to FAIL against
a deliberately broken copy, not merely to pass against the good one:

Mutation Rejected
pane runs the bare snippet βœ…
keep-alive constant deleted βœ…
suffix dropped from the composition βœ…
keep-alive leaks into the shared RUNNER_PROBE_SCRIPT βœ…
budget stops deriving from RUNNER_PROBE_TIMEOUT_MS βœ…

One assertion initially rejected the fix itself. code_only drops printf lines
so 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 to
assign 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 live
probe 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

  • 588 tests pass (42 files); 3 new
  • 102 probes: 99 PASS, 3 pre-existing SKIPPED, 0 FAIL, 0 REGRESSION
  • typecheck clean, bash -n clean
  • .oh/evals/RESULTS.md regenerated against this branch's base

Submitted-by: Claude

πŸ€– Generated with Claude Code

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>
@ryaneggz
ryaneggz merged commit c77ec64 into development Aug 13, 2026
4 checks passed
@ryaneggz
ryaneggz deleted the fix/761-gate-probe-lifetime branch August 13, 2026 05:32
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant