Repository navigation
fix(ag2-sparrow): the start's undo reports a restart truthfully and never stops a bridge by name - #6204
Conversation
…ever stops a bridge by name Follow-ups from the code-owner review of #6151: 1. The seam test at the former final manager stop asserted nothing (the undo no longer calls stop_supervisor). It now injects a successor where that stop used to be (after the retired run exited, before the manager's last read), asserts no stop names the job after the start, that the successor is still running, and that connect reports it. 2. A retire whose run ended with an exit its manager restarts (the claude launcher's session stop failing) was reported as "was stopped". The undo now reads how the run ended from the manager itself (systemd ExecMainCode/ExecMainStatus, launchd "last exit code" / "last terminating signal") and says the job starts again after its restart delay, with the stop command and uninstall. retired() also takes the caller's retry code: the claude launcher's 74 was mapped to 75, leaving a session whose stop failed unsupervised. 3. _wire_bound's root-replaced/problem branch called unwire_bridge, which stops the bridge by name. It now uses the same rule as the pending undo (_unwire_ours): a registered or running bridge is left and reported with its stop command; only an unregistered unit still holding this attempt's bytes is removed. Fixes #6199 (points 1-3; point 4 planned on the issue) Stand: Echo Act IV Pro Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Coverage Gate✅ Diff coverage PASSES the 95% bar. Whole-tree (informational): 92%. Diff CoverageDiff: origin/main...HEAD, staged and unstaged changes
Summary
packages/ag2-sparrow/tests/connect_undo_seams.pyLines 248-256 248 return done
249
250 def stop_seen(shape, *a, **kw):
251 if shape.name == self.local.name and STARTS & {fake.runner.verb(c) for c in fake.runner.calls}:
! 252 stops.append(shape.name)
253 return real_stop(shape, *a, **kw)
254 refused = []
255 with mock.patch.object(cm, "is_macos", return_value=mac), \
256 mock.patch.object(cm, "_await_exit", side_effect=exited_then_replaced), \
|
liususan091219
left a comment
There was a problem hiding this comment.
Ready from my side; not approving yet because the approve bar isn't met: this PR has no approval from an ag2-sparrow expert. @qingyun-wu, who wrote the #6151 code-owner review these three points come from, is the natural one to give it.
Verified at 8db3232 (base 1f7f639 = current main), in throwaway worktrees:
- Head:
test_claude_code_connect(136),test_codex_local_connect(122),test_claude_code_adapter(143) andtest_bridge_exclusiveall OK locally. CI is green, and thelicense/clastatus is present andsuccess. - Fail-first: I ran the head tests against the base
ag2_sparrow/sources. The failures match the PR body: claude-local 6 failures and 9 errors (KeyError: 'restarts', the restart-report test on systemd and launchd, the replaced-root bridge test, the unregistered-plist test), codex-local 3 failures, and adapter 1 failure (75 != 74, the session-stop-failed case). - Mutations: I re-added
stop_supervisor(shape)before the lastjob_identityread in_retire_run. The new seam test then fails in all 4 subcases on both adapters (['claude-local'] != [],['codex-local'] != []). I also re-addedunwire_bridge(...)in_wire_bound's problem branch, and the replaced-root bridge test fails on both adapters. So point 1's test now really pins that nothing stops the job by name, which the old test did not. - Point 2 logic: I checked it against what the managers do. systemd's unit has
Restart=alwaysandRestartPreventExitStatus=75, so only exit code 75 reads as not restarting. On launchd, the--launchdshim maps 75 to 0, andKeepAlive={SuccessfulExit: false}makessuccess == keepthe right predicate. An unreadable ending is reported as unknown, never as stopped. - The 74-vs-76 fix:
retired()used to keep onlycm.EXIT_RETRY(76) while the claude launcher's retry code is 74, so a retire whose session stop failed was held at 75 and left unsupervised. The fix is real. The codex connector callsretired()with the default, which is correct because it uses the shared retry code.cm.retired(has no other caller. - Point 3:
_wire_boundnow goes through_unwire_ours, the same rule the pending undo uses.unwire_bridgestays only for uninstall.
The tests drive the real CLIs against fake manager front-ends and assert on behavior. None of them reads module source text or reimplements the unit under test. No hardcoded paths are added. A missing live round trip is not blocking here, since the change only touches the error-path undo.
— Sutando (susans-MacBook-Pro)
qingyun-wu
left a comment
There was a problem hiding this comment.
Reviewed at 8db3232. Approving.
Undo never stops a start it did not make. _undo_start -> _retire_run still signals only the exact run (pid + start time), and no step names the job to its manager. The replaced seam test (connect_undo_seams.py:233) injects another actor's start right after the retired run exits, and asserts no stop_supervisor for the local shape and no stop verb after the start. It passes on base too, because main already has the earlier fix, so it pins the behavior rather than failing first. Its proof is the mutation: re-adding stop_supervisor(shape) before the final read fails it on both adapters. test_seam_a_successor_after_the_signal_is_never_stopped still passes.
The restart report is truthful. connect_common.py:870-892: on systemd, ExecMainCode 1/2/3 maps to "restarts" except exit 75 (matches Restart=always + RestartPreventExitStatus=75 in render_service). On launchd, success == keep_alive_on_success matches KeepAlive {SuccessfulExit: false} together with launchd_exit mapping logical 75 to 0. A missing record (ExecMainCode=0, (never exited)) gives None, and _retire_run (:1441) then reports "cannot be read" instead of "stopped". I checked the last exit code = N / (never exited) line format against a real launchctl print on macOS, and the regex matches it.
Retry code. retired(..., retry=EXIT_RETRY) in launcher.py:623 keeps the claude launcher's 74. I confirmed that reverting it fails test_a_retire_whose_session_stop_fails_asks_to_be_retried_not_retired. The codex default of 76 is unchanged.
Bridge. _wire_bound (connect_bridge.py:385) now goes through _unwire_ours, so a registered or running bridge is left and reported with its stop and uninstall commands. Only an unregistered unit that still holds this attempt's bytes is unlinked. unwire_bridge remains only on the uninstall paths.
Tests (local, high load): test_bridge_exclusive and test_claude_code_adapter OK. In the connect suites, test_a_holder_that_takes_the_root_after_the_clean_check_keeps_the_seat fails identically on origin/main (a host Python cache file under the fake home), so it is environmental. test_a_launcher_holding_the_agent_lock_past_the_wait_is_left_and_reported failed once on its 2 s wait under load and passes in isolation. CI: 45 pass, 3 skipped, none failing.
Non-blocking: on systemd, _retire_run could compare after["since"] with ident["since"] before using ExecMainCode/ExecMainStatus. If a successor started and exited inside the window, the exit record is the successor's, and the line would attribute its ending to this operation's pid. The window is narrow and the restart claim still holds for the job, so this is wording only.
Fixes #6199 (points 1–3; point 4 planned on the issue)
What changed, and why
Follow-ups from the code-owner review of #6151.
test_seam_a_successor_at_the_final_manager_stop_is_never_stoppedpatchedstop_supervisor, which the undo no longer calls, so its successor list stayed empty. It is replaced bytest_seam_a_successor_at_the_former_final_stop_is_left_and_reported: it injects another actor's start exactly where the final stop used to be (after the retired run exited, before the manager's last read), and asserts that nostop_supervisorfor the local shape runs after the start, that no stop verb reaches the manager, that the successor is alive and still the manager's run, and (connect) that the report names its pid.job_identitynow also reads how the last run ended, from the manager itself: systemdExecMainCode/ExecMainStatus, launchdlast exit code/last terminating signal, checked against each shape's restart policy (RestartPreventExitStatus=75; launchdKeepAlive/SuccessfulExit)._retire_runreportspid N ended with an exit its manager restarts, not as a retirement, so the job starts again after its restart delay, and an unreadable ending is reported as such, never as stopped. The undo's "could not be stopped" line now gives both the stop command and<command> uninstall.While writing the fail-first test I found the issue's premise does not hold today:
cm.retired()keeps onlycm.EXIT_RETRY(76), but the claude launcher's retry code is 74, so a retire whosestop_sessionfailed exited 75. The manager then never restarted it, and the tmux session was left running with no launcher supervising it.retired()now takes the caller's retry code, and the launcher passes its 74. That makes the outcome the issue describes (the manager restarts the job, and the new launcher adopts the session) and the report says so._wire_bound's root-replaced/problem branch stopped the bridge by name (throughunwire_bridge). It now goes through_unwire_ours, the rule the pending undo already uses: a registered or running bridge is left and reported with its stop command (and uninstall). Only an unregistered unit that still holds this attempt's bytes is removed.unwire_bridgestays for uninstall, which is a deliberate stop.Where to look first
packages/ag2-sparrow/ag2_sparrow/connect_common.py:job_identity(the newrestartsfield),_retire_run,_undo_start,retiredpackages/ag2-sparrow/ag2_sparrow/connect_bridge.py:_wire_boundpackages/ag2-sparrow/tests/connect_undo_seams.py: the two seam tests (mixed into both the claude-local and codex-local suites)<acted>.stop-failsswitch that ends a retire with the launcher's retry code.Behavior this proves
respawns == 1).stop/bootoutto it, and the report gives its stop command. On launchd, an unregistered bridge plist from a refused bootstrap is removed.Documentation
N/A: no user-facing feature changes. Only the wording of the error lines a failed connect/install prints changes, and no canonical doc quotes those lines.
Before / after evidence
Fail-first (new and changed tests run against
origin/mainsources, with only the three source files reverted), excerpted:The codex-local suite fails the same way (
how='connect'on systemd and launchd, and the replaced-root bridge test: 3 failures).Mutations (applied at this head, then reverted):
_retire_run(stop_supervisor(shape)just before the manager's last read). The new seam test fails in all 4 subcases (connect/install × systemd/launchd) on both adapters:AssertionError: Lists differ: ['claude-local'] != [], and likewise['codex-local'] != [].unwire_bridge(cfg, command, bridge)in_wire_bound's problem branch. The replaced-root bridge test fails on both adapters:AssertionError: 'the bridge this attempt wired (...-bridge.service) is active and was left, ...' not found.At HEAD, every one of these passes.
Tests run
Each run used
env -i,nice -n 19, the shared test lock and load < 10, with no live service or workspace touched:Live path
N/A. This changes the error-path undo of the
connect/installCLIs, not a running bridge, delivery loop or startup. Both managers are exercised through real child processes (the launchd branch with its shim) under fake manager front-ends.Residuals
ExecMainCode=0, launchd(never exited)), the line says so and does not claim a stop.🤖 Generated with Claude Code