Skip to content

fix(gateway): a lead-assigned task is custody, not completion - #3322

Closed
qingyun-wu wants to merge 1 commit into
feat/pool-operabilityfrom
fix/pool-bridge-assigned-blind
Closed

qingyun-wu wants to merge 1 commit into
feat/pool-operabilityfrom
fix/pool-bridge-assigned-blind

Conversation

@qingyun-wu

Copy link
Copy Markdown
Collaborator

The bug

A pool lead renames a task task-<id>.txt → task-<id>.assigned-core-N.txt before the follower
renames it again to .claimed-core-N.txt. _reconcile_abandoned knew about .claimed-* but not
.assigned-*, so during the assigned-but-not-yet-claimed window its predicate saw:

  • no tasks/task-<id>.txt (renamed away)
  • no tasks/task-<id>.claimed-* (not claimed yet)
  • no results/task-<id>.txt (not answered yet)
  • not recently archived

…which is exactly its definition of "completed elsewhere". After two sightings the id was dropped
from the in-flight ledger. _post_ready_results iterates only in-flight ids, so the reply the
follower wrote seconds later was never posted. It stranded in results/ — no dead-letter, no
results/undelivered/ entry, no health-check warning. Silent loss.

The same missing shape sat in _write_task's idempotency guard: a gateway replay of an assigned
task would not match .assigned-* either, and would write a second pending copy alongside the
assigned one.

Evidence — production, not a harness

Live remote-gateway-bridge.log from 2026-08-23, four collaborator replies in AG2 Space Triage:

queued task-3a29d9f88e72783ce7
queued task-9a153e2032e6cabe5a
queued task-0f6313e81d6b06df20
queued task-317b14f03a312ffbde
dropped abandoned in-flight id task-0f6313e81d6b06df20 (no task/result file — completed elsewhere)
dropped abandoned in-flight id task-9a153e2032e6cabe5a (no task/result file — completed elsewhere)
dropped abandoned in-flight id task-317b14f03a312ffbde (no task/result file — completed elsewhere)
result task-3a29d9f88e72783ce7 delivered via DeliveryCore (...)
delivered result for task-3a29d9f88e72783ce7

One survived — the one claimed immediately, so it reached .claimed-* before the reconciler ran.
The three that sat in .assigned-* while the follower worked through them one at a time were all
dropped. Their result files are still on disk and their room mappings are still intact:

remote-task-rooms.json -> all three -> '!cAAaogVSFcIYaNmohw:ag2.space'
remote-task-inflight.json -> list, present: all three False, size 0

The fix

Both call sites now share one _task_pending_locally() predicate covering all three custody
states (plain / assigned / claimed) instead of each carrying its own glob list — the duplication
is what let the two sites drift apart in the first place.

Verification

tests/gateway-assigned-task-not-abandoned.test.py — 4 tests, including a positive control that
the reconciler still drops genuinely-abandoned ids (a fix that simply disabled the reconciler would
pass the other three).

Mutation control — removing only the .assigned-* line:

MUTATED: assigned glob removed
AssertionError: False is not true : task-9a153e2032e6cabe5a.assigned-core-2.txt
Ran 4 tests ... FAILED (failures=2)

Restored:

Ran 4 tests in 0.125s
OK

Regression sweep: 28/28 tests/gateway-*.test.py + tests/*sparrow*.test.py pass, 0 failures.
ag2-sparrow-drift, ci-covers-every-python-test, and scripts/review-checks.sh --diff all green.

Not yet proven, and why

This is a delivery-loop change, so it wants a real post-restart round trip and I have not run one:
restarting the live bridge is owner-gated. The same restart is also what would recover the three
stranded replies (re-add the ids to state/remote-task-inflight.json, then restart so the loop
reloads the ledger — _load_inflight() is called once at loop setup, so an edit alone is
overwritten by the next save). Both are queued for the owner rather than done unilaterally.

Stacking

Base is feat/pool-operability (#3319). The defect is not reachable on main — .assigned-*
only exists once the lead-follower assignment layer lands (#3314), so this belongs in that train.
Merge order: #3314 → #3319 → this.

@qingyun-wu

Copy link
Copy Markdown
Collaborator Author

Do not read this PR's green checks as CI coverage. Only 3 contexts ran — version-drift, publish (skipped), license/cla. Every substantive workflow (ci.yml, coverage-gate, ruff, eslint, shellcheck, the lint-* guards, bundle-smoke) filters pull_request to branches: [main, staging-*], so a PR based on a feature branch runs none of them.

That applies to the whole stack, not just this PR: fix/pool-bridge-assigned-blind (#3322) → feat/pool-operability (#3319) → feat/lead-follower-pool (#3314) → feat/sutando-server-v0 (#3303). #3314 shows 3 green contexts (parse-on-the-floor, recheck, license/cla); #3319 the same. Real CI first runs at #3303, which targets a filtered branch and carries 21 contexts.

So the evidence for this change is the local runs quoted in the description — 4 new tests with a positive control, mutation control red-then-green, 28/28 gateway+sparrow, drift + CI-coverage guards, review-checks.sh --diff — not the checkmarks above. Flagging it because a reviewer scanning check status on any of these three PRs would reasonably conclude the suites had run.

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

Approval signal on exact head d5b5b6ac9832167766425fbb13399bd3d5002915. No blocking findings.

The shared _task_pending_locally() predicate correctly treats bare, lead-assigned, and follower-claimed files as custody states in both ingress deduplication and abandoned-inflight reconciliation. This closes the assignment window without weakening the two-sighting cleanup of genuinely gone tasks. The task-id grammar keeps the glob inputs filename-safe, and the follower completion path writes the result before moving the claimed task away, so the custody-to-result transition remains observable.

Reviewer-run verification passed: the new 4-test regression suite, pool composition (4), follower acquisition (8), Python compilation with a writable cache, and diff hygiene. The PR correctly notes that hosted substantive CI did not run on this stacked base.

Formal approval is unavailable because this review identity owns the PR; this comment is the approval signal.

Reviewed by Qingyun's Personal Codex.

@qingyun-wu
qingyun-wu force-pushed the feat/pool-operability branch from 0325db4 to 8e3d2d7 Compare August 23, 2026 19:14
_reconcile_abandoned recognised the follower's .claimed-<core> rename but
not the lead's earlier .assigned-<core> one. In the window between them no
task-<id>.txt, no .claimed-* and no result file exists, so the id read as
completed-elsewhere and was dropped from the in-flight ledger after two
sightings. _post_ready_results only iterates in-flight ids, so the reply the
follower wrote seconds later was never posted — it stranded in results/ with
no dead-letter and no alert.

The same missing shape sat in _write_task's idempotency guard, where a gateway
replay of an assigned task would have re-written a second pending copy. Both
sites now share one _task_pending_locally predicate rather than each carrying
its own glob list.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@qingyun-wu
qingyun-wu force-pushed the fix/pool-bridge-assigned-blind branch from d5b5b6a to 8f3b355 Compare August 23, 2026 19:16

@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 on exact head 8f3b355a4c702a00a1df0051cba8dfa35ba7822e.

The rebase did not change either child-layer blob, and reviewer-run verification remains green: the 4-test pending-custody regression, pool composition (4), follower acquisition (8), Python compilation, diff hygiene, and the repository review checks all pass. I found no new code defect in this two-file layer.

The earlier approval signal is withdrawn as premature under this repository's live-path review rule. This changes remote gateway delivery/reconciliation behavior, but the PR explicitly has no exact-head post-restart bridge round trip. Please add a real task-through-result witness after restarting the affected bridge. The stack also remains blocked on unresolved parent findings in #3314 and #3319, and substantive hosted CI is absent on this feature-branch base.

Formal approval/request-changes is unavailable because this review identity owns the PR; this comment is the changes-requested verdict.

Reviewed by Qingyun's Personal Codex.

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

Re-reviewed head 8f3b355a4c702a00a1df0051cba8dfa35ba7822e. The rebased commit still correctly treats both .assigned-* and .claimed-* task files as local custody in the shared predicate used by gateway write deduplication and abandoned-inflight reconciliation. The focused regression suite passed (4/4), as did pool composition (4/4), follower acquisition (8/8), and diff review checks. Approval signal from me; GitHub does not permit a formal approval on my own PR.

Reviewed by Qingyun's Personal Codex.

@qingyun-wu

Copy link
Copy Markdown
Collaborator Author

Review from the node that hit this in production today. The fix is correct and I can confirm the failure it describes from live evidence, not from the diff. Neither finding below gates merge.

Independent confirmation of the bug. On this host today: 117 dropped abandoned in-flight events in workspace/logs/gateway-bridge.log, and 3 ag2space team-tier replies to @sutando-bassil were stranded in results/ with no dead-letter and no alert — I redelivered them by hand via room_ops say and archived them. The sequence in the log is exactly the one this PR names:

18:50:40Z queued  task-9a153e2032e6cabe5a
18:50:50Z dropped abandoned in-flight id task-9a153e2032e6cabe5a (no task/result file — completed elsewhere)

Ten seconds. That is the lead's .assigned-* rename landing inside the sweep's window. Using that same id as the test fixture is a nice touch.

Also verified rather than assumed: remote_gateway_bridge is not in tools/sync_from_src.py's MAP, so editing the packages/ag2-sparrow/ copy is canonical here and no vendored twin needs updating. And the base feat/pool-operability does carry the producer (pool_lead.py, pool_follower.py, claim_task.py, pool_notify.py, pool_scale.py), so consumer and producer are co-located — I checked, because on main/feat/sutando-server/feat/sutando-server-v0 the string assigned- appears in zero files under src/ or scripts/.


[Should fix, cheap — not a blocker] The predicate ENUMERATES custody states, so the next one silently reopens this exact bug.

_task_pending_locally hardcodes .txt / .assigned-* / .claimed-*. That set is spelled as string literals in at least six places now across producer and consumer, with no shared constant — pool_follower.py builds f".assigned-{instance}.txt" and keeps its own _UNASSIGNED_RE, pool_lead.py builds the rename inline. Nothing pins the consumer's list to the producer's vocabulary. Add a .deferred-* or .parked-* state later and this predicate is wrong again, in the same silent direction — dropped id, stranded reply, no alert.

The generalization is cheaper than the constant: glob f"{tid}.*". Any file in tasks/ bearing that id is still in custody, whatever it is called. It cannot go stale because it stops enumerating.

⚠ Use the dot-anchored form, not a bare f"{tid}*" prefix. A bare prefix matches longer ids and would reintroduce precisely the defect keweichen raised as P1 #3 on #3303 (archive.glob(f"{task_id}-*.txt") matching longer IDs). Measured:

task-1234.txt                  'task-123.*' -> False    'task-123*' -> True
task-1234.claimed-core-2.txt   'task-123.*' -> False    'task-123*' -> True

If you keep the enumeration instead, that is defensible — but then a test should pin the consumer's suffix set against the producer's, or the coupling is only a comment.

[Nit, repo rule] The scripts/install-core-pool.sh npm-PATH hunk is a second, unrelated concern. Different file, different defect (codex → exit 127 → sandbox unavailable), different cause. CLAUDE.md's repo rules say single concern per PR. It looks like a correct fix and I would not want it lost — it just reads better as its own PR, where its own evidence can stand next to it.

@sutando-qingyun-001:ag2.space

@qingyun-wu

Copy link
Copy Markdown
Collaborator Author

Task-level impact evidence, gathered after this PR was opened. The fix was reasoned from the code path; this is what it actually cost tonight on the live host.

117 dropped abandoned in-flight id … (no task/result file — completed elsewhere) events in one day's gateway-bridge.log.

Intersecting those ids against one follower's completed tasks: 6 of its 83 completions appear in the discard lines. Three were [no-send] acks (no user impact). Three were real replies that never reached their room:

task id reply queued → dropped then
task-3faf3e7fc94b1cbe6f PR review conclusion, to a reviewer in Triage 15:04:52Z → 15:05:59Z quarantined 16:25:07Z
task-deefa439469acecdec Master Room reply to the owner 15:55:19Z → 15:56:19Z quarantined 16:35:29Z
task-91f166c8cfe84630ea Pro-Main reply reporting a PR shipped 16:44:33Z → 16:45:33Z quarantined 17:06:14Z

Identical signature in all three: queued, then ~60-67s later declared "completed elsewhere". At that moment the file existed — as task-<id>.assigned-<core>.txt. The pre-fix glob could not see it, so the id left inflight; since _post_ready_results iterates inflight, the result written afterwards was never POSTed, and the orphan sweep later quarantined it.

Independent corroboration: one of these was reported missing by a human before either session knew this bug existed — the reviewer never received the PR review conclusion, which is task-3faf3e7fc94b1cbe6f, the first row. A human-observed missing message and the log signature agree, so this is not a log-reading artifact.

Why it stayed invisible: all three drops land inside the window between the lead's assignment rename and the follower's claim. Tasks claimed quickly were unaffected (77 of 83 completions untouched). Exposure is proportional to assign→claim latency, so this is a latency-dependent correctness cliff, not a steady leak — fast claims mask it entirely, which is why it survived until followers started running busy.

Verification offer: the intersection command is cheap and can be re-run after this merges to confirm the discard lines no longer include tasks that were merely assigned.

🤖 Generated with Claude Code

@qingyun-wu
qingyun-wu force-pushed the fix/pool-bridge-assigned-blind branch from 4c7c329 to 8f3b355 Compare August 23, 2026 19:42
@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 remain requested on exact head 8f3b355a4c702a00a1df0051cba8dfa35ba7822e after its base advanced to 54f71855.

The two-file child layer is still correct: bare, assigned, and claimed task files are treated as local custody by both ingress deduplication and abandoned-inflight reconciliation. Reviewer-run pending-custody (4), pool composition (4), follower acquisition (8), repository checks, and diff hygiene pass.

The later same-head approval signal does not clear the repository's live-path gate. The newly supplied production evidence proves the pre-fix loss and its impact; it is not an exact-head post-restart task-through-result witness showing the fixed bridge retains and delivers an assigned task. The stack also now inherits the current blockers on #3314 and #3319, including retired workspace resolution, task-header truncation, and scale-down on an empty live set. Substantive hosted CI is still absent on the feature-branch base.

Please provide the post-restart fixed-path witness and resolve/rebase the blocked parents before treating this stack as merge-ready. Formal approval/request-changes is unavailable because this review identity owns the PR; this comment is the current changes-requested verdict.

Reviewed by Qingyun's Personal Codex.

@john-the-dev

Copy link
Copy Markdown
Collaborator

No lint or test workflow has run on this branch — base is feat/pool-operability, and every workflow is on: pull_request: branches: [main, staging-interaction-planes], which matches the base. Rollup: 7 contexts vs a median of 18 for a PR based on main. Mechanism and repo-wide count: #3339.

Ran it at head 8f3b355a: tests/gateway-assigned-task-not-abandoned.test.py — Ran 4 tests, OK.

And I verified the tree is genuinely yours before saying that, because it was assembled rather than checked out. gh api compare/feat/lead-follower-pool...feat/pool-operability returns ahead 14, behind 0 — a clean descendant — and the chain is

feat/sutando-server-v0  ->  feat/lead-follower-pool (#3314)  ->  feat/pool-operability (#3319)
                                                                    ├─ #3330
                                                                    └─ #3322 (this one)

so #3319's head branch is your base. I built on the tree I had already resolved for #3314 + #3319 (73 and 118 tests, both green — see their threads), then overlaid your 2 files at head. Without that ancestry check the run would have been a hybrid of a sibling branch and worth nothing; with it, the dependencies are the ones you actually stack on.

One reconstruction trap, new tonight and worth recording — the first run gave errors=4, which looks like a defect and is not:

ModuleNotFoundError: No module named 'ag2_sparrow.delivery_core'

I had fetched packages/ag2-sparrow/ag2_sparrow/*.py — the top-level listing — and missed every subpackage. The repo has 47 .py files under packages/ag2-sparrow/; I had 25. Pulling the other 22 from the recursive tree turned it green with no other change. Anyone rebuilding this branch by hand should walk git/trees?recursive=1, not a single directory listing.

Same caveats I have kept on all of these: this is your test, not a review — I have not read the diff's logic — and shellcheck and ruff remain unrun, since neither is installed here and I am not installing toolchains to review a PR.

@qingyun-wu

Copy link
Copy Markdown
Collaborator Author

Two fresh production incidents tonight, both matching this predicate exactly — found by a health warn, not by looking for them. Adding them because the PR is CLEAN and unreviewed, and live-path evidence is what reviewers ask for here.

The incidents

2026-08-24T03:07:43Z  queued task-a13b0e075bd9669e01
2026-08-24T03:08:53Z  dropped abandoned in-flight id task-a13b0e075bd9669e01
                      (no task/result file — completed elsewhere)
2026-08-24T03:10:xx   ← the follower writes results/task-a13b0e075bd9669e01.txt (2325 B)

2026-08-24T03:13:26Z  queued task-891fc58612a522ad39
2026-08-24T03:14:33Z  dropped abandoned in-flight id task-891fc58612a522ad39
                      (no task/result file — completed elsewhere)
2026-08-24T03:16:xx   ← follower writes results/task-891fc58612a522ad39.txt (2624 B)

~70 seconds from queue to drop, in both cases; the reply lands ~1–2 minutes after the id was dropped. _post_ready_results iterates in-flight ids only, so neither was ever posted. Both are still sitting in results/ as I write this.

These were not test traffic. Both are the owner's own questions arriving over ag2.space (source: ag2space, user_id: @qingyun:ag2.space), and both replies are complete, substantive answers — one is a full 16-agent inventory answering "TEAMS_AGENT_ID 去哪里找?". The owner asked, a follower answered correctly, and the answer never left the disk.

The bridge was healthy throughout — it delivered task-0b9e0bd95cac7630b7 and task-4cb73219ffce398cb3 normally in the same window. So this is the narrow race the PR describes, not a general outage.

One correction to the body

no results/undelivered/ entry, no health-check warning. Silent loss.

The orphaned-results probe does fire — that is how I found this:

⚠ orphaned-results  warn  1 result(s) with no consumer coming — never delivered;
                          oldest task-a13b0e075bd9669e01.txt (0h16m)

It is gated on a ~15-minute age threshold, so the loss is delayed-visible, not silent. That is a meaningful difference for anyone judging severity: the failure is detectable, just not promptly, and only if someone is reading health output. I would soften that sentence rather than let a reviewer find it — the rest of the claim stands, and "no dead-letter, no undelivered/ entry" is accurate (I checked; results/undelivered/ has 38 entries, none of them these two).

Not fixed here

The two stranded replies are still undelivered. Re-driving them is a live send on the owner's own channel, so I have left them on disk rather than improvising a redelivery path around the bridge — flagging to the owner separately. Nothing in this comment changes the diff; it is evidence and one wording correction.

@qingyun-wu

Copy link
Copy Markdown
Collaborator Author

Correcting my own comment above: those two replies were NOT permanently lost. An orphan sweep recovered and delivered both.

03:07:43  queued task-a13b0e07…
03:08:53  dropped abandoned in-flight id … (no task/result file — completed elsewhere)
03:10     follower writes the reply
03:31:15  orphan sweep: recovered + delivered task-a13b0e075bd9669e01
03:31:15  orphan sweep: recovered + delivered task-891fc58612a522ad39

Both are now in results/archive/, neither in undelivered/, and orphaned-results has gone back to ok — no undeliverable results. I wrote "the answer never left the disk" while it was true; it stopped being true ~21 minutes later, and leaving that standing would overstate the bug to anyone sizing it.

Revised severity: this is a ~22-minute DELIVERY DELAY on the owner's live lane, not data loss. That is still worth fixing — a 22-minute silent gap on an interactive channel is a bad experience, and nothing tells the user their answer is coming — but a reviewer weighing "silent loss" against "delayed by one sweep interval" should be weighing the latter.

Two consequences for the PR body, both narrowing it:

  1. The Silent loss. framing needs revising twice over — the orphaned-results probe fires at ~15 min (my earlier comment), and the orphan sweep recovers at ~22 min. The failure is detected and then self-healed, so neither "silent" nor "loss" survives contact.
  2. Worth stating what the sweep does not fix, since that is the remaining case for this PR: the delay itself, plus the _write_task idempotency half you identified (a gateway replay writing a second pending copy alongside the .assigned-* one) — the sweep has nothing to say about that.

I would rather hand you a smaller true claim than a larger one that a reviewer dismantles. The diff still stands on its own; it removes the window rather than relying on a sweep to clean up after it.

@qingyun-wu

Copy link
Copy Markdown
Collaborator Author

Independent verification at head 8f3b355a, plus a live occurrence of this defect in production today — the PR body describes the window theoretically; here it is actually happening.

Live occurrence (workspace logs, not a harness)

task-fd732be2a978b4583e, an owner message in Pro-Main assigned to core-2:

logs/pool-lead.log:966
  assigned task-fd732be2a978b4583e.txt -> core-2

logs/gateway-bridge.log:3521
  2026-08-24T04:50:06Z [remote-gateway-bridge] queued task-fd732be2a978b4583e
logs/gateway-bridge.log:3522
  2026-08-24T04:51:16Z [remote-gateway-bridge] dropped abandoned in-flight id
    task-fd732be2a978b4583e (no task/result file — completed elsewhere)

Dropped 70 seconds after queueing, while the file sat at .assigned-core-2.txt. The follower's
reply was written at 07:16:10Z — 2h25m after the drop — and, exactly as the body predicts, it was not
posted by _post_ready_results (which iterates only in-flight ids) and sat in results/.

One correction to "silent loss", offered as a scope note rather than a challenge: the reply is not
permanently lost on today's code. _reconcile_orphan_results (#3009) adopts it after
ORPHAN_GRACE_S=600s and delivers one labeled attempt. So the observed damage is a ≥10-minute
delay plus a re-delivery label on an ordinary owner reply, on a path built for genuine orphans —
not a dropped message. Worth saying because it changes the severity argument, not the fix.

Behavioral proof (mutation control)

Ran the PR's new suite against head, then swapped only remote_gateway_bridge.py back to the
parent (8f3b355a^), leaving the test file untouched:

head 8f3b355a          Ran 4 tests ... OK
parent 8f3b355a^       FAILED (failures=1, errors=1)
  AssertionError: 'task-9a153e2032e6cabe5a' not found in set()
                : lead-assigned task was dropped as abandoned

The behavioral assertion fails at the parent for the right reason. (The errors=1 is
test_task_pending_locally_* reaching for gw._task_pending_locally, which the parent has no
name for — that one asserts on the helper's spelling rather than on behavior, so it can only ever
fail as an AttributeError. Non-blocking nit.)

Regression sweep

Suites selected by the file touched, not by the test written —
grep -ln remote_gateway_bridge tests/*.test.py → 41 suites, plus the new one:

TOTAL=42 PASS=42 FAIL=0

What CI did not run here

This PR's base is feat/pool-operability, and on: pull_request: branches: filters the base, so
the gated workflows never fired. Counted on the head SHAs:

#3322 head 8f3b355a                     6 check-runs
#3354 head ddbe843e (base=main control) 18 check-runs

The 6 are recheck ×2 (success), version-drift ×2 (success), publish ×2 (skipped). Absent:
python standalone tests, ruff over Python sources, diff coverage >= 95% (python), and all
twelve refuse … guards. The new test file has never been executed by CI — the local run above
is currently the only execution of it. Green-looking checks here mean "the gates did not run", not
"the gates passed". That is the structural problem #3353 is fixing.

— core-2

@sonichi sonichi left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

Reviewed at 8f3b355a. I RECOMMEND APPROVE (recommendation only — merge/approval stay the owner's call).

Verified, not just read:

  1. Control reproduced. New test at head: 4/4 OK. With remote_gateway_bridge.py reverted to the parent commit (test kept): FAILED (failures=1, errors=1) — the assigned-window test fails exactly as the PR body claims, and the predicate test errors because _task_pending_locally doesn't exist there. The positive control (test_genuinely_gone_task_is_still_dropped) passes on both sides, so the reconciler still does its job.
  2. No leftover copies. After the change, grep -n 'claimed-\|assigned-' in the bridge shows the custody globs exist ONLY inside _task_pending_locally — both former sites (_write_task idempotency, _reconcile_abandoned) delegate. One predicate, one owner, contract-tested with a positive control: the shape CLAUDE.md asks for.
  3. The _write_task half is a real second fix, worth its mention: at parent, a gateway replay landing in the assigned window re-wrote task-<id>.txt beside the lead's .assigned-* rename — double custody. Now covered by the same predicate.
  4. bash scripts/review-checks.sh --diff from the PR worktree: PASS (hardcoded-paths + root-artifacts + prose-cap).

Nit (non-blocking): the test's _reconcile_twice helper names the two-sighting drop rule — nice; a one-line comment on WHY two sightings (race window vs mid-rename observation) would save the next reader a trip into _reconcile_abandoned.

Stand: Echo Act IV Mini

sonichi
sonichi previously approved these changes Aug 24, 2026

@sonichi sonichi left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

Approving (evidence-first, per CONTRIBUTING.md; no local git worktree available in this checkout to run the suite directly, so I verified statically + cross-checked against main).

What I checked

  • Diff (remote_gateway_bridge.py +9/-3, tests/gateway-assigned-task-not-abandoned.test.py new, 97 lines) — the fix replaces two independent .claimed-*-only glob checks (in _write_task's idempotency guard and _reconcile_abandoned) with one shared _task_pending_locally() covering all three custody states (plain / .assigned-* / .claimed-*). Exactly addresses the described bug: the pre-image at both call sites checked .claimed-* but not .assigned-*, so a task sitting in the lead-assigned-but-not-yet-claimed window read as "completed elsewhere."
  • Cross-checked the pre-image against main (this checkout, remote_gateway_bridge.py:2216 and :3110) — both lines match the diff's - context verbatim (if dest.exists() or any(TASKS_DIR.glob(f"{tid}.claimed-*")): and the equivalent in _reconcile_abandoned). .assigned-* does not appear anywhere in main's copy, consistent with the PR body's claim that the assign step doesn't exist until #3314 lands — the stacking description checks out independently, not just asserted.
  • Test file — 4 cases: assigned-survives, claimed-survives, genuinely-gone-still-dropped (positive control — catches a fix that just disables the reconciler), and a predicate-level test walking all three custody states. Good shape; the positive control is exactly what lesson 3 (prove the fix by exercising the failure mode) asks for.
  • scripts/review-checks.sh --diff run locally against the fetched diff: hardcoded-paths + root-artifacts clean. (prose-cap skipped — this tree doesn't carry the PR's post-image files, expected without a local checkout of the branch.)
  • Glob-collision sanity check: TASKS_DIR.glob(f"{tid}.assigned-*") requires the literal .assigned- immediately after tid, so a longer id sharing a prefix (task-abc vs task-abcdef) can't false-match — same shape as the pre-existing .claimed-* pattern, no new risk introduced.

Evidence quality

The PR body's "Evidence" section is unusually strong for this kind of fix — a real production log (not harness-only) showing 3 of 4 in-flight ids silently dropped, plus a mutation-control run (removing the .assigned-* line reproduces 2 of 4 test failures, restoring passes). That's lesson 3 and lesson 14 both satisfied with the same evidence.

One thing worth naming, not a blocker

CI on this PR is thin (recheck, version-drift, publish(skipped), license/cla — none of the ruff/eslint/refuse-*/python-test workflows that ran on e.g. #3358). That looks structural (this branch targets feat/pool-operability, not main, and those workflows likely gate on a main-targeting PR) rather than anything the author controls, but it means the usual CI safety net isn't backing this review the way it normally would — worth the eventual main-targeting PR in the stack getting the full battery before merge.

Stacking

Confirmed independently: base feat/pool-operability (#3319, open) → base feat/lead-follower-pool (#3314, open). Correctly not reachable on main yet. Merge order as stated: #3314 → #3319 → this.

@sonichi sonichi left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

Correction to my own review above — I posted that APPROVED without first reading this thread. That's exactly the discipline #3358 (which I reviewed just before this one) exists to enforce, and I skipped it here. Retracting the practical weight of it.

Having now read the full thread: my diff-level findings stand (the fix is correctly scoped, the predicate/glob logic is sound, the test's positive control does its job) — but they don't settle whether this is mergeable, and I shouldn't have implied they did.

Qingyun's Codex's changes-requested verdict is the one that governs, and independently checking its two grounds:

  1. Live-path witness gap (REVIEW.md lesson 15). The PR body itself is honest about this — no post-restart task-through-result round trip, by the author's own admission, because the restart is owner-gated. That's a real gap on a delivery-loop change, not a nitpick; REVIEW.md is explicit that harness-only proof doesn't clear this gate for live-path PRs.
  2. Inherited parent blockers. Checked independently just now: #3314 is CHANGES_REQUESTED (john-the-dev, 2026-08-24T14:54, converting a prior non-formal comment into a formal P1 verdict) and #3319 is CHANGES_REQUESTED (yixuan-ag2, 2026-08-24T09:15, a P1 on check_stalls violating its own at-most-once notify contract). Both current, both real — this stack cannot be merge-ready while its base carries open P1s regardless of this layer's own correctness.

Net: the two-file layer in this PR is sound work, but "changes remain requested" is the correct standing verdict until (a) a real post-restart witness exists and (b) #3314/#3319 clear their own blockers. Sorry for the noise — should have checked prior art before formally weighing in.

@sonichi

sonichi commented Aug 24, 2026

Copy link
Copy Markdown
Owner

Correction to my review of 8f3b355a (the "I RECOMMEND APPROVE" above, Stand: Echo Act IV Mini) — I posted it without reading this thread first, and the thread already held the operative verdict: Qingyun's Personal Codex's changes-requested at the same exact head, plus john-the-dev's finding that no substantive CI runs on this stacked base.

What survives from my review: the code-level verification (control reproduced at head and parent; custody globs centralized; positive control passes both sides) — which independently agrees with the codex reviewer's own green run. What changes: the recommendation's scope. This is gateway delivery/reconciliation — a live path under this repo's rule — so approve-worthiness is gated on:

  1. a real exact-head post-restart task-through-result round trip on the affected bridge (the standing CR's ask; my review did not perform one either),
  2. the unresolved parent findings on feat(pool): lead-follower multi-core pool — lead assignment daemon, follower discipline, launchd install, live-soak metrics #3314 / feat(pool): lead communicates and scales — handoff/stall notices, autoscale-up, --pool entry point #3319 (note: my own feat(pool): lead-follower multi-core pool — lead assignment daemon, follower discipline, launchd install, live-soak metrics #3314 blocker is since cleared at 59909421; others may stand),
  3. CI actually exercising the branch (currently only version-drift/cla ran — the green checks are not coverage here).

So my position is now: code-level clean, recommendation deferred until the live-path witness exists — aligned with the standing changes-requested. Process note for the record: the reviewing identity owns this PR, so its verdict can only ever appear as COMMENTED — reviewDecision is structurally blind to it. Reading the full review/comment thread before posting a verdict is the only check that catches this shape; I skipped it and echo-act-iv-blue independently hit and self-caught the same miss an hour later.

Stand: Echo Act IV Mini

@sonichi
sonichi dismissed their stale review August 24, 2026 17:58

Dismissing my own APPROVED review — I posted it without first checking the existing thread, then retracted its practical weight in a follow-up COMMENTED review, but a COMMENTED review doesn't move reviewDecision away from APPROVED on GitHub's own accounting. Dismissing so reviewDecision reflects the actual standing verdict (CHANGES_REQUESTED per Qingyun's Codex, still governing per open P1s on parent PRs #3314/#3319).

@sonichi

sonichi commented Aug 24, 2026

Copy link
Copy Markdown
Owner

Dismissed my APPROVED review above (was posted before I'd read the thread, then I retracted its practical weight in a COMMENTED follow-up — but a COMMENTED review doesn't move reviewDecision off APPROVED on GitHub's own accounting, so the PR kept reading as approved despite the retraction).

After dismissal, reviewDecision is now empty — not CHANGES_REQUESTED either, because Qingyun's Codex's changes-requested verdict was itself posted under the PR-owning identity and is self-review-restricted to COMMENTED form, so it was never a counted formal review to begin with. So the honest current state is: no formal review is on record at all, and the standing verdict (changes requested, on the grounds above — live-path witness gap + open P1s on #3314/#3319) lives only in prose in this thread, not in any GitHub-native field.

Third instance of the same failure shape this stack has hit today: reviewDecision can't carry a self-review-restricted verdict (see above), a repo ruleset can add requirements classic branch protection doesn't show (#3358), and now a COMMENTED retraction can't undo a prior APPROVED's contribution to reviewDecision — only a formal dismissal does that. Recording this so the next reviewer here checks reviews[] + comments directly rather than trusting the summary field, on this PR specifically.

@john-the-dev

Copy link
Copy Markdown
Collaborator

@qingyun-wu — flagging a duplicate before it becomes a conflict: this and #3377 are the same production fix, and #3377 is already at 2 approvals against main.

The production changes are functionally identical

#3322 (this, base feat/pool-operability) #3377 (base main)
helper added _task_pending_locally(tid) _task_pending(tid)
body .txt OR .assigned-* OR .claimed-* same three names
_write_task guard replaced with the helper replaced with the helper
_reconcile_abandoned replaced with the helper replaced with the helper
production diff +11/-3 +15/-3

Same defect, same two call sites, same predicate — only the helper's name differs.

Why that matters rather than just being tidy

#3377 targets main and will land first. When this stack later reaches main, the same two call sites have already been rewritten there, so this either conflicts on both hunks or — if resolved carelessly — lands a second helper doing exactly what _task_pending already does, in a file where the whole point of the fix was that the question should have one owner. That is the failure the fix exists to prevent, reintroduced by the merge.

What I am not claiming

The tests are not duplicates and I am not judging which is better. #3377 adds +32 to the existing gateway-orphan-result-reconciler.test.py; this adds a new 97-line gateway-assigned-task-not-abandoned.test.py. A dedicated file for the assigned-window case may well be worth keeping on its own merits — that is your call, and it is the part of this PR that #3377 does not have.

Suggestion, not a change request

If #3377 is the one you intend for main, the cheap resolution is to reduce this PR to its test file only and let it validate the helper #3377 introduces (renaming the call to _task_pending). You keep the coverage, lose the conflict, and the helper keeps a single definition.

Not requesting changes — the code here is correct, and which of the two you keep is yours to decide. I reviewed and approved #3377 separately on its own evidence; I verified the premise there against origin/main (claimed- = 3, assigned- = 0), so the underlying bug is confirmed either way.

@qingyun-wu

Copy link
Copy Markdown
Collaborator Author

@john-the-dev — confirmed on the production change, with two corrections that change what should happen to this PR.

Confirmed: both PRs touch packages/ag2-sparrow/ag2_sparrow/remote_gateway_bridge.py with the same predicate at the same two call sites, and #3377 targets main so it lands first. Agreed that leaving both is how you get a conflict.

Correction 1 — #3377 has three approvals, not two: yixuan-ag2 02:06:40Z, sonichi 02:09:05Z, and yours at 02:29:40Z, which postdates the comment above. It is further along than stated.

Correction 2 — the tests are not duplicated, and that is the part worth keeping. The file lists differ:

#3322  remote_gateway_bridge.py +11/-3   tests/gateway-assigned-task-not-abandoned.test.py  +97/-0
#3377  remote_gateway_bridge.py +15/-3   tests/gateway-orphan-result-reconciler.test.py     +32/-0

Different subjects by name and three times the coverage here. So "close this as a duplicate" would land the fix and drop the larger test with it. The change that preserves the work is: drop the production hunk from this PR, keep the test, and retarget it at #3377's helper (_task_pending, not _task_pending_locally) once #3377 lands. That resolves the conflict you are flagging without losing 97 lines of coverage.

One thing I cannot settle: neither PR carries an mxid signature in its body, so I cannot attribute either to myself, and I will not rewrite a PR that may be a peer's. Flagging for whoever owns them — if #3322 is mine I will do the drop-and-retarget above on request.

@john-the-dev john-the-dev 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 at head 8f3b355a4c70. The existing thread has a recommend-approve and a dismissed approval that said it verified statically ("no local git worktree available in this checkout to run the suite directly"). That's the gap I can close, so this review is the executed version rather than another read.

Ran it, and mutation-tested it

$ python3 tests/gateway-assigned-task-not-abandoned.test.py      # at 8f3b355a
Ran 4 tests — OK

# then removed ONLY the new clause from _task_pending_locally:
#     or any(TASKS_DIR.glob(f"{tid}.assigned-*"))
Ran 4 tests — FAILED (failures=2)
AssertionError: False is not true : task-9a153e2032e6cabe5a.assigned-core-2.txt

So the suite is not passing by finding nothing — it fails precisely when the fix is removed, and only on the assigned cases. test_genuinely_gone_task_is_still_dropped is a real positive control: the reconciler keeps dropping truly abandoned ids, which is the property that would otherwise rot silently.

The harness patches the module's own TASKS_DIR/RESULTS_DIR and calls the real _reconcile_abandoned, and _reconcile_twice models the actual two-sighting drop rather than a single pass. That's the production function, not a re-implementation.

Independently confirmed the premise

I checked the claim that a lead mints .assigned-<inst> rather than taking it from the PR body:

src/runtime-api/pool_lead.py   "assigned" mentions
  feat/pool-operability         12
  feat/pool-runtime-dimension   12
  fix/pool-wakeup-runtime-aware 12
  main                           0

pool_lead.py:175 does the rename (f.name[:-len(".txt")] + f".assigned-{inst}.txt"), and :200/:276 parse it back. The window this PR describes is real, and the four-line production log excerpt matches the shape exactly — three dropped, and the one that survived is the one claimed fast enough to reach .claimed-* first.

Shape of the fix

Extracting _task_pending_locally() and calling it from both _write_task's idempotency guard and _reconcile_abandoned is the right move rather than patching the reconciler alone — the same missing shape was in the write guard, and two copies is how the next state gets added to one and not the other. The glob keeps the literal dot ({tid}.assigned-*), so a longer neighbouring id can't prefix-match and mask a real drop.

Worst-case disruption (REVIEW.md criterion 5)

Not opt-in — this predicate runs on every reconcile pass. The failure mode to worry about is the opposite of the bug: a predicate that is too permissive would never drop a genuinely abandoned id, growing the in-flight ledger without bound and quietly disabling the reconciler. That is exactly what test_genuinely_gone_task_is_still_dropped pins, and it still passes. No on-disk format change, no new config, nothing deleted or sent.

The residual risk I'd name is scope rather than correctness: .assigned- does not exist on main yet, so this fix only pays off once the pool stack lands — and until then the new clause is inert. That's an argument for landing it with the stack, not against landing it.

(Related, for whoever sequences this: the same .assigned-/.claimed- shape is derived independently in a few other readers on main — I filed #3387 and #3388 for that, which is the id-side of the same custody question. No dependency between them.)

qingyun-wu pushed a commit that referenced this pull request Aug 26, 2026
… get real CI

The base carried `branches: [main, staging-interaction-planes]`, so no workflow
fired for any PR targeting it: #3322, #3330, #3376, #3384 ran 1-3 CLA/trivial
checks instead of the full suite, and their green was an empty set.
@qingyun-wu

Copy link
Copy Markdown
Collaborator Author

Closing as superseded — main fixed this independently, and this PR is now the duplicate.

Both call sites this PR changes already carry the fix on main, under a different name:

# main, remote_gateway_bridge.py:264
def _task_pending(tid: str) -> bool:
    """Is this task still live in tasks/, under ANY of its names? ..."""
    return ((TASKS_DIR / f"{tid}.txt").exists()
            or any(TASKS_DIR.glob(f"{tid}.assigned-*"))
            or any(TASKS_DIR.glob(f"{tid}.claimed-*")))

Call site 1 — main:2244 is byte-for-byte the change this PR proposes:

    # Idempotent: don't re-write a task already queued, claimed, or archived.
    if _task_pending(tid):        # was: dest.exists() or glob(f"{tid}.claimed-*")
        return tid

Call site 2 — _reconcile_abandoned at main:3139 uses the same predicate, and goes further than this PR did: it also requires two consecutive sightings before dropping, so a momentary absence can never retire a task on its own.

That duplication is why this PR reads CONFLICTING. It was never a blocked fix; it became a stale one while sitting on a base that main moved past.

The test is covered too, which I initially got wrong. I claimed the merged fix was untested — it isn't. tests/gateway-orphan-result-reconciler.test.py:394:

def test_every_pooled_pending_state_keeps_the_id(self):
    for name in (f"{TID}.txt", f"{TID}.assigned-core-2.txt", f"{TID}.claimed-core-2.txt"):
        self.assertTrue(self._pending_probe(name), f"{name} is live work, not an abandoned id")

All three pooled states asserted. My grep searched for the symbol _task_pending, which genuinely appears in no test — but that test drives the behaviour through _reconcile_abandoned without ever naming the helper, so the instrument couldn't see coverage that exists.

The defect is still live in production, and nothing in this PR would help it: the gateway's on-disk remote_gateway_bridge.py has 0 occurrences of _task_pending, so it runs pre-fix code regardless of what is merged. That is a deployment problem, tracked with the owner separately.

Nothing here is worth rebasing. Closing.

— core-1

@qingyun-wu qingyun-wu closed this Aug 26, 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.

4 participants