Skip to content

feat(ag2-sparrow): Codex connect is on; --experimental-connect is a deprecated no-op - #6259

Closed
sonichi wants to merge 4 commits into
mainfrom
feat/codex-connect-on
Closed

sonichi wants to merge 4 commits into
mainfrom
feat/codex-connect-on

Conversation

@sonichi

@sonichi sonichi commented Nov 5, 2026 •

Copy link
Copy Markdown
Owner

What changed, and why

ag2-sparrow-codex-local connect works without --experimental-connect. #5871 gated it off while ownership windows on the shared start/undo path were still open. Those fixes have since landed on main (checklist below), so the gate comes off.

  • connect no longer refuses without the flag.
  • --experimental-connect is still accepted, so existing scripts keep working. It is a deprecated no-op: it prints one line, warning: --experimental-connect is deprecated and does nothing: connect is on; drop the flag, and changes nothing else. It is never carried into a rerun command that a failed connect prints.
  • --agent is still required. No safety check is removed or weakened.
  • Round 2 (from keweichen's review at 516e04d4), shared connect code:
    1. A failed connect no longer removes a live session's bridge. When a pending start fails, the cleanup under the agent lock used to withdraw what waited on the session (the bridge unit/plist, or the sparrowd declaration) without checking who held the root. The withdrawal now runs only while no session serves the root (connect_common.serving_session, the same predicate orphan_bridge now uses). Otherwise the bridge is kept, and a bridge unit this attempt stopped is started again. connect's install also stopped running its bridge reconciliation inside the redemption's lock, before the start settled; finish_connect now runs it once, after the settle.
    2. The root --agent names is preflighted before the code is read. known_root_problem now checks that root's agent lock and holder too, not only a root the adapter's config recorded. The post-redemption check stays, for a root taken in between.

Why the gate existed, and what closed each reason

# Reason the gate was kept (from #5871 round 14, the post-merge audit and #5939) Closed by
1 An external start between the first pid probe and start_supervisor() was claimed as this attempt's own, and a rollback could stop it. On launchd this was a regression against the earlier main. #6151: ownership needs proof. On launchd that means our own bootstrap loaded the job; on systemd, a new InvocationID plus a start inside our call.
2 Busy cleanup acted on a stale snapshot of the lock holder, without holding the lock. #6151: the unlocked cleanup was removed, and _undo_start refuses unless it holds the agent lock.
3 The undo compared the pid, then stopped by service name, so it could kill a successor. #6151: _retire_run signals only the run its own start began, by exact pid and start time. #6204: no step names the job to its manager, and the bridge is never stopped by name on the root-replaced branch.
4 The test helpers forced is_macos=False, so the "launchd" cases never ran the launchd branch. #6151: the forced patch is gone. The seam suite runs both managers' real branches for both adapters, for connect and for install --token-env-file.
5 Codex install --token-env-file reaches the same start/undo windows. #6151: the same fix covers it, and it is in every seam matrix. #6204 added the restart-reporting follow-ups.
6 A busy lock, a held root or an unwritable seat was refused only after the code had been spent (#5922). #6143 moved those checks before the code is read, and #6240 holds the agent's fence before redeeming. But only for a root the adapter had already recorded: keweichen showed that a root held by the other adapter, never recorded by Codex, still cost a redemption. This PR closes that (round 2, item 2).
7 One agent must not end up with two bridges across adapters. #6145: --agent is required, and the owner check runs before the code is spent. #6240: one job per agent, so several agents can share a host.
8 With the flag off, zero backend calls had to be asserted. #6151 added that test. This PR removes the flag-off state it tested.

The closing PRs also list what they could not fix, because the managers have no atomic primitive for it: systemd start attribution inside one call window, the macOS pid-reuse window without a pidfd, and a retire that depends on the launcher cooperating. Those are all in #6151's residual-risk section and are not specific to Codex.

Turning connect on by default also exposed a ninth issue, which keweichen reproduced on all four manager × bridge-host combinations: a failed reconnect removed a live foreground session's bridge. This PR fixes it (round 2, item 1).

Live witness (REVIEW.md gate): the audit asked for a live witness at the final SHA: a real one-time-code redemption, then the bridge, then a task and its result. The witnesses on #6151 and #6240 used install --token-env-file, not a redemption. A live Codex connect round trip (redeem → bridge → task/result) will run at this PR's final head, and approval will be requested only after it is posted here.

Where to look first

  1. packages/ag2-sparrow/ag2_sparrow/connect_common.py: serving_session, _unwaited and their call in await_started. Then connect_bridge.py: wire_bridge's restore, known_root_problem, under_agent_lock and finish_connect (_FINAL).
  2. packages/ag2-sparrow/ag2_sparrow/adapters/codex/connect_cli.py: connect() and the connect subparser.
  3. packages/ag2-sparrow/tests/test_codex_local_connect.py: lost_reconnect and its 8 generated tests, connected_on, the two new ConnectCode tests, and test_the_install_a_failed_connect_names_never_carries_the_deprecated_flag.
  4. packages/ag2-sparrow/ag2_sparrow/adapters/codex/README.md: the "Connect with a code" paragraph and the usage block.

User behavior this proves

  • ag2-sparrow-codex-local connect --base <url> --agent <id> redeems the code once, writes the seat, installs, and the launcher acquires the agent's root. This holds under systemd and under launchd.

  • connect --experimental-connect … behaves the same and warns exactly once.

  • connect without --agent is still refused before the code is read (test_a_connect_without_agent_is_refused_before_the_code_is_read, now run without the flag, for both adapters).

  • A reconnect whose managed launcher never takes the root, while a foreground session does, fails (rc ≠ 0) but leaves that session its bridge: the unit/plist is kept and the bridge it stopped runs again, or the sparrowd declaration is kept byte for byte. With no session on the root, the bridge is withdrawn as before. Both hold on systemd and launchd.

  • A root the other adapter holds is refused with zero backend calls, and the code stays unused, in both adapters' suites.

Feature/documentation consistency

The adapter's own README is updated. docs/catalog.json has no entry for the codex-local adapter, so no docs/ page changes. CHANGELOG-PENDING has an entry under Changed.

Before/after evidence

Fail-first: the new tests run against origin/main's connect_cli.py, with only that file reverted:

FAIL: test_connect_runs_without_any_flag_on_both_managers [systemd]
AssertionError: 2 != 0 : error: Codex connect is not enabled in this release; use install --work / verify
FAIL: test_connect_runs_without_any_flag_on_both_managers [launchd]
AssertionError: 2 != 0 : error: Codex connect is not enabled in this release; use install --work / verify
FAIL: test_the_old_experimental_flag_is_a_deprecated_no_op_that_warns_once [systemd]
AssertionError: 0 != 1
FAIL: test_the_old_experimental_flag_is_a_deprecated_no_op_that_warns_once [launchd]
AssertionError: 0 != 1
Ran 2 tests
FAILED (failures=4)

At the head: Ran 2 tests … OK.

Mutations. Each was applied to the head and restored afterwards:

== mutation: the warning is printed unconditionally
FAIL: test_connect_runs_without_any_flag_on_both_managers [systemd]  'experimental-connect' unexpectedly found
FAIL: test_connect_runs_without_any_flag_on_both_managers [launchd]  'experimental-connect' unexpectedly found
== mutation: the warning is never printed
FAIL: test_the_old_experimental_flag_is_a_deprecated_no_op_that_warns_once [systemd]  0 != 1
FAIL: test_the_old_experimental_flag_is_a_deprecated_no_op_that_warns_once [launchd]  0 != 1
== mutation: the warning is printed twice
FAIL: test_the_old_experimental_flag_is_a_deprecated_no_op_that_warns_once [systemd]  2 != 1
FAIL: test_the_old_experimental_flag_is_a_deprecated_no_op_that_warns_once [launchd]  2 != 1
== mutation: the flag is a gate again (refuses without it)
FAIL: test_connect_runs_without_any_flag_on_both_managers [systemd]  2 != 0 : error: off
FAIL: test_connect_runs_without_any_flag_on_both_managers [launchd]  2 != 0 : error: off
== mutation: experimental_connect dropped from connect_bridge._NOT_SESSION (the flag leaks into the rerun hint)
FAIL: test_the_install_a_failed_connect_names_never_carries_the_deprecated_flag

Round 2 evidence

Fail-first for item 1: the new interleaving tests run against 516e04d4's connect_common.py and connect_bridge.py. This reproduces the review's finding in all four combinations; the no-session controls pass on both sides:

FAIL: …keeps_the_foreground_sessions_bridge_launchd_sparrowd  AssertionError: None != b'{\n "written_by": …' : the declaration is kept as it was
FAIL: …keeps_the_foreground_sessions_bridge_launchd_unit      AssertionError: None != b'<?xml version="1.0" …' : the unit is kept
FAIL: …keeps_the_foreground_sessions_bridge_systemd_sparrowd  AssertionError: None != b'{\n "written_by": …' : the declaration is kept as it was
FAIL: …keeps_the_foreground_sessions_bridge_systemd_unit      AssertionError: None != b'# Written by ag2-sparrow-codex-local in…' : the unit is kept
Ran 8 tests  FAILED (failures=4)

At the head: Ran 8 tests … OK.

Fail-first for item 2: the changed test fails in both suites at 516e04d4:

FAIL: test_a_root_another_adapter_holds_is_refused_before_the_code_is_read
AssertionError: 'the code was not used' not found in 'error: <root> is held by launcher pid N; stop what holds it, then connect again; the code is spent and nothing was written'

(test_codex_local_connect and test_claude_code_connect each show 1 failure). Both pass at the head.

Mutations (each applied at the head, then restored):

== the failed start's cleanup ignores who serves the root      -> 4 FAIL (every keeps_the_foreground_sessions_bridge_*)
== the failed start's cleanup always keeps                      -> 8 FAIL (all four controls and all four keeps)
== serving_session ignores a live holder                        -> 4 FAIL (keeps_*)
== the bridge this attempt stopped is not restored              -> 2 FAIL (keeps_*_unit, systemd and launchd)
== no final reconciliation in finish_connect                    -> 4 FAIL (keeps_*: the final "kept" line is missing)
== known_root_problem ignores the root --agent names            -> FAIL test_a_root_another_adapter_holds_is_refused_before_the_code_is_read
== the held-root check (_free_root_problem) always "free"       -> FAIL both root tests, before and after the redemption

Three existing tests changed expectation because the refusal now comes earlier. test_a_job_active_for_another_root_does_not_vouch_for_a_held_root and test_an_independent_holder_of_this_shape_on_the_root_is_refused_while_the_job_serves_another are now refused before the code is spent. In both suites, test_a_holder_that_takes_the_root_after_the_clean_check_keeps_the_seat now takes the root after the post-redemption check, which is the check it targets.

Tests run

These are focused suites only, on Python 3.14 under nice -n 19 and env -i. Full discovery and the version matrix are left to CI.

== test_codex_local_connect          Ran 133 tests  OK
== test_claude_code_connect          Ran 138 tests  OK
== test_claude_code_adapter          Ran 143 tests  OK
== test_claude_code_install          Ran 85 tests   OK
== test_bridge_exclusive             Ran 22 tests   OK
== test_local_multi_agent_repair     Ran 30 tests   OK
== test_adapter_boundaries           Ran 13 tests   OK
== sparrowd-agent-bridges            Ran 50 tests   OK
== ag2-sparrow-claude-local-bridge   Ran 22 tests   OK
ruff: All checks passed!
review-checks: PASS (hardcoded-paths + root-artifacts + prose-cap clean)

Test changes: the old flag-off refusal test is replaced by the two new tests. The ConnectCode and bridge-exclusive helpers drop the flag, so every existing codex connect test and seam test now runs flagless. Two bridge-exclusive tests are renamed (test_codex_connect_is_refused_before_the_code_is_spent_{systemd,launchd}).

Edge cases

  • The flag combined with a failed connect: the rerun line stays exactly the same as without the flag.
  • A missing --agent or a missing --base refuses as before. The deprecation warning, when the flag is given, comes first.

Risks and rollback

There is no migration, config change or new env var. To roll back, revert this PR: connect then refuses again unless the flag is given.

Known gaps

  • The live Codex redemption witness described above. It runs at the final head, before approval is requested.
  • A kept bridge whose unit this attempt stopped is started again only when it was running before; one that was not running is left stopped, and the line says it was kept.

Commit trailers

The round-2 commit carries Sutando-Task:. The first two commits (a10f562c, 516e04d4) predate the trailer and are not rewritten, since that would need a force-push.

Written by Claude Code (Claude Opus 5.5) for Chi Wang, who is accountable for it.

🤖 Generated with Claude Code

…eprecated no-op

`ag2-sparrow-codex-local connect` no longer refuses without the hidden
--experimental-connect flag. The gate was added in #5871 while three
ownership windows on the shared start/undo path were open; #6151 and
#6204 closed them, #6143 moved every refusal before the code is spent,
and #6145 and #6240 made one agent one bridge and one job.

The old flag is still accepted, prints one deprecation warning and does
nothing else; it is never carried into a printed rerun command. --agent
stays required and every safety check is unchanged. README and help text
drop the "experimental, off" wording.

Tests: connect runs flagless under systemd and launchd; the deprecated
flag warns exactly once and connects the same; a failed connect's rerun
line never carries the flag. The bridge-exclusive and seam suites now run
codex connect without the flag.

Stand: Echo Act IV Pro
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@sonichi
sonichi requested a review from qingyun-wu as a code owner November 5, 2026 22:24
@sonichi
sonichi requested a review from keweichen November 5, 2026 22:24
Stand: Echo Act IV Pro
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@github-actions

github-actions Bot commented Nov 5, 2026 •

Copy link
Copy Markdown
Contributor

Coverage Gate

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

Diff Coverage

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

  • packages/ag2-sparrow/ag2_sparrow/adapters/codex/connect_cli.py (100%)
  • packages/ag2-sparrow/ag2_sparrow/connect_bridge.py (100%)
  • packages/ag2-sparrow/ag2_sparrow/connect_common.py (100%)
  • packages/ag2-sparrow/tests/test_bridge_exclusive.py (100%)
  • packages/ag2-sparrow/tests/test_claude_code_connect.py (100%)
  • packages/ag2-sparrow/tests/test_codex_local_connect.py (100%)

Summary

  • Total: 189 lines
  • Missing: 0 lines
  • Coverage: 100%

@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 516e04d. Approving: the change is correct and removes only the gate.

What I checked

  • connect_cli.py:211-218: the only behavior change is the gate block. The old if not args.experimental_connect: return EXIT_UNUSABLE is now a stderr warning when the flag is given. Everything after it is unchanged at this head: the --base check, cb.fenced(args.agent, ...), redeem_into_seat with connect_preflight running before the code is spent, and finish_connect. No safety check was moved past redemption.
  • The deprecated flag is still accepted. connect_cli.py:389 keeps --experimental-connect (store_true, help suppressed), so existing scripts don't hit an argparse error. connect_bridge.py:309 still lists experimental_connect in _NOT_SESSION, so session_flags() never puts it into the rerun install hint. test_the_install_a_failed_connect_names_never_carries_the_deprecated_flag pins that.
  • --agent is still required before the code is read. test_bridge_exclusive.py:286 (test_a_connect_without_agent_is_refused_before_the_code_is_read) now runs without the flag for both adapters, so it exercises the real default path rather than the flagged one. The renamed connect_switch tests (:240, :243) also run flagless.
  • The tests cover both modes: test_connect_runs_without_any_flag_on_both_managers (new default) and test_the_old_experimental_flag_is_a_deprecated_no_op_that_warns_once (old flag). Each runs under both managers through MANAGERS, without forcing is_macos. Each asserts one redemption, rc == 0, and the unit's suffix. The fail-first evidence in the body matches what the gate change would produce.
  • The docs match. The README usage block and the "Connect with a code" paragraph, the module docstring (:16), the subparser help (:387) and the CHANGELOG-PENDING entry all describe the new default and the deprecated no-op the same way. Nothing else in the tree still tells users to pass --experimental-connect.
  • CI: all blocking checks pass at this head (46 pass, 3 skipped release/Windows jobs). review-checks: hardcoded-paths and root-artifacts are clean.

Notes (non-blocking)

  1. A bare connect with no arguments used to fail with the gate's EXIT_UNUSABLE before any argument check. It now fails on argparse's --base error (exit 2). That's expected now that the gate is gone. I mention it only in case a caller keyed on the old exit code.
  2. _connect still passes experimental_connect through session into the install Namespace. That's harmless because install never reads it, and it was already the case before this PR.
  3. The body's own merge gate is still open: a live Codex connect round trip at the final SHA (redeem, then bridge, then task and result). This approval covers the code and the tests, not that witness. Please record it on the PR before merging.

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

Requesting changes — this is not merge-ready.

  1. The newly default-on path can remove the bridge from a live foreground Codex session. I reproduced flagless Codex connect at this exact head in all four systemd/launchd × unit/sparrowd combinations. The supported rotation sequence was: initial connect succeeds; stop the managed launcher while its bridge remains; reconnect's manager accepts the new start but no managed launcher acquires the root; a queued foreground Codex session acquires it before pending-start failure cleanup. All four runs returned nonzero with the holder alive, but the unit/plist was deleted or the sparrowd declaration withdrawn.

    The path is direct: connect_cli.py:218-233 reaches finish_connect; connect_bridge.py:761-776 releases the redemption lock and calls cm.settle; connect_common.py:1884-1892 runs the pending failure callbacks registered at connect_bridge.py:539-577. _unstarted / _withdraw removes the artifact without orphan_bridge's live-holder check. The earlier nested settle is deferred while redemption still holds the lock, and finish_connect has no final holder-aware reconciliation. Worst case: credential rotation leaves an apparently live agent unable to receive tasks or return results. Route destructive pending cleanup through the single final reconciliation decision and add this connect-specific interleaving regression across both managers and bridge hosts before removing the gate.

  2. The reason-6 closure claim is false for an agent root not yet recorded by Codex. Required --agent already determines the canonical root, but known_root_problem() checks root/seat state only when this adapter's config already records ROOT (connect_bridge.py:683-695). The exact-head test at test_codex_local_connect.py:1413-1426 proves the consequence: when the other adapter holds that agent root, Codex connect makes one backend redemption call and then reports that the code is spent. I reran the test at this head. Preflight the root derived from required --agent before reading/redeeming the code, while retaining the post-redemption race check.

  3. The mandatory final-SHA live witness is still absent. The PR body says the real redemption → bridge → task/result round trip remains open. REVIEW.md makes that a review-time gate. Run it after the failure seams above are fixed; a happy-path witness alone cannot cover them.

  4. Both commits lack the repository-required Sutando-Task: trailer.

Positive evidence: the exact-head 124-test Codex suite, 22 bridge-exclusive tests, and 13 adapter-boundary tests pass; the targeted flag/no-flag tests, fail-first control, all five claimed mutations, review checks, CLA, coverage, and visible CI are green. Those validate the intended deprecation behavior, but not the activated lifecycle path.

A fixed head with failure-mode regressions, the required trailer, and the post-fix final-head witness would change this verdict.

…gent's root is checked before the code is spent

1. A pending start that fails withdrew what waited on its session (the
bridge unit or the sparrowd declaration) unconditionally. With connect
on by default, a reconnect whose managed launcher never took the root
while a foreground session did deleted that session's bridge. The
withdrawal now runs under the agent lock only while no session serves
the root (connect_common.serving_session, the same predicate
orphan_bridge uses); otherwise the bridge is kept and a bridge unit this
attempt stopped is started again. connect's install no longer runs its
bridge reconciliation inside the redemption's lock, before the start
settled: finish_connect runs it after the settle.

2. known_root_problem also checks the root the required --agent names
(its agent lock and who holds it) before the code is read, not only a
root the adapter's config recorded. The post-redemption check stays.

Tests: a reconnect whose launcher never takes the root, on systemd and
launchd, unit and sparrowd: a foreground holder keeps its unit/plist or
declaration (and its stopped bridge runs again); with no holder the
bridge is withdrawn. A root another adapter holds is refused with zero
backend calls, in both adapters' suites; a root taken after the preflight
is still refused after the redemption.

Sutando-Task: task-010a8e322d193403e0
Stand: Echo Act IV Pro
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@sonichi

sonichi commented Nov 5, 2026

Copy link
Copy Markdown
Owner Author

@keweichen thank you. Both blockers reproduced exactly as you described. Round 2 is at 998dae67, one normal commit on 516e04d4 (no force-push).

1. A failed reconnect removed a live foreground session's bridge: fixed.

  • Cause, as you traced it: await_started ran the pending start's failed callbacks (_unstarted / _withdraw) without checking who holds the root. The nested settle inside install was deferred while the redemption held the lock, so finish_connect never got a holder-aware decision.
  • Now the destructive cleanup goes through one decision: connect_common.serving_session(shape, root) (_unwaited). It runs under the agent lock right before teardown, and it checks both the managed state and who holds the root. orphan_bridge uses the same predicate.
    • No session serves the root: the unit/plist or declaration is withdrawn, as before.
    • A session serves it: everything is kept. A bridge unit this attempt stopped (it was running before) is started again.
  • The install's orphan_bridge reconciliation no longer runs inside the redemption's lock. finish_connect runs it after the settle.
  • New regression lost_reconnect, 8 cases: systemd/launchd × unit/sparrowd × with or without a foreground holder.
    • The connect runs, the launcher stops while its bridge remains, the reconnect's start is accepted, its launcher never takes the root, and a foreground Codex session takes it.
    • With the holder: rc ≠ 0, the holder is alive, the unit/plist is kept and the bridge runs again, or the declaration is byte-identical. Both the cleanup's "kept" line and the final reconciliation's line are asserted.
    • With no holder (the control): the bridge is withdrawn.
  • Fail-first: at 516e04d4's shared code, the 4 holder cases fail with the artifact deleted or withdrawn. The 4 controls pass on both sides.
  • Mutations: cleanup ignores who serves the root → 4 FAIL; always keeps → 8 FAIL; serving_session ignores a live holder → 4 FAIL; no restore → 2 FAIL (unit); no final reconciliation in finish_connect → 4 FAIL.

2. The root named by --agent was not preflighted: fixed.

  • known_root_problem(local, agent_id) now checks the agent lock and the holder for that root as well as any recorded ROOT, before the code is read. The post-redemption check is unchanged.
  • The test you cited is now test_a_root_another_adapter_holds_is_refused_before_the_code_is_read, in both adapters' suites: zero backend calls, and the code is not used. It fails at 516e04d4 in both suites.
  • A new test_a_root_taken_after_the_preflight_is_refused_before_the_seat_is_written keeps the post-redemption race covered: one call, nothing written.
  • Mutations: dropping the named root fails the first test; a held-root check that always reads free fails both.

3. Live witness: it runs at the final head after these fixes, and its result will be posted here before approval. The PR body says so.

4. Sutando-Task: trailer: it is on 998dae67. The first two commits predate it and are not rewritten, since that would need a force-push. The PR body notes this.

Suites (local, focused):

  • codex-local connect 133
  • claude-local connect 138
  • claude-local adapter 143
  • claude-local install 85
  • bridge-exclusive 22
  • multi-agent repair 30
  • adapter boundaries 13
  • sparrowd-agent-bridges 50
  • claude-local-bridge 22

All OK; ruff and review-checks are clean. I'll report CI, coverage and CLA once they finish. @qingyun-wu: this round changes the shared start-cleanup and preflight code since your approval at 516e04d4, so I'm re-requesting your review.

(agent-assisted, Claude Code for Chi Wang)

@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 exact head 998dae67d4831da9df0a678e9340a35551ffcb0e (round 2 on top of 516e04d4). Read the round-2 delta to the shared connect code end to end, plus the activated paths it reaches (under_agent_lock, settle/await_started, _reconciled, orphan_bridge, redeem_into_seat).

Verdict: the code looks correct and I see no blocker. Not approvable yet: REVIEW.md lesson 15 makes the live round trip a review-time gate for a bridge/connect path, and the PR body itself says approval waits for it. Posting the real connect witness at this head (redeem → bridge → task → result) is what would turn this into an APPROVE. The earlier APPROVED review at 516e04d4 is stale: this round changes shared cleanup and preflight code, so it should not count as approval of this head.

Round 2, item 1 (a failed reconnect removed a live session's bridge): fixed as described

  • connect_common.py:1889: the failed-start cleanup is now _undo_start(p) + _unwaited(p) under the agent lock. The order matters and is correct: the undo runs first, so serving_session sees the post-undo manager state.
  • connect_common.py:1905-1924: serving_session treats an active job, a live holder, and an unreadable holder as "served". All three keep the bridge, so an ambiguous read fails toward keeping it, not tearing it down. orphan_bridge (connect_bridge.py:453) now uses the same predicate, so both teardown decisions use one rule and are not two copies that could drift.
  • connect_bridge.py:407-409 + finish_connect :782-799: inside connect, the install's reconciliation is deferred through _FINAL and runs after cm.settle, outside the redemption's lock. finish_connect clears the per-thread entry on entry, so a leftover entry from an earlier call cannot leak in.
  • wire_bridge :569-595: restore is registered only when the bridge unit was active before this attempt stopped it. A bridge that was not running is not started by the kept path.
  • The lost_reconnect matrix (2 managers × unit/sparrowd × with/without a foreground holder) exercises the reported interleaving, with the no-holder controls. The fail-first and mutation results in the round-2 comment match what the diff would produce.

Round 2, item 2 (preflight the root named by --agent): fixed

  • known_root_problem(local, agent_id) (connect_bridge.py:701-716) checks the lock and holder for both the recorded ROOT and named_root(agent_id), before read_code. The seat check stays scoped to the recorded root. test_a_root_another_adapter_holds_is_refused_before_the_code_is_read asserts zero backend calls. test_a_root_taken_after_the_preflight_is_refused_before_the_seat_is_written keeps the post-redemption race covered.

Non-blocking notes

  1. connect_common.py:1907-1909: when _undo_start declines to stop a job it cannot prove it owns, and that job is active but never took the root, serving_session reports "served", and the bridge is kept and possibly restarted with no session on the root. This fails toward keeping the bridge, which I think is the right side. A one-line mention in the residual-risk list would make it explicit.
  2. connect_bridge.py:577 restore restarts the bridge from the unit file this attempt just rewrote. In the tested case the bytes are identical (the test asserts it). If a future change makes the rewritten unit differ, for example a new seat path, the kept path would start the new unit, not the old one. Consider a comment, or assert this in the helper.
  3. _FINAL is a module-level dict keyed by thread id. It is fine for the CLI's single thread. It would need a lock if connect ever ran concurrently in-process.

Evidence

  • CI at this head: 46 pass, 3 skipped (release and Windows jobs), none cancelled or timed out. Coverage gate passes.
  • Locally at this head: the codex-local connect, claude-local connect and bridge-exclusive suites: 293 passed, 215 subtests passed.
  • Worst-case disruption (lesson 5): connect is now on by default for Codex. The new worst case is a failed connect leaving a bridge running while a foreground session holds the root. That is the intended state, and no worse than before. --experimental-connect is still accepted as a no-op, so existing scripts do not break.

Outstanding before approval: the final-head live witness. keweichen's CHANGES_REQUESTED also needs that reviewer to re-check.

@github-actions

github-actions Bot commented Nov 6, 2026

Copy link
Copy Markdown
Contributor

@cla-assistant check

@sonichi

sonichi commented Nov 6, 2026

Copy link
Copy Markdown
Owner Author

Live witness at 859f74a (codex-local, token path): PASS

The owner approved this run. The token path is the primary one, since the app issues no acp. code for an existing agent. Setup: launchd host, Codex CLI 0.166.0 (logged in), a fresh venv built from a worktree at exactly this head (matching byte for byte), and a test agent whose relay token was passed only through a mode-600 --token-env-file with AGENT_MXID pinned (deleted after). Before starting, nothing served the agent: no labels and no processes. All times below are UTC on 2026-11-06.

# Step UTC Result
1 install --agent <id> --token-env-file <file> 00:32:40–00:32:41 PASS: rc=0, session and bridge jobs loaded, launcher and connector holding the root
2 verify --agent <id> 00:33:13 PASS: all rows. Bridge connected; non-owner: refused unless attested collaborator; sandbox read-only
3 Mention in the proof room: "what is 6+7? one word" sent 00:33:26 PASS: the task arrived as access_tier: team, collaborator: true, and Codex replied "13" at 00:33:31
4 launchctl kickstart -k the session job, then verify 00:33:32–00:33:40 PASS: new job, launcher and connector PIDs; verify passes
5 Mention: "what is 3+3? one word" sent 00:33:40 PASS: Codex replied "6" at 00:33:44
6 uninstall --agent <id> 00:33:58–00:33:59 PASS: both jobs stopped; both plists and the per-agent config dir removed; no labels, plists or adapter processes left
7 connect --base … --agent <id> without --experimental-connect and without a code (stdin not a TTY) 00:33:59 PASS: rc=2, error: no code: pass --code - and pipe the code in, or run connect in a terminal to be asked for it; the code was not used. There is no gate or experimental refusal and nothing was written.

On --allow-collaborator: codex-local has no such flag at this head, and none is needed. Its README says the shared admission gate serves the owner and an attested collaborator (Team, the broker's collaborator: true) by default; --allow-non-owner widens that to other non-owners. Step 3's team/collaborator task was served without any flag, consistent with that.

Not exercised: a real acp. redemption, since no code exists for an existing agent. The connect-with-code path is covered only by the gate check in step 7.

Cleanup: the venv, worktree, token file and agent work root were removed, and a scratch vault entry from an earlier attempt was deleted.

@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 exact head 859f74a7bff4dd39697e6d3e7898d783ea41bafe, including round 2 and the main merge. No new code blocker found; the relay-token lifecycle now has the real post-restart witness required by REVIEW.md lesson 15.

The exact-head witness records install, verify, a collaborator task/reply, a session kickstart, a second task/reply, and uninstall. That satisfies the lesson's real-message-through-the-restarted-service requirement for the relay-token path. It is author-provided evidence, not a live run I performed. It does NOT prove an acp. redemption: the final flagless command stops at missing-code validation. I am not counting that refusal as a successful connect. The API-only mint route creates a new agent and is not exposed by the checked app sources; redemption remains harness-verified.

Code checks: connect_common.py:1889,1905-1924 re-reads serving state after undo under the agent lock, preserving a live or unreadable holder's bridge. connect_bridge.py:407-409,782-799 defers connect reconciliation until after settlement; :701-734 checks the agent-named root before reading/spending the code. The eight reconnect interleavings and the preflight/post-redemption race cases ran in the local Codex suite. Worst-case disruption is a failed rotation interrupting delivery; the holder-aware keep/restore and no-holder teardown controls address that. The old flag remains accepted and there is no state migration.

The main merge has an empty remerge diff and no changes to the three connect implementation files relative to round 2. It imports main's trigger-header support, not additional connect edits; the imported agent-trigger tests pass locally, and the live witness is after that merge.

Validation: review checks pass. The local Codex suite collected 133 tests, with three startup/seam failures; all three pass unchanged in a focused rerun (3/3). Two failures explicitly report the fake launcher's two-second acquisition timeout; I do not report the initial full suite as clean. Exact-head CI and CLA are green (46 successful checks, three conditional skips).

Verdict: code and relay-token runtime evidence support approval at this head. This updates the earlier review's evidence assessment; it does not assert live acp. redemption or clear another reviewer's outstanding change request. Normal approval/merge gates still apply.

@sonichi

sonichi commented Nov 6, 2026

Copy link
Copy Markdown
Owner Author

Closing for now, on the repository owner's decision. Turning on the one-time-code connect command only makes sense once the app has a flow that hands out those codes, and it doesn't yet. The connect docs now lead with the relay-token install path. The work here (the review rounds, the live witnesses and the fixes for the findings) stays on this branch, and the PR can be reopened when the app flow exists. Thanks to keweichen and qingyun-wu for the reviews.

🤖 Generated with Claude Code

@sonichi sonichi closed this Nov 6, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants