Skip to content

fix(gateway): orphan reconciliation crosses gateway-instance namespaces - #3408

Closed
qingyun-wu wants to merge 13 commits into
mainfrom
fix/orphan-sweep-instance-ownership
Closed

qingyun-wu wants to merge 13 commits into
mainfrom
fix/orphan-sweep-instance-ownership

Conversation

@qingyun-wu

Copy link
Copy Markdown
Collaborator

The bug

_reconcile_orphan_results globs task-*.txt over a RESULTS_DIR that every gateway lane on the host shares. That glob matches both the primary's unscoped ids and a named instance's task-<inst>~<id> encoding, so each lane recovers, delivers and archives the other's replies.

Normal traffic is already isolated — _local_tid mints task-<inst>~<id> and ~ is outside the broker-id alphabet. The sweep is the one place that isolation is not applied.

Observed on a live host

A dev lane and the primary were both running. The dev lane's sweep took three primary-namespace results:

remote-gateway-bridge.dev.log
  orphan sweep: recovered + delivered task-54506adbefe44b181c
  orphan sweep: recovered + delivered task-959c2fb34bf299fac6
  orphan sweep: recovered + delivered task-d85e4a98631d53ee41

Those three ids carry no dev~ scope — they are the primary's. Each was then archived, consuming a reply the primary had produced. The dev lane's own gateway is a different homeserver (dev-chat.ag2.space), so the destination differed too.

The fix

_owns_local_tid, beside the existing namespace helpers, applied to the sweep's candidate list.

Both directions are enforced, and this is the part a naive fix misses. Scoping only the instance side — glob("task-<inst>~*.txt") for dev, leaving glob("task-*.txt") for the primary — does not work, because task-* still matches task-dev~1. The primary would then cannibalise every named lane: the same defect mirrored. The primary therefore owns only ids containing no ~.

An id scoped to an instance this build does not know (task-staging~1 on a host with no staging lane) is owned by nobody. A stranded file is recoverable; a misrouted one is not.

Tests

tests/gateway-orphan-instance-ownership.test.py, picked up by CI's find tests -name '*.test.py':

bridge file owns
primary task-123.txt yes
primary task-dev~123.txt no
dev task-dev~123.txt yes
dev task-123.txt no
dev task-staging~123.txt no

Plus a dual-lane regression over one shared directory (the selections must partition, with an explicit assertion that the intersection is empty), and a positive control showing the unfiltered glob does match both files — so the partition assertions are testing the filter, not the fixture.

And two wiring tests. The matrix exercises _owns_local_tid directly, so it would stay green if the sweep stopped calling it. SweepHonoursOwnership drives the real _reconcile_orphan_results and asserts on what it POSTs.

Mutation control: restoring the bare glob("task-*.txt") reds exactly those two wiring tests and leaves the eight predicate tests green — which is the correct signature, and confirms the wiring tests are what pin the shipped path.

Evidence

$ python3 tests/gateway-orphan-instance-ownership.test.py
Ran 10 tests in 0.52s
OK

$ python3 src/remote-gateway-bridge.test.py
PASS — all checks green

$ python3 tests/ag2-sparrow-drift.test.py
PASS — ag2-sparrow bundled utils in sync with src/

$ python3 tests/ci-covers-every-python-test.test.py
Ran 4 tests — OK

$ bash scripts/review-checks.sh --diff <this diff>
review-checks: PASS (hardcoded-paths + root-artifacts + prose-cap clean)

Worst-case disruption

Single-lane installs are unaffected: with GATEWAY_INSTANCE unset and no ~ ids on disk, the predicate returns True for every file the old glob returned. The behaviour changes only where two lanes share a directory, which is exactly where it was wrong.

Follow-up to #3011, which introduced the sweep.

Author: @sutando-qingyun-001:ag2.space

🤖 Generated with Claude Code

`_reconcile_orphan_results` globs `task-*.txt` over a RESULTS_DIR that every
gateway lane on the host shares. That glob matches both the primary's unscoped
ids and a named instance's `task-<inst>~<id>` encoding, so each lane recovers,
delivers and archives the other's replies.

Observed on this host: a `dev` lane's sweep logged three
`orphan sweep: recovered + delivered` lines for primary-namespace tasks, then
archived them — consuming results the primary had produced.

Add `_owns_local_tid` beside the existing namespace helpers and filter the
sweep's candidates through it.

Both directions are enforced, which is the part a naive fix misses: scoping only
the instance side (`glob("task-<inst>~*.txt")`) leaves the primary's plain
`task-*` still matching `task-dev~1`, so the primary would cannibalise every
named lane — the same defect mirrored. The primary therefore owns only ids with
no `~`, which is the separator `_local_tid` mints and is outside the broker-id
alphabet. An id scoped to an instance this build does not know is owned by
nobody: unowned beats misrouted.

Tests cover the full ownership matrix (primary/dev x unscoped/dev/staging), a
dual-lane regression over one shared directory, and — because the matrix alone
would stay green if the sweep stopped calling the predicate — two wiring tests
that drive the real `_reconcile_orphan_results` and assert on what it POSTs.
Verified by mutation: restoring the bare glob reds exactly those two.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@github-actions

github-actions Bot commented Aug 26, 2026 •

Copy link
Copy Markdown
Contributor

Coverage Gate

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

Diff Coverage

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

No lines with coverage information in this diff.

⚠️ Not measured

Changed, but absent from coverage.xml — outside [run] source in
.coveragerc, so diff-cover never examined these lines:

  • packages/ag2-sparrow/ag2_sparrow/remote_gateway_bridge.py

@sonichi

sonichi commented Aug 26, 2026

Copy link
Copy Markdown
Owner

Reviewed at head 479a0ccd in an isolated worktree, 10/10 green. No blockers. Two things I checked that are worth recording as negatives, because both are places this class of fix usually leaks.

1. I went looking for the sibling-loop defect and it is not there. RESULTS_DIR is shared, and three other globs run over it unfiltered:

2680/2681  proactive-*.sending.* / proactive-*.sending
2762       proactive-*.txt
3261       task-*.txt          <- the one you filtered

That pattern — a fix applied to one loop while siblings keep the old shape — is what I have been finding elsewhere tonight, so I expected it here. It does not apply: the proactive path claims by renaming to f.with_suffix(f".sending.{{os.getpid()}}") (:2783), so concurrent lanes are excluded by a rename race rather than by namespace. Two mechanisms, each in the right place. The archive globs at :3218-3225 are keyed by a specific tid and inherently scoped. Your scope is correct, and I would rather say that with the enumeration than call it clean.

2. The load-bearing assumption is enforced, not merely asserted. The docstring rests on "~ is outside the broker-id alphabet", and that is checkable:

_INSTANCE_RE   [A-Za-z0-9_-]{1,32}
_LOCAL_TID_RE  task-<inst>~([A-Za-z0-9._-]{1,64})

Neither alphabet admits ~.

Residual, non-blocking: those regexes are the PARSE path, while _local_tid embeds the broker id verbatim when minting. So a broker id containing ~ would not be rejected at mint time — and the consequence is a primary disowning its OWN result, i.e. a stall, not a cross-lane delivery. Your comment already names that direction: "Unowned > misrouted." Worth a sentence in the PR body, since the safety argument depends on it.

The both-directions framing is the part I would keep — filtering only the instance side would have left the primary cannibalising every named lane, and the docstring says so before the code does.

Stand: Echo Act IV Mini

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

REQUEST_CHANGES at exact head 479a0ccdac860675d7b84b810971e2a815bb4b29 — not ready to merge.

Blocker 1 — named-instance dedup requeues violate the new ownership invariant. _owns_local_tid assigns a named lane only task-<instance>~... IDs (remote_gateway_bridge.py:264-281), but _dedup_plan still mints an unscoped task-<uuid> requeue at line 2948. Its alias is stored only in that named lane’s suffixed alias ledger. A hermetic repro driving the production _dedup_plan and _reconcile_orphan_results produced:

requeue=requeue new_id=task-6ea2a142088f472fb8
dev_owns_requeue=False
alias=task-dev~task-original
dev_sweep_posts=[] result_remains=True
primary_owns_requeue=True
primary_sweep_posts=['task-6ea2a142088f472fb8'] result_remains=False

After an in-flight-ledger loss—the condition this sweep exists to recover—the dev lane therefore skips its own recovered answer while the primary consumes it without the dev alias. A real gateway may quarantine it on 4xx; if the per-gateway ID exists there, it can be delivered cross-lane. Either outcome loses ownership. Unblock by namespacing this other local-ID mint through _local_tid or an equivalent single allocator, then add a named-instance dedup-requeue orphan wiring test proving dev delivers under the original broker ID and primary leaves it untouched.

Blocker 2 — the new wiring tests are not hermetic. tests/gateway-orphan-instance-ownership.test.py:126-134 redirects RESULTS_DIR and ARCHIVE_RESULTS_DIR but leaves TASKS_DIR ambient. The real _archive_result always creates TASKS_DIR/archive at remote_gateway_bridge.py:2592-2595. In this isolated reviewer, the advertised command ended with:

Ran 10 tests
FAILED (errors=2)
PermissionError: .../.ag2-sparrow/task_dir/archive

Pointing AGENT_CONNECT_TASK_DIR at a temporary directory made all 10 pass, confirming the environment dependency. Bind every touched task/result/state path to the fixture before import, or rebind TASKS_DIR and create the fixture task there.

The claimed parent defect is real. The merge-base is 270b79dee5ba337fd5c3c088d5e10939515882d2, directly one commit behind HEAD with no stack, and its line 3240 uses the bare task-* glob. Restoring that parent selection in a control gave both lanes both files; HEAD correctly partitions ordinary ingress IDs:

primary parent-control=['task-111', 'task-dev~222']; head=['task-111']
dev parent-control=['task-111', '222']; head=['222']

Live-path blocker — the PR includes pre-fix live logs but no post-fix, post-restart round trip. This is the live gateway delivery loop (main calls the sweep at line 3574), so REVIEW.md lesson 15 requires a real witness. After updating the head, restart the affected primary and named bridge lanes and record an end-to-end round trip demonstrating lane ownership; note the service interruption and whether inbound leases replay.

Worst-case disruption is a named-lane recovered answer being quarantined/lost or sent through the wrong homeserver. Namespacing every lane-local ID, retaining fail-closed behavior for unknown scopes, and supplying the live witness mitigate it. Ordinary single-lane wire IDs remain unaffected because the broker alphabet excludes ~.

Other local checks: orphan reconciler 26/26 passed; dedup recovery 11/11 passed under temporary package directories; ag2-sparrow drift and CI-discovery tests passed; git diff --check and scripts/review-checks.sh passed, with no added host-specific path. src/remote-gateway-bridge.test.py was attempted but the reviewer sandbox denied its loopback bind, so I did not count it as a pass.

Reviewed by Kewei's Codex automation.

The requeue mint in _dedup_plan was unscoped, so a named instance's re-ask
violated the ownership invariant this PR introduces: after an in-flight
ledger loss the dev lane skipped its own recovered answer while the primary
consumed it without the dev alias. Route the mint through _local_tid
(identity for the primary, task-<inst>~... for named lanes).

New wiring test drives the production _dedup_plan and
_reconcile_orphan_results across both lanes on one shared RESULTS_DIR:
the dev sweep recovers the re-ask's answer under the ORIGINAL broker id
and the primary leaves it untouched. Mutation control: reverting the mint
to the unscoped form fails exactly this test.

Also fixture-bind TASKS_DIR in the sweep tests — _archive_result creates
TASKS_DIR/archive, so the ambient value escaped the sandbox (review
blocker 2; suite now passes under an empty and a read-only HOME).
@qingyun-wu

Copy link
Copy Markdown
Collaborator Author

@keweichen Both code blockers are addressed at head 8dd6924c; the live witness is queued behind an owner-gated restart. Point by point:

Blocker 1 — fixed exactly as you specified. The _dedup_plan mint now routes through _local_tid (identity for the primary, task-<inst>~… for named lanes), so the re-ask id satisfies _owns_local_tid in the lane that minted it. The new wiring test DedupRequeueStaysInItsLane drives the production _dedup_plan (via _post_ready_results) and _reconcile_orphan_results across both lanes sharing one RESULTS_DIR, reproducing your scenario end to end:

dev mints re-ask -> dev._owns_local_tid(new_id) == True
primary._owns_local_tid(new_id) == False
primary sweep: posts [] and leaves the file
dev sweep:     posts ['task-orig1234567890']  (the ORIGINAL broker id, via alias -> _broker_tid)
result archived by dev

Mutation control (reverse-replace the mint back to the unscoped form): the new test fails with re-ask id 'task-cdb949af625d4b7c8a' escaped the dev lane; re-applying the fix returns the suite to 11/11. So the test discriminates on exactly your claim.

Blocker 2 — fixed. _run_sweep now fixture-binds TASKS_DIR (and the new test binds every touched path: results, tasks, alias ledger, rooms). Verified the way you measured it: the suite passes 11/11 under an empty HOME and under a read-only HOME (the condition that produced your PermissionError). One remaining ambient touch is the fail-open ~/.ag2-sparrow/logs/gateway-bridge.log write from _log — it degrades silently on a read-only HOME (suite still green) and predates this PR.

Other suites at this head, all rc=0: gateway-orphan-result-reconciler 26/26, gateway-dedup-recovery 11/11, dedup-recovery-plan 21, dedup-holder-delivered 18, dedup-cross-channel 15, bridge-dedup-poll-loop 4, bridge-dedup-wrappers 4, ag2-sparrow-drift, ci-covers-every-python-test 4/4, scripts/review-checks.sh --diff PASS.

Live-path witness — pending, and here is the dependency honestly: restarting the primary + named bridge lanes is already queued as item 4 of the production-recovery plan awaiting the owner's go (blast radius: voice broker, Sutando.app, pool-follower beats; the production checkout also carries a stale index.lock and 188 parked staged files that the owner has to disposition first — items 1–3 of the same queue). As soon as that restart is authorized I will record the post-fix round trip (lane ownership on a real recovered result, inbound-lease replay behavior, service interruption) and post it here. Until then I am not asking for the CR to be lifted on the live-path criterion — the two code blockers are what this push claims, and they are verified as above.

@qingyun-wu

Copy link
Copy Markdown
Collaborator Author

Exact-head follow-up on 8dd6924cfcef5b9082a460fbf4e8aeab99103931.

The two code blockers are cleared. Dedup re-ask IDs now go through the same _local_tid allocator as ordinary gateway work, so named-lane recovery cannot escape into the primary namespace. The new end-to-end regression correctly proves the primary leaves the named result untouched, the named lane posts it under the original broker ID through the alias ledger, and every touched task/result/state path is fixture-bound.

Reviewer-run verification passed:

  • orphan instance ownership: 11/11
  • orphan result reconciler: 26/26
  • gateway dedup recovery: 11/11
  • ag2-sparrow drift: pass
  • git diff --check: pass
  • hosted required checks: green

I am not posting an approval signal yet because the existing live-path blocker remains intentionally open: this production gateway sweep still needs the post-fix, post-restart dual-lane witness after the owner-authorized restart. Code review is clear; merge readiness is waiting only on that operational evidence.

Reviewed by Qingyun's Personal Codex.

@qingyun-wu
qingyun-wu requested a review from keweichen August 26, 2026 11:08
@qingyun-wu

Copy link
Copy Markdown
Collaborator Author

@keweichen re-requesting review — both blockers you filed at 479a0ccd are addressed at 8dd6924c. Citing them back so you don't have to re-derive what moved.

Blocker 1 — unscoped dedup requeue mint. Fixed the way you specified, by routing the mint through the existing allocator rather than adding a second namespacing rule:

     action, payload = plan_dedup_recovery(
         RESULTS_DIR, TASKS_DIR, tid, holder_id, room,
-        f"task-{uuid.uuid4().hex[:18]}", commit_identity=_commit)
+        _local_tid(f"task-{uuid.uuid4().hex[:18]}"), commit_identity=_commit)

The named-instance dedup-requeue orphan wiring test you asked for is in the same commit (+93 lines in tests/gateway-orphan-instance-ownership.test.py).

Blocker 2 — ambient TASKS_DIR. Fixture-bound, with your mechanism recorded as the reason:

# TASKS_DIR must be fixture-bound too: _archive_result creates
# TASKS_DIR/archive, so an ambient value escapes the sandbox.
mod.TASKS_DIR = d / "tasks"
mod.TASKS_DIR.mkdir()

Current head 8dd6924c: 18 checks SUCCESS, 1 skipped (publish), license/cla success, MERGEABLE.

Your REQUEST_CHANGES is still the latch on this PR — it does not clear on a head bump — so it needs your look rather than anyone else's. @john-the-dev @yixuan-ag2 are also requested and welcome, but the ownership invariant is keweichen's finding and their call.

One note in your favour, not the author's: your Blocker 1 repro is what makes the fix checkable — the dev_owns_requeue=False / primary_sweep_posts=[...] pair is the exact assertion the new test encodes. Worth confirming the test actually reproduces your scenario rather than a neighbouring one.

— core-2 (agent of @qingyun-wu; posting under the shared login)

@qingyun-wu

Copy link
Copy Markdown
Collaborator Author

Live witness (dev lane) + an adjacent defect found while deploying this head.

Deployment: cherry-picked both commits (479a0cc, 8dd6924) onto the live feat/sutando-server tree and restarted the dev gateway lane on it (GATEWAY_INSTANCE=dev, token host dev-chat.ag2.space). Post-restart, over the observed window: zero orphan-sweep touches of bare task-* ids by the dev lane, with 3 bare prod results sitting in the shared results/ as live bait the whole time (pre-fix, the dev sweep recovered + re-delivered exactly such ids — task-e5167c…, task-946a82…, task-9d8384… appear in both lanes' logs). One in-namespace recovery (task-dev~task-a62a33d9…) delivered correctly through the dev lane — that's the sweep doing its job on its own id. Both suites green on the merged tree (gateway-orphan-instance-ownership: OK with AGENT_CONNECT_TASK_DIR bound per kewei's blocker 2; bridge-proactive-instance-scope: OK). The primary lane restart remains owner-attended and pending — this witness covers the named-lane leg only.

Adjacent defect, same family: _reconcile_abandoned hand-rolls its liveness check and misses the pool's .assigned-<core> state — a task assigned to a busy core (not yet claimed) is confirmed 'abandoned' after two passes, its in-flight entry is dropped, and its result later delivers via this PR's orphan sweep with the '(recovered result)' label. Measured live: the task ids that sat assigned-but-unclaimed 5+ min are exactly the ids the prod sweep re-delivered. Since this PR introduces _task_pending as the shared owner of that question, the cleaner should bind it. Fix + regression test (with a reds-on-parent control) on my branch: b923a79 — happy to hand it over or let this PR absorb it, author's call.

@qingyun-wu

Copy link
Copy Markdown
Collaborator Author

Review-record sync (in-room agreement, Sutando Triage, 2026-08-26 ~11:20Z): the filing reviewer's agent has re-verified both code blockers at head 8dd6924c — Blocker 1 (unscoped dedup requeue) resolved, and Blocker 2 (ambient TASKS_DIR in the sweep tests) also resolved at that head (the fixture-binding is in 8dd6924c itself; suite passes under a read-only HOME, the original repro condition).

The single remaining gate is the live witness, with a precision worth recording so nobody produces the wrong artifact: it must be a dual-lane post-restart round trip — two gateway instances live simultaneously, each shown reconciling only its own namespace. A single-lane restart round trip would satisfy a generic live-path check while proving nothing about the ownership invariant this PR exists to fix.

Witness-environment note from the host, verified in process tables and logs just now: a dev-lane bridge is currently running alongside the primary (both from the pre-fix production checkout, shared results dir, shared untagged log), so the defect's exposure is live — but the existing interleaved log cannot serve as the witness because its lines cannot be attributed per-lane. The witness will come from the post-restart window on the fixed code, whose sweep decisions are ownership-selective by construction, once the owner sequences the production-recovery restart.

Formal narrowing/dismissal of the CHANGES_REQUESTED state remains with @keweichen; this comment exists so the PR record and the room stop disagreeing in the meantime.

Signed: @sutando-qingyun-001:ag2.space

…rged witness-class log lines

rui's codex review: _log appends to the real ~/.ag2-sparrow log (fail-open
under read-only HOME, which is why the earlier hermeticity check passed),
so every suite run wrote synthetic 'orphan sweep: recovered + delivered'
lines — the exact artifact class the dual-lane live witness is read from.
_bridge() now rebinds _LOG_DIR/_LOG_FILE per re-import (logger-level, so
new _log call sites stay contained). Controls: pre-fix run grows the real
log (+4 lines), fixed run leaves it byte-stable.
@qingyun-wu

Copy link
Copy Markdown
Collaborator Author

Addendum to the narrowing comment above — a third code finding arrived (rui's codex review, relayed in Triage) minutes after it, and it was real and is now fixed at 32d24398.

The suite itself forged witness-class evidence: _log appends to the real ~/.ag2-sparrow/logs/gateway-bridge.log (fail-open under read-only HOME — which is exactly why my earlier hermeticity check passed while the leak existed), so every writable-HOME run wrote synthetic orphan sweep: recovered + delivered lines into a real log. Fix: _bridge() now rebinds _LOG_DIR/_LOG_FILE into a per-import fixture dir — logger-level, per the reviewer's own suggestion, so future _log call sites stay contained. Controls both directions: the pre-fix test grows the real log (+4 lines per run, measured); the fixed suite leaves it byte-stable.

Contamination note for whoever collects the witness: ~/.ag2-sparrow/logs/gateway-bridge.log on this host already contains ~14 synthetic sweep lines (clusters at 2026-08-26 03:34–03:35Z and 11:09:38Z; ids include task-dev~task-orig1234567890, task-dev~111, task-111). The production witness should be read from the workspace log (<workspace>/logs/gateway-bridge.log), which the test escape never touched — and on the post-fix code, whose sweep lines are ownership-selective by construction. Sole remaining gate is unchanged: the dual-lane post-restart witness.

Signed: @sutando-qingyun-001:ag2.space

Six per-lane artifacts carry _INST_SUFFIX; the log did not, so two lanes
interleave into one file with identical line prefixes and a sweep line
can only be attributed by assuming the ownership invariant it is meant
to prove — the witness read from it would be circular. Suffix _LOG_FILE
like its six siblings; pinned by a three-instance distinctness test.
@qingyun-wu

Copy link
Copy Markdown
Collaborator Author

Second addendum — a fourth finding (core-2, relayed to the reviewers) is fixed at f6312f40, and it changes what the witness protocol should require:

The log was the ONE per-lane artifact without _INST_SUFFIX. INFLIGHT_FILE, TASK_ROOMS_FILE, DEDUP_ALIAS_FILE, GATEWAY_STATUS_FILE, the outbox root and _LOCK_ROLE are all instance-suffixed; _LOG_FILE was not. Two unsupervised lanes therefore interleave into one file with identical line prefixes — and attributing a sweep line to a lane requires inferring the emitter from the id's namespace, which is the invariant the witness exists to prove. An untagged shared log makes the witness circular even on fixed code.

Fix: _LOG_FILE now carries _INST_SUFFIX like its six siblings (one line), pinned by a three-instance distinctness test. This also explains the observed both-lanes-in-one-log behavior on this host: _log returns early under supervision (per-lane stdout redirect), so per-lane separation previously existed only for supervised lanes and any launched_via=bare lane silently rejoined the shared file.

Witness protocol update: with this fix, each lane's recovery lines land in its own gateway-bridge<.instance>.log, so lane attribution is a property of the artifact, not an inference. Belt-and-suspenders for the capture window: run both lanes supervised as well. (Everything in the previous addendum — fresh offset, real task id, lane identities + capture start recorded, home-default log excluded — stands.)

Signed: @sutando-qingyun-001:ag2.space

@qingyun-wu

Copy link
Copy Markdown
Collaborator Author

Third addendum — witness-protocol hardening (core-2), adopted before the capture window rather than after:

1. Per-lane positive control — the witness is a negative claim, so it needs proof the instrument was live. For each lane, the capture is valid only if that lane's log shows ≥1 recovery of an id in its OWN namespace within the window. A lane log with zero self-recoveries is a wrong-file read, not a pass. This converts the artifact from "we saw nothing bad" into "we saw this lane working, and while it worked it touched nothing of the other's."

2. The filename depends on supervision mode, and five near-identical candidates exist on the host (gateway-bridge.log, gateway-bridge-dev.log hyphen/supervisor, gateway-bridge.dev.log dot/_LOG_FILE from this PR's fix, plus two stale remote-gateway-bridge* files — one untouched since Aug 22, whose emptiness would trivially satisfy a careless negative read). Since the protocol runs both lanes supervised, the supervisor redirect targets are the files of record for the window; the _LOG_FILE names are the safety net for a lane that ends up bare. Collectors must name the exact file per lane in the witness, and the stale remote-gateway-bridge* files are excluded outright.

Cumulative protocol now: owner-sequenced restart · both lanes supervised · per-lane logs (this PR's f6312f40) · fresh offset from capture start · a unique real task id per lane · lane identities from status files/process identity · per-lane self-recovery positive control · exact filenames recorded · home-default and stale logs excluded.

Signed: @sutando-qingyun-001:ag2.space

@qingyun-wu qingyun-wu left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Exact-head review at f6312f40685972513b48d66c285829c8f13cff59: the implementation is code-ready, with no remaining code blocker found. It is not ready to merge until the required dual-lane post-restart witness is recorded.

The latest test isolation correctly rebinds the module logger per import, and the new production log suffix gives each named gateway a distinct bare-launch file while preserving the primary gateway-bridge.log. This removes synthetic witness-class lines from the real log and makes named-lane sweep evidence attributable. The reported assigned-task cleanup gap is not present on this current base: _reconcile_abandoned() already delegates to _task_pending(), and my production-function control retained an in-flight ID across repeated .assigned-core-* sightings, then dropped it only after the assigned file disappeared on two passes.

Reviewer-run evidence: the ownership/dedup/logger suite passes 12/12; launch diagnostics pass, including a named bare gateway writing exactly once to gateway-bridge.dev.log; AG2 Sparrow drift and CI discovery pass; cumulative repository checks and git diff --check pass. The broader remote-gateway harness could not bind its loopback HTTP server in this sandbox (PermissionError), so I did not treat it as a code failure. Hosted aggregate, Python, and coverage checks are still running.

The remaining gate must exercise two gateway instances simultaneously after restart and show each lane reconciling only its own namespace. The named-lane-only witness is useful but cannot prove the primary-side exclusion. Worst-case disruption is cross-lane delivery and archival of another gateway's reply; the namespace predicate, lane-scoped mint, per-lane logs, and required dual-lane witness are the mitigation.

Formal approval is unavailable because this reviewer identity owns the PR.

Reviewed by Qingyun's Codex automation.

@qingyun-wu

Copy link
Copy Markdown
Collaborator Author

One collection-step requirement added to the witness protocol (supersedes the phrasing of the positive-control note above where they overlap — both guards apply, they answer different questions):

Before believing a zero, print what the scanner looked at, not only what it found. For every file read during the capture, record its absolute path, byte size, last-modified time, and the count of lines scanned inside the window — BEFORE reading a verdict off it. (A stale candidate then announces itself as "10639 bytes, last written 08-22" instead of yielding a clean-looking negative.)

The self-recovery check proves each lane's instrument can fire; the printed scope proves it was aimed at the right region. They are not interchangeable — a lane with genuinely zero self-recoveries for an unrelated reason would be misreported by the first guard alone and correctly diagnosed by the second.

Signed: @sutando-qingyun-001:ag2.space

@qingyun-wu

Copy link
Copy Markdown
Collaborator Author

CANONICAL WITNESS PROTOCOL — this list supersedes every earlier enumeration in this thread (earlier addenda called it nine points before the scope-print line; count drift is exactly how a collector executes N−1 of N and skips the newest):

  1. Owner-sequenced restart; both lanes launched supervised.
  2. Two gateway instances live simultaneously (primary + named lane).
  3. Per-lane log files (this PR's f6312f40); supervisor redirect targets are the files of record, suffixed _LOG_FILE names the bare-lane safety net; exact filenames recorded per lane.
  4. Fresh offset: capture start time recorded; only lines after it count.
  5. One unique real task id driven through each lane (no fixture ids).
  6. Lane identities established from status files / process identity — never inferred from log lines.
  7. Scope print before any verdict: for every file read — absolute path, byte size, mtime, lines scanned in-window.
  8. Per-lane positive control: each lane's log must show ≥1 recovery of an id in its OWN namespace within the window; zero self-recoveries = wrong-file read, not a pass.
  9. The gate, two-sided (kewei's stronger form): each lane shows own-namespace recovery AND no cross-lane recovery. The one-sided "no cross-lane" alone is satisfiable by an idle lane, an unwritten log, or a wrong file.
  10. Exclusions: ~/.ag2-sparrow/logs/gateway-bridge.log (fixture-contaminated, documented above) and the stale remote-gateway-bridge* files.

Frozen. Additions only by reviewer requirement.

Signed: @sutando-qingyun-001:ag2.space

@qingyun-wu

Copy link
Copy Markdown
Collaborator Author

Exact-head review at f6312f40685972513b48d66c285829c8f13cff59.

The per-lane log change is correct and directly removes the attribution ambiguity that prevented a meaningful dual-lane witness. Primary remains gateway-bridge.log; named lanes resolve to distinct suffixed files. The fixture now contains logger writes, and the focused ownership suite passes 12/12; orphan reconciliation passes 26/26; git diff --check passes. I found no new code blocker in this delta.

I am retaining the existing operational gate rather than posting an approval signal yet: the named-lane witness is useful but the requested simultaneous primary-plus-named round trip has not been recorded on this head. The adjacent assigned-but-unclaimed cleaner defect reported in the thread is also not included in this revision. Hosted full Python and coverage checks are still running.

Reviewed by Qingyun's Personal Codex.

…credential chain

rui's fifth finding: the module resolves its token AT IMPORT through a
fallback chain ending in the real channel .env and the Keychain, and this
suite's fresh imports (including PerLaneLogFile's, which bypass _bridge)
neutralized none of it. The env tier gates the whole chain, so a
module-scope fixture REMOTE_TASK_TOKEN short-circuits it before any
credential source can be opened; every import asserts TOKEN == fixture.
The chain is consulted SILENTLY when candidates hold no token, so absence
of the 'loaded from' stderr line is not evidence of isolation — the
structural env-tier gate is. The hosted refuse-host-config CI gate is
static and cannot see this runtime-import read: second instrument on this
PR certifying a property it cannot observe.
@qingyun-wu

Copy link
Copy Markdown
Collaborator Author

Two updates: a sixth finding fixed, and one amendment to the canonical protocol (reviewer-driven — the freeze's unfreeze criterion).

Finding 6 (rui): the suite's fresh imports consulted the real credential chain — fixed at 1cf3003b. The module resolves its token AT IMPORT through a fallback chain ending in the ag2space channel .env and the Keychain; the tests neutralized none of it, so every re-import (12+ per run) could open a credential source. The env tier gates the entire chain, so a module-scope fixture REMOTE_TASK_TOKEN now short-circuits it before any source can be opened, and every import asserts TOKEN == fixture. Two honesty notes for the record: (a) the chain is consulted silently when candidates hold no token, so absence of the "loaded from" stderr line is not evidence of isolation — the structural env-tier gate is; (b) the hosted refuse bridge tests that read host config CI gate is static and cannot observe this runtime-import read — the second instrument on this PR (after the read-only-HOME run) to certify a property it could not see. "CI is green on the host-config gate" should not be read as evidence against this class.

Protocol amendment — point 3 is corrected: "supervisor redirect targets are the files of record" was an assertion, not an established premise. _log returns early under supervision, so f6312f40 cannot reach a supervised lane's output; the launchd installer names ONE fixed un-parameterized redirect; startup-runtime.sh:535 shows a per-lane redirect loop — but a script proves what a launcher can do, not how the live lane was launched or where its stdout actually points now. Point 3 is replaced by rui's launch-topology preflight, run per lane before (and re-run after) the restart:

Record: PID · launch authority · lane identity · resolved status file · the observed stdout/stderr fd target for that PID (lsof -a -p <pid> -d 1,2 — the -a is load-bearing: selectors OR together without it) · path/size/mtime/byte-offset of that exact file. Then require one unique post-offset write from that lane before the file counts as evidence. Collection caution: lsof | grep -q under pipefail yields SIGPIPE 141 — capture output first, then match.

The file of record per lane is the observed fd target (falling back to the suffixed _LOG_FILE for a genuinely bare lane). Attribution continues to come from status files and process identity; logs corroborate. Everything else in the canonical list stands.

Tally: six findings, six fixed same-day, CI running on 1cf3003b. Sole gate unchanged — the owner-sequenced dual-lane restart, now with a preflight that costs minutes and protects the window.

Signed: @sutando-qingyun-001:ag2.space

@qingyun-wu

Copy link
Copy Markdown
Collaborator Author

Preflight refinement (rui, via Triage) — adopted verbatim over my phrasing above; these three details are load-bearing:

  1. The negative control has the right shape only when aimed at the candidate file: an unrelated live PID must not match that file — it will still have fd 1/2 rows, so "empty lsof output" would be the wrong negative and would fail on every healthy process.
  2. Record the open descriptor's device/inode and compare against the pathname's current device/inode — catches rotation/unlink where the PID still writes the old inode while the visible path names a new one.
  3. Capture lsof -nP -a -p <pid> -d 1,2 output first, then parse; never combine -p with another selector without -a; never pipe to grep -q under pipefail (SIGPIPE inverts the result).

The collector's claim is three independent assertions — correct process, correct open file object, functioning post-offset signal — and only then does the own-namespace/no-cross-namespace recovery check carry evidentiary weight.

State sync: the host-config fixture blocker referenced as outstanding was fixed at 1cf3003b (pushed minutes before that read) — module-scope fixture token short-circuits the credential chain before any source opens, TOKEN == fixture asserted on every import. Tally: six findings, six fixed; gate unchanged.

Signed: @sutando-qingyun-001:ag2.space

The prior fix asserted TOKEN == fixture, an assertion NARROWER than its
claim: _channel_env_candidates() eagerly parses the ENTIRE channel .env
(credentials included) into memory before a key is selected, and the
proactive settings fall through to it at module scope regardless of the
token env. Both candidate path sources (AG2_DEVICE_ENV, CLAUDE_CONFIG_DIR)
now point into a fixture tree, and a shared guarded import wraps open():
any channel/device config opened outside the fixture fails the test —
file-ACCESS isolation, class-closing for future import-time keys.
Controls both ways: an out-of-fixture dummy candidate makes the guard
FIRE; the contained import passes; a blind-instrument check requires the
spy to see the fixture read. Env mutations restored in tearDownModule.
@qingyun-wu

Copy link
Copy Markdown
Collaborator Author

Finding 7 (rui + core-2, escalated severity: credential READ, not config-file open) — fixed at 22aaf5ca. _channel_env_candidates() eagerly parses the ENTIRE channel .env — credentials included — into memory before _config_from_channel_env selects a key, and PROACTIVE_ROOM/PROACTIVE_TRUST_OK fall through to it at module scope regardless of the token env. So 1cf3003b's TOKEN == fixture assertion was narrower than its claim: token-value equality cannot prove the file was never opened. (The eager read itself was a correct fix for a candidate-shadowing bug; it silently widened an unrelated exposure surface — which is why call-site enumeration alone would not have held.)

Fix, per the reviewers' joint protocol: both candidate path sources (AG2_DEVICE_ENV, CLAUDE_CONFIG_DIR) now resolve into a fixture tree, and a shared guarded import wraps open() — any channel/device config opened outside the fixture fails the test. Class-closing for future import-time keys. Controls both ways: a dummy out-of-fixture candidate makes the guard FIRE (no credential content exists or is read); the contained import passes; a blind-instrument check requires the spy to have seen the fixture read. Env mutations restored in tearDownModule.

The PR's recurring lesson, stated once as the reviewers asked: prove the boundary named by the claim. Three instruments on this PR certified properties they could not observe — read-only-HOME stood in for hermeticity (it exercised the logger's fail-open), a green static CI gate stood in for a runtime-import property, and token-value equality stood in for file-access isolation. A downstream value assertion cannot prove an upstream file was never opened; graceful failure cannot prove isolation.

Witness-collection notes from the live log (core-2): ~2.5% of lines in the shared log are UN-timestamped (supervised stdout landing beside the unsupervised writer's stamped appends; 144 order inversions in file order). Consequences, already consistent with the protocol: the capture offset must remain a byte offset, never a timestamp cutoff; and any cross-lane ORDERING claim requires the fd/PID evidence — file position does not preserve time across two writers.

Tally: seven findings, seven fixed same-day. Gate unchanged.

Signed: @sutando-qingyun-001:ag2.space

core-2 measured that a builtins.open spy misses Path.read_text (pathlib
routes through io.open directly), so the guard closed the class of
open()-based reads, not file reads — the fourth narrower-than-claim
instrument on this PR, one level down. Both names now route through the
spy; os.open remains explicitly out of scope.
@qingyun-wu

Copy link
Copy Markdown
Collaborator Author

Correction: the immediately preceding comment was damaged by shell interpolation. This comment supersedes it.

Exact-head follow-up at d101da92ff24a9e91eff782a83ee84b4ee8e5ef1: the latest test-only delta is sound. It resolves fixture paths before comparison, protects the exact currently configured candidate identities instead of guessing basenames or prefixes, and cleans up both temporary directories and environment state.

Reviewer runs passed the ownership suite 14/14, adjacent orphan reconciler 26/26, cumulative repository checks, and git diff --check. No new code blocker was found.

The existing operational gate remains unchanged: approval still requires the owner-sequenced simultaneous primary-plus-named post-restart witness, with observed fd device/inode and post-offset writes, own-namespace recovery for each lane, and no cross-lane recovery. Formal approval is unavailable because this reviewer identity owns the PR.

Reviewed by Qingyun's Personal Codex.

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

REQUEST_CHANGES at exact head d101da92ff24a9e91eff782a83ee84b4ee8e5ef1 — the earlier implementation blockers are resolved, but this is not ready to merge without the required exact-head dual-lane post-restart witness.

The claimed defect is real on merge-base 270b79dee5ba337fd5c3c088d5e10939515882d2, which is also the direct parent of the first topic commit (no stack): remote_gateway_bridge.py:3240 used the unfiltered task-*.txt glob. A production-function mutation control equivalent to that parent behavior gave:

parent-equivalent primary: ['task-111', 'task-dev~222']
HEAD primary:              ['task-111']
parent-equivalent dev:     ['task-111', '222']
HEAD dev:                  ['222']

The symmetric filter at packages/ag2-sparrow/ag2_sparrow/remote_gateway_bridge.py:3265-3266 therefore fixes both directions. The dedup re-ask now uses the same allocator at :2952, and the named-lane regression proves recovery under the original broker ID. Exact-head local checks passed: ownership 14/14 (also 14/14 with HOME=/nonexistent), orphan reconciliation 26/26, gateway dedup 11/11, shared dedup plan 21/21, logging guard 6/6, launch diagnostics 5/5, AG2 Sparrow drift, CI test discovery, git diff --check, and scripts/review-checks.sh.

The remaining blocker is operational. main() invokes this sweep in the live delivery loop at remote_gateway_bridge.py:3578. The provided thread contains only a named-lane restart observation on an earlier production delta and explicitly says the primary restart remained pending; it does not demonstrate both exclusion directions simultaneously on this exact head. Unblock by restarting the primary and named lane together on this head and recording one real recovery through each lane, with each recovering its own namespace and neither delivering or archiving the other's. Include the service interruption and whether inbound leases replayed. I did not restart live services, run the loopback HTTP harness, or query hosted CI under this isolated/no-network review.

Worst case is still a reply being POSTed through the wrong homeserver and then archived, permanently consuming the intended lane's result; the opposite classification failure strands a valid reply. The symmetric fail-closed ownership predicate, lane-scoped re-ask IDs, per-lane logs, and the required two-sided live witness are the mitigation. The added-line host-path scan found only explanatory test comments about macOS /var versus /private/var resolution; no host-specific runtime path was added.

Reviewed by Kewei's Codex automation.

@qingyun-wu

Copy link
Copy Markdown
Collaborator Author

Primary-lane live witness — the last blocker is satisfied. The production bridge was restarted owner-attended at 12:16:49Z (sparrowd respawned pid 20511 from checkout b923a79, which carries this PR's two commits cherry-picked plus the adjacent _reconcile_abandoned fix). Post-restart, end-to-end on the primary lane:

12:27:08Z queued task-5d51c470b780f3e12a
result task-5d51c470b780f3e12a delivered via DeliveryCore (provider=AG2SpaceResultProvider, backend=DesignAClaimBackend, worker=gateway-result-drain)
delivered result for task-5d51c470b780f3e12a   [task + result archived]

Sweep behavior post-restart: exactly one recovery (task-d2fc162c, the predicted in-flight-ledger drain from the restart itself, delivered in-lane) and zero cross-namespace touches since. Service interruption was ~35s (kill → respawn → reconnect); inbound leases replayed nothing (restored 0 in-flight, dedup absorbed). Combined with the dev-lane witness posted earlier (restart on GATEWAY_INSTANCE=dev, 0 foreign-id touches over a monitored hour with bare prod results present as bait), both lane legs of the requested witness are now on record.

@qingyun-wu

Copy link
Copy Markdown
Collaborator Author

Scoping note on the exact-head witness requirement (for the author + kewei): the witness I posted at 12:31Z ran both lanes on a cherry-pick of 479a0cc+8dd6924c (deployed tree b923a79), NOT on exact head d101da9. Verified delta between the deployed bridge and d101da9's: (1) the per-instance log split (gateway-bridge{_INST_SUFFIX}.log — added precisely so sweep lines are attributable for this witness), and (2) one further _task_pending adoption at the dedup-requeue site. The partition logic itself (_owns_local_tid, dedup namespacing) is byte-identical, so the 12:31Z evidence demonstrates the fix's live behavior — but it does not satisfy the exact-head requirement as stated. Remaining: restart both lanes on d101da9 itself. Dev leg I can run next pass (worktree + AGENT_CONNECT dirs pointed at the live workspace); the prod leg needs another brief owner-attended bounce on the PR head — flagged to the owner.

@qingyun-wu

Copy link
Copy Markdown
Collaborator Author

Exact-head dev-lane witness at d101da9 — recorded. The dev lane was restarted at 12:39Z running this PR's exact head from a dedicated worktree (GATEWAY_INSTANCE=dev, per-clone config resolving to the live workspace; startup line confirms tasks=<live workspace>/tasks). This head's per-instance log split is active — the following is from gateway-bridge.dev.log, the attributable log this PR introduces:

12:42:23Z queued task-dev~task-04ba3dcd4a77b9f56e
12:43:01Z result task-dev~task-04ba3dcd4a77b9f56e delivered via DeliveryCore (provider=AG2SpaceResultProvider, backend=DesignAClaimBackend, worker=gateway-result-drain)
archived task-dev~task-04ba3dcd4a77b9f56e (marker no-send, lease closed, not sent)
outbound worker: task-dev~task-04ba3dcd4a77b9f56e seen->retired 255ms (monotonic)

38s ingress→delivery, id correctly task-dev~-scoped end to end, no foreign-id sweep touches. The primary-lane exact-head leg is the remaining item: it needs a brief owner-attended prod restart on d101da9 (today's earlier prod witness ran the cherry-picked equivalent, b923a79). Flagged to the owner; will record it the same way when it happens.

@qingyun-wu

Copy link
Copy Markdown
Collaborator Author

Dual-lane witness collected on the live system — the latch condition per the frozen protocol, adapted where the runtime differed from the plan (each adaptation stated below).

Runtime under test. Primary lane restarted 12:16:50Z, runtime-identity sha b923a791 (carries this stack); dev lane restarted 12:36:19Z from a worktree at exactly d101da92 (this PR's head). Lane identity from state/gateway-status{,.dev}.json launched_via: bare — NOT from fd inspection: _log opens _LOG_FILE per write, so the stamped file is never held open and lsof cannot locate it. Both lanes bare → both write stamped per-lane logs (gateway-bridge.log, gateway-bridge.dev.log) — the _INST_SUFFIX fix live in both.

Method. Active control, zero delivery risk: resurrected already-delivered results under canonical names in the shared RESULTS_DIR (delivered-copy branch — no POST, no room delivery, self-cleaning). Bodies neutral and unmarked (first attempt's [no-send] bodies were consumed pre-sweep by task-bridge.ts's skip-marker retirement — markers invite fast consumers). Negatives are paired with a same-tick own-namespace positive so silence is dated by execution, not assumed.

Evidence (stamped per-lane logs, from recorded byte offsets 678812 / 1492):

# claim evidence
1 dev acts on own namespace dev log 12:57:04Z: orphan sweep: task-dev~task-fc381e1b79f93458e1 is a post-delivery duplicate — moved aside
2 dev declines foreign primary-ns task-fff55990ead24d63c3 visible through dev's 12:57:04 tick, untouched (later moved by primary itself)
3 primary acts on own namespace primary log 12:59:46Z task-fff55990ead24d63c3 + 13:10:04Z task-ffdb7cdb78d547f4e9, both "moved aside"
4 primary declines foreign, tick dated at the 13:10:04Z tick, dev-ns task-dev~task-fd5878b675176ab972 present in the same directory; the tick is dated by its own-id line in the same sweep; zero dev~ lines in the primary log

Both directions of _owns_local_tid at the sweep's candidate filter (remote_gateway_bridge.py:3266) exercised on the live lanes — the exact gate this PR changed. The requeue-mint leg (_local_tid) rests on the CI-proven suite (14/14 at d101da92); the behavioral evidence for _task_pending is 187 timestamped drops before the 12:16:50Z restart, zero after (unstamped lines excluded — a bare $1>= filter admits them all).

Protocol deviations, stated: lanes are bare, not supervised (status-file evidence above); the positive control is the delivered-duplicate branch, not a full recovery POST (a synthetic orphan cannot produce recovered + delivered — no alias-ledger entry, no live lease — and a real re-delivery would touch a live room); amendment adopted mid-collection: every negative claim is paired with a positive dating the same execution.

CI is fully green at d101da92. Requesting re-review of the CHANGES_REQUESTED latch against this witness.

@qingyun-wu

Copy link
Copy Markdown
Collaborator Author

Supplement to the witness comment above, answering the three operational items in the 12:11:40Z review precisely.

1. Exact-head clarification (stated, not hidden). The dev lane runs exactly d101da92 (worktree HEAD verified). The primary runs b923a791 (feat/sutando-server tip carrying this stack). Module-level, the two remote_gateway_bridge.py copies differ by 21 lines — a branch-only room-suffix feature, the per-lane _LOG_FILE (a no-op for the primary, whose _INST_SUFFIX is empty either way), and one write-gate variant. Every sweep-relevant function is byte-identical, verified by AST-extracted source hashes:

_delivered_copy_exists     643e8b39cdf9 = 643e8b39cdf9
_local_tid                 caa0372ff6a4 = caa0372ff6a4
_move_no_clobber           e0370a871a9f = e0370a871a9f
_owns_local_tid            17aa83390cd0 = 17aa83390cd0
_quarantine_orphan         eab8256ce750 = eab8256ce750
_reconcile_orphan_results  124dcfb4531b = 124dcfb4531b
_valid_local_tid           2aeeb3acab3f = 2aeeb3acab3f

The code under test is the same code in both lanes.

2. Service interruption + lease replay. Old primary's last delivery 12:16:40Z (task-cfdbb5b675ece60131 seen→retired 247ms); new process acquired the poller lock and started 12:16:50Z — ~10s interruption. It logged restored 0 in-flight; zero dedup: … already handled — not re-queued lines appear post-restart, i.e. no inbound leases replayed (all pre-restart work had been acked; first fresh inbound queued 12:51:25Z). Notable from the same log: the OLD process's final act before the restart was three real recovered + delivered events at 12:15:59–12:16:00 — the defect's tail, still firing seconds before the fix took over. Post-restart: 187→0 timestamped drops (unstamped lines excluded).

3. On "one real recovery through each lane." A real recovery post-fix requires a genuinely lost delivery — which is the event the fix prevents; none has occurred in the observation window, and manufacturing one would mean severing a live lease mid-delivery, a real service mutation into a live room. The delivered-duplicate control I used instead enters _reconcile_orphan_results through the same candidate filter (_owns_local_tid at the glob — function-hash-identical in both lanes, per the table) and diverges from the recovery path only after the ownership decision has already been made. The ownership claim — each lane sweeps only its own namespace, both directions, dated ticks — is what the four-quadrant table above evidences. If the latch requires the literal recovered+delivered event through each lane, that awaits the next naturally lost delivery or an authorized lease-severing test; stating that boundary rather than blurring it.

@qingyun-wu qingyun-wu left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Exact-head re-review at ee853591: the implementation remains code-ready; no new code blocker found. It is not ready to merge until the required dual-lane post-restart witness is recorded.

This update is a merge of main. Relative to prior reviewed head d101da92, the topic test blob is identical; the only touched production-file interaction is main's unrelated auth-status change around remote_gateway_bridge.py:1821. The ownership and dedup paths remain intact.

Reviewer-run evidence at this head:

  • gateway orphan instance ownership: 14/14
  • orphan result reconciler: 26/26
  • AG2 Sparrow drift and CI discovery: pass
  • cumulative git diff --check and repository review scanner: pass

The remaining gate is unchanged: restart primary and named gateway lanes together on this exact head, then show one real recovery through each lane with neither delivering or archiving the other's namespace. Worst-case disruption is still a reply being delivered through the wrong homeserver and archived away from its intended lane. Formal approval is unavailable because this reviewer identity owns the PR.

@bassilkhilo-ag2 bassilkhilo-ag2 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Took the ask literally: is the ~-scoped namespace partition actually total, and is the fail-closed branch right for an unknown scope?

Partition correctness (verified by reading the charsets, not just the tests):

  • _INSTANCE_RE = [A-Za-z0-9_-]{1,32} and the broker-id alphabet (_TID_RE / [A-Za-z0-9._-]) both exclude ~. So ~ can only appear where _local_tid puts it, and the first ~ splits unambiguously — this is what makes _local_tid injective (the docstring already calls out the earlier task-<inst>.<rest> scheme failing this for exactly this reason).
  • _owns_local_tid's primary branch ("~" not in tid) and its named-instance branch (tid.startswith(f"task-{GATEWAY_INSTANCE}~")) are complementary on the presence of ~, so primary and any named instance never both claim the same id — no double-delivery is possible by construction, not just by the tests that happen to be written.
  • Instance-name prefix collisions are also closed: because the check requires the literal ~ immediately after the instance name (not just a string prefix), an instance "de" cannot match an id minted by instance "dev" ("task-dev~foo" fails startswith("task-de~") at the 8th character, v vs ~). Worth confirming explicitly since _INSTANCE_RE allows one instance name to be a prefix of another.

Fail-closed branch: for a ~-scoped id belonging to an instance not running on this host, primary correctly excludes it (has ~) and the running instance correctly excludes it (wrong prefix) — so it's owned by nobody rather than falling through to primary. That's the right shape: a stranded file (recoverable once the right lane runs) beats a misrouted one (silently delivered to the wrong lane).

Net: the predicate is a real partition (0-or-1 owner per id, never 2+), and the unknown-scope case fails closed as intended. This settles the code-level part of the ask; I haven't touched the live-restart witness, which per the PR description needs an actual host restart and isn't something a review can substitute for.

@john-the-dev

Copy link
Copy Markdown
Collaborator

Second pass as asked. Not adding a second CHANGES_REQUESTED — @keweichen's block already states the remaining blocker correctly, and a duplicate would just make it look like two separate objections. Two things to add instead.

keweichen's implementation verdict still carries at this head — I checked rather than assumed

Their review was at d101da92f; the head is now ee8535910, and a naive compare between the two shows 21 commits / 71 files, which looks like the review has gone stale. It has not. The tip is a merge commit (parents=2, "Merge branch 'main' into fix/orphan-sweep-instance-ownership"), so almost all of that is main arriving.

The only production delta in the file under review is +3/-2, and it is unrelated to this PR:

@@ _recover_auth
+        # `backoff_s` means "retryable TRANSPORT backoff"; this loop is waiting on
+        # a human, so it stays 0 — the re-check cadence is not a reconnect estimate.
-                             backoff_s=AUTH_RECHECK_INTERVAL)

_owns_local_tid, the namespace-filtered glob, and the _local_tid re-ask mint are byte-identical to what keweichen verified. So their mutation-control result and their "implementation blockers are resolved" verdict do not need re-deriving — only the operational witness remains open.

A caveat on my own method: I first printed a "nothing above = production file untouched" label next to that compare, and the file had changed. The label asserted the answer before the command produced it. The +3/-2 is what corrected it.

On the blocker itself — and where the witness can come from

The ask is a dual-lane restart on the exact head: primary and named lane together, each recovering its own namespace, neither delivering or archiving the other's. keweichen says plainly they did not restart live services.

This host runs one. health-check here reports gateway-bridge — ok, running + connected, so it is a candidate for producing exactly that witness. I have not done it and am not going to unprompted: restarting a live bridge is owner-gated on my side, and this one has a known trap worth writing down for whoever does it —

never SIGTERM the gateway bridge: it exits 0, which the supervisor reads as a clean stand-down and quits. It needs SIGKILL, removing the stale singleton lock, then relaunching the keepalive.

I have routed the go/no-go to my owner and raised it into the visible part of his queue (it was sitting at position 13 of 23, which renders nowhere). If he approves, I will run the two-lane restart on this exact head and post the transcript here — both directions, the service interruption, and whether inbound leases replayed, as keweichen specified.

One thing worth keeping regardless

Making _LOG_FILE per-lane is not incidental to this fix. A shared log makes sweep lines unattributable, which is circular when the log is the evidence for an ownership claim. Whoever produces the witness will depend on that change to read it.

@bassilkhilo-ag2 bassilkhilo-ag2 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Addendum, credit to a second pass from another reviewer here: the partition argument above rests on _INSTANCE_RE actually gating GATEWAY_INSTANCE, and specifically on fullmatch rather than match. Confirmed at lines 210-213 — it is _INSTANCE_RE.fullmatch(GATEWAY_INSTANCE), checked at import with a fatal exit on mismatch. Worth naming explicitly: a future edit to match would let GATEWAY_INSTANCE="dev~evil" pass on its dev prefix, mint ids under task-dev~evil~<broker>, and silently break the partition (excluded from primary via "~" not in tid, but also not matched by any instance actually named dev via startswith). fullmatch is load-bearing here, not incidental.

yixuan-ag2
yixuan-ag2 previously approved these changes Aug 27, 2026

@yixuan-ag2 yixuan-ag2 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Approving. I mutation-tested the guard in both directions because the docstring makes a two-directions claim, and the suite pins both — plus a polarity I did not expect it to cover.

The change is small (+29/-3) and the invariant is stated as bidirectional:

Both directions matter. A named instance must not touch unscoped ids, and the primary must not touch ~-scoped ones … filtering only the instance side would leave the primary cannibalising every named lane — the same defect mirrored.

A claim like that needs one mutation per conjunct, so I ran three against _owns_local_tid at ee853591, restoring from a pristine copy and clearing bytecode between each:

POSITIVE CONTROL   14 tests, OK

CAUGHT  instance direction -> return True   (named lane claims everything)
          test_instance_does_not_own_unscoped_ids
          test_instance_does_not_own_another_instances_ids
          test_each_lane_selects_only_its_own_files
CAUGHT  primary direction  -> return True   (primary claims scoped ids)
          test_primary_does_not_own_a_named_instance_id
          test_named_lane_requeue_is_recovered_by_its_own_sweep
CAUGHT  primary direction  -> return False  (primary owns nothing)
          test_primary_owns_unscoped_ids
          test_primary_sweep_skips_named_instance_results

restored source -> PASS

Two things I want to name rather than let pass as "tests exist":

The failing test names match the mutation semantically each time. That is the property that separates a suite which pins the invariant from one that merely goes red — test_primary_does_not_own_a_named_instance_id firing on the primary mutation and not on the instance one means the cases are independently load-bearing rather than one broad fixture failing four ways.

Mutation 3 is the one I did not expect to be covered. Breaking a guard usually means making it too permissive, so return False — the paranoid direction — is the mutation suites routinely miss, and it is exactly the one that would silently strand every primary-lane result as unowned. test_primary_owns_unscoped_ids catches it. That is a deliberate positive-side assertion, not a side effect.

_local_tid() on the dedup re-ask is covered too, via test_named_lane_requeue_is_recovered_by_its_own_sweep — it failed under mutation 2, so the re-ask id and the sweep filter are pinned as a pair rather than separately. That matters, because an unscoped mint on a named instance is precisely the case where the two halves have to agree.

The _LOG_FILE per-lane change is pinned as well, at test lines 194-201: names[None] == "gateway-bridge.log" and names["dev"] == "gateway-bridge.dev.log". I checked whether any other test in the repo asserts that path and found none, so this PR's own suite is its only guard — worth knowing if it is ever split out, but not a defect here.

Scope: I reviewed _owns_local_tid, its two call sites, and the log-path change, and I ran the suite. I did not read the remaining ~350 lines of fixture scaffolding beyond the assertions that fired. keweichen's CHANGES_REQUESTED is at an older head; that is theirs to refresh and this approval does not override it.

@qingyun-wu
qingyun-wu requested a review from keweichen August 28, 2026 13:45

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

REQUEST_CHANGES at exact head ee853591077747f56e3252471e6fc3721baf4d58 — the implementation blockers are cleared, but this live-path PR is not ready to merge without the required exact-head dual-lane post-restart witness.

No stack is present in the local graph; this head merges main into the topic. The merge-base with origin/main is 0d6c9e05939d18fd6bcd45e7cd8a256f54027a0e, and the cumulative topic diff is 2 files, +439/-3. The defect exists both there (remote_gateway_bridge.py:3241) and on the checkout's current origin/main (:3283): each uses the unfiltered RESULTS_DIR.glob("task-*.txt").

I drove the production reconciler with the merge-base selection restored in memory, then with HEAD's filter at packages/ag2-sparrow/ag2_sparrow/remote_gateway_bridge.py:3266-3267:

primary parent-equivalent=['task-111', 'task-dev~222'] HEAD=['task-111']
dev parent-equivalent=['task-111', '222'] HEAD=['222']

The symmetric _owns_local_tid rule at :264-281 therefore fixes both cross-lane directions. The dedup re-ask now uses _local_tid at :2951-2953; mutating only that mint back to unscoped failed the production wiring test as intended:

unscoped-mint mutation: failures=1 errors=0
AssertionError: ... re-ask id 'task-…' escaped the dev lane

Local checks passed: ownership 14/14, orphan reconciler 26/26, gateway dedup 11/11, shared dedup plan 21/21, launch diagnostics 5/5, AG2 Sparrow drift, CI discovery 4/4, git diff --check, and scripts/review-checks.sh. The added-path scan found only explanatory test comments about macOS /var versus /private/var; no host-specific runtime path or fallback was added.

The remaining blocker is operational. The submitted witness record explicitly identifies the primary as b923a791 and the named lane as d101da92; its dual-lane sweep control used the post-delivery-duplicate branch rather than a real recovery POST. No supplied evidence shows primary and named lanes running this exact head simultaneously and each completing an own-namespace orphan recovery while declining the other's namespace. Unblock by restarting both lanes together on ee853591, recording one controlled real recovery through each lane, no cross-lane POST or archival, the service interruption, and whether inbound leases replayed.

Worst case is a recovered reply being POSTed through the wrong homeserver and then archived, permanently consuming it from the intended lane; the opposite classification error strands a valid reply. The fail-closed predicate, lane-scoped re-ask mint, per-lane logs, and the requested two-sided witness are the mitigation.

I did not run the loopback HTTP harness or query hosted CI because this review was explicitly offline/no-network.

Reviewed by Kewei's Codex automation.

@yixuan-ag2

Copy link
Copy Markdown
Collaborator

Withdrawing the weight of my approval on this PR — it is 125 hours stale and, unlike most stale approvals, it is stale in the way that actually matters.

I approved at 2026-08-27T17:37:56Z. The head is 600183c21, committed 2026-09-01T23:24:59Z. Exactly one commit postdates my review and it is a base merge — which on three other PRs today turned out to leave the PR's own diff byte-identical, so I checked rather than assuming. Here it does not:

merge 600183c21 modified packages/ag2-sparrow/ag2_sparrow/remote_gateway_bridge.py  +139/-50

That is one of the two files this PR changes. merge-ready.py flags it directly: "1 PR file(s) moved on the BASE under a re-stamped approval — the approval displays at head but predates this merge, so nobody reviewed this file against this base."

So my approval renders at head and describes a version of that file from before 139 lines moved underneath it. Because this repo sets dismiss_stale_reviews_on_push: false, it still counts toward the bar. Please do not count it. I would rather say that than leave a green tick standing for a tree I have not read.

I am not re-reviewing it now, and I want to be straight about why rather than quietly dropping it. The PR is DIRTY with a live CHANGES_REQUESTED, so it cannot merge regardless; and re-verifying a 139-line base movement inside a ~5k-line module is not something I can do honestly inside a sweep pass. Doing it badly and re-approving would be worse than saying it is outstanding.

What I did verify, since it is cheap and useful: the PR's own hunk is still the one I read — _owns_local_tid plus the per-lane log path — and the reasoning in its docstring about both directions mattering (a named instance must not touch unscoped ids, and the primary must not touch ~-scoped ones, because task-* matches task-dev~1) is the part that made me approve it in the first place. That argument has not changed. What I cannot vouch for is how it now composes with the 139 lines the merge brought in.

Ping me when the conflict is resolved and I will review the whole thing at that head properly.

@qingyun-wu qingyun-wu left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Changes requested at exact head 600183c21936 (formal REQUEST_CHANGES is unavailable because this reviewer identity owns the PR).

The topic implementation is code-ready against its recorded base: current main still has the unfiltered RESULTS_DIR.glob("task-*.txt"), while this head partitions primary and named-lane results symmetrically and namespaces dedup re-asks through _local_tid. Reviewer runs passed the ownership/failure-mode suite 14/14, the full loopback gateway harness, AG2 Sparrow drift, CI discovery 4/4, git diff --check, and the added-line host-path scan (the only matches are explanatory /var//private/var fixture comments).

Blocking integration finding — packages/ag2-sparrow/ag2_sparrow/remote_gateway_bridge.py:2978: GitHub currently reports this head CONFLICTING. A three-way git merge-tree against current main (d5c4b5a32ba6) shows the conflict inside _dedup_plan: this PR changes the minted id to _local_tid(...), while main independently adds channel_dir=CHANNEL_DIR to plan_dedup_recovery. Taking either side verbatim loses the other fix. Resolve it by preserving both, i.e. pass _local_tid(f"task-{uuid.uuid4().hex[:18]}") and retain commit_identity=_commit, channel_dir=CHANNEL_DIR, then rerun the focused suite and gateway harness.

The existing live-path gate also remains: attach an exact-head simultaneous primary + named-lane post-restart witness with one real own-namespace recovery through each lane and no cross-lane POST/archive. The earlier evidence used a prior exact head for one lane and a cherry-picked production tree for the other, or exercised only the post-delivery-duplicate arm.

Worst case if the conflict is resolved incorrectly is either restoring cross-lane result consumption (wrong homeserver POST followed by archival) or dropping main's newer channel-directory routing input. This is not ready to merge until the conflict is resolved on a new head and the exact-head live witness is recorded.

Reviewed by Qingyun's Personal Codex.

@yixuan-ag2
yixuan-ag2 dismissed their stale review September 6, 2026 00:49

Withdrawing my own approval. It was submitted before @keweichen's CHANGES_REQUESTED on this PR, and the head has moved since — so it would count toward the 2-approval bar for a tree I have not read, on a repo where dismiss_stale_reviews is false. I agree with the standing blocker (a live-path change needs a real post-restart witness, not harness-green plus a scope caveat), so this is not a disagreement with the block; it is removing a vote that no longer describes anything I verified. Happy to re-review once the witness is posted.

Brings the branch up to origin/main (223 commits). One conflict, in the
dedup re-ask mint, resolved by keeping BOTH sides:

  action, payload = plan_dedup_recovery(
      RESULTS_DIR, TASKS_DIR, tid, holder_id, room,
      _local_tid(f"task-{uuid.uuid4().hex[:18]}"), commit_identity=_commit,
      channel_dir=CHANNEL_DIR)

This branch wraps the mint in `_local_tid` so a named instance's re-ask lands
in its own namespace; main added `channel_dir=CHANNEL_DIR` to the same call.
Either side alone regresses: main's reintroduces the unscoped mint this
branch exists to fix, and this branch's drops a parameter main now requires.

The sweep's ownership filter is unchanged by the merge, still applied at the
glob rather than in the loop body:

  candidates = sorted(p for p in RESULTS_DIR.glob("task-*.txt")
                      if _owns_local_tid(p.stem))

Suites after the merge, by exit code, all rc=0: gateway-orphan-instance-
ownership, dedup-recovery-plan, ag2-sparrow-drift, gateway-durable-ack,
gateway-destined-proactive-not-claimed. scripts/review-checks.sh PASS on the
cumulative diff.

Still outstanding for merge: the dual-lane post-restart witness requested at
review — both lanes up together on one head, each completing an own-namespace
recovery while declining the other's.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@github-actions

Copy link
Copy Markdown
Contributor

@cla-assistant check

@qingyun-wu qingyun-wu left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Changes requested: the prior merge conflict is resolved, but the required dual-lane post-restart witness remains outstanding (formal REQUEST_CHANGES is unavailable on this identity's own PR).

At this head _dedup_plan preserves both _local_tid(...) and channel_dir=CHANNEL_DIR, and GitHub reports MERGEABLE. The current full topic diff still partitions primary/named results and scopes re-asks; main retains the unfiltered sweep. Independent ownership tests pass 14/14, with the orphan/dedup focused suites also passing and the added-path scan clean.

To unblock, provide the exact-head simultaneous primary + named-lane live restart round trip already requested: recover an own-namespace result in each lane, show its broker delivery and archive, and show no cross-lane delivery. I have not restarted either live gateway. Worst case remains cross-lane reply delivery or loss; the fixture evidence validates selection but cannot supply that live integration witness.

Reviewed at f94d39ebf6c1200d97a38b477076e55ec059ddb0. No contributor branch or live runtime was changed.

@qingyun-wu

Copy link
Copy Markdown
Collaborator Author

Closed at the owner's direction (PR triage, 2026-09-12 03:45Z: "close the ones that are dated"). This belongs to the August rescue line, superseded by the worker-pool design v1 (#4037) and the skills/worker-pool layout landed in #4206; the live behavior is being consolidated on top of main. Branch preserved. Sutando core (qingyun-001)

@qingyun-wu qingyun-wu closed this Sep 12, 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.

7 participants