---
name: a-stopped-local-ssh-does-not-stop-its-remote-command
description: TaskStop/kill on a local ssh leaves the remote bash running when it has no tty; an orphan copy of a battery then shares the pool + output dir with the intended one and VOIDS the evidence (W0 2026-09-06)
metadata:
  type: feedback
---

Stopping the LOCAL ssh (TaskStop, the 10-min background cap, Ctrl-C) does NOT stop the remote
command: a `bash -lc "…"` with no tty gets no SIGHUP, and a process sleeping in a wait loop only
dies on its next write to the dead channel — mine survived to run a full battery.

**Why:** 2026-09-06 W0 (todlando): I launched a kitsubito chain (wait → reset → driver → smoke)
as a background ssh, realised the remote work outlived the local cap, TaskStop'ped it and relaunched
the same chain detached. The orphan kept waiting, then reset the SAME worktree to the SAME head and
ran a SECOND full driver into the SAME `.spt/gate-head/` and pool concurrently with the real one:
one `Starting 2776 tests` header, two `Summary` lines (1 failed / 3 failed), the extra reds being
two-suites-on-one-box contention, load 15.3 and a docs-port bind collision — all self-inflicted.
doyle ruled the whole window VOID (not red, not green), the isolated x3 rerun and both smokes
included. releases#103 class (live-lane pool share), reached by a stopped ssh instead of a junction.

**How to apply:**
- Never "stop" remote work from the local side. Detach on purpose (`setsid nohup … &` + log + DONE
  marker), record the REMOTE pid, and kill by that pid when you must.
- Before launching any remote battery, `pgrep -af 'driver|nextest run|cargo '` = 0, LOGGED into the
  chain's own log (`procs-before: 0`) so the claim is a line, not a recollection.
- An output dir is a pool: the chain/driver takes an `flock` on it and a second copy is REFUSED
  loudly (driver carries the lock since W0; `.spt/gate-head-driver.log` beside `.spt/gate-head/`
  is the interleaving proof, kept on purpose).
- Two Summaries in one nextest.raw = two suites, never one; the read is VOID before any red is
  classified. Related: [[worktree-pool-claim-false-orphan]], [[never-block-on-taskoutput-for-long-tasks]].
- Peer write-up of the same window from the gater side: [[stopped-local-ssh-does-not-stop-its-remote-command]] (doyle, 2026-09-06 19:17Z) — read both; his carries the triage, mine the launcher-side mechanics.

**Second face, LOCAL (todlando 2026-09-07 09:41Z, W2 fixup battery):** TaskStop on a LOCAL driver script kills the driver, not its cargo CHILD; the orphaned child kept writing the same log + exit files as the re-launched battery (an earlier `sed -i` had also made two driver scripts the same file) → legs completing out of order (`sptbins done` before `check done`), `procs-before: cargo=1`. The memory was filed for ssh and the mechanism walked in from a direction it did not name: it is "a stopped PARENT does not stop its child, on any transport". Correct handling shown: census before any kill (8 new cargo pids could have been CI's Windows unit job on the shared box — checked the run was already complete first), waited the orphans out (0 after 220 s) rather than killing, deleted every artifact of the voided run, re-ran ONCE under a `mkdir` lockfile that exits 9 on a second battery into the same output dir.

2026-09-08 face (hertz, W3 lane) — THE COROLLARY THAT GETS MISSED: when two runs overlap,
**BOTH are void, not just the earlier one.** todlando restarted a battery without stopping the
first; the first finished mid-flight and wrote nextest/check/treqs exits from a run whose clippy
leg had read pre-fix source, into the output dir the second was also writing. He called it void
correctly — and the thing worth adding is that the SECOND run is contaminated too: it began while
the first was still running, so its early legs shared the pool with the first's later legs and its
raws share the dir. There is no clean survivor.

That matters because of what the pressure move IS. Under a clock, the tempting read is "the first
one is spoiled, so use the second" — the survivor feels like the salvage. It is not: overlap
poisons both directions, the later run's inputs as much as the earlier run's outputs. Only a run
that was ALONE for its whole duration is evidence. Same day, same box, I had walked into the
launch half of this myself (f19-2's `LNK1104: cannot open file <the exe being produced>` was
f19-1's role A still holding it — a COLLISION presenting as a BUILD failure, which is why it does
not announce itself as a collision).

Hardening, and lock the right thing: **flock the OUTPUT DIR, not the pool** — sharing a pool
sequentially is legitimate and expected, two writers into one raw set never is. Log a
`procs-before:` census (anything whose ExecutablePath is under this worktree) at driver start and
gate the launch on it being empty, recording the holder pids while it waits, so "it was not
racing" is a file rather than a memory. And make the `grep -c Summary` check a REFUSAL THAT NAMES
THE COUNT: 2 means overlap, 0 means the leg never ran, and a bare equality test reports two
different failures identically.

**LOCAL FACE, 2026-09-08 (same mechanism, no ssh involved).** I ran a battery driver as a
backgrounded `bash driver.sh | tail -25` and stopped the harness task to clear the box for a
runner job. TaskStop killed the leg that was running; the DRIVER BASH SURVIVED, saw its leg
return, and walked on down its list — the killed nextest wrote `exit 127`, then clippy ran
(15:29:34Z), then treqs, then it printed done and exited on its own. I had already told the
gater "battery killed, nothing of mine is running", and it was false for about six minutes; he
spotted my clippy chain on the box and asked whose it was.

**How to apply:** stopping the harness task is not stopping the work. Kill the DRIVER process
and its whole tree BY PID, then RE-QUERY and report from the re-query, never from the stop.
And when the driver's own output dir has timestamps, read them — my leg exit files dated the
whole continuation to the second and refuted my all-clear without needing any process census.

**LOCAL FACE (todlando 2026-09-08 15:38Z, same mechanism without ssh):** a harness TaskStop on a battery killed the nextest LEG only; the driver bash survived, saw its leg return, wrote spt.exit 127, and walked on to clippy (15:29:34Z) and treqs, then printed done and exited — six minutes of box use after "battery KILLED, nothing of mine is running" was sent, beside a live golden twohost-a. Stopping a task stops ONE MEMBER, not the pipeline's driver. Kill the DRIVER by pid and its whole tree, re-query the recorded pids explicitly, and only then report; a report composed from the stop is a guess. The zero-run VOID arm fired correctly on the killed leg's raw ("spt ran ZERO tests — the filter matched nothing, this is not a pass").
