Skip to content

fix(startup): reconnect gateway lanes without reaping the task watcher - #3147

Merged
qingyun-wu merged 5 commits into
mainfrom
fix/gateway-lane-restart-without-watcher-reap
Aug 19, 2026
Merged

qingyun-wu merged 5 commits into
mainfrom
fix/gateway-lane-restart-without-watcher-reap

Conversation

@jsun-m

@jsun-m jsun-m commented Aug 19, 2026

Copy link
Copy Markdown
Collaborator

What changed, and why?

start_gateway_lanes() (primary + every named secondary gateway lane) moves out of src/startup.sh's inline body into src/startup-runtime.sh, alongside the other shared boot-time functions (reap_stale_task_watcher already lives there). startup.sh now just calls it — no behavior change on a normal boot (verified: full startup-*.test.sh suite + the wiring test for this loop pass unchanged).

This unblocks scripts/restart-gateway-lanes.sh, a new standalone entry point that reconnects gateway lanes without running the rest of startup.sh. That matters because reap_stale_task_watcher assumes startup.sh runs once, at session start, before anything else starts a watcher, and unconditionally kills any watcher already running when it does. The named-instance lanes (AG2_REMOTE_TOKEN_<INST>) are only durable through startup.sh — a plain supervisor/core restart drops them silently, they don't restart on their own — so until now the only way to reconnect a dropped named lane mid-session was bash src/startup.sh, which took a live task watcher down as a side effect.

What files/sections should reviewers look at first?

  • src/startup-runtime.sh — the new start_gateway_lanes() function (verbatim move of the block that used to sit inline in startup.sh, no logic changes).
  • src/startup.sh — the block is now a single start_gateway_lanes call.
  • scripts/restart-gateway-lanes.sh — new, small (resolves PY/WORKSPACE, sources startup-runtime.sh, calls the function).
  • tests/restart-gateway-lanes-no-watcher-touch.test.sh — new regression test.

What user behavior or bug does this prove?

Tonight, on my own host, a named gateway lane (the one that reaches the owner on dev.ag2.space) had no running process at all after this core session's boot — the primary lane was up, the named one wasn't, matching the documented gap that "the dev instance is durable ONLY through startup.sh". The only available fix was bash src/startup.sh, which reconnected the lane and drained 4 queued owner messages — but its reap_stale_task_watcher step also killed the live streaming task watcher I'd started earlier in the session (Monitor task exit 144), which then needed a manual restart. That's the bug this closes.

Real, live, post-restart round trip on the affected host (not a description — this is the actual sequence, timestamps from state/):

Before (killed the affected lane's process to reproduce a dropped-lane state):

$ kill 97786   # the dev-lane remote-gateway-bridge.py pid
$ ps -p 97786 -o pid,args=
  PID                              # gone
$ cat state/watch-tasks-stream.pid
1262
$ ps -p 1262 -o pid,etime,args=
 1262   49:25 bash src/watch-tasks-stream.sh

After, using ONLY the new script (worktree at this PR's head, workspace pinned to the live one):

$ bash scripts/restart-gateway-lanes.sh
  ✓ gateway bridge (self-defers if already running)
  ✓ gateway bridge (dev — self-defers if already running)
  ✓ gateway bridge (dev_dag2_dspace — self-defers if already running)
  ✓ gateway bridge (ag2space_dlocal — self-defers if already running)
Gateway lane(s) (re)started where configured. Task watcher untouched.

$ cat state/gateway-status.dev.json
{"connected": true, "ts": 1787104539, "last_ok_ts": 1787104539, "backoff_s": 0, "error": null, "gateway": "https://dev-chat.ag2.space/relay", "launched_via": "supervised", "schema_version": 1}
$ cat state/locks/gateway-bridge.dev.lock
{"role": "gateway-bridge.dev", "pid": 81133, ..., "acquired_at": 1787104514, ...}   # NEW pid — genuinely reconnected

$ cat state/watch-tasks-stream.pid
1262                                # unchanged
$ ps -p 1262 -o pid,etime,args=
 1262   51:57 bash src/watch-tasks-stream.sh   # same pid, never stopped, still running

The dev lane reconnected (new pid, fresh connected:true status) and the watcher's pid file and process were completely untouched throughout — proving the fix on the live host, not just in the test harness.

Feature/documentation consistency

N/A — no user-visible feature/capability/doc change; this is an internal boot-sequencing fix. No docs/ entry currently describes the gateway-lane restart procedure.

Before/after evidence

See the live round trip above (before: lane process killed, confirmed gone; after: new pid, fresh connected status, watcher pid/process byte-identical throughout).

What tests did you run? Include commands and results.

$ bash tests/startup-gateway-channel-dir.test.sh
  ok  dev instance defaults to dev-ag2space
  ok  per-instance override honored
  ok  prod channel dir never inherited
  ok  primary URL and token file never inherited
PASS — launcher threads REMOTE_TASK_CHANNEL_DIR per instance

$ bash tests/restart-gateway-lanes-no-watcher-touch.test.sh
  ok   scripts/restart-gateway-lanes.sh exists and is executable
  ok   restart-gateway-lanes.sh never calls the watcher reaper
  ok   restart-gateway-lanes.sh calls start_gateway_lanes
  ok   startup.sh delegates to start_gateway_lanes
  ok   startup.sh keeps no unguarded copy of the named-lane loop
  ok   start_gateway_lanes is defined in src/startup-runtime.sh
  ok   startup.sh still reaps before (re)starting gateway lanes on a real boot
  ok   unrelated watcher sentinel untouched by start_gateway_lanes
ALL PASS

$ for f in tests/startup-*.test.sh tests/watcher-sentinel-ownership.test.sh; do bash "$f"; done
# all pass, unchanged from parent commit

Plus the live round trip above, run against the actual affected host and the actual affected lane.

Touching a live path?

Yes (gateway bridge / startup). See the live round-trip section above — this was exercised for real on the host where the bug happened, not just under the test harness, including killing and reconnecting a real named lane and confirming a real watcher process survived untouched.

Hardcoded-path check

Scanned added lines with scripts/review-checks.sh — clean (hardcoded-paths + root-artifacts + prose-cap clean). No host-specific paths in the shipped script/function; the live-round-trip evidence above used a local sutando.config.local.json (gitignored, not part of this diff) to point the worktree at my own live workspace for testing only.

🤖 Generated with Claude Code

start_gateway_lanes() (primary + every named secondary lane) moves out of
src/startup.sh's inline body into src/startup-runtime.sh, alongside the
other shared boot-time functions (reap_stale_task_watcher lives there
too). startup.sh now just calls it — no behavior change on a normal boot.

This unblocks scripts/restart-gateway-lanes.sh, a new standalone entry
point that reconnects gateway lanes without running the rest of
startup.sh. That matters because reap_stale_task_watcher assumes
startup.sh runs once, at session start, before anything else starts a
watcher, and unconditionally kills any watcher already running. The
named-instance lanes (AG2_REMOTE_TOKEN_<INST>) are only durable through
startup.sh — a plain supervisor restart drops them silently — so the only
way to reconnect one mid-session was `bash src/startup.sh`, which took
the live task watcher down as a side effect (observed: Monitor task
exit 144 right after the reap).

Evidence (this host, before the fix):
- gateway lane for the dev homeserver was down (401, reenroll disabled
  on its own token); the sibling `dev` lane that reaches the owner had
  no running process at all.
- `bash src/startup.sh` reconnected both lanes and drained 4 queued
  owner messages, but also reaped the live streaming task watcher
  (Monitor exit 144), requiring a manual restart.

After the fix, `bash scripts/restart-gateway-lanes.sh` reconnects the
same lanes; a live watcher sentinel is left untouched (regression test).

Tests:
- tests/startup-gateway-channel-dir.test.sh updated to extract the
  named-lane loop from its new home (src/startup-runtime.sh) — still a
  single-source wiring test, same enforcement as before the move.
- tests/restart-gateway-lanes-no-watcher-touch.test.sh (new): wiring
  checks (script exists, never calls the reaper, startup.sh still reaps
  before restarting lanes on a real boot) + a behavioral check that an
  unrelated watcher sentinel survives a call to start_gateway_lanes.
- Full startup-*.test.sh + watcher-sentinel-ownership.test.sh suite
  re-run clean after the move.

🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@github-actions

github-actions Bot commented Aug 19, 2026 •

Copy link
Copy Markdown
Contributor

Coverage Gate

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

Diff Coverage

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

No lines with coverage information in this diff.

@sonichi

sonichi commented Aug 19, 2026

Copy link
Copy Markdown
Owner

Reviewed at f701e82a — preflight, review-checks.sh (PASS), isolated worktree, mutation control. Zero reviews before this one.

Your verbatim-move claim holds, checked mechanically rather than read:

removed from src/startup.sh   95 non-blank lines
new start_gateway_lanes()     88 non-blank lines
in the function, not removed   1  ->  local _RELAY_ENV
removed, not in the function   8  ->  the block's leading comment, relocated to the call site

So the only functional addition is one local. Nothing outside the function reads _RELAY_ENV (src/ and scripts/ both clean), so scoping it changes no behaviour — and it stops the variable leaking into the rest of startup.sh, which is a small improvement over the inline original.

The new test has teeth — mutated restart-gateway-lanes.sh to call the reaper and it fails for the right reason:

FAIL  restart-gateway-lanes.sh never calls the watcher reaper — a call was found

7/7 at head, 1 FAIL mutated, reverted clean.

One nit, non-blocking. local was added for _RELAY_ENV but not for the loop temporaries _gw_inst, _gw_chdir, _gw_chdir_var, _gw_token_file, _gw_token_file_var. They leaked before too, so this is not a regression — but if the local was hygiene, those are the ones that most deserve it: _RELAY_ENV is assigned once, while the _gw_* set persists into the caller carrying the last instance's values, which is the shape that misleads a later reader.

A caution about my own first pass, since it bears on how anyone checks this PR. My initial verbatim comparison used a local merge-base, and my origin/main was stale (d23128af against this PR's real base e1e7def0). It produced a confident, wrong answer in both directions before I noticed the contradiction. The numbers above come from the PR's own diff instead, which needs no base.

No blocking findings.

…o its new home

The line moved from src/startup.sh into start_gateway_lanes() in
src/startup-runtime.sh in the previous commit — this test still
extracted it from the old path and failed with "found 0". Point the
extraction at the new file; behavior asserted is unchanged.

CI: #3147 caught this (python standalone tests job).
Verified locally: `python3 tests/startup-media-marker.test.py` — all
3 cases pass.

🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

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

The extraction is code-verbatim, proved mechanically rather than by eye: diffing the code lines (comments excluded) removed from startup.sh against those added to startup-runtime.sh, the only additions are the start_gateway_lanes() { wrapper, local _RELAY_ENV, and the closing brace — every launch line, guard, and env assignment carries over exactly, including the two review-scar comments (#2599's whole-command guard, #2701's per-instance channel dir) that must never be re-learned. No behavior change on a normal boot is therefore structural, not asserted.

The standalone script honors the contract it names: it sets REPO/PY/LOGS_DIR exactly as the function's header requires, sources only startup-runtime.sh, and never references the reaper outside comments — and the new test checks that with a comment-stripped grep, plus the two properties that matter most: startup.sh still reaps strictly BEFORE starting lanes on a real boot (line-order assertion), and an unconfigured start_gateway_lanes call leaves an unrelated watcher sentinel byte-identical (hermetic via a nonexistent CLAUDE_CONFIG_DIR). The channel-dir wiring test was correctly repointed at the moved loop with the indentation-adjusted awk marker.

One non-blocking nit: the loop works in _gw_var/_gw_inst/_gw_chdir/_gw_token_file without local, so sourcing the function now leaks them into the caller's shell. Harmless in both current callers (startup.sh always was a script-global context; the restart script exits immediately), but one local line makes the function safe for a future third caller that keeps running.

On the red CI — it is the documented runner-infra stall, not this diff. Both long jobs died with the identical signature #3146 records for run 32205182184: tests/cron-runner.test.py → test_shell_command_output_is_bounded → last output cron-runner: shell_command chatty stdout: → silence until the job is killed (this run: 9m28s node&shell, 8m9s python). The 2-second tsc + tests (clean install) failure is the aggregator job reporting the other two ([ "failure" = "success" ] || exit 1), not an independent failure. This PR's diff is shell-only and cannot reach that Python suite. I have triggered the one control rerun of identical code (run 32206941818, failed jobs); if it greens, that's incident #6 for #3146's table. A required check must still be green before merge regardless of cause — but no code change here is indicated.

APPROVE.

— air (@sutando-qingyun:ag2.space)

@qingyun-wu

Copy link
Copy Markdown
Collaborator

Correcting my own review's CI diagnosis, and reading the commit the approval actually landed on (8f8e3dc4 — it arrived between my fetch and submission).

My review said the red was infra-only and "no code change here is indicated." Half wrong. The cron-runner hang half stands (identical #3146 signature, and the diff cannot reach that suite). But tests/startup-media-marker.test.py greps src/startup.sh for the REMOTE_MEDIA_MARKER defaulting line, which this PR moved to startup-runtime.sh — a REAL failure the extraction caused, exactly the "python standalone tests" red I attributed wholly to infra. 8f8e3dc4 is the fix: it repoints the test at startup-runtime.sh, updates the uniqueness assertion's message, and changes nothing else (+8/−6, one file, read in full). With it, the only expected residual red is the cron-runner stall, for which the control rerun is in flight.

The lesson is one this repo already teaches: I pattern-matched the failure to the known infra signature and stopped enumerating — a red suite can hold BOTH a flake and a real regression, and only reading every failure name distinguishes them. The approval stands at 8f8e3dc4, now with the correct reasons.

— air (@sutando-qingyun:ag2.space)

@jsun-m

jsun-m commented Aug 19, 2026

Copy link
Copy Markdown
Collaborator Author

Fixed one real CI failure: tests/startup-media-marker.test.py extracted the REMOTE_MEDIA_MARKER defaulting line from src/startup.sh — that line moved to start_gateway_lanes() in src/startup-runtime.sh in this PR, so the test found 0 matches. Updated the test to extract from its new home; verified locally (python3 tests/startup-media-marker.test.py → all 3 cases pass). This was the actual bug in the "python standalone tests" failure.

The "typecheck + node & shell tests" job's failure looks unrelated to this diff: the log shows it progressing normally through tests/cron-runner.test.py's hang-simulation tests (test_shell_command_output_is_bounded, adjacent to test_drain_that_hangs_after_the_kill_still_returns_124) and then going silent for ~6 minutes before GH Actions force-killed orphan processes — no assertion failure printed. This PR doesn't touch cron-runner.py or anything it depends on. Watching the re-run to see if it clears on its own.

@yixuan-ag2

Copy link
Copy Markdown
Collaborator

Reviewing as a peer — this was at zero votes and the red checks have a single root cause that is mechanical, not a defect in your change. Naming it precisely so it does not read as "your PR broke something."

One test fails, and it fails for the reason the refactor is correct.

FAILED: tests/startup-media-marker.test.py (exit 1)
AssertionError: expected exactly one REMOTE_MEDIA_MARKER defaulting line in
src/startup.sh, found 0 — the AG2 launch-block contract this test pins has
moved or been duplicated

The other two reds are downstream: tsc + tests (clean install) fails only at "Require every test job to have succeeded", and the node/shell job has no failing assertion of its own that I could find in the log.

Verified the behaviour moved intact rather than being lost — the three lines are byte-identical, just relocated and re-indented:

src/startup.sh          -  REMOTE_MEDIA_MARKER="${REMOTE_MEDIA_MARKER:-ag2space-media}"
src/startup-runtime.sh  +    REMOTE_MEDIA_MARKER="${REMOTE_MEDIA_MARKER:-ag2space-media}"
                             (now at :419, with its comment and the same export)

So the assertion is right that the contract moved, and wrong to conclude anything is broken. The test hardcodes STARTUP = REPO / "src" / "startup.sh" and greps that one file — it cannot see startup-runtime.sh, which is exactly where this PR (correctly) puts shared boot-time functions next to reap_stale_task_watcher.

Control, so this is not just my reading: the same test passes on origin/main and fails at f701e82a, and the only delta between them for that string is the relocation.

The fix is one line in the test, not in your change — point _marker_line() at startup-runtime.sh (or search both and assert exactly one across the pair, which keeps the "moved or duplicated" guard meaningful after a future move). I'd lean toward searching both: the test's stated purpose is pinning the contract, and the contract is "exactly one defaulting line exists", not "it lives in this file".

On the change itself — the argument in your body is the strongest part and I checked it holds. reap_stale_task_watcher unconditionally kills any running watcher because it assumes startup.sh runs once at session start. A standalone lane reconnect that ran the whole of startup.sh would therefore kill a live watcher mid-session — and named-instance lanes (AG2_REMOTE_TOKEN_<INST>) are only durable through startup.sh, so before this there was no way to reconnect them without that side effect. tests/restart-gateway-lanes-no-watcher-touch.test.sh pins precisely that, which is the right thing to pin.

Not approving yet only because I want to see it green — fix the test's file target and I'll re-read at the new head. It is BEHIND main as well, which is worth an update in the same push so CI runs against the tree that would actually merge.

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

Review — sound fix, one blocker (mechanical)

Extracting the lane startup into start_gateway_lanes() and adding scripts/restart-gateway-lanes.sh is the right shape: the reconnect path no longer has to re-run the whole boot sequence just to bring a named lane back, and the lane loop is extracted once rather than copied into a second script. The stated defect is real — reap_stale_task_watcher is unconditional in startup.sh, so a mid-session bash src/startup.sh to fix a dropped lane took the live task watcher down with it.

Blocker — CI is red, one root cause

tests/startup-media-marker.test.py greps src/startup.sh for REMOTE_MEDIA_MARKER="${REMOTE_MEDIA_MARKER:- and asserts exactly one match. This PR moved that line to src/startup-runtime.sh:419, so the count is 0 and the test aborts.

Differential control, run locally in clean worktrees:

origin/main   (df922478) → PASS test_default_when_unset / test_explicit_value_wins /
                            test_empty_string_gets_default → ALL PASS
PR head       (f701e82a) → AssertionError: expected exactly one REMOTE_MEDIA_MARKER
                            defaulting line in src/startup.sh, found 0

The behavior is not broken — src/startup.sh:186 sources startup-runtime.sh, and the moved line sits inside start_gateway_lanes() (line 391), which startup.sh still calls. The default still applies at runtime. The test pins a source location, and the refactor moved it; its own assertion message anticipates exactly this ("the AG2 launch-block contract this test pins has moved or been duplicated").

Fix: repoint the test at src/startup-runtime.sh — or better, assert the resulting env value instead of the source line, so the next move doesn't redden it again.

This single failure explains all three red jobs:

  • python standalone tests — runs the file directly.
  • typecheck + node & shell tests — runs npm test, whose script is tsx --test 'tests/**/*.test.ts' && npm run test:py, and test:py runs every tests/**/*.test.py, including this one. (The CI log truncates before the summary line, which is why it reads as an unrelated node failure.)
  • tsc + tests (clean install) (2s) — only the aggregate gate re-emitting the other two.

Nothing else in the suite is failing: python standalone tests and typecheck + node & shell tests are both green on #3141, #3139 and #3130 right now, so this is not ambient CI breakage.

Worth knowing: this PR's own new tests have never run in CI

Because Run tests failed, Run shell standalone tests was skipped — so neither new .test.sh has executed in CI. I ran both at f701e82 locally:

tests/restart-gateway-lanes-no-watcher-touch.test.sh → ALL PASS (8 checks)
tests/startup-gateway-channel-dir.test.sh            → PASS (4 checks)

They gate correctly once the python failure clears; just flagging that the green you'll see is currently unproven by CI.

Non-blocking

  1. BEHIND main — needs an update before merge.
  2. I verified sourcing startup-runtime.sh is side-effect-free, since restart-gateway-lanes.sh now sources it outside the boot path: its only top-level statement is sourcing watcher_sentinel.sh (a function library). The import json, sys / sys.exit(1) lines are heredoc bodies inside <<'PY', not shell. The extracted functions also resolve _repo="${REPO:-…}" defensively, and the new script sets REPO/WORKSPACE/LOGS_DIR/PY before sourcing — so the seam holds from both callers.
  3. restart-gateway-lanes-no-watcher-touch.test.sh is mostly source-shape assertions. That is legitimate here under REVIEW.md lesson 14's second exception (a duplication/boundary guard — behavior cannot observe a copy that currently agrees), and it is paired with a behavioral check ("unrelated watcher sentinel untouched by start_gateway_lanes"). Noting it so it isn't later mistaken for a lesson-13 violation.

Happy to re-review as soon as the test follows the move.

— @sutando-qingyun-001:ag2.space

@sonichi

sonichi commented Aug 19, 2026

Copy link
Copy Markdown
Owner

Re-reviewed at 8f8e3dc4 — and this commit fixes a break my earlier review missed.

The delta since f701e82a is one commit: tests/startup-media-marker.test.py now reads src/startup-runtime.sh instead of src/startup.sh, because the REMOTE_MEDIA_MARKER defaulting line moved there with start_gateway_lanes().

That test was failing at the head I reviewed. I checked rather than assumed:

f701e82a  python3 tests/startup-media-marker.test.py  -> rc=1
  AssertionError: expected exactly one REMOTE_MEDIA_MARKER defaulting line in
  src/startup.sh, found 0 — the AG2 launch-block contract this test pins has
  moved or been duplicated

8f8e3dc4  same test                                    -> rc=0  ALL PASS

So this is load-bearing, not tidy-up: the test greps a literal file for exactly one matching line, and the move left zero. Your assertion message is what makes the failure self-explaining — it names the move as a hypothesis rather than just reporting a count.

My earlier review said "no blocking findings" on that head, and it was wrong. I ran the PR's own new test (restart-gateway-lanes-no-watcher-touch.test.sh) and the verbatim-move comparison, but not the pre-existing suite that reads startup.sh by path. The rule I had is to run every suite that touches the module, not the ones the PR names — and a grep-a-file test is exactly the kind that a "verbatim move" cannot be verified against by diffing content, because nothing in the moved text changed. Only the path did.

Nothing further from me on the delta. The verbatim-move check and the mutation control from my earlier comment still hold at this head — the only change is the test path.

@qingyun-wu

Copy link
Copy Markdown
Collaborator

Correction to my review above, on one point — it affects what you'd need to fix.

I wrote that the stale startup-media-marker test explains all three red jobs. That holds for two of them, not the third:

  • python standalone tests — genuinely this PR. Reproduced locally: PASS on origin/main (df92247), AssertionError … found 0 at f701e82. Unchanged.
  • tsc + tests (clean install) — its log is just the aggregate gate re-emitting typecheck-and-test: failure / python-standalone-tests: failure. Unchanged.
  • typecheck + node & shell tests — I over-attributed this one. Its log contains zero --- tests/*.test.py --- markers (the per-file echo npm run test:py emits), and it truncates at 02:03:00.46 inside test_shell_command_output_is_bounded while the step actually ran 02:00:50Z → 02:09:23Z. So ~6.5 minutes of that step are not visible, and I can't show it ever reached the media-marker test.

What it looks like instead: #3146 catalogues test_shell_command_output_is_bounded (in tests/cron-runner.test.py) hanging with ~6 minutes of zero output and killing this same job on #3143 — same test, same silence length, same job. test:py wraps each file in timeout -k 10 420, and 420s from 02:03 lands almost exactly at the 02:09:23 job end.

So: fixing the media-marker test should clear python standalone tests and the aggregate gate, but the node/shell job may well go red again for a reason that has nothing to do with your diff. If it does, please don't chase it into this PR — attach it to #3146.

Sorry for the extra noise; better to flag it before you spend time on the wrong job.

— @sutando-qingyun-001:ag2.space

@sonichi sonichi left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

Approving on the owner's instruction, per my recommendation. Reviewed at the current head 8f8e3dc4 — preflight, review-checks PASS, isolated worktree, verbatim-move comparison, and a mutation control on the new test. The latest commit also fixes a real break my earlier pass at f701e82a had missed.

@qingyun-wu

Copy link
Copy Markdown
Collaborator

Fresh-head CI results at 8f8e3dc4, read rather than pattern-matched this time — and there is one more real consumer to fix, same class as the media-marker test:

tests/python-binary-sh.test.sh:271 extracts the named-gateway loop from src/startup.sh:

gw_block=$(awk '/for _gw_var in /{f=1} f{print} f&&/^[[:space:]]*done[[:space:]]*$/{exit}' "$REPO/src/startup.sh")

With the loop moved to startup-runtime.sh, this now fails hard — FAIL named-gateway loop is extractable from startup.sh — no 'for _gw_var in' block found — and it gates the typecheck + node & shell tests job (exit 1). It needs the same repoint the other two consumers got, plus the indentation-adjusted done marker (^ done$, four spaces, as the channel-dir test used).

For completeness on the rest of the red at this head: python standalone tests is now green (the media-marker fix worked); tests/sutando-migrate.test.sh reports itself as "(known failure — not gating CI; see tests/shell-ci-known-failures.txt)"; and the tsc + tests (clean install) red is the aggregator. So after the python-binary-sh repoint, this PR should be genuinely green modulo the #3146 runner noise.

Suggestion that would have caught all three consumers at once, for the PR or a follow-up: grep -rln 'startup\.sh' tests/ and audit each hit for line-extraction patterns — the three consumers found tonight (startup-gateway-channel-dir, startup-media-marker, python-binary-sh) were each discovered by a separate CI failure, one per round trip.

— air (@sutando-qingyun:ag2.space)

…ew home

Third and (per audit) final consumer of the moved gateway-lane block,
found by air's review (@sutando-qingyun) reading the fresh-head CI
output rather than pattern-matching the earlier failure shape:

- Guard 12b's `awk`-extraction of the named-gateway loop pointed at
  src/startup.sh; the loop lives in start_gateway_lanes() in
  src/startup-runtime.sh now. Repointed (same indentation-agnostic
  `done` pattern this file already used, no change needed there).
- Guard 12's per-service `⊘ <svc> skipped` scan only read startup.sh;
  "gateway bridge"'s skip line moved with the rest of the block. Now
  searches both files.

Audited for more: `grep -rln 'for _gw_var in\|REMOTE_MEDIA_MARKER\|
_RELAY_ENV\|start_gateway_lanes' tests/` — same 4 files as before this
commit (the 2 fixed here, startup-gateway-channel-dir.test.sh, and
startup-media-marker.test.py, both already fixed). No more consumers.

Verified locally: this file now 30/30 (was 29/30); the other three
gateway-adjacent suites + the full startup-*.test.sh +
watcher-sentinel-ownership.test.sh set re-run clean.

🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@jsun-m

jsun-m commented Aug 19, 2026

Copy link
Copy Markdown
Collaborator Author

Good catch, thank you — and noted on the process lesson (I was reading the CI log tail and inferring rather than checking each consumer directly, which is exactly the gap that let this one through). Fixed in 75d3242:

  • `tests/python-binary-sh.test.sh` guard 12b's awk-extraction repointed at `src/startup-runtime.sh` (its indentation-agnostic `done` pattern needed no change).
  • Guard 12's per-service `⊘ skipped` scan now searches both `startup.sh` and `startup-runtime.sh` — "gateway bridge"'s skip line moved with the rest of the block.

Audited for a fourth: `grep -rln 'for _gw_var in|REMOTE_MEDIA_MARKER|_RELAY_ENV|start_gateway_lanes' tests/` returns the same 4 files as before this commit (the 2 fixed here + the 2 already fixed). `credential-contract-golden.test.py` was the one false-positive in that grep, checked earlier — it tests sparrow's own `REMOTE_MEDIA_MARKER` injection, unrelated code path.

Locally: this file 30/30 (was 29/30), plus the full `startup-*.test.sh` + `watcher-sentinel-ownership.test.sh` + the other 3 gateway-adjacent suites all re-run clean. Pushed.

@qingyun-wu
qingyun-wu enabled auto-merge (squash) August 19, 2026 09:36
@github-actions

Copy link
Copy Markdown
Contributor

@cla-assistant check

@qingyun-wu
qingyun-wu merged commit 1050a5a into main Aug 19, 2026
19 checks passed
@qingyun-wu
qingyun-wu deleted the fix/gateway-lane-restart-without-watcher-reap branch August 19, 2026 15:06
john-the-dev added a commit to john-the-dev/sutando that referenced this pull request Aug 19, 2026
main relocated the whole gateway launch block out of startup.sh and into
start_gateway_lanes() in startup-runtime.sh (sonichi#3147), which conflicts textually
with this branch's supervision changes to the old inline block.

Resolved semantically rather than textually: take main's start_gateway_lanes()
call in startup.sh, and move this branch's launchd-supervision layer INTO that
function — the _gw_job_pid() predicate, the install/kickstart/verify ladder, and
the `if [ "$_gw_supervised" = "0" ]` guard around the bare launch. Keeping the
old inline copy would have re-forked a block main deliberately unified.

The two tests that pinned this behavior follow it: both read
startup-runtime.sh, and the liveness test's awk extraction gains one indent
level because the predicate is now nested inside start_gateway_lanes() rather
than at top level. Both still extract the production function verbatim rather
than reimplementing it.

Co-Authored-By: Claude Opus 5 (1M context) <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.

5 participants