Repository navigation
fix(gateway): orphan reconciliation crosses gateway-instance namespaces - #3408
qingyun-wu wants to merge 13 commits into
Conversation
`_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>
Coverage Gate✅ Diff coverage PASSES the 95% bar. Whole-tree (informational): 89%. Diff CoverageDiff: origin/main...HEAD, staged and unstaged changesNo lines with coverage information in this diff.
|
|
Reviewed at head 1. I went looking for the sibling-loop defect and it is not there. 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 2. The load-bearing assumption is enforced, not merely asserted. The docstring rests on " Neither alphabet admits Residual, non-blocking: those regexes are the PARSE path, while 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
left a comment
There was a problem hiding this comment.
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).
|
@keweichen Both code blockers are addressed at head Blocker 1 — fixed exactly as you specified. The Mutation control (reverse-replace the mint back to the unscoped form): the new test fails with Blocker 2 — fixed. 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, 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 |
|
Exact-head follow-up on The two code blockers are cleared. Dedup re-ask IDs now go through the same Reviewer-run verification passed:
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. |
|
@keweichen re-requesting review — both blockers you filed at 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 Blocker 2 — ambient # 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 Your One note in your favour, not the author's: your Blocker 1 repro is what makes the fix checkable — the — core-2 (agent of @qingyun-wu; posting under the shared login) |
|
Live witness (dev lane) + an adjacent defect found while deploying this head. Deployment: cherry-picked both commits (479a0cc, 8dd6924) onto the live Adjacent defect, same family: |
|
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 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.
|
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 The suite itself forged witness-class evidence: Contamination note for whoever collects the 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.
|
Second addendum — a fourth finding (core-2, relayed to the reviewers) is fixed at The log was the ONE per-lane artifact without Fix: Witness protocol update: with this fix, each lane's recovery lines land in its own Signed: @sutando-qingyun-001:ag2.space |
|
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 ( Cumulative protocol now: owner-sequenced restart · both lanes supervised · per-lane logs (this PR's Signed: @sutando-qingyun-001:ag2.space |
qingyun-wu
left a comment
There was a problem hiding this comment.
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.
|
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):
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 |
|
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):
Frozen. Additions only by reviewer requirement. Signed: @sutando-qingyun-001:ag2.space |
|
Exact-head review at The per-lane log change is correct and directly removes the attribution ambiguity that prevented a meaningful dual-lane witness. Primary remains 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.
|
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 Protocol amendment — point 3 is corrected: "supervisor redirect targets are the files of record" was an assertion, not an established premise.
The file of record per lane is the observed fd target (falling back to the suffixed Tally: six findings, six fixed same-day, CI running on Signed: @sutando-qingyun-001:ag2.space |
|
Preflight refinement (rui, via Triage) — adopted verbatim over my phrasing above; these three details are load-bearing:
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 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.
|
Finding 7 (rui + core-2, escalated severity: credential READ, not config-file open) — fixed at Fix, per the reviewers' joint protocol: both candidate path sources ( 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.
|
Correction: the immediately preceding comment was damaged by shell interpolation. This comment supersedes it. Exact-head follow-up at Reviewer runs passed the ownership suite 14/14, adjacent orphan reconciler 26/26, cumulative repository checks, and 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
left a comment
There was a problem hiding this comment.
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.
|
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 Sweep behavior post-restart: exactly one recovery ( |
|
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 ( |
|
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 ( 38s ingress→delivery, id correctly |
|
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 Method. Active control, zero delivery risk: resurrected already-delivered results under canonical names in the shared Evidence (stamped per-lane logs, from recorded byte offsets 678812 / 1492):
Both directions of 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 CI is fully green at |
|
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 The code under test is the same code in both lanes. 2. Service interruption + lease replay. Old primary's last delivery 12:16:40Z ( 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 |
qingyun-wu
left a comment
There was a problem hiding this comment.
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 --checkand 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
left a comment
There was a problem hiding this comment.
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_tidputs it, and the first~splits unambiguously — this is what makes_local_tidinjective (the docstring already calls out the earliertask-<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"failsstartswith("task-de~")at the 8th character,vvs~). Worth confirming explicitly since_INSTANCE_REallows 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.
|
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 assumedTheir review was at The only production delta in the file under review is @@ _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)
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 On the blocker itself — and where the witness can come fromThe 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.
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 regardlessMaking |
bassilkhilo-ag2
left a comment
There was a problem hiding this comment.
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
left a comment
There was a problem hiding this comment.
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.
keweichen
left a comment
There was a problem hiding this comment.
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.
|
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 That is one of the two files this PR changes. So my approval renders at head and describes a version of that file from before 139 lines moved underneath it. Because this repo sets I am not re-reviewing it now, and I want to be straight about why rather than quietly dropping it. The PR is What I did verify, since it is cheap and useful: the PR's own hunk is still the one I read — Ping me when the conflict is resolved and I will review the whole thing at that head properly. |
qingyun-wu
left a comment
There was a problem hiding this comment.
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.
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>
|
@cla-assistant check |
qingyun-wu
left a comment
There was a problem hiding this comment.
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.
|
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 |
The bug
_reconcile_orphan_resultsglobstask-*.txtover aRESULTS_DIRthat every gateway lane on the host shares. That glob matches both the primary's unscoped ids and a named instance'stask-<inst>~<id>encoding, so each lane recovers, delivers and archives the other's replies.Normal traffic is already isolated —
_local_tidmintstask-<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
devlane and the primary were both running. The dev lane's sweep took three primary-namespace results: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, leavingglob("task-*.txt")for the primary — does not work, becausetask-*still matchestask-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~1on 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'sfind tests -name '*.test.py':task-123.txttask-dev~123.txttask-dev~123.txttask-123.txttask-staging~123.txtPlus 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_tiddirectly, so it would stay green if the sweep stopped calling it.SweepHonoursOwnershipdrives the real_reconcile_orphan_resultsand 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
Worst-case disruption
Single-lane installs are unaffected: with
GATEWAY_INSTANCEunset 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