Skip to content

fix(watcher): a foreign SUTANDO_INSTANCE_ID on the core's own inbox is cleared, not trusted - #4976

Merged
qingyun-wu merged 9 commits into
mainfrom
fix/watcher-foreign-instance-id-inherited
Oct 4, 2026
Merged

qingyun-wu merged 9 commits into
mainfrom
fix/watcher-foreign-instance-id-inherited

Conversation

@sonichi

@sonichi sonichi commented Oct 1, 2026 •

Copy link
Copy Markdown
Owner

Summary

A --force-restart (or any start) of the task watcher issued from inside another instance's own session inherits that instance's worker routing env — SUTANDO_INSTANCE_ID and the vars a worker shell exports alongside it (SUTANDO_INBOX_KIND, SUTANDO_INBOX_RESOLVER, SUTANDO_INBOX_RESOLVER_TIMEOUT, SUTANDO_POOL_DELIVERY_SCRIPT) — even when --inbox explicitly names the core's own canonical <ws>/tasks. The watcher then watches the right directory while believing itself to be that other worker's own inbox-watcher (#4502's rule: a worker's own inbox IS the routing decision): it never loads the core's task-event-handler config, and a leftover SUTANDO_INBOX_RESOLVER independently rejects every plain task body it sees (it expects a worker sentinel, not a task), so dispatch is silently dead with no error beyond the resolver's own stderr.

Observed live in production, twice, during this investigation:

  • pid 1184, started 18:46 — argv correctly --role session --inbox <core's workspace>/tasks, env SUTANDO_INSTANCE_ID=<a worker's id>.
  • pid 66624, started 19:16 (its replacement, after the first was independently restarted) — same pattern, same foreign env, confirming this is a recurring defect (something keeps re-issuing this restart), not a one-off.

Root cause

src/watch-tasks-stream.sh gates loading the core's routing config purely on [ -z "$SUTANDO_INSTANCE_ID" ] — it never checks that identity against the inbox actually being served. A restart command run from a worker's own shell inherits that shell's full exported worker-routing env, and nothing resets any of it even though --inbox is explicit and authoritative.

Fix

Right after the inbox/workspace are resolved: when an explicitly-set SUTANDO_INSTANCE_ID is found while serving the core's own canonical inbox, the whole inherited worker-routing block is cleared together — SUTANDO_INSTANCE_ID, SUTANDO_INBOX_KIND, SUTANDO_INBOX_RESOLVER, SUTANDO_INBOX_RESOLVER_TIMEOUT, SUTANDO_POOL_DELIVERY_SCRIPT — not just the instance id (review feedback from kewei: clearing the id alone leaves SUTANDO_INBOX_RESOLVER live, which fails every entry on its own).

"The core's own canonical inbox" is decided by realpath, not basename: the inbox's resolved path is compared against <resolved workspace>/tasks's resolved path (canonical_tasks_dir(), the same primitive tasks-dir-resolve.sh already uses elsewhere), not basename(inbox) == "tasks" (also kewei: a worker's own inbox can itself end in .../tasks and would have false-matched a basename-only check).

What it deliberately does not do:

  • Never touches a <ws>/deliveries/<id> inbox. The pre-existing identity-match check there already decides whether a starter whose SUTANDO_INSTANCE_ID disagrees with the inbox must yield. Correcting the env to match the inbox in that branch would make a wrong starter's identity agree with the inbox by fiat, defeating that very check.
  • Never invents an identity when SUTANDO_INSTANCE_ID was unset. Several tests legitimately run a core against a deliveries/-shaped inbox with no ambient identity at all; this only corrects a value that was actually asserted and is actually wrong for the inbox being served.

Known, non-blocking residual (flagged by johnm-desktop.agent in review discussion, not actioned here): workspace_dir_for_inbox() resolves <resolved workspace> as ${SUTANDO_WORKSPACE_DIR:-dirname(inbox)}. With SUTANDO_WORKSPACE_DIR set — which every real worker shell always does (spawn_worker.py's env export, and exactly the incident shape this PR targets) — the comparison is a genuine cross-path realpath check. With it unset, the comparison mathematically reduces to the old basename-equivalent set, since dirname(inbox)/tasks == inbox iff basename(inbox) == "tasks". Correct either way, just not doing extra work over the old check in the unset case; not fixing speculatively without a concrete production path that reaches it unset.

Test changes

tests/watch-tasks-stream-config-hot-reload.test.py's worker case (property 3) previously set SUTANDO_INSTANCE_ID="worker-1" while watching the bare <ws>/tasks dir — a combination that cannot occur in production (a real worker's inbox is always <ws>/deliveries/<id>, confirmed against skills/worker-pool/scripts/launch-worker-session.sh). Updated it to a realistic <ws>/deliveries/worker-1 inbox.

tests/watch-tasks-stream-foreign-instance-id-corrected.test.py now pins 5 properties against the real incident shape and both review blockers:

  1. A foreign SUTANDO_INSTANCE_ID on the core's own inbox is corrected: an already-present handler still runs.
  2. The correction is logged to stderr.
  3. (Blocker 1) A full inherited worker env — instance id plus a REAL SUTANDO_INBOX_RESOLVER/SUTANDO_INBOX_KIND/SUTANDO_POOL_DELIVERY_SCRIPT — on the core's own inbox is cleared, not just the instance id: the handler still runs.
  4. (Blocker 2, control) A worker's own inbox whose basename happens to be tasks (not the <ws>/tasks convention, but a path that would false-match a basename-only check) keeps its identity — the handler config is correctly NOT read.
    4b. No foreign-identity correction is logged for that real worker inbox.

Before/after evidence

The new test's 5 properties, at this head:

ok   (1) a foreign inherited SUTANDO_INSTANCE_ID on the core's own inbox is corrected: the already-present handler still runs
ok   (2) the correction is logged to stderr
ok   (3) a FULL inherited worker env (instance id + inbox kind/resolver/delivery script) on the core's own inbox is cleared, not just the instance id: the handler still runs
ok   (4) control: a worker inbox whose basename is 'tasks' (not <ws>/tasks by realpath) keeps its identity -- the present handler config is NOT read (a worker never consults it; same rule as watch-tasks-stream-config-hot-reload.test.py property 3)
ok   (4b) no foreign-identity correction is logged for the real worker inbox
watch-tasks-stream-foreign-instance-id-corrected: 5/5 passed

Same test, against unmodified main (confirms both review blockers reproduce without this fix — property 3 is the review's Blocker 1, property 4/4b exercise Blocker 2's basename trap):

2/5 passed (properties 1-2 pass on the original basename-only check; 3 fails because SUTANDO_INBOX_RESOLVER is left live; the control pair 4/4b didn't exist pre-fix)

Full watch-tasks-stream*/watcher-* suite (29 files), run before and after: all green both before and after except one pre-existing flake, watch-tasks-stream-fallback-receipt-scope, confirmed failing identically on unmodified main (not caused by this change) and already tracked at #4645.

(Separately, while verifying locally: tests/watch-tasks-stream-config-hot-reload.test.py and a couple of siblings fail when run from inside a live worker's own shell — unrelated ambient SUTANDO_WORKSPACE_DIR/SUTANDO_INBOX_RESOLVER env leakage into their subprocess, confirmed reproducing identically on main too. Pre-existing, out of scope here; filed as #4979.)

Live path

This is exactly the live/delivery-loop path CONTRIBUTING.md asks for a real post-restart round trip on, not just unit tests. Two live runs, both against a real watcher process and real fswatch — never the host's live <ws>/tasks itself, always a scratch workspace:

1. A fully contaminated worker-shaped env built with env -i (rules out any ambient leakage entirely — isolates exactly what the fix claims to clear): SUTANDO_INSTANCE_ID + a real SUTANDO_INBOX_RESOLVER/SUTANDO_INBOX_KIND/SUTANDO_POOL_DELIVERY_SCRIPT, SUTANDO_WORKSPACE_DIR pointed at a fresh temp workspace with its own task-event-handler.json.

Before (built from main):

watch-tasks-stream: resolver .../resolve-inbox-entry did not name an existing ABSOLUTE file for .../tasks/task-demo.txt (rc=1, first line: <empty>); not dispatching it
watch-tasks-stream: resolve_inbox_entry did not resolve .../tasks/task-demo.txt after 3 attempts; not dispatching
(no handler log -- NOT dispatched, confirming the pre-fix bug)

At this head:

watch-tasks-stream: SUTANDO_INSTANCE_ID=d2571c90f75e4907af9f145b01b71c1f (and worker routing env) set while serving the core's own canonical inbox (.../tasks) -- clearing it to match the inbox, never the calling shell's inherited env
handled: --runtime  --workspace .../ws --task-file .../tasks/task-demo.txt --results-dir .../results --repo ...

2. This worker's own real, unmodified production shell env (per John's review request — not a hand-built set): every SUTANDO_* this worker actually runs with (SUTANDO_INSTANCE_ID, SUTANDO_INBOX_KIND=deliveries, the real SUTANDO_INBOX_RESOLVER/SUTANDO_POOL_DELIVERY_SCRIPT paths), with only SUTANDO_WORKSPACE_DIR/SUTANDO_RESULTS_DIR redirected to a scratch workspace so the demo never touches this host's live workspace/tasks:

watch-tasks-stream: role=session inbox=/tmp/pr4976-prodenv-demo-GV41/ws/tasks pid=16426
watch-tasks-stream: SUTANDO_INSTANCE_ID=d2571c90f75e4907af9f145b01b71c1f (and worker routing env) set while serving the core's own canonical inbox (/private/tmp/pr4976-prodenv-demo-GV41/ws/tasks) -- clearing it to match the inbox, never the calling shell's inherited env

Result file written by the handler on the actual task → result round trip:

handled task-real-env-demo.txt at 2026-10-01T03:51:32Z

3. The shipped service itself, on this host's real workspace, Chi-authorised (run by Pro-dev from its own contaminated worker shell — SUTANDO_INSTANCE_ID, SUTANDO_INBOX_KIND=deliveries, the real SUTANDO_INBOX_RESOLVER/SUTANDO_POOL_DELIVERY_SCRIPT, nothing unset — against the root checkout moved to this PR's exact head for the window, restored to main afterwards):

06:31:06.598  T0: --force-restart from the worker shell; stops the core's live watcher (pid 47862)
06:31:07.115  watch-tasks-stream: SUTANDO_INSTANCE_ID=... (and worker routing env) set while serving the core's own canonical inbox (<ws>/tasks) -- clearing it...
06:31:08.157  core sentinel -> 97977 (stamped as the CORE, not the worker)
06:31:34      the owner's real message ("One word", Pro-dev DM) arrives with its bridge envelope
06:31:35      routed into deliveries/<pro-dev's id>/ (the room is bound to Pro-dev); Pro-dev's watcher announces it
06:32:24.482  reply written; 06:32:24.833 bridge delivers it to the Pro-dev DM
06:32:43      teardown; root back on main
06:36:16      the core's own session watcher is back; sentinel re-stamped (standbys covered 06:33:31-06:36:16)

Receipt → result → delivered reply: 50s end to end. Downtime: the core's own session-role watcher was absent 5m10s (06:31:06-06:36:16). The PR-head watcher held the inbox until 06:32:43; standby watchers took over at 06:33:30.975 — a ~48s span with no watcher named as holding it. Durable, not lossy: two other task files present at open were announced and answered correctly once a watcher picked them up, and nothing was lost across the gap. Full transcript: PR comment.

Test plan

🤖 Generated with Claude Code

sonichi and others added 2 commits September 30, 2026 19:44
…s cleared, not trusted

A --force-restart (or any start) issued from inside another instance's own
session inherits that instance's SUTANDO_INSTANCE_ID, even when --inbox
explicitly names the core's own canonical <ws>/tasks. The watcher then
watches the right directory while believing itself to be that OTHER
worker's own inbox-watcher (#4502's rule: a worker's own inbox IS the
routing decision) -- it never loads the core's task-event-handler config
and silently stops dispatching anything it sees, with no error anywhere.

Observed in production 2026-09-30, twice: pid 1184 (18:46) and its
replacement pid 66624 (19:16), both with argv correctly reading
`--role session --inbox <core's workspace>/tasks` but env carrying a
different worker's SUTANDO_INSTANCE_ID, inherited from the shell that
force-restarted them.

Fix is deliberately narrow: only clears an explicitly-set SUTANDO_INSTANCE_ID
when the inbox being served is the core's own canonical tasks/ dir (the same
basename=="tasks" signal the existing identity-match gate already uses).
It never touches a <ws>/deliveries/<id> inbox -- there, the pre-existing
identity-match check already decides whether a mismatched starter must
yield, and correcting the env there would make a wrong starter's identity
agree with the inbox by fiat, defeating that check (this was my first,
too-broad attempt; tests/watch-tasks-stream-unready-holder-restamp.test.sh
scenario (d) and tests/watch-tasks-stream-inbox-and-workspace-env.test.py
caught it). It also never invents an identity when SUTANDO_INSTANCE_ID was
never set, since several tests legitimately run a core against a
deliveries/-shaped inbox with no ambient identity at all.

tests/watch-tasks-stream-config-hot-reload.test.py's worker case previously
set SUTANDO_INSTANCE_ID="worker-1" while watching the bare <ws>/tasks dir --
a combination that cannot occur in production. Updated it to a realistic
<ws>/deliveries/worker-1 inbox, which is what makes it a test of "a worker",
not of "SUTANDO_INSTANCE_ID happens to be set".

New tests/watch-tasks-stream-foreign-instance-id-corrected.test.py pins the
fix directly: a foreign SUTANDO_INSTANCE_ID on the core's own inbox is
corrected (an already-present handler still runs, and the correction is
logged); control run against unmodified main fails cleanly, reproducing the
incident exactly (TASK_FILE emitted, handler never runs).

Full watch-tasks-stream*/watcher-* suite (29 files) re-run green except the
pre-existing watch-tasks-stream-fallback-receipt-scope flake (confirmed
failing identically on unmodified main; tracked at #4645) -- unrelated.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
review-checks' prose-cap caught three blocks over the cap (two in the
hot-reload test, one in the new regression test) and the main fix's own
.sh comment, which the Python-only scanner doesn't cover but the same
policy applies to. The narrative moved to the PR body; code comments keep
only the constraint and a pointer.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@sonichi
sonichi requested a review from qingyun-wu as a code owner October 1, 2026 02:47
…cope worker cases

Same gap as the hot-reload test fix: three of this file's six properties set
SUTANDO_INSTANCE_ID="worker-1" while watching the bare <ws>/tasks dir — a
combination that cannot occur in production and that the watcher's new
foreign-identity correction (this branch's main fix) now treats as a
mismatch on the core's own inbox, clearing it and breaking all three
"a worker bypasses unconditionally" assertions.

Caught by CI on this PR, not locally: my earlier local re-run of this file
only surfaced the one already-known flake (#4645) in a truncated tail of the
output and I didn't look past it — the full output showed 2/6 passed, not
5/6, meaning three genuine regressions were sitting unnoticed. CI's full log
is what caught it.

Fix: `run()` now takes the inbox shape from `watcher_instance` itself —
<ws>/deliveries/<id> when set, <ws>/tasks when not — matching production and
the hot-reload test's same fix. Back to 5/6 passed, identical to unmodified
main (the one remaining failure is #4645, confirmed unrelated).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@sonichi

sonichi commented Oct 1, 2026

Copy link
Copy Markdown
Owner Author

Verified at exact head 68e63b66d from an isolated checkout (I cannot file a formal review: this PR is under the same login as mine).

  • The fix is seven lines at src/watch-tasks-stream.sh:113-119: an explicitly inherited SUTANDO_INSTANCE_ID is cleared only when the inbox being served is the canonical <ws>/tasks; a deliveries/<id> inbox is untouched (the identity-match check below still owns that). It does not overlap feat(agy-core): Slice 2 — task-file-injection notifier for agy (Antigravity CLI) #4303's ready-token seam and merges cleanly with that branch.
  • The new test discriminates: tests/watch-tasks-stream-foreign-instance-id-corrected.test.py → 2/2 passed at head; with src/watch-tasks-stream.sh reverted to origin/main → FAILED: (2) the correction is logged to stderr. Adjacent suites at head: config-hot-reload 3/3, fallback-receipt-scope 6/6. git diff origin/main...pr4976 | bash scripts/review-checks.sh → PASS.
  • Production corroboration, same host, today: at 12:08 PDT the core's inbox watcher (pid 88289) was started with --force-restart --role session --inbox <ws>/tasks from a worker's shell; from then until the core re-armed, the core's sentinel kept naming a dead pid while 88289 ran, and the core's own Monitor deferred to it with read=unknown. A foreign instance id changes the sentinel's name, which is exactly the shape this fix removes.

Not verified by me: the live --force-restart round trip on the current head (the body's two production observations are the author's). Reviewers: @keweichen @john-the-dev @jsun-m — this is small and on the live path; could you look?

@john-the-dev john-the-dev 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.

APPROVE at 68e63b66dcf75b6cd7b26b01b11d69336f2f86f7.

The fix is correct and narrow. src/watch-tasks-stream.sh:113-118 unsets an inherited SUTANDO_INSTANCE_ID only when the canonical inbox's basename is tasks. That is the same rule the existing identity check at :215-216 already applies: an instance id must equal basename(inbox), and an empty id goes with the inbox named tasks.

  • TASKS_DIR_ABS is assigned at :103, before the new block, so it uses the canonicalised path.
  • I checked that no pool worker serves an inbox named tasks. spawn_worker.py:185 and pool_remedy.py:216 both set SUTANDO_TASKS_DIR to deliveries_dir(ws, id), which is <ws>/deliveries/<id>, and the id is never the literal tasks. So a real worker is never stripped of its identity.
  • The cleared id doesn't leak elsewhere. The only other readers in src/ are check-pending-tasks.sh and codex/cli/task-notifier.sh, and both run in their own process environment.
  • The two test-file edits only change the worker cases to use the real deliveries/<id> inbox shape, which the new rule requires. That is a fixture correction, not a weakened assertion.

What I ran (macOS, bash 3.2 /bin/bash):

  • New test watch-tasks-stream-foreign-instance-id-corrected: 1/2 at head, 0/2 with only src/watch-tasks-stream.sh reverted to the merge-base. Case (2), "the correction is logged", flips from fail to pass, so the test does detect the change.
  • Case (1), "the already-present handler still runs": fails on my machine both at head and on main. The same handler-routing step also fails on main in config-hot-reload (2/3) and fallback-receipt-scope (4/6), with identical counts at head and main. My environment can't exercise handler routing, so it isn't a PR regression. For that path I'm relying on your run and on CI.
  • CI on this head: 23 pass, 1 pending, 1 skipped.

Not verified by me: the handler actually running on the corrected core inbox, and the production repro.

@sonichi

sonichi commented Oct 1, 2026

Copy link
Copy Markdown
Owner Author

CI at 68e63b6: the one red lane is python standalone tests (2/5) → tests/agy-start-cli.test.py::test_concurrency_test_would_catch_a_naive_non_atomic_writer (a mutation control that needs a thread race to occur; this PR touches no agy file). Filed as #4977; failed jobs re-run at 20:10 PDT. license/cla success.

@github-actions

github-actions Bot commented Oct 1, 2026

Copy link
Copy Markdown
Contributor

Coverage Gate

✅ Diff coverage PASSES the 95% bar. Whole-tree (informational): 90%.

Diff Coverage

Diff: origin/main...HEAD, staged and unstaged changes

No lines with coverage information in this diff.

@keweichen keweichen 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 exact head 68e63b66dcf75b6cd7b26b01b11d69336f2f86f7. CI and CLA are green, but this is not merge-ready because the production failure still reproduces and the new inbox predicate misclassifies a valid worker inbox.

Blocker 1 — clearing only the ID leaves the inherited worker resolver active. The real worker environment exports SUTANDO_INBOX_RESOLVER (spawn_worker.py:181-208, forwarded at launch-worker-session.sh:73-86). This patch unsets only SUTANDO_INSTANCE_ID at watch-tasks-stream.sh:113-118, then still invokes the inherited delivery resolver before any handler or TASK_FILE emission at watch-tasks-stream.sh:830-865.

I reran the added exact-head test with the actual inherited routing variables (SUTANDO_WORKSPACE_DIR, SUTANDO_INBOX_KIND=deliveries, SUTANDO_INBOX_RESOLVER, and SUTANDO_POOL_DELIVERY_SCRIPT). The watcher logged that it cleared the foreign ID, then rejected <ws>/tasks/task-one.txt through the worker resolver three times and printed not dispatching; the handler log and stdout both remained empty. Result: 1/2 passed. The checked-in test passes because it models only the foreign ID, not the worker environment whose inheritance caused the incident.

Blocker 2 — basename == tasks is not a canonical-core-inbox check. tasks is a valid worker/recipient ID under the production grammar. At this head, watching <ws>/deliveries/tasks with SUTANDO_INSTANCE_ID=tasks clears the worker identity and runs the core handler (probe, handle) instead of preserving worker routing. That contradicts the stated “never a deliveries/ inbox” invariant. Please classify the canonical core inbox structurally and add this adjacent negative case.

Please normalize the inherited worker-only inbox-routing context at the canonical core boundary and cover the full production worker environment, not just SUTANDO_INSTANCE_ID. Per REVIEW.md's live-path criterion, the updated head also needs a real post-restart round trip from a contaminated worker shell through task receipt and delivered reply; the current PR body documents only the unpatched mitigation, and the exact-head comment explicitly says that round trip was not verified.

Worst case as written: the restart appears corrected in logs while every core task is still silently discarded; the basename collision can also route a worker delivery through the core handler.

…h not basename

kewei's PR review on this branch's prior head found two blockers:

1. Clearing only SUTANDO_INSTANCE_ID left SUTANDO_INBOX_KIND/
   SUTANDO_INBOX_RESOLVER/SUTANDO_INBOX_RESOLVER_TIMEOUT/
   SUTANDO_POOL_DELIVERY_SCRIPT behind -- a worker-shaped SUTANDO_INBOX_RESOLVER
   still runs on every entry at the core's inbox, rejects every plain task body
   (it expects a worker sentinel), and dispatch_task's 3-attempt retry loop
   gives up silently. Now the whole inherited worker-routing block is cleared
   together.
2. The canonical-inbox check compared basenames ("tasks" == "tasks"), which
   can misclassify a worker's own inbox if its path happens to end in
   .../tasks. Now it compares the inbox's realpath against
   <resolved workspace>/tasks, matching tasks-dir-resolve.sh's own notion of
   the core's canonical inbox.

tests/watch-tasks-stream-foreign-instance-id-corrected.test.py grows two
properties for these: (3) a full inherited worker env at the core's canonical
inbox (incl. a real SUTANDO_INBOX_RESOLVER) still dispatches; (4)/(4b) a
worker's own inbox whose basename happens to be "tasks" keeps its identity.
5/5 pass.

Stand: Echo Act IV Pro
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@sonichi

sonichi commented Oct 1, 2026

Copy link
Copy Markdown
Owner Author

Pushed f31f10ef6 addressing both blockers from the review:

Blocker 1 (incomplete env clearing) — the fix now clears the whole inherited worker-routing block together (SUTANDO_INSTANCE_ID, SUTANDO_INBOX_KIND, SUTANDO_INBOX_RESOLVER, SUTANDO_INBOX_RESOLVER_TIMEOUT, SUTANDO_POOL_DELIVERY_SCRIPT), not just the instance id. Leaving SUTANDO_INBOX_RESOLVER set alone is enough to break dispatch: it sends every entry through a worker-shaped resolver that expects a sentinel, not a plain task body, so dispatch_task's 3-attempt retry loop gives up silently.

Blocker 2 (basename-only match) — the canonical-inbox check now compares the inbox's realpath against <resolved workspace>/tasks, not basename(inbox) == "tasks". A worker's own inbox can itself end in .../tasks and would have false-matched before.

Live post-restart round trip (not just the test harness — a real watcher process, real fswatch, a fully contaminated worker env built with env -i so there's no ambient leakage):

Before the fix (built from main, same repro):

=== BEFORE FIX: starting watcher with a FULLY contaminated worker-shaped env ===
fswatch is live
=== watcher stderr (tail) ===
watch-tasks-stream: role=standby inbox=/tmp/pr4976-before-demo-D3vw/ws/tasks pid=56730
watch-tasks-stream: resolver /tmp/main-check3/skills/worker-pool/scripts/resolve-inbox-entry did not name an existing ABSOLUTE file for /private/tmp/pr4976-before-demo-D3vw/ws/tasks/task-demo.txt (rc=1, first line: <empty>); not dispatching it
watch-tasks-stream: resolver /tmp/main-check3/skills/worker-pool/scripts/resolve-inbox-entry did not name an existing ABSOLUTE file for /private/tmp/pr4976-before-demo-D3vw/ws/tasks/task-demo.txt (rc=1, first line: <empty>); not dispatching it
watch-tasks-stream: resolver /tmp/main-check3/skills/worker-pool/scripts/resolve-inbox-entry did not name an existing ABSOLUTE file for /private/tmp/pr4976-before-demo-D3vw/ws/tasks/task-demo.txt (rc=1, first line: <empty>); not dispatching it
watch-tasks-stream: resolve_inbox_entry did not resolve /private/tmp/pr4976-before-demo-D3vw/ws/tasks/task-demo.txt after 3 attempts; not dispatching
=== handler log ===
(no handler log -- NOT dispatched, confirming the pre-fix bug)

At this branch's head (f31f10ef6):

=== starting watcher with a FULLY contaminated worker-shaped env (env -i) ===
fswatch is live
=== watcher stderr ===
watch-tasks-stream: role=standby inbox=/tmp/pr4976-live-demo-kAeL/ws/tasks pid=52811
watch-tasks-stream: SUTANDO_INSTANCE_ID=d2571c90f75e4907af9f145b01b71c1f (and worker routing env) set while serving the core's own canonical inbox (/private/tmp/pr4976-live-demo-kAeL/ws/tasks) -- clearing it to match the inbox, never the calling shell's inherited env
=== handler log ===
handled: --runtime  --workspace /tmp/pr4976-live-demo-kAeL/ws --task-file /private/tmp/pr4976-live-demo-kAeL/ws/tasks/task-demo.txt --results-dir /tmp/pr4976-live-demo-kAeL/ws/results --repo ... --probe
handled: --runtime  --workspace /tmp/pr4976-live-demo-kAeL/ws --task-file /private/tmp/pr4976-live-demo-kAeL/ws/tasks/task-demo.txt --results-dir /tmp/pr4976-live-demo-kAeL/ws/results --repo ...

Both runs used the same env: SUTANDO_INSTANCE_ID set + a real SUTANDO_INBOX_RESOLVER/SUTANDO_INBOX_KIND/SUTANDO_POOL_DELIVERY_SCRIPT, started via env -i to rule out ambient-shell leakage, with SUTANDO_WORKSPACE_DIR pointed at a fresh temp workspace carrying its own task-event-handler.json.

Tests: tests/watch-tasks-stream-foreign-instance-id-corrected.test.py now has 5 properties (full inherited worker env at the core's canonical inbox still dispatches; a worker inbox whose basename happens to be tasks keeps its identity) — 5/5 passing.

(Separately: while verifying this locally I found tests/watch-tasks-stream-config-hot-reload.test.py and a couple of sibling tests fail when run from inside a live worker's own shell, because of ambient SUTANDO_WORKSPACE_DIR/SUTANDO_INBOX_RESOLVER leakage into their subprocess env — confirmed this reproduces identically on main, so it's pre-existing test fragility unrelated to this fix, not a regression. Not fixing it here to keep this PR to the two confirmed blockers; happy to file it separately if useful.)

@sonichi

sonichi commented Oct 1, 2026

Copy link
Copy Markdown
Owner Author

Verified at exact head f31f10ef6 from an isolated checkout, clean env (env -u SUTANDO_* …):

  • tests/watch-tasks-stream-foreign-instance-id-corrected.test.py → 5/5; with src/watch-tasks-stream.sh reverted to origin/main → 2/5 (the two new properties — full inherited worker env at the canonical inbox still dispatches; a worker inbox whose basename is tasks keeps its identity — both fail on main, as does the original).
  • Adjacent: config-hot-reload 3/3, fallback-receipt-scope 6/6. git diff origin/main...HEAD | scripts/review-checks.sh → PASS. Added comment blocks are within the 2-line cap.
  • The check is now realpath(inbox) == canonical_tasks_dir("$WORKSPACE_DIR/tasks") (src/watch-tasks-stream.sh:115-116), and the unset covers SUTANDO_INSTANCE_ID SUTANDO_INBOX_KIND SUTANDO_INBOX_RESOLVER SUTANDO_INBOX_RESOLVER_TIMEOUT SUTANDO_POOL_DELIVERY_SCRIPT — the set spawn_worker.py:181-208 exports, with SUTANDO_RESULTS_DIR/SUTANDO_WORKSPACE_DIR left alone because they name the shared workspace.

One thing to know, not a blocker: workspace_dir_for_inbox (src/tasks-dir-resolve.sh:32-35) prefers an inherited SUTANDO_WORKSPACE_DIR over the inbox's parent, so the comparison is against the inherited workspace's tasks/. That is correct for every real spawn (a worker gets the host's own workspace, spawn_worker.py:191) and the live round trip above exercises it; it would miss only a worker shell pointed at a different workspace than the inbox being served, which nothing spawns today.

@keweichen both blockers are addressed at this head; could you re-review? @john-the-dev your approval is at 68e63b66d, so the rule needs one at this head.

@john-the-dev john-the-dev 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.

COMMENT at f31f10ef65a4a627385a558cac9e927dd596cb7e. The code is right. I'm holding my approval on the live-path evidence, which is the one part of keweichen's request this head doesn't close.

First, a correction to my earlier review: my approval at 68e63b66d missed both of the defects keweichen found. I checked that worker inboxes are deliveries/<id> but never considered tasks as an id, and I never followed the inherited resolver. Thanks for catching both.

Code: both blockers are fixed.

  • Realpath, not basename. watch-tasks-stream.sh:115-116 now compares realpath(inbox) against canonical_tasks_dir("$WORKSPACE_DIR/tasks").
  • Full routing env. The unset now covers INSTANCE_ID, INBOX_KIND, INBOX_RESOLVER, INBOX_RESOLVER_TIMEOUT and POOL_DELIVERY_SCRIPT. I checked that set against what spawn_worker.py and launch-worker-session.sh export.
  • Variables left set, and why that's fine:
    • RESULTS_DIR and WORKSPACE_DIR name the shared workspace (pool_delivery.results_dir is <ws>/results).
    • SUTANDO_TMUX_SESSION also survives, but its only consumer, _tmux_wake (:984), has no caller at this head.

What I ran (macOS, bash 3.2):

  • New test at head: 5/5.
  • With only src/watch-tasks-stream.sh at the merge-base: 2/5; (1), (2) and (3) fail.
  • With only the predicate put back to basename == tasks (unset list kept): 3/5; (4) and (4b) fail.
  • So each property detects its own fix.
  • CI: 25 pass, 1 pending.

Not closed: the live path. keweichen asked for a real post-restart round trip from a contaminated worker shell, through task receipt to a delivered reply. As far as I can find, the evidence at this head is still:

  1. The PR body's "Live path" section. It describes force-restarting from an uncontaminated shell, which is the mitigation, not this patch.
  2. The env -i harness runs. These are tests, not the production watcher.

The PR body also still describes the removed basename == "tasks" fix and the old 2/2 test, so anyone reading it gets the wrong mechanism. I'll approve at this head once the body is updated and it shows one live run: start the patched watcher from a shell with a worker's full env exported, drop a real task into <ws>/tasks, and show it dispatched and answered (the watcher's stderr correction line, plus the result file / delivered reply).

@sonichi

sonichi commented Oct 1, 2026

Copy link
Copy Markdown
Owner Author

Body updated at the same head f31f10ef6 (no new commit): the mechanism section now describes the realpath check and the five properties, and the Live path section carries round trip #2 — the patched watcher started from the worker's own unmodified env against a scratch workspace, task dropped, stderr correction line, result file. I read it back; the old basename/2-test text is gone. @john-the-dev that was the condition in 5374739108; @keweichen the live round trip from your review is there now.

@john-the-dev john-the-dev 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.

APPROVE at f31f10ef65a4a627385a558cac9e927dd596cb7e. The head is unchanged since my COMMENT (review 5374739108), so the code and test evidence there still stand:

  • New test: 5/5 at head; 2/5 with only the source at the merge-base; 3/5 with only the predicate put back to basename == tasks.
  • CI is green.

Both parts of my hold are done in the body:

  • Mechanism. The Fix section now describes the realpath comparison and the five properties. The remaining basename mentions only explain why basename alone is wrong, plus the honest residual that with SUTANDO_WORKSPACE_DIR unset the check reduces to the old set. I agree that residual is non-blocking, since every spawned worker exports it.
  • Live run. Live round trip #2 starts the patched watcher from a worker's real, unmodified SUTANDO_* env, with only the workspace and results dirs redirected to scratch. It drops a task, shows the stderr correction line, and shows the handler's result file. That is the run I asked for.
    • It runs against a scratch workspace, not the host's live tasks/. I think that's the right trade-off, and I'm not asking for a run against production.
    • I read the run from the body. I did not reproduce it.

keweichen's CHANGES_REQUESTED at 68e63b66d is theirs to lift. This approval doesn't speak for them.

@keweichen keweichen 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.

Re-reviewed exact head f31f10ef65a4a627385a558cac9e927dd596cb7e. The two original dispatch blockers are fixed in the main path: an isolated-log full-worker-env probe dispatches at this head and fails at the partial fix, and <ws>/deliveries/tasks retains worker routing. CI and CLA are green. Changes are still requested for the following current-head blockers.

Blocker 1 — the same core-inbox decision still falls back to basename == tasks in the unready-holder recovery path. The new canonical predicate is established at watch-tasks-stream.sh:115-118, but the live-holder re-stamp at lines 213-219 independently decides that an empty instance ID is core only when the physical inbox basename is tasks.

Exact-head repro: I made <ws>/tasks a symlink to a physical directory named physical-inbox, started a session holder, removed its readiness sentinel, then started a second session watcher. The second watcher correctly found the live holder and emitted WATCHER_HELD, but it left the sentinel absent and printed no re-stamped line. The same harness with an ordinary physical <ws>/tasks restored the sentinel to the holder PID and logged the re-stamp. Reuse the canonical-core predicate here and add this adjacent case; otherwise a supported physical-path spelling leaves the live core watcher without the readiness record this recovery path promises.

Blocker 2 — regression property (3) can pass without the full-env task being dispatched. At the wait/assert on lines 178-188, log already contains handle from property (1), so the predicate is true before watcher p3 processes task-full-env.txt.

I ran the current test body against exact prior head 68e63b66, whose resolver leak is known and visible in stderr. Property (3) still printed ok while the resolver rejected the task three times and logged not dispatching. Resetting the log immediately before property (3) made that same source fail property (3), while f31f10ef remained 5/5. Please give this property fresh state or assert a new/task-specific handler record so it actually guards the bug it names.

The rewritten body improves the live evidence, but the live-path gate remains open. I have now read live run #2: it starts the exact-head role=session watcher from this worker shell's real inherited SUTANDO_* environment, redirects workspace/results to a fresh scratch workspace, drops a task, and shows the scratch handler's result file. This resolves the stale-body point in the initially submitted version of this review; the body now accurately describes the current mechanism and tests.

Run #2 is still a standalone scratch integration, not a restart of the shipped/running service: it does not exercise the supervisor/--force-restart lifecycle or real resolved workspace, and no result consumer/outbox/bridge claims the file or delivers a reply to the origin. Per REVIEW.md, approval still needs an exact-head post-restart round trip through the shipped service from contaminated worker environment to task receipt, result production and consumption, and delivered reply, with restoration/downtime noted.

Verdict: not merge-ready. The worst case is that the exact silent-dispatch regression can return while its named test stays green, and a symlinked core inbox cannot repair its readiness record after a live-holder handoff.

… fix a weak test assertion

Two more findings from kewei's formal review at f31f10e:

1. The unready-holder re-stamp path (around line 215) still decided "empty
   instance id means core" by `basename(inbox) == "tasks"`, even though the
   dispatch-time check a few lines above it was already switched to a realpath
   comparison. A `<ws>/tasks` symlinked to a physically-named directory (e.g.
   `physical-inbox`) has `basename(TASKS_DIR_ABS) == "physical-inbox"`, so the
   re-stamp silently never fired for a holder on a symlinked core inbox. Now
   reuses the same `CANONICAL_CORE_TASKS_DIR` realpath comparison already
   computed earlier in the file. New case (e) in
   tests/watch-tasks-stream-unready-holder-restamp.test.sh pins this: a
   symlinked core inbox re-stamps exactly like a physical one — confirmed to
   fail on the basename-only version and pass on the fix.

2. tests/watch-tasks-stream-foreign-instance-id-corrected.test.py property (3)
   asserted `"handle" in log`, but the log is shared across properties and
   property (1) already wrote "handle" to it — so (3) passed trivially even
   when its own task (task-full-env.txt) was never dispatched. The test
   handler now records which task file it actually ran for, and (1)/(3) each
   assert their own task by name. Confirmed: at the pre-Blocker-1-fix head
   (68e63b6) this version of (3) now correctly FAILS (it previously passed
   spuriously).

5/5 on tests/watch-tasks-stream-foreign-instance-id-corrected.test.py, all
green on tests/watch-tasks-stream-unready-holder-restamp.test.sh.

Stand: Echo Act IV Pro
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@sonichi

sonichi commented Oct 1, 2026

Copy link
Copy Markdown
Owner Author

Verified at exact head 9ef25f6e7 from an isolated checkout, clean env:

  • Blocker 1 (re-stamp path): src/watch-tasks-stream.sh:217 now compares "$TASKS_DIR_ABS" = "$CANONICAL_CORE_TASKS_DIR" in the unready-holder re-stamp, the same predicate as the identity block. tests/watch-tasks-stream-unready-holder-restamp.test.sh gains case (e), the symlinked <ws>/tasks -> physical-inbox repro from the review: 26 checks, ALL TESTS PASS at head; with src/watch-tasks-stream.sh at f31f10ef6 the two (e) checks fail exactly as described (sentinel left empty, no re-stamped line).
  • Blocker 2 (property 3 could pass on a stale log): the handler now records handle <task basename> and properties (1)/(3) wait on their own task's record (handle task-full-env.txt). watch-tasks-stream-foreign-instance-id-corrected.test.py: 5/5 at head; with the watcher at 68e63b66d — the review's exact case — 2/5, and (3) is now among the failures (previously it passed there); at origin/main 2/5.
  • Adjacent: config-hot-reload 3/3, fallback-receipt-scope 6/6. git diff origin/main...HEAD | scripts/review-checks.sh → PASS.

Live-path gate: Chi has authorised me to run the shipped-service round trip on this host's real workspace (restart of the core's watcher from a contaminated worker shell, at this exact head, in a quiet period); it will be posted here with the task → result → delivered-reply transcript and the downtime, once the core has a quiet window.

@keweichen both current-head blockers are addressed with your repros as pinned cases; @john-the-dev your approval was at f31f10ef6, this is a new head.

@john-the-dev john-the-dev 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.

COMMENT at 9ef25f6e7e2987f79007a6c1d90644af70eca756. The code is right. I'll approve once the live-path run lands.

keweichen's two new blockers are fixed, and I checked both on macOS with bash 3.2:

  • Re-stamp path. watch-tasks-stream.sh:217 now uses "$TASKS_DIR_ABS" = "$CANONICAL_CORE_TASKS_DIR", assigned at :115. New case (e) in unready-holder-restamp.test.sh covers a symlinked <ws>/tasks → physical-inbox.
    • The suite passes at head.
    • With only the source at f31f10ef6, (e) fails: the sentinel is empty and nothing is re-stamped. The stderr shows the second watcher yielding to the live holder on physical-inbox without restoring the record, which is exactly keweichen's repro.
  • Property (3) is task-specific now. The stub handler logs handle <task basename>, and (1) and (3) each wait for their own task.
    • 5/5 at head.
    • 2/5 with only the source at 68e63b66d, with (3) failing as it should. On my f31f10e run it would have passed on (1)'s stale line.
  • CI right now: 20 pass, 5 pending.

Why COMMENT and not APPROVE: REVIEW.md #15. "A live-path PR is not approvable on harness proof alone … never grounds to approve without it". This PR changes the watcher's startup path, so it needs a task to flow end-to-end through the restarted, shipped service.

  • My APPROVE at f31f10ef6 accepted the scratch-workspace run #2. That was below this bar, and keweichen was right to hold it.
  • You say the real-workspace run is authorised for a quiet window. When it's posted, I'll approve at whatever head it ran on, provided it shows these four things:
    1. The watcher restarted from a contaminated worker shell.
    2. The stderr correction line.
    3. A real task in the core's tasks/.
    4. The reply delivered to its origin room, with the downtime noted.

@keweichen keweichen 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.

Re-reviewed exact head 9ef25f6e7e2987f79007a6c1d90644af70eca756. The two runtime blockers from my f31f10ef6 review are fixed:

  • watch-tasks-stream.sh:215-218 now uses the same canonical realpath comparison for the empty-instance/core re-stamp path. The new symlinked-inbox case (e) re-stamped the holder at this head; the same case fails with the f31f10ef6 source. On this host, the file's unrelated (a)/(b) process-table probes returned unknown at both heads, while every new (e) assertion passed; both CI shell lanes are green.
  • watch-tasks-stream-foreign-instance-id-corrected.test.py:134-144 now records the handled task basename, and property (3) waits for task-full-env.txt. It is 5/5 here and the strengthened property fails against 68e63b66d, so the stale-log false positive is closed. Adjacent config-hot-reload and fallback-receipt suites passed 3/3 and 6/6.

CI, CLA, git diff --check, syntax checks, and review-checks.sh are green. Changes are still requested for three current-head gates:

Blocker 1 — the new re-stamp predicate regresses a supported custom core inbox. watch-tasks-stream.sh:217 now equates an empty-instance/core seat with the default physical <workspace>/tasks. The watcher explicitly supports the inbox and workspace being named independently (contract and rationale); that contract suite is 4/4 at this head.

Exact probe with SUTANDO_WORKSPACE_DIR=<ws>, no instance ID, and an explicit <custom>/tasks inbox: start a session holder, remove its <ws>/state readiness sentinel, then start a second session watcher on the same inbox. At f31f10ef6, the challenger emits WATCHER_HELD, restores the holder PID, and logs re-stamped. At 9ef25f6e7, it still yields to the live holder but leaves the sentinel absent and emits no re-stamp line. I reproduced both outcomes independently. Core ownership cannot be inferred only from the default inbox location; preserve recovery for a configured/explicit core-owned inbox and add this separate-workspace case to the regression test.

Blocker 2 — the required shipped-service witness is still pending. The PR's latest comment explicitly says the exact-head live run will follow. The body currently contains standalone scratch-workspace watcher runs from the prior head; no restarted shipped service consumes the result and delivers a reply. Because this changes the startup/delivery loop, REVIEW.md lesson 15 and CONTRIBUTING.md require an exact-head post-restart task → produced result → consumed result → delivered reply witness, with downtime/restoration noted. The default-path code evidence is now good; this live witness remains the runtime gate.

Blocker 3 — added comments carry review/incident provenance that the repository forbids. AGENTS.md:45 caps code comments at two lines and says PR, issue, person, and incident history belongs in the commit/PR body. This exact commit adds a three-line comment naming “kewei's review (sutando#4976)” at watch-tasks-stream-unready-holder-restamp.test.sh:119-121; the cumulative diff also adds #4502, incident, and “Blocker 1 (kewei)” provenance in the watcher tests (for example foreign-instance-id-corrected.test.py:150-173 and config-hot-reload.test.py:43-45). The new module docstring also still says “Two properties” while the executable test reports five. Keep the non-obvious behavioral constraints, but move provenance/history to the PR body, make the summary match the test, and keep each inline comment within the two-line cap.

Verdict: not merge-ready yet. The worst-case disruption remains a silently dead or permanently unready core task inbox: the default-path implementation and negative controls now look correct, but a supported custom-inbox path regressed and the shipped path has no exact-head witness. Once those are resolved and the in-code provenance is trimmed, I can re-check the current head for approval. The existing john-the-dev approval predates this commit, so the two-current-maintainer-approval merge gate is also not yet met.

sonichi and others added 3 commits September 30, 2026 21:37
…ox location; trim comment provenance

Blocker 1: the re-stamp arm's "empty SUTANDO_INSTANCE_ID means core" check was
changed in the prior commit to also require the inbox to equal the canonical
<workspace>/tasks by realpath. That regressed a SUPPORTED configuration: a
core watching a custom inbox with its own SUTANDO_WORKSPACE_DIR and no
instance id (pinned by tests/watch-tasks-stream-inbox-and-workspace-env.test.py).
The re-stamp arm's question is "is this seat the core?", which an empty
instance id already answers on its own -- the inbox-location check belongs
only to the earlier env-clearing block (deciding whether an INHERITED id is
foreign), not here, where `sentinel-names-pid` already scopes the write to
this seat's own sentinel. Dropped the inbox condition from the empty-id arm
entirely. New case (f) in watch-tasks-stream-unready-holder-restamp.test.sh
pins a custom inbox + separate workspace re-stamping correctly; confirmed to
fail against the prior commit's realpath-gated version and pass on the fix.

Blocker 3: trimmed comment references to the reviewer, review round, and
issue numbers in tests/watch-tasks-stream-{config-hot-reload,
foreign-instance-id-corrected,unready-holder-restamp}, keeping only the
behavioral constraint each comment exists to state. Updated the
foreign-instance-id-corrected module docstring, which still described two
properties, to describe all five.

Blocker 2 (the shipped-service live witness) is being run separately against
this head once pushed.

5/5 on the corrected-id test, all green (including new case (f)) on the
re-stamp test.

Stand: Echo Act IV Pro
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…orrected docstring

The module docstring still carried an issue number, a date and a pid in its
module docstring, matching exactly what AGENTS.md's comment rule forbids.
Kept the behavioral statement (a worker's own inbox is the routing decision),
dropped the issue number, date and pid.

Stand: Echo Act IV Pro
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…t-scope's docstring

My own grep for leftover provenance used a #49[0-9][0-9] pattern, which never
matched #4502 -- missed by Pro-dev's independent check, not mine. The
pre-existing #4502/#4503 reference at the top of the file (predating this PR)
is untouched; only the one this PR itself added to run()'s docstring is
trimmed.

Stand: Echo Act IV Pro
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@sonichi

sonichi commented Oct 1, 2026

Copy link
Copy Markdown
Owner Author

CI at ab95b0f01: the one red lane is again tests/agy-start-cli.test.py::test_concurrency_test_would_catch_a_naive_non_atomic_writer (#4977, the mutation control that needs a thread race to occur; this PR touches no agy file). Failed jobs re-run at 21:52 PDT.

@sonichi

sonichi commented Oct 1, 2026

Copy link
Copy Markdown
Owner Author

Live witness at exact head ab95b0f01 on this host's real workspace (Chi-authorised; run by Pro-dev from its own worker shell, i.e. the contaminated environment the fix is for — SUTANDO_INSTANCE_ID=17c6c3222a4a483b8c68652d58018ad7, SUTANDO_INBOX_KIND=deliveries, the real SUTANDO_INBOX_RESOLVER and SUTANDO_POOL_DELIVERY_SCRIPT paths; nothing unset). The root checkout was moved to ab95b0f01 for the window so the shipped src/watch-tasks-stream.sh is the one that ran; restored to main afterwards.

06:31:06.598  T0: bash src/watch-tasks-stream.sh <ws>/tasks --force-restart --role session --inbox <ws>/tasks   (from the worker shell)
06:31:06.6    watch-tasks-stream: --force-restart: stopping watcher pid 47862 (session) on <ws>/tasks      (the core's live watcher)
06:31:07.115  watch-tasks-stream: SUTANDO_INSTANCE_ID=17c6c3222a4a483b8c68652d58018ad7 (and worker routing env) set while serving the core's own canonical inbox (<ws>/tasks) -- clearing it to match the inbox, never the calling shell's inherited env
06:31:08.157  core sentinel <ws>/state/watch-tasks-stream-@sutando-sonichi:ag2.space.pid -> 97977 (the new watcher, stamped as the CORE, not as the worker); fswatch child 98173
06:31:34      the owner's real message ("One word", Pro-dev DM, event $XVmupgktFPiBrVp6GE-1m2TG_Wm1pXJB5PAQ2urRBRA) arrives as <ws>/tasks/task-e25cf5d2fac836806c.txt with its bridge envelope
06:31:35      the watcher's routing delivers it into deliveries/17c6c3222a4a483b8c68652d58018ad7/ (the room is bound to Pro-dev); Pro-dev's own watcher announces TASK_FILE
06:32:24.482  reply written: results/task-e25cf5d2fac836806c.txt
06:32:24.833  bridge delivers it to the Pro-dev DM: event $V3mX3-WahYyRGO6Y8sJ2FWuGGFOYyoHE8Os8P8KFlMc
06:32:43.158  teardown: new watcher stopped; 06:32:43.812 root checkout back on main@5e08f0ab8
06:33:30.975  standby watchers hold <ws>/tasks (pids 26720/26686)
06:36:16      the core's own session watcher is back (pid 58051, child of the core session 82712); sentinel re-stamped

Receipt → result → delivered reply: 50 s end to end. Downtime: the core's own session-role watcher was absent 06:31:06 → 06:36:16 (5 m 10 s); during it the PR-head watcher served the inbox until 06:32:43, then standbys from 06:33:31. Two other task files present at open were announced by the PR-head watcher to the worker's stream instead of the core's (one was Pro-dev's own pool-ask to the core, "no reply needed"); both were archived with results by the time the core's watcher returned — no task was lost. The same restart on main's watcher is what produced the silent-dispatch incident this PR fixes (the earlier env -i runs in the body show that side).

An earlier attempt at the previous head 9ef25f6e7 (21:56–22:07 PDT) showed the same correction line and core-sentinel stamp but closed without a probe; attempt 1 at f31f10ef6 used a hand-written probe with no bridge envelope and so could not show delivery.

@keweichen this is the shipped-service witness from your review; @john-the-dev your approval is at f31f10ef6, the head is ab95b0f01; @qingyun-wu CODEOWNERS approval for src/ is still needed.

@john-the-dev john-the-dev 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.

APPROVE at ab95b0f01eb7ce4e3ecd5615461c6ebaedab47de. This supersedes my COMMENT at 9ef25f6e7, where the open item was the live run.

keweichen's three 9ef25f6e7 blockers, checked at this head

  1. Custom core inbox: fixed.
    • In watch-tasks-stream.sh:216 the re-stamp arm now treats an empty SUTANDO_INSTANCE_ID alone as "this seat is the core". It no longer also requires TASKS_DIR_ABS == CANONICAL_CORE_TASKS_DIR.
    • New case (f) in watch-tasks-stream-unready-holder-restamp.test.sh covers a custom inbox with a separate SUTANDO_WORKSPACE_DIR and no id.
    • I ran it in a fresh clone:
      • At head: ALL TESTS PASS.
      • With only src/watch-tasks-stream.sh reverted to 9ef25f6e7: (f) fails on both the sentinel re-stamp and the re-stamped stderr line. The new case reproduces kewei's probe.
    • The relaxation is safe: with no instance id the seat is the core by definition, and the write still goes only to $WORKSPACE_DIR/state.
  2. Shipped-service witness: provided at the exact head, in the comment of 2026-10-01T13:37Z. It meets REVIEW.md #15:
    • The real workspace's live watcher was force-restarted from a contaminated worker shell (SUTANDO_INSTANCE_ID, INBOX_KIND=deliveries, resolver and pool paths all set).
    • The correction line was emitted, and the core sentinel was stamped to the new pid.
    • A real owner message arrived with its bridge envelope, was routed, answered (results/task-e25cf5d2…) and delivered to the DM ($V3mX3…), 50 s end to end.
    • Downtime was stated (5 m 10 s without a session-role watcher), with standby coverage and restoration.
  3. Provenance: trimmed.
    • The added lines carry no kewei, #NNNN, sutando#, incident or Blocker 1 text; I grepped every + line of gh pr diff.
    • The module docstring now says "Five properties".

CI: 27 pass, 1 skipped.

Non-blocking

  • During the witness window, two core task files were announced to the worker shell's stream, because that shell had started the PR-head watcher. Both still got results. This comes from running the restart from a worker shell, not from the code. It is still worth a line in the runbook: whoever runs --force-restart on the core inbox owns its announcements until the core re-arms its own watcher.

Merge gate: keweichen's CHANGES_REQUESTED at 9ef25f6e7 stands until kewei re-reviews. CODEOWNERS approval for src/ (qingyun-wu) is still needed, as the comment says.

@keweichen keweichen 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.

Re-reviewed exact head ab95b0f01eb7ce4e3ecd5615461c6ebaedab47de. The runtime blockers from my 9ef25f6e7 review are resolved:

  • watch-tasks-stream.sh:215-217 now treats an empty instance ID as the core seat without tying ownership to the default inbox location. In the focused separate-workspace/custom-inbox repro, 9ef25f6e7 yielded but left the sentinel absent (restamped_log=no); this head restored the holder PID and logged the re-stamp. New case (f) passed, as did the symlinked-core case (e).
  • The task-specific full-worker-env regression remains 5/5, config hot reload 3/3, fallback receipt scope 6/6, and the independent inbox/workspace contract 4/4. On this host the unready-holder file's pre-existing (a)/(b) process-table probes still returned unknown at both old and new heads; every changed (e)/(f) assertion passed, and both CI shell lanes are green.
  • The previously cited review/incident provenance is removed and the new regression module now accurately documents all five properties.

The shipped-service witness satisfies the live-path gate. It is scoped to this head and the real workspace: the live core watcher was force-restarted from the worker's actual contaminated environment; the correction line and core sentinel were observed; a real owner input arrived with its bridge envelope, routed through the configured worker, produced a result, and the bridge delivered the reply. The measured receipt-to-delivery time was about 50 seconds, the 5m10s core-session-watcher absence and restoration were reported, and the root checkout was restored. This exercises the affected shipped component rather than a scratch harness. The production watcher blob is also unchanged from 09df259f through this head (854983700b4718188a69a10a1ba1c62a935ecfab).

CI, CLA, git diff --check, syntax checks, and review-checks.sh are green. One repository-rule blocker remains:

Blocker — the newly added case (f) comment still exceeds the two-line cap. AGENTS.md:45 requires code comments to be at most two lines. watch-tasks-stream-unready-holder-restamp.test.sh:133-136 is a four-line comment. Please retain the non-obvious empty-ID/custom-inbox constraint but condense it to at most two lines. The automated prose-cap reports green because it scans Python comments, not this shell block.

Non-blocking body accuracy: the witness timeline shows the PR watcher stopped at 06:32:43 and standbys appeared at 06:33:30.975, so “covered the inbox throughout” overstates roughly 48 seconds with no named watcher. That does not invalidate the successful round trip—the reply was delivered before teardown and queued tasks were later processed—but the body should describe the durable/no-loss gap rather than continuous watcher coverage.

Also non-blocking for this run: checking a PR out over the live checkout is the evidence method CONTRIBUTING.md warns weakens commit provenance. Future public witnesses should use the documented detached-worktree flow and include the contemporaneous rev-parse, resolved workspace, a sanitized task envelope, and the delivery receipt; private event and worker identifiers are unnecessary.

Verdict: not merge-ready solely because the added four-line comment violates the repository's explicit comment contract; I found no remaining runtime blocker. If the next push only condenses that test comment and leaves the production watcher blob unchanged, the live restart does not need to be repeated; I will re-check the new head and CI. john-the-dev now has a current-head approval; the requested CODEOWNER review is still outstanding, and this review remains blocking until the comment is fixed.

The prose-cap scanner only covers Python, so this .sh comment (4 lines)
slipped through CI green while still violating AGENTS.md's comment rule.
Kewei caught it by eye. Condensed to 2 lines, same constraint kept
(empty-id-alone for a custom inbox + separate workspace).

src/watch-tasks-stream.sh is untouched (blob 8549837,
unchanged since 09df259) -- test comment + PR body only, no source change,
so the shipped-service live witness need not be repeated.

Stand: Echo Act IV Pro
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@github-actions

github-actions Bot commented Oct 1, 2026

Copy link
Copy Markdown
Contributor

@cla-assistant check

@sonichi

sonichi commented Oct 1, 2026

Copy link
Copy Markdown
Owner Author

Verified at fc1856dc7: the only change since ab95b0f01 is tests/watch-tasks-stream-unready-holder-restamp.test.sh (+2/−4, the case (f) comment condensed to two lines); src/watch-tasks-stream.sh is blob 854983700b4718188a69a10a1ba1c62a935ecfab, identical to 09df259f and to the head the live witness ran on, so per the review the restart does not need repeating. The re-stamp suite is ALL TESTS PASS; review-checks PASS; the two remaining >2-line comment runs in that file (:1-4, :70-72) are pre-existing on main, not this PR's. @keweichen ready for your re-check; @john-the-dev your approval at ab95b0f01 is now stale on a test-comment-only push; @qingyun-wu CODEOWNERS approval for src/ is the last gate.

@john-the-dev john-the-dev 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.

Re-approving at exact head fc1856dc7f94cbe205a532ed1ff1b914d06036b8 (my ab95b0f01 approval went stale on the push).

Delta ab95b0f01...fc1856dc7: 1 commit, 1 file. tests/watch-tasks-stream-unready-holder-restamp.test.sh changes +2/−4: the case (f) comment is condensed from four lines to two and keeps the empty-id / custom-inbox constraint. That resolves keweichen's remaining comment-cap blocker. bash -n passes on the new file.

The production watcher is unchanged: src/watch-tasks-stream.sh is blob 854983700b47… at both heads, which is the blob the shipped-service witness ran. So the live round trip still holds, and per keweichen's note it doesn't need to be repeated.

CI at the time of review: 20 pass, 5 pending (python standalone 1/3/5, shell serial tail), 1 skipping (Windows). This approval does not certify the pending lanes. Merge still needs them green, plus keweichen's re-check and the CODEOWNER review.

@keweichen keweichen 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.

APPROVE at fc1856dc7f94cbe205a532ed1ff1b914d06036b8. This supersedes my CHANGES_REQUESTED at ab95b0f01; its sole remaining blocker is fixed.

The exact ab95b0f01..fc1856dc7 delta is one commit in one test file, +2/−4 comment lines. Case (f)'s constraint is now exactly two comment lines at tests/watch-tasks-stream-unready-holder-restamp.test.sh:133-134, satisfying AGENTS.md. There is no executable change, and src/watch-tasks-stream.sh remains blob 854983700b4718188a69a10a1ba1c62a935ecfab, identical to the shipped-service witness head. The live restart therefore remains applicable and does not need repeating.

Exact-head evidence:

  • Foreign worker environment 5/5, config hot reload 3/3, fallback receipt scope 6/6, and inbox/workspace contract 4/4 all pass.
  • Every changed unready-holder case (e)/(f) assertion passes. This host still reports the same unrelated process-table failures in pre-existing (a)/(b)/(c) that I disclosed on the prior head; both CI shell lanes are green.
  • git diff --check, bash -n, and cumulative review-checks.sh pass. Added-line scans are clean for review/incident provenance and hardcoded host paths.
  • The PR body now accurately names the approximately 48-second no-watcher interval rather than claiming continuous coverage.
  • Current CI and CLA are fully green: 27 pass, 1 intentional Windows skip.

Worst-case disruption remains the original silent core-dispatch failure if a worker's routing environment leaks into a core watcher start. The full routing-env reset, realpath-scoped core-inbox decision, worker-inbox negative control, custom-inbox recovery coverage, and shipped end-to-end witness mitigate that path. This comment-only delta introduces no new runtime risk.

Verdict: ready to merge from my review standpoint. john-the-dev also has a current-head approval; the requested qingyun-wu CODEOWNER approval remains a separate merge gate.

@sonichi

sonichi commented Oct 1, 2026

Copy link
Copy Markdown
Owner Author

@qingyun-wu reminder at Chi's request: CODEOWNERS (* @sonichi @qingyun-wu) makes your approval the one GitHub requires here, since the author is under the sonichi login. This PR has two reviewer approvals at its current head and green CI; please respond when you can — an approval, or a comment saying what you need.

@qingyun-wu
qingyun-wu merged commit 6c6e96a into main Oct 4, 2026
28 checks passed
@qingyun-wu
qingyun-wu deleted the fix/watcher-foreign-instance-id-inherited branch October 4, 2026 04:50
qingyun-wu pushed a commit that referenced this pull request Oct 4, 2026
…ically (#4996)

* fix(test): make the naive-writer mutation control collide deterministically

The control in tests/agy-start-cli.test.py ran 8 naive writers on one
shared `.tmp` name and asserted that at least one raised. With a
one-field payload the write window is microseconds, so a fast runner
serialises the eight calls, nobody collides, and the control fails
although the production atomic seed() is correct (2/20 locally under
CPU load; twice in CI on #4976).

Hold every writer's staged file behind a threading.Barrier(8) between
its write and its os.replace: the first rename wins, the other seven
target a name that no longer exists, so the control now asserts exactly
7 FileNotFoundError on every run instead of hoping for a race.

Stand: Echo Act IV Pro
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* fix(test): force the same interleaving on the real seed writer and the naive control

The previous commit put the barrier only inside the synthetic naive
writer, so the production-path concurrency test still ran seed() under
free executor scheduling: a regression from unique mkstemp staging back
to one fixed `.tmp` could serialise and pass it (4/20 on review) while
the control passed regardless.

Both tests now share one harness that patches the module's os.replace
with a wrapper holding every writer at an 8-party barrier before it
renames, so all writers stage before any rename on every run. The two
tests differ only in the writer: seed() must raise nothing and leave
intact JSON with no stray staging file; the naive fixed-name writer must
fail 7 of 8 with FileNotFoundError. With seed() mutated to a fixed
staging name the production test fails 20/20 under load and the control
still passes; with the real seed() both pass 20/20.

Stand: Echo Act IV Pro
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
sonichi added a commit that referenced this pull request Oct 6, 2026
Reviewed by Rui's Stand and kewei-red (both REQUEST_CHANGES on the same
blocker): this PR's unconditional `unset SUTANDO_INSTANCE_ID` made the agy
watcher's sentinel the bare canonical name on EVERY inbox, not just the
core's own <ws>/tasks. On agy's default tasks-agy inbox (runs alongside the
Claude/Codex core, task-notifier.sh:12-16) that collided with a live core
watcher's own sentinel and then deleted it on exit -- exactly the regression
`test_spawned_watcher_gets_a_distinct_sentinel_not_the_canonical_one` existed
to pin. Reproduced before this fix:

  core watcher pid=90838 sentinel=90838
  agy-shaped watcher pid=91722 sentinel=91734   <- overwrote the core's sentinel
  after agy watcher exits: sentinel=No such file or directory   <- removed it

Fix: a shared helper, inbox_is_canonical_core_tasks_dir (src/tasks-dir-
resolve.sh), mirrors the equality check watch-tasks-stream.sh already used
inline (now routed through the same helper). task-notifier.sh clears its
instance id only when its own TASKS_DIR passes that check; otherwise it keeps
"agy-task-notifier" as before, so a non-canonical inbox's sentinel can never
collide with the canonical one. One helper, both callers, so the two can't
drift apart again (the drift that caused this bug and #4976 before it).

Re-reproduced after the fix: the agy watcher stamps its own distinct
sentinel immediately, and the core's canonical sentinel is byte- and
mtime-identical before, during, and after the agy watcher's full lifecycle
(restored test_tasks_agy_shaped_inbox_never_touches_the_canonical_sentinel).
The canonical-inbox clearing behavior (foreign id cleared, existing
fixtures) is unchanged -- covered by the existing suite, which was already
written against a canonically-named fixture dir.

Stand: Echo Act IV Pro
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
sonichi added a commit that referenced this pull request Oct 6, 2026
#4303)" (#5166)

* Revert "feat(agy-core): Slice 2 — task-file-injection notifier for agy (Antigravity CLI) (#4303)"

This reverts commit 4a72ddf. #4303 was green on its own head but was not
re-run against #4976 and #4945, which had landed on main; together they
turned four notifier and tmux tests red on main. The forward fix (#5161) is
still in review, so main is restored first; agy Slice 2 re-lands with that
fix once it is approved.

Stand: Echo Act IV Pro
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(agy): retire the task notifier's leftover watcher session on rerun

The reverted launcher no longer knows about the "<session>-watcher" tmux
session #4303 started, and it returns early once the core session exists,
so an update left the old notifier running. Every run of start-cli.sh now
kills an exact-name leftover watcher on its own socket before that guard.
Regressions run a real tmux server on an isolated socket.

Stand: Echo Act IV Pro
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
sonichi added a commit that referenced this pull request Oct 6, 2026
… tmux targets

#4303 merged after #4976 and #4945 without being re-run against them:

- #4976 makes watch-tasks-stream.sh clear SUTANDO_INSTANCE_ID on the core's
  canonical inbox, so the agy notifier's watcher stamps watch-tasks-stream.pid
  while the notifier polled the per-instance name it forced. The launch receipt
  never appeared ("did not report ready within 12s"). The notifier and
  start-cli.sh now run the watcher with no instance id, as the Codex core does.
- #4945's scan forbids bare `-t "$SESSION"` targets; the notifier's six
  capture-pane/send-keys calls now use the exact "=$SESSION:" form.

Stand: Echo Act IV Pro
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
sonichi added a commit that referenced this pull request Oct 6, 2026
Reviewed by Rui's Stand and kewei-red (both REQUEST_CHANGES on the same
blocker): this PR's unconditional `unset SUTANDO_INSTANCE_ID` made the agy
watcher's sentinel the bare canonical name on EVERY inbox, not just the
core's own <ws>/tasks. On agy's default tasks-agy inbox (runs alongside the
Claude/Codex core, task-notifier.sh:12-16) that collided with a live core
watcher's own sentinel and then deleted it on exit -- exactly the regression
`test_spawned_watcher_gets_a_distinct_sentinel_not_the_canonical_one` existed
to pin. Reproduced before this fix:

  core watcher pid=90838 sentinel=90838
  agy-shaped watcher pid=91722 sentinel=91734   <- overwrote the core's sentinel
  after agy watcher exits: sentinel=No such file or directory   <- removed it

Fix: a shared helper, inbox_is_canonical_core_tasks_dir (src/tasks-dir-
resolve.sh), mirrors the equality check watch-tasks-stream.sh already used
inline (now routed through the same helper). task-notifier.sh clears its
instance id only when its own TASKS_DIR passes that check; otherwise it keeps
"agy-task-notifier" as before, so a non-canonical inbox's sentinel can never
collide with the canonical one. One helper, both callers, so the two can't
drift apart again (the drift that caused this bug and #4976 before it).

Re-reproduced after the fix: the agy watcher stamps its own distinct
sentinel immediately, and the core's canonical sentinel is byte- and
mtime-identical before, during, and after the agy watcher's full lifecycle
(restored test_tasks_agy_shaped_inbox_never_touches_the_canonical_sentinel).
The canonical-inbox clearing behavior (foreign id cleared, existing
fixtures) is unchanged -- covered by the existing suite, which was already
written against a canonically-named fixture dir.

Stand: Echo Act IV Pro
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
sonichi added a commit that referenced this pull request Oct 6, 2026
… tmux targets

#4303 merged after #4976 and #4945 without being re-run against them:

- #4976 makes watch-tasks-stream.sh clear SUTANDO_INSTANCE_ID on the core's
  canonical inbox, so the agy notifier's watcher stamps watch-tasks-stream.pid
  while the notifier polled the per-instance name it forced. The launch receipt
  never appeared ("did not report ready within 12s"). The notifier and
  start-cli.sh now run the watcher with no instance id, as the Codex core does.
- #4945's scan forbids bare `-t "$SESSION"` targets; the notifier's six
  capture-pane/send-keys calls now use the exact "=$SESSION:" form.

Stand: Echo Act IV Pro
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
sonichi added a commit that referenced this pull request Oct 6, 2026
Reviewed by Rui's Stand and kewei-red (both REQUEST_CHANGES on the same
blocker): this PR's unconditional `unset SUTANDO_INSTANCE_ID` made the agy
watcher's sentinel the bare canonical name on EVERY inbox, not just the
core's own <ws>/tasks. On agy's default tasks-agy inbox (runs alongside the
Claude/Codex core, task-notifier.sh:12-16) that collided with a live core
watcher's own sentinel and then deleted it on exit -- exactly the regression
`test_spawned_watcher_gets_a_distinct_sentinel_not_the_canonical_one` existed
to pin. Reproduced before this fix:

  core watcher pid=90838 sentinel=90838
  agy-shaped watcher pid=91722 sentinel=91734   <- overwrote the core's sentinel
  after agy watcher exits: sentinel=No such file or directory   <- removed it

Fix: a shared helper, inbox_is_canonical_core_tasks_dir (src/tasks-dir-
resolve.sh), mirrors the equality check watch-tasks-stream.sh already used
inline (now routed through the same helper). task-notifier.sh clears its
instance id only when its own TASKS_DIR passes that check; otherwise it keeps
"agy-task-notifier" as before, so a non-canonical inbox's sentinel can never
collide with the canonical one. One helper, both callers, so the two can't
drift apart again (the drift that caused this bug and #4976 before it).

Re-reproduced after the fix: the agy watcher stamps its own distinct
sentinel immediately, and the core's canonical sentinel is byte- and
mtime-identical before, during, and after the agy watcher's full lifecycle
(restored test_tasks_agy_shaped_inbox_never_touches_the_canonical_sentinel).
The canonical-inbox clearing behavior (foreign id cleared, existing
fixtures) is unchanged -- covered by the existing suite, which was already
written against a canonically-named fixture dir.

Stand: Echo Act IV Pro
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
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.

4 participants