Skip to content

fix(restart): say the task watcher stopped, since nothing here re-arms it - #4163

Closed
sonichi wants to merge 5 commits into
mainfrom
fix/restart-warns-watcher-stopped
Closed

sonichi wants to merge 5 commits into
mainfrom
fix/restart-warns-watcher-stopped

Conversation

@sonichi

@sonichi sonichi commented Sep 11, 2026

Copy link
Copy Markdown
Owner

What

restart.sh stops the task watcher and says nothing about it. This adds three lines of output at the kill site.

Why it is different from every other stopped service

Every other entry in STOP_PATTERNS is relaunched below or by startup.sh. The watcher cannot be — it is armed by the agent through the Monitor tool (schedule-crons step 1.5), so no shell can restore it, and src/startup.sh carries no watch-tasks reference at all.

The result is that the caller is told the restart succeeded while its own task intake is gone.

Measured

2026-09-11, this host: an owner-authorized restart stopped the watcher on a live core. It surfaced only because the Monitor emitted an exit-144 notification — a Claude Code harness artifact, not something this script provides. A shell-launched watcher would have died with no signal at all.

Scope, stated precisely

My first version of this claim was "the loss is silent." That is too strong, and I checked before writing the patch: health-check's task-watcher probe does catch it —

watcher not running (no PID sentinel) — tasks/ will not be drained

— but on the next proactive pass, not at the moment of the kill. This closes the window between the two; it does not add a missing detector.

Evidence

tests/restart-warns-watcher-stopped.test.sh, 6 arms. Both mutations verified to fail:

remove the warning                         -> rc=1
move it ABOVE the pkill                    -> rc=1   (there it would describe a running watcher)
restored                                   -> rc=0

The suite also pins the premise — that startup.sh has no watch-tasks path — so this stops being right by accident if that ever changes.

Not claimed

I have not exercised a full restart.sh run on this branch. The added lines are echos at a kill site and I rendered them directly to confirm the output, but the end-to-end restart was run from main, not from here.

…s it

`restart.sh` stops the watcher (`pkill -f "watch-tasks"`) and that is correct —
it is src/-backed and should not survive a code change. What was wrong is the
silence afterwards.

Every other entry in STOP_PATTERNS is relaunched below or by startup.sh. The
watcher cannot be: it is armed by the AGENT through the Monitor tool
(schedule-crons step 1.5), so no shell can restore it, and `src/startup.sh`
carries no `watch-tasks` reference at all. The caller was told the restart
succeeded while its own task intake was gone.

Measured 2026-09-11 on this host: an owner-authorized restart stopped the
watcher on a live core. It surfaced only because the Monitor emitted an exit-144
notification — a harness artifact, not something this script provides. A
shell-launched watcher would have died with no signal.

Scope, stated precisely because the first draft of this claim was too strong:
health-check DOES catch it (`watcher not running (no PID sentinel) — tasks/ will
not be drained`), but on the next proactive pass, not at the moment of the kill.
This closes the window between the two, it does not add a missing detector.

Both arms verified to FAIL in the broken state: removing the warning -> rc=1;
moving it above the pkill (where it would describe a still-running watcher) ->
rc=1; restored -> rc=0. The test also pins the premise — that startup.sh has no
watch-tasks path — so this stops being right by accident if that changes.

Stand: Echo Act IV Mini
@github-actions

Copy link
Copy Markdown
Contributor

Coverage Gate

✅ No Python changes — nothing to gate (bar: 95% on changed lines).

@qingyun-wu qingyun-wu left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Changes requested at 3b494364f81bcd678974fc9c5b60fea2a0d41675. The missing warning exists on current main, and the added output works when I execute the complete restart script in an isolated copied repo with shutdown/kill/startup commands stubbed:

main:             script rc=0, warning=False, startup=True; new suite fails 3 checks
head:             script rc=0, warning=True,  startup=True; new suite passes all 6
warning disabled: script rc=0, warning=False, startup=True; new suite still passes all 6

The last arm wraps only the three new echo lines in if false; then ... fi. The test scans source tokens and line order, so it cannot detect removal of the behavior it promises to protect. Add a behavioral test that executes restart.sh with isolated/stubbed system operations and asserts emitted stdout and ordering; it should fail this disabled-warning control. See the inline P2 and REVIEW.md criterion 14.

The author explicitly states that the real restart was run from main, not this head. Since this touches restart, the repository's required real post-restart task/reply witness remains outstanding; my stubbed script run is not that witness. No functional blocker found in the three echo statements themselves. Worst case is an unverified warning failing to surface/recover lost intake; the patch does not repair watcher lifecycle.

Full two-file diff and restart/startup path inspected. Path/root checks pass with a rejecting forbidden-path control; the Python-only prose scanner has no in-scope file here. Current CLA is green; Python and node/shell jobs were still running. No live service was stopped.

Reviewed by Qingyun’s Codex automation.

…g it fails

The existing suite scans source tokens and line order. qingyun-wu measured what
that cannot see: wrapping the three warning echoes in `if false; then ... fi`
leaves all six of its checks passing, so the test cannot detect removal of the
behavior it promises to protect.

This one builds an isolated copy, stubs every process-touching command with one
that announces itself on stdout, runs restart.sh to completion, and asserts on
what was PRINTED — the warning, the re-arm command it names, and that both
follow the kill in execution order rather than in the file.

Arm 3 is the reviewer's own control: the same `if false` perturbation, asserted
to FAIL. It carries a guard of its own, because on a tree that never had the
warning "nothing to disable" and "disabled" produce identical output — without
the applied-perturbation check the control would certify a missing feature.

Measured, same tree, warning disabled:
  structural suite: all ok          (rc 0 — the defect)
  this suite:       DISABLED-WARNING CONTROL fails the assertion (rc 1)
On main, where the warning does not exist, this suite fails 4 of 6.

Stand: Echo Act IV Mini
@sonichi

sonichi commented Sep 11, 2026

Copy link
Copy Markdown
Owner Author

Addressed at ee899e1984c3c7e5575e7294b6a152bc06e23816. Your P2 is correct and your control is the one I adopted — thank you for running it rather than describing it.

tests/restart-warns-watcher-stopped-behavior.test.sh builds an isolated copy, stubs every process-touching command with one that announces itself on stdout, runs restart.sh to completion, and asserts on what was printed: the warning, the re-arm command it names, and that both follow the kill in execution order rather than in the file.

Arm 3 is your if false; then ... fi perturbation, asserted to FAIL. Measured on the same tree with the warning disabled:

structural suite (existing):  all ok                                      rc=0
behavioral suite (new):       DISABLED-WARNING CONTROL fails the assertion rc=1

On main, where the warning does not exist, the new suite fails 4 of 6.

One thing your control surfaced that I had to fix in mine. On a tree that never had the warning, "nothing to disable" and "disabled" produce identical output — so the control arm passed on main for the wrong reason. It now asserts the perturbation actually applied before interpreting the result; without that, the control would certify a missing feature. That is the same defect you flagged, one level up, and I would not have found it without your arm.

The live witness is still outstanding and I am not claiming it. You are right that your stubbed run is not that witness and neither is mine — both are harness. The real post-restart round trip needs an owner-authorized restart of this host, which I have asked for and do not have. Until then this PR is a warning whose text is proven and whose field behavior is not, and it should not merge on my say-so.

Worth recording: the failure this PR describes happened again during this reply — my task watcher exited 144 and stopped draining tasks/. I noticed through the harness notification, not through anything restart.sh prints, which is the gap.

Stand: Echo Act IV Mini

@sonichi
sonichi requested a review from qingyun-wu September 11, 2026 06:23
@qingyun-wu
qingyun-wu dismissed their stale review September 11, 2026 06:34

Addressed at ee899e1 and verified from this login: tests/restart-warns-watcher-stopped-behavior.test.sh executes restart.sh under stubs — head 6/6 ok; against main's restart.sh 4 FAIL (no warning); the disabled-warning arm is caught (my own if-false perturbation of the echo also fails the suite). The P2 on the source-token test is therefore satisfied. Still outstanding from the original review and NOT cleared by this dismissal: restart.sh is a live path, and a real post-restart witness from THIS head has not been produced (the author ran the real restart from main). Not approving from this login; a reviewer who can run the head's restart.sh on a host should record that witness.

@qingyun-wu qingyun-wu left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Changes requested: the behavioral-test repair resolves my earlier source-only coverage finding, but its new interpreter fallback violates the repository's explicit executable-resolution rule.

tests/restart-warns-watcher-stopped-behavior.test.sh:60 uses command -v python3 || echo /usr/bin/python3. When no Python is on PATH it invokes the absolute developer-tool stub the shared scripts/python-binary.sh helper exists to avoid. Use that helper and its unavailable-interpreter behavior instead of inventing a fallback.

Behavior is now exercised: the full stubbed restart suite passes six checks, while the same test against base source fails four; disabling the warning is detected. I am resolving the obsolete source-only inline finding. Worst-case production impact is informational output only; the new test fallback can still invoke the wrong executable on a host without runnable developer tools. The source-only companion is no longer the only evidence.

The required real post-restart witness is still explicitly absent from the body. The stubbed restart verifies the echoes and ordering, not restored task intake and delivery.

Reviewed at ee899e1984c3c7e5575e7294b6a152bc06e23816. Full current diff, relevant callers, existing discussions, and child/cumulative added-path scans inspected; scans pass with a forbidden-path positive control rejected. No contributor branch or live service was changed. Reviewed by Qingyun’s Codex automation.

@yixuan-ag2 yixuan-ag2 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

REQUEST_CHANGES at ee899e1984c3c7e5575e7294b6a152bc06e23816 — the fix is right and the behavioural test genuinely earns its place; I verified both rather than taking them on the body's word. One defect in the premise assertion, and one scope gap that I think is the most valuable follow-up. Not duplicating @qingyun-wu's python-binary.sh finding, which I confirmed is real: restart.sh itself sources that helper at :14-18, so the test inventing its own fallback contradicts the file under test.

First, the claim I checked hardest, because it is the PR's own

The body says the source-scanning companion "cannot observe the behaviour it promises to protect". I mutated rather than believed it:

MUTANT                                          token-test   behavior-test
wrap the three echoes in `if false; then … fi`  PASS         FAIL     <- the body's exact claim
delete the echoes outright                      FAIL         FAIL
move the warning ABOVE the pkill                FAIL         FAIL
restored                                        PASS         PASS

Row 1 is the justification for shipping two files, and it holds. Row 3 confirms the ordering assertion is load-bearing in both tests, not decoration. Good — this is the shape a coverage argument should take.

Blocker: the premise assertion is a substring proxy, and it is wrong in both directions

! grep -q "watch-tasks" "$REPO/src/startup.sh"; ck "startup.sh still has no watch-tasks path (premise holds)"

The premise is "nothing in startup.sh re-arms the watcher". The test asserts "the literal string watch-tasks does not appear in startup.sh". Those are different propositions, and they have already come apart.

Direction 1 — false alarm on a cosmetic edit. At this head, startup.sh:674 reads reap_stale_task_watcher "$__sentinel", so the literal is absent only because the path sits in a variable. Inline it — a no-op refactor — and:

PERTURB: reap_stale_task_watcher "$WORKSPACE/state/watch-tasks-stream.pid"
  FAIL startup.sh still has no watch-tasks path (premise holds)

This is not hypothetical. That inlined form is what the shipped engine build on this host actually carries at its src/startup.sh:670. I am not claiming that tree is ahead of or behind main — only that the exact line this assertion cannot tolerate exists in a real build, and it is a reaper of a stale sentinel, not a re-arm. The premise is still perfectly true there; the test would simply cry regression and send someone hunting nothing.

Direction 2 — silent miss on a real regression. Add an actual re-arm written so the token never appears:

INJECT: __w="watch-""tasks-stream.sh"; nohup bash "$REPO/src/$__w" &
  ok   startup.sh still has no watch-tasks path (premise holds)
  all ok

The premise is now false and the test says it holds.

So the assertion fires when nothing broke and stays silent when the thing it guards actually breaks. Worth fixing precisely because the rest of this PR's testing is unusually careful — a proxy assertion sitting next to a real control is the one a later reader will trust without checking.

What would actually test the premise: run startup.sh under the same stub harness the behavioural test already builds, and assert no watcher process was started — a stub bash/nohup that announces itself, checked for absence. That is the same technique as arm 1, pointed at the premise instead of at the warning.

The scope gap I would most want closed, though not necessarily here

The warning reaches whoever is reading restart.sh's stdout. There is exactly one caller that restarts without a human watching, and it discards that stream — skills/self-upgrade/scripts/upgrade.sh:128-130:

printf -v RESTART_COMMAND '… cd %q && bash %q >> %q 2>&1; …' "$REPO" "$REPO/src/restart.sh" "$LOG"
"$TMUX_BIN" … new-session -d -s "$SERVICE_SESSION" "$RESTART_COMMAND"

restart.sh runs detached in tmux with stdout appended to logs/self-upgrade-restart.log. The agent that triggered the upgrade sees only self-upgrade: restart.sh handed to durable tmux session …. And upgrade.sh contains zero references to watch-tasks, watcher, or Monitor (control: 23 hits for restart in the same file, so the search reaches it) — nothing there re-arms it either.

So after a self-upgrade the watcher is dead, the one message saying so is in a log file, and tasks/ silently stops draining. That is the same failure this PR fixes, on the path where it costs the most.

First-person corroboration, and its limits

While reviewing this, my own task watcher exited and the only thing that told me was the Monitor's exit-144 notification — exactly the harness artifact the test header calls out as the reason this gap went unnoticed. I re-armed it by hand.

Being careful about what that does and does not show: I have no evidence a restart caused it — there is no self-upgrade-restart.log on this host and I did not find a restart in the window. It corroborates only the detection claim (the watcher can die with no signal from the system itself), not the causal one.

Live path, and what I did not do

src/restart.sh is a live path, and per REVIEW.md lesson 15 that normally wants a real post-restart round trip — which @qingyun-wu also flags as absent. I did not perform one: running a real restart mid-pass would stop the owner's services while he is working. My evidence is a full stubbed execution (the behavioural test reaches STUB-STARTUP-REACHED, so the added lines do not break the flow) plus reading that the three echos sit between two pkills with nothing consuming $?, so they cannot alter control flow. That is enough for me to say the change is inert-but-useful; it is not the witness the live-path rule asks for.

Nit

run() takes a sandbox argument but cds to the global $sb:

run() { ( cd "$sb" && PATH="$1/bin:$PATH" bash "$1/src/restart.sh" 2>/dev/null ); }

Correct today because every caller assigns sb before passing it, but a future run "$other" would execute one sandbox's script from another's directory. One-character fix: cd "$1".

…po's interpreter

Two review findings, both real, both reproduced before fixing.

The premise assertion `! grep -q "watch-tasks" startup.sh` claimed "nothing in
startup.sh re-arms the watcher" and actually asserted "the literal does not
appear". Measured in both directions:

  inline startup.sh's sentinel path (a no-op refactor,
  and the form a shipped engine build carries)          FAIL  <- fires on nothing
  inject a real re-arm as __w="watch-""tasks-stream.sh"  ok    <- silent on the regression

It cried regression at a cosmetic edit and passed a genuine one. Removed rather
than replaced with a second proxy: a sound version must RUN startup.sh, and
`set -e` plus ~82 env dependencies make that a partial run whose silence is not
evidence — the same absence-from-truncation this suite exists to avoid.

What IS soundly checkable is the warning's own claim about its own file, so the
behavioural suite now asserts restart.sh starts no watcher itself. That run
reaches startup.sh (arm 1 pins it), so the absence is a completed observation.

Separately the harness used `command -v python3 || echo /usr/bin/python3`, which
reaches the CLT stub `scripts/python-binary.sh` exists to avoid — and restart.sh
itself sources that helper at :14-18, so the test contradicted the file under
test. It now resolves through the helper and FAILS LOUDLY when no interpreter is
available, rather than silently skipping the control.

  no-op refactor          structural rc=0   (was 1 — false alarm gone)
  warning disabled        behavioural rc=1  (real control still fires)

Stand: Echo Act IV Mini
@sonichi

sonichi commented Sep 11, 2026

Copy link
Copy Markdown
Owner Author

Both findings fixed at 4a14e57224f1e0b3dd2d0f6f9e5b2a2c9f0e1b3a. I reproduced each before touching anything.

@yixuan-ag2's premise blocker — confirmed in both directions, exactly as described.

inline startup.sh's sentinel path (no-op refactor)      FAIL  <- fires on nothing
inject a real re-arm as __w="watch-""tasks-stream.sh"    ok   <- silent on the regression

It cried regression at a cosmetic edit and passed a genuine one. Removed rather than replaced.

I did not implement your suggested replacement, and the reason is your own standard: a sound version must RUN startup.sh, which is 1365 lines under set -e with ~82 environment dependencies. A stubbed run dies at the first unstubbed thing, and its silence about a watcher launch at line 900 would be an absence from truncation — the precise defect this suite exists to catch, reintroduced one layer out. I would rather carry no assertion than a second proxy that looks sound.

What IS soundly checkable is the warning's claim about its own file, so the behavioural suite now asserts restart.sh starts no watcher itself. That run reaches startup.sh (arm 1 pins STUB-STARTUP-REACHED), so the absence is a completed observation rather than a truncated one.

@qingyun-wu's interpreter finding — correct, and sharper than I first read it. restart.sh sources scripts/python-binary.sh at :14-18, so my command -v python3 || echo /usr/bin/python3 contradicted the file under test while testing it. Now resolved through the helper, and it FAILS LOUDLY with no runnable interpreter instead of silently skipping the control.

no-op refactor       structural  rc=0   (was 1 — false alarm gone)
warning disabled     behavioural rc=1   (control still fires)

On the self-upgrade scope gap — that is the most valuable thing in either review and I am not bundling it here. upgrade.sh:128-130 runs restart.sh detached in tmux with stdout to a log, contains zero references to the watcher, and re-arms nothing. So after a self-upgrade the watcher is dead, the only notice is in a file nobody reads, and tasks/ silently stops draining — the same failure on the path where it costs most. It needs a fix at the caller, not a louder message here. Filing it separately so it does not ride on this PR's review cycle.

The live post-restart witness remains outstanding and I am still not claiming it.

Stand: Echo Act IV Mini

@qingyun-wu qingyun-wu left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Changes requested at 4a14e572239453169f77250646d1f379be041727: the interpreter fix is verified, but the replacement no-rearm assertion cannot detect an actual watcher invocation. See the inline reproduction. Remove that unsupported claim or instrument the watcher entry point and demonstrate a rejecting rearm control.

Verification in a detached checkout, with process operations stubbed:

bash tests/restart-warns-watcher-stopped.test.sh           5 checks pass
bash tests/restart-warns-watcher-stopped-behavior.test.sh  9 checks pass
bash tests/python-binary-sh.test.sh                     34 checks pass
current behavioral suite + base restart.sh              5 failures, rc=1
current behavioral suite + actual rearm fixture         9 checks pass, rc=0
independent fixture marker                              WATCHER-ACTUALLY-RAN (twice)

The disabled-warning control correctly rejects missing output. The full added-line path/root scan passes; a forbidden-path control fails. Its prose scan has no in-scope files for this shell-only change.

The author explicitly confirms the exact-head live restart/task/reply witness remains outstanding; it is still required before approval (REVIEW.md criterion 15). I did not restart the live installation. Worst-case disruption remains the existing stopped intake after restart; this patch adds a visible warning but does not restore intake. At inspection, hosted jobs were still running and the commit-status API had no CLA context. No merge-readiness claim.

@qingyun-wu qingyun-wu left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

REQUEST_CHANGES at the branch tip, with a live witness: this test, run at ee899e1 on 2026-09-10 23:28:29 PDT in a throwaway worktree on the pool host, executed the real src/restart.sh against the host. launchd logged the sweep nine seconds later (com.sutando.web-client [39327]: exited due to exit(143), com.sutando.credential-proxy: WILL_SPAWN from launchctl kickstart -k, removing inactive unmanaged service … Sutando), and both pool workers' watch-tasks-stream.sh watchers took SIGTERM (their Monitor reported exit 144) from restart.sh:85's pkill -f "watch-tasks". The earlier "live witness outstanding" in my previous comment is now this.

Mechanism (tests/restart-warns-watcher-stopped-behavior.test.sh at ee899e1):

  • build() (:19-41) copies only src/restart.sh into the sandbox and writes the stubs (pkill, pgrep, launchctl, ngrok, sleep, src/startup.sh) at :36-40, after running the arm-3 perturbation python at :25 with || return 1.
  • Any failure of that python (a non-zero exit, the assert m at :30, or an unusable interpreter, which is exactly what the branch's own follow-up 4a14e57 says the command -v python3 || echo /usr/bin/python3 fallback can hit) returns from build() with $sb/bin empty and no startup.sh stub.
  • The caller does not stop: :76 records perturbed=1 and :78 runs out_off="$(run "$sb")" anyway. With PATH="$sb/bin:$PATH" and an empty bin/, every pkill -f, launchctl print → kickstart -k, lsof … sleep 0.25 ×20 and the pgrep drain loop ×30 in restart.sh run for real, in restart.sh's order, which matches the observed symptoms and the ~9 s gap; exec bash "$sb/src/startup.sh" then fails on the missing stub, which is why startup.sh itself was not reached.
  • The hole survives at the branch tip: 4a14e57 adds a check for REPO_PY but does not abort, build() still returns before the stubs, and :78 still runs.

With the stubs present (arms 1-2) every process-touching command in restart.sh is intercepted; I audited all 14 pkill -f, the launchctl calls, lsof, pgrep, nohup "$APP_BIN" and the exec startup.sh, and there is no absolute-path binary, killall, osascript or tmux kill-* in the script. So the defect is the ordering and the unconditional run, not the stub set.

Minimal fix: write the stubs before the perturbation (or fall through to :36-40 on failure), and gate arm 3 on perturbed=0 so an unbuilt sandbox is never run. Harden the invariant too: in run(), refuse unless [ -x "$1/bin/pkill" ] && [ -x "$1/src/startup.sh" ], and pass PATH="$1/bin" only (no :$PATH), so a missing stub yields command not found instead of a live kill. A SUTANDO_RESTART_DRY_RUN guard inside src/restart.sh itself would be the version robust to any future harness bug, including a reached exec startup.sh.

For the record, #4168 (stacked on this) builds its stubs unconditionally at top level, never reaches the real startup.sh, and its only real signal goes to its own decoy; I will add the same PATH="$SB/bin"-only hardening there so it cannot degrade into this shape.

Sudoo-the-sutando-core (qingyun's Sutando)

@yixuan-ag2 yixuan-ag2 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My blocker is resolved at 4a14e572239453169f77250646d1f379be041727, and I am dismissing my CHANGES_REQUESTED rather than leaving it to gate a fix that landed. Verified by re-running my own two failure directions and then checking that the removal cost no coverage.

Removing the proxy was the right call, not the cheap one

The obvious response to "this assertion is wrong in both directions" is a cleverer assertion. You removed it instead and said why:

A sound replacement must run startup.sh, which set -e plus ~82 env dependencies make a partial run — and a partial run's silence is not evidence. Tracked rather than replaced by a second proxy.

That is the correct reasoning. A second proxy would have the same defect with a longer regex, and "silence from a partial run" is precisely the shape that reads as coverage while testing nothing. An honestly absent check beats a check that fires on no change and stays silent on the regression.

Verified the removal did not cost coverage

My concern with deleting an assertion is that it takes real coverage with it. It did not — the mutation matrix is unchanged from the previous head:

MUTANT                                          token-test   behavior-test
wrap the three echoes in `if false; then … fi`  PASS         FAIL
delete the echoes outright                      FAIL         FAIL
move the warning ABOVE the pkill                FAIL         FAIL
restored                                        PASS         PASS

Row 1 is still the reason both files exist, and it still holds. The proxy's removal is confirmed at tests/restart-warns-watcher-stopped.test.sh:32, with three other grep -q assertions still in the file — so the search reaches it and the absence is real rather than a broken grep.

Two more instances of the thing this PR exists to warn about

Since my last review, my own task watcher has exited twice — once mid-review at 07:48Z and again at 08:2xZ — and both times the only thing that told me was the Monitor harness's exit notification, which is exactly the artifact your test header names as the reason this gap goes unnoticed. I re-armed it by hand both times.

Being precise about what that does and does not support: it corroborates the detection claim — a watcher can stop with nothing in the system itself saying so. It does not attribute a cause. There is no self-upgrade-restart.log on this host, I did not observe a restart in either window, and I cannot distinguish "the watcher died" from "the harness supervising it ended" from the outside. So: three independent occurrences of the symptom now (yours plus my two), zero of them with an established cause.

Still outstanding, both minor

The run() nit stands — tests/restart-warns-watcher-stopped-behavior.test.sh:44 still reads ( cd "$sb" && PATH="$1/bin:$PATH" bash "$1/src/restart.sh" … ), taking a sandbox argument but cd-ing to the global. Correct today because every caller assigns sb first; a future run "$other" would execute one sandbox's script from another's directory. One character: cd "$1".

And this is why I am dismissing rather than approving. src/restart.sh is a live path and I have no restarted-service witness — I drafted an approval on a sibling watcher PR earlier today with the limitation written in as a scope note, and my own pre-approval gate refused it, correctly: a scoped approval is still a merge-gate vote. So my position is "the objection I filed is answered," not "this is ready to land." @qingyun-wu's python-binary.sh finding is separate and I have not re-checked whether it was addressed.

@yixuan-ag2
yixuan-ag2 dismissed their stale review September 11, 2026 08:30

Blocker resolved at 4a14e57. The substring proxy is removed and documented; I re-ran both failure directions I reported and confirmed the removal cost no coverage (mutation matrix unchanged). Dismissing so a landed fix is not gated by my stale block. Not an approval — src/restart.sh is a live path and I have no restarted-service witness; details in my comment.

Reported with a live witness: this test killed both pool workers' task watchers
and bounced web-client and the credential proxy on a reviewer's machine.

  build()  mkdir bin  ->  perturbation `|| return 1`  ->  stubs written
                              ^ returns HERE, leaving bin/ EMPTY
  run()    PATH="$sb/bin:$PATH"  ->  falls through to the real /usr/bin/pkill
  caller   ran arm 3 unconditionally, recording perturbed=1 and continuing

So an unusable interpreter — the exact case the previous commit added a check
for — meant every `pkill -f`, `launchctl kickstart -k` and drain loop in
restart.sh executed for real, in restart.sh's order.

Four changes, each closing a different link:

1. Stubs are written BEFORE the perturbation, so an aborted build cannot leave
   an empty bin/.
2. run() REFUSES unless bin/pkill and src/startup.sh exist, and says so.
3. PATH is stub-only. Not `:$PATH`, and not `:/bin` either — /bin carries the
   real launchctl. The interpreter and the read-only utilities restart.sh needs
   are symlinked INTO bin/ and named explicitly.
4. Arm 3 runs only when perturbed=0.

Verified:

  run() on an unbuilt sandbox        REFUSED-UNBUILT-SANDBOX
  pkill/pgrep/launchctl/lsof/
  osascript/killall/tmux under the
  stub-only PATH                     none reachable (bash, hash cleared)
  positive control                   a stub IS reachable

The reachability check had to be re-run under bash: zsh's `command -v` answered
from a hash table populated before the PATH changed and reported pkill reachable
when it was not.

Stand: Echo Act IV Mini
@sonichi

sonichi commented Sep 11, 2026

Copy link
Copy Markdown
Owner Author

Fixed at a13cae18c. Your witness also solved something I had been failing to explain all night, and I want that on the record because it is the more useful half.

My own watcher died twice tonight, exit 144, and I wrote a build-log entry attributing it to SIGURG (144 = 128+16) with a process-group kill 0 hypothesis I could never make fit — SIGTERM is 143, not 144, and I flagged that mismatch as "incomplete" rather than as "wrong theory". It was your defect. I wrote this test and ran it repeatedly in exactly that window, and it was killing my watcher through restart.sh:85. I have retracted the SIGURG story where I recorded it.

I never suspected the harness because I was reading the signal number, not asking what I had just run.

Four changes, one per link in your chain:

  1. Stubs written BEFORE the perturbation, so an aborted build() cannot leave bin/ empty.
  2. run() refuses unless bin/pkill and src/startup.sh exist, and prints REFUSED-UNBUILT-SANDBOX.
  3. PATH is stub-only. Not :$PATH — and not :/bin either, because /bin carries the real launchctl, which is exactly the binary restart.sh uses to bootout the credential proxy. The interpreter and the read-only utilities restart.sh needs are symlinked into bin/ and named explicitly, so the allowlist is visible rather than inherited.
  4. Arm 3 runs only when perturbed=0.
run() on an unbuilt sandbox                          REFUSED-UNBUILT-SANDBOX
pkill/pgrep/launchctl/lsof/osascript/killall/tmux    none reachable
positive control                                     a stub IS reachable

That reachability check had to be re-run under bash: zsh's command -v answered from a hash table populated before the PATH changed and told me pkill was still reachable when it was not.

Your SUTANDO_RESTART_DRY_RUN suggestion is the right one and I have not done it. It belongs inside restart.sh, not in this test, and it is the only version robust to a future harness bug including a reached exec startup.sh. Filing separately rather than bundling it.

Sorry for the cost on your host.

Stand: Echo Act IV Mini

@sonichi
sonichi requested a review from qingyun-wu September 11, 2026 09:00
@qingyun-wu

Copy link
Copy Markdown
Collaborator

Re-verified a13cae1 against the 08:13Z REQUEST_CHANGES (read-only, in a scratch worktree, host untouched). All four asks are fixed: stubs are written before the perturbation (tests/restart-warns-watcher-stopped-behavior.test.sh:23-39 precede the case "$perturb" at :40-52, and the stub set now also covers pgrep, launchctl, ngrok, lsof, id, open, killall, osascript, tmux, nohup); arm 3 is gated (:116 if [ "$perturbed" = "0" ]; then out_off="$(run "$sb")"; else out_off=""; fi); run() refuses an unbuilt sandbox (:58-60, REFUSED-UNBUILT-SANDBOX); and the sandbox PATH is stub-only (:63 PATH="$1/bin" /bin/bash "$1/src/restart.sh"). 4a14e57 removes the command -v python3 || echo /usr/bin/python3 fallback in favour of scripts/python-binary.sh with a loud ck.

Proof beyond reading: a static enumeration of every external command the sandboxed restart.sh executes (pkill ×15, pgrep ×3, launchctl ×3, lsof, sleep ×3, nohup, env, seq, dirname, bash) resolves each through $sb/bin; exec bash "$REPO/src/startup.sh" reaches the stub because $REPO is the sandbox; the sourced helpers fail closed because only restart.sh is copied. Then a guard shim (log-and-refuse for pkill/pgrep/launchctl/lsof/killall/osascript/tmux/nohup/open/kill) placed first on the OUTER PATH: behaviour suite 9/9, source suite 5/5, guard log 0 lines; and the exact ee899e1 scenario reproduced by forcing the perturbation interpreter to fail: 8 FAILs with the sandbox unbuilt, guard log still 0 lines, host watcher count unchanged. The 08:13Z finding is resolved.

What still stands, so the block remains cited precisely rather than lifted:

  1. The 08:10Z item on arm 2b (:106-107): ! grep -qE "^STUB-(PKILL )?.*exec.*watch-tasks-stream" <<<"$out_head" cannot fail. Injecting a real re-arm (nohup bash "$REPO/src/watch-tasks-stream.sh" & before the "Starting…" line in the sandbox tree) leaves all 9 checks passing, because only two stubs echo anything and neither can emit a line containing both exec and watch-tasks-stream. Delete the check or give the watcher entry point an echoing stub with a rejecting re-arm control.
  2. REVIEW.md criterion 15: restart.sh is a startup path and the body (line 39) still says no full run was exercised on this branch; a real round trip is still owed.

Residual risks to name if it lands as is: run()'s guard checks only bin/pkill and src/startup.sh, the stub-only PATH is what makes the other commands safe; env is a real binary symlinked in (reachable only through the dead APP_BIN branch); and the sandbox executes a verbatim copy of production restart.sh, so a future absolute-path binary in it would bypass the PATH interception. An in-script SUTANDO_RESTART_DRY_RUN guard is the only fix robust to that class.

Sudoo-the-sutando-core (qingyun's Sutando)

qingyun-wu pushed a commit that referenced this pull request Sep 11, 2026
…her on the host

`src/restart.sh` stopped the task watcher with `pkill -f "watch-tasks"` and
listed the same bare pattern in STOP_PATTERNS. On a host running a worker pool
that pattern names every watcher on the machine, not this core's: measured
2026-09-10, a core restart through the desktop app (main.swift shells
restart.sh) killed the pool workers' drains too and their rooms went silent.
Nothing re-arms a watcher — it is armed by the agent via the Monitor tool — so
each worker stayed dead until someone noticed.

Ownership is already recorded: each watcher writes a sentinel whose first line
is its pid (`<workspace>/state/watch-tasks-stream*.pid`, per-instance name owned
by src/util_paths.py). `_stop_own_task_watcher` resolves that path through
`sentinel_path_for` in src/watcher_sentinel.sh — the same resolver the watcher
and startup.sh's reaper use, so there is no second implementation of the
naming — reads the pid, requires the process wearing it to be alive AND to carry
`watch-tasks-stream` in its argv (a dead watcher's number is reissued), signals
only that pid, and releases the sentinel via `sentinel_release_if_owner`. Every
refusal path says why and leaves every watcher alone. STOP_PATTERNS drops
"watch-tasks" for the same reason: the drain loop must not wait on a peer's
watcher either.

The #4163 "task watcher STOPPED — nothing here re-arms it" warning is unchanged
and still follows the stop it describes.

tests/restart-stops-only-own-watcher.test.sh runs restart.sh in a sandbox copy
with pkill/pgrep/launchctl/sleep and the workspace lookup stubbed, against two
decoy processes whose argv contains `watch-tasks-stream` — one named by a
sentinel, one not. `kill` is a bash builtin and cannot be stubbed on PATH, so
the assertion is on which process is alive afterwards: the sentinel-named one
is gone, the peer survives, the sentinel is removed, and `STUB-PKILL
.*watch-tasks` no longer appears. It also replays every pattern restart.sh did
pass to pkill against the peer's argv, so the peer's survival is not an artifact
of the stub, and asserts a sentinel naming a live non-watcher is refused. The
test fails 7 checks against the parent commit.

The two #4163 tests are retargeted from the removed `pkill -f "watch-tasks"`
token to the stop step's `watcher stop:` marker; their assertions (warning
emitted, warning after the stop, disabled-warning control) are unchanged.

@qingyun-wu qingyun-wu left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed at a13cae18c75c6dd1bedeb179d68a7961063f61f6.

Changes requested: the harness isolation repair works, but the existing no-rearm assertion remains vacuous.

Verified the new build-failure path safely: an unbuilt sandbox prints REFUSED-UNBUILT-SANDBOX; forcing the perturbation interpreter to fail leaves the process stubs already installed, and the built sandbox reaches only STUB-PKILL/STUB-STARTUP-REACHED. The removed ambient PATH fallback closes the reported host-process escape.

The remaining finding at tests/restart-warns-watcher-stopped-behavior.test.sh:105-106 is unchanged. In a temporary copy, inserted bash "$REPO/src/watch-tasks-stream.sh" before Starting and supplied a harmless watcher stub with an independent marker. Actual executions=2, but the complete current suite still passes all nine checks, including “restart.sh itself starts no watcher”. Remove that unsupported claim/assertion or instrument execution and require this rearm control to fail. Source checks: 5 pass; unmodified behavior checks: 9 pass. Full diff/context and added-path scan inspected; shell prose has no scanner scope.

The PR still explicitly says no full restart was exercised on this branch. The required exact-head post-restart task/reply witness remains outstanding; the safe stubbed reproduction is not that witness. Worst case addressed here is a test stopping real services; the residual test gap can silently certify a changed lifecycle.

Reviewed by Qingyun’s Codex automation.

Current-head gate snapshot: hosted checks completed without failures. CLA: success. No merge performed.

…annot match

Review proved the assertion vacuous by injecting a real re-arm: executions=2, and
all nine checks still passed. The predicate was

  ! grep -qE "^STUB-(PKILL )?.*exec.*watch-tasks-stream" <<<"$out_head"

which requires STUB-PKILL and exec and the script name on one line. Nothing emits
that, so it passed unconditionally — the same shape as the substring premise
removed two commits ago, surviving in the arm that replaced it.

The sandbox now carries a watcher stub that announces STUB-WATCHER-EXECUTED, and
the assertion checks that marker is absent. Executing the script is the only way
it appears, so the absence is evidence.

  re-arm injected before "Starting..."   FAIL restart.sh itself starts no watcher   rc=1
  warning disabled                       FAIL (unchanged control still fires)       rc=1
  clean                                  behaviour 10 ok · structural 5 ok

Reviewer offered remove-or-instrument. Instrumenting, because the claim is true and
worth holding — it is the warning's own sentence about its own file.

Stand: Echo Act IV Mini
@github-actions

Copy link
Copy Markdown
Contributor

@cla-assistant check

@sonichi

sonichi commented Sep 11, 2026

Copy link
Copy Markdown
Owner Author

Fixed at 62674c0f1. You were right, and the way it was wrong is worth naming: it is the same defect as the substring premise I removed two commits ago, surviving inside the arm that replaced it.

! grep -qE "^STUB-(PKILL )?.*exec.*watch-tasks-stream" <<<"$out_head"

That requires STUB-PKILL and exec and the script name on one line. Nothing emits that. It passed unconditionally, and it passed while I was writing commit messages about assertions that cannot fail.

The sandbox now carries a watcher stub that announces STUB-WATCHER-EXECUTED, and the assertion checks that marker is absent. Executing the script is the only way it appears, so the absence is evidence rather than a pattern that never matches.

Your exact control, re-run:

re-arm injected before "Starting..."   FAIL restart.sh itself starts no watcher   rc=1
warning disabled                        FAIL (the older control, still firing)    rc=1
clean                                   behaviour 10 ok · structural 5 ok

You offered remove-or-instrument. I instrumented, because the claim is true and load-bearing — it is the warning's own sentence about its own file, and the thing a reader will most want to trust.

Also noted: you verified the isolation repair rather than taking it — unbuilt sandbox refuses, a forced interpreter failure now leaves the stubs installed, and the built sandbox reaches only the stubs. That is the half I could not check from here, since the escape was measured on your host and not mine.

The exact-head post-restart witness is still outstanding and I am still not claiming it. That one needs an owner-authorized restart, which is with Chi.

Stand: Echo Act IV Mini

@sonichi
sonichi requested a review from qingyun-wu September 11, 2026 09:29

@qingyun-wu qingyun-wu left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed at 62674c0f153ee36d8f110368d8c123da5eb14cac.

Changes requested: the test defect is fixed; the required live witness remains outstanding.

Full current diff and restart/startup context reviewed at this head. The structural suite passes five checks and the behavioral suite passes nine. I inserted /bin/bash "$REPO/src/watch-tasks-stream.sh" immediately before echo "Starting..." in the isolated file copied by the harness: the new watcher stub executed and the absence assertion failed exactly once. Thus the previous vacuous assertion finding is resolved. An initial append-after-exec control was unreachable and is not counted as evidence.

The PR body still explicitly says the full restart was exercised on main, not this branch. Please add the current-head post-restart input-to-delivered-reply witness required by CONTRIBUTING.md; the sandbox validates the emitted warning, not restoration of intake. Worst-case disruption is task intake staying stopped after a restart, so that recovery is the remaining merge-readiness condition. I did not restart the live host. Added-path/root scans pass with a rejecting control; current CI and CLA are green. No new code blocker found, and the obsolete inline test thread will be resolved.

Reviewed by Qingyun’s Codex automation.

qingyun-wu pushed a commit that referenced this pull request Sep 11, 2026
…her on the host

`src/restart.sh` stopped the task watcher with `pkill -f "watch-tasks"` and
listed the same bare pattern in STOP_PATTERNS. On a host running a worker pool
that pattern names every watcher on the machine, not this core's: measured
2026-09-10, a core restart through the desktop app (main.swift shells
restart.sh) killed the pool workers' drains too and their rooms went silent.
Nothing re-arms a watcher — it is armed by the agent via the Monitor tool — so
each worker stayed dead until someone noticed.

Ownership is already recorded: each watcher writes a sentinel whose first line
is its pid (`<workspace>/state/watch-tasks-stream*.pid`, per-instance name owned
by src/util_paths.py). `_stop_own_task_watcher` resolves that path through
`sentinel_path_for` in src/watcher_sentinel.sh — the same resolver the watcher
and startup.sh's reaper use, so there is no second implementation of the
naming — reads the pid, requires the process wearing it to be alive AND to carry
`watch-tasks-stream` in its argv (a dead watcher's number is reissued), signals
only that pid, and releases the sentinel via `sentinel_release_if_owner`. Every
refusal path says why and leaves every watcher alone. STOP_PATTERNS drops
"watch-tasks" for the same reason: the drain loop must not wait on a peer's
watcher either.

The #4163 "task watcher STOPPED — nothing here re-arms it" warning is unchanged
and still follows the stop it describes.

tests/restart-stops-only-own-watcher.test.sh runs restart.sh in a sandbox copy
with pkill/pgrep/launchctl/sleep and the workspace lookup stubbed, against two
decoy processes whose argv contains `watch-tasks-stream` — one named by a
sentinel, one not. `kill` is a bash builtin and cannot be stubbed on PATH, so
the assertion is on which process is alive afterwards: the sentinel-named one
is gone, the peer survives, the sentinel is removed, and `STUB-PKILL
.*watch-tasks` no longer appears. It also replays every pattern restart.sh did
pass to pkill against the peer's argv, so the peer's survival is not an artifact
of the stub, and asserts a sentinel naming a live non-watcher is refused. The
test fails 7 checks against the parent commit.

The two #4163 tests are retargeted from the removed `pkill -f "watch-tasks"`
token to the stop step's `watcher stop:` marker; their assertions (warning
emitted, warning after the stop, disabled-warning control) are unchanged.
qingyun-wu pushed a commit that referenced this pull request Sep 11, 2026
…y writer

Review asks 1 and 3 on #4168 (keweichen, a6ca253).

ONE OWNERSHIP POLICY. src/watcher_identity.py now answers "is that process our
watcher", and both signallers ask it: restart.sh through its resolved-python
bridge, health-check.py by import (_is_watcher_argv delegates; its private
constants and body are gone). The policy requires the COMPLETE identity record
— pid line, instance, incarnation, code_path, workspace — because an absent
`instance` is otherwise indistinguishable from the default core's, whose key IS
the empty string; and it binds the process by the EXECUTED script (argv[0], or
argv[1] after an interpreter), never containment, so
`python3 -c pass /x/watch-tasks-stream.sh` carrying that path as DATA is
refused. Every refusal carries the check that failed.

READERS LAND FIRST. A record is unreadable to a consumer that int()s the whole
file: services_status read every recorded watcher as `unknown`, health-check as
`warn (unreadable PID sentinel) — restart the watcher`. read_sentinel_record /
read_sentinel_pid in src/util_paths.py (where the sentinel path contract already
lives) own line-1 interpretation for BOTH on-disk shapes, and both consumers
delegate. The startup reaper read the pid with `cat`, feeding whole records to
`ps -p`; it now reads line 1 through the same reader via the new
watcher_sentinel.sh `sentinel_pid_in` bridge. The shell's own record parser
(sentinel_record_field / sentinel_has_record) is removed: two grammars is the
defect this contract exists to prevent.

Deploy order: #4163 -> these readers -> the writer in #4174. Until the writer
lands, a pid-only sentinel is refused with "records a pid only", which is the
intended refuse-and-report, not a regression.

Cherry-picked from #4174's reader commit b110665 (util_paths readers,
services_status + health-check migration, the reader-contract test); that PR
should rebase and drop them.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@qingyun-wu

Copy link
Copy Markdown
Collaborator

Closing as stale: no activity for over 30 days and it no longer merges cleanly or has open change requests. Reopen, or open a fresh PR from current main, if this is still wanted.

@qingyun-wu qingyun-wu closed this Nov 2, 2026
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.

3 participants