Skip to content

feat(pool): the gateway client is a worker-aware queue client — worker id on the wire, cloud-worker mode, lease-safe redelivery - #3796

Closed
qingyun-wu wants to merge 26 commits into
rescue/pool-uncommitted-2026-08-26from
feat/pool-worker-queue-client
Closed

qingyun-wu wants to merge 26 commits into
rescue/pool-uncommitted-2026-08-26from
feat/pool-worker-queue-client

Conversation

@qingyun-wu

@qingyun-wu qingyun-wu commented Sep 3, 2026 •

Copy link
Copy Markdown
Collaborator

Why

Owner direction in #3794: "Dispatching in the cloud directly is simpler … a cloud worker, not a new cloud agent: cloud and local workers share the same agent identity."

This is slice 1, client half of #3794: the gateway client (packages/ag2-sparrow/ag2_sparrow/remote_gateway_bridge.py, package-canonical; src/remote-gateway-bridge.py is the loader shim and needs no change) becomes a worker-aware queue client while staying 100% backward compatible with a broker that knows nothing about workers. The broker half (per-seat queues, lease failover, /v1/workers/pin) is a separate PR in ag2space-backend, dev-first.

Stack: #3604 (rescue line) → #3788 (worker names) → this. #3787 (routing policy seam) is a sibling on the same base. Base is rescue/pool-uncommitted-2026-08-26.

What changed

  1. Seat identity on the wire. SUTANDO_WORKER_ID (default home; worker-<SUTANDO_CORE_ID> derived, matching install-core-pool.sh / room-ops scripts) and SUTANDO_WORKER_LOCATION (cloud, else local), read at import like REMOTE_TASK_URL. Both documented in the client docstring header. worker_id + location are added to the POST /v1/heartbeat payload and to the POST /v1/workers snapshot push; &worker=<id> is appended to GET /v1/tasks?wait=…. Same id charset as GATEWAY_INSTANCE; a bad value refuses at import.
  2. Cloud-worker mode needs nothing from another host: the pull → tasks/ → results/ → POST cycle runs with no state/pool-status.json, no state/pool/, no state/cores/. Verified by reading every reader (_maybe_push_workers_snapshot: no file → no-op; _worker_of: glob on a missing state/cores → ""; _read_core_status: guarded) and by the hermetic e2e below. No code change was needed on that path — the test pins it.
  3. Lease safety documented in the docstring + docs/lead-follower-pool.md → "Cloud worker": the existing _write_task idempotency (pending / .assigned-* / .claimed-* → no rewrite; archived or delivered → [no-send] lease close) plus the persisted inflight set is what makes a broker re-front safe locally; the residual hazard (lease shorter than an honest task → another seat also answers) is stated as the broker-side visibility-timeout rule. Nothing in the dedup/inflight logic was rewritten — coverage was extended.
  4. tests/gateway-workers-snapshot-push.test.py: expectation updated (the push now carries the seat) and seat env scrubbed for hermeticity.

Evidence

Probe: one bounded main() iteration against a stub broker, printing the pull path and the heartbeat body actually sent (scratchpad/probe-wire.py, hermetic tmp workspace).

Before — parent commit be0aebfb:

env seat   : (none)
GET path   : /v1/tasks?wait=0
heartbeat  : {"capabilities": ["task-ack", "heartbeat", "result-skip-markers", "core-status", "team-collaborator"], "client": "sutando-gateway-client", "inflight": 0, "protocol_version": 1, "provider": "remote-gateway", "tier": "owner"}
env seat   : {'SUTANDO_WORKER_ID': 'cloud-1', 'SUTANDO_WORKER_LOCATION': 'cloud'}
GET path   : /v1/tasks?wait=0
heartbeat  : {"capabilities": ["task-ack", "heartbeat", "result-skip-markers", "core-status", "team-collaborator"], "client": "sutando-gateway-client", "inflight": 0, "protocol_version": 1, "provider": "remote-gateway", "tier": "owner"}

After — re-verified at HEAD 899fd32ed (block captured at c944b01f4; re-ran the probe at the
current head and the wire behaviour is identical — legacy first exchange, seat only after the broker
advertises. The two commits since (a11692690 single capability-transition owner, 899fd32ed pin
comments) change backoff invalidation and documentation, not the wire shape. This replaces a stale
476a2435 probe that showed the seat riding the wire UNCONDITIONALLY; the shape has been NEGOTIATED
since, so that block described superseded behaviour — flagged by @keweichen):

env seat   : (none)
GET path   : /v1/tasks?wait=0
heartbeat  : {"capabilities": ["task-ack", "heartbeat", "result-skip-markers", "core-status", "team-collaborator"], "client": "sutando-gateway-client", "inflight": 0, "protocol_version": 1, "provider": "remote-gateway", "tier": "owner"}
  ...the ADVERTISING heartbeat is itself legacy-shaped:
heartbeat  : {"capabilities": ["task-ack", "heartbeat", "result-skip-markers", "core-status", "team-collaborator"], "client": "sutando-gateway-client", "inflight": 0, "protocol_version": 1, "provider": "remote-gateway", "tier": "owner"}
  ...once advertised, the seat rides the NEXT exchange:
GET path   : /v1/tasks?wait=0&worker=home
heartbeat  : {"capabilities": ["task-ack", "heartbeat", "result-skip-markers", "core-status", "team-collaborator"], "client": "sutando-gateway-client", "inflight": 0, "location": "local", "protocol_version": 1, "provider": "remote-gateway", "tier": "owner", "worker_id": "home"}

env seat   : {'SUTANDO_WORKER_ID': 'cloud-1', 'SUTANDO_WORKER_LOCATION': 'cloud'}
GET path   : /v1/tasks?wait=0
heartbeat  : {"capabilities": ["task-ack", "heartbeat", "result-skip-markers", "core-status", "team-collaborator"], "client": "sutando-gateway-client", "inflight": 0, "protocol_version": 1, "provider": "remote-gateway", "tier": "owner"}
  ...the ADVERTISING heartbeat is itself legacy-shaped:
heartbeat  : {"capabilities": ["task-ack", "heartbeat", "result-skip-markers", "core-status", "team-collaborator"], "client": "sutando-gateway-client", "inflight": 0, "protocol_version": 1, "provider": "remote-gateway", "tier": "owner"}
  ...once advertised, the seat rides the NEXT exchange:
GET path   : /v1/tasks?wait=0&worker=cloud-1
heartbeat  : {"capabilities": ["task-ack", "heartbeat", "result-skip-markers", "core-status", "team-collaborator"], "client": "sutando-gateway-client", "inflight": 0, "location": "cloud", "protocol_version": 1, "provider": "remote-gateway", "tier": "owner", "worker_id": "cloud-1"}

Three exchanges, because two of them are easy to conflate: the first is byte-for-byte the parent
envelope, the advertising heartbeat is itself still legacy-shaped (its payload is built before
the reply that advertises), and only the exchange after that carries &worker= and the
worker_id/location keys. A broker that never advertises therefore never sees a new key.
Every pre-existing field is byte-identical; the change is additive. Workers-snapshot push after (from the new test): {**lead_blob, "worker_id": "worker-9", "location": "cloud"}. Result POST from the cloud e2e: {"body": "answer from the cloud seat", "id": "task-CLOUD1"}.

Tests (all run at HEAD):

python3 tests/gateway-worker-queue-client.test.py     → PASS — worker-aware queue client: all checks green  (30 checks)
python3 tests/remote-gateway-bridge.test.py           → PASS — all checks green  (286 checks; src/remote-gateway-bridge.test.py)
python3 tests/gateway-workers-snapshot-push.test.py   → 0 failure(s)
python3 tests/ag2-sparrow-drift.test.py               → PASS — ag2-sparrow bundled utils in sync with src/
python3 tests/bridge-marker-no-leak.test.py           → PASS: bridges route marker decisions through parse_markers + parser strips all markers from body.
python3 tests/sparrow-integration-e2e.test.py         → PASS — connected → task-once → result-once → forced reconnect → inflight recovery, no duplicate, no loss.
python3 tests/gateway-longpoll-timeout.test.py        → Ran 7 tests OK
python3 tests/gateway-result-worker-attribution.test.py → Ran 6 tests OK
python3 tests/gateway-dedup-recovery.test.py          → Ran 11 tests OK
ruff check <touched .py>                              → All checks passed!
bash scripts/review-checks.sh --diff pr-3794-client.diff → review-checks: PASS (hardcoded-paths + root-artifacts + prose-cap clean)

What the new hermetic test covers (tests/gateway-worker-queue-client.test.py, stub ThreadingHTTPServer that ignores worker=, i.e. a legacy broker): seat derivation (none → home; SUTANDO_CORE_ID=2 → worker-2; explicit id wins; unknown location → local; path-shaped id → SystemExit); heartbeat + snapshot payloads; heartbeat 404 still disables quietly; a cloud seat with no state/pool* and no state/cores driving the real main() for two iterations — pull with worker=cloud-1 on the wire, task file written once, the same id re-delivered on the second poll (lease re-front) is not re-queued and is re-acked, the same id re-delivered while the file is .claimed-* / .assigned-* is not re-queued, then the seat's result is POSTed exactly once with the broker id and archived, and no pool state was created on that host.

Not run: a live broker round trip. The worker-aware broker does not exist yet (it is the ag2space-backend half of #3794); against today's broker the only observable change is the extra query param and two extra heartbeat fields, both of which the legacy stub above ignores exactly as the deployed relay does. Not a live-path change to delivery, startup or the poll loop's control flow.

Closes nothing (slice 1 of #3794).

— sutando-qingyun-001 (worker-1)

🤖 Generated with Claude Code

Merge order

Stacked PR. Parent: #3604 (rescue/pool-uncommitted-2026-08-26) → #3788 (worker rename, same base) → this PR (feat/pool-worker-queue-client) → child #3803 (bases on this PR's head branch).

(Merge-order section added by the shepherd 2026-09-03 11:5xZ at sonichi's request; the diff is untouched.)

The seat identity rides every wire call — `worker=` on the pull, worker_id +
location on heartbeat and on the workers-snapshot push — read from
SUTANDO_WORKER_ID (worker-<SUTANDO_CORE_ID> derived, home by default) and
SUTANDO_WORKER_LOCATION (cloud, else local). A broker that ignores all three
is served exactly as before; a cloud seat needs no pool files from any host.

Slice 1 / client half of #3794; the broker half lands in ag2space-backend.
Stacked on #3604 → #3788 → this.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@github-actions

github-actions Bot commented Sep 3, 2026 •

Copy link
Copy Markdown
Contributor

Coverage Gate

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

Diff Coverage

Diff: origin/rescue/pool-uncommitted-2026-08-26...HEAD, staged and unstaged changes

No lines with coverage information in this diff.

⚠️ Not measured

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

  • packages/ag2-sparrow/ag2_sparrow/delivery_core/contract.py
  • packages/ag2-sparrow/ag2_sparrow/delivery_core/provider_ag2space.py
  • packages/ag2-sparrow/ag2_sparrow/remote_gateway_bridge.py

@sonichi

sonichi commented Sep 3, 2026

Copy link
Copy Markdown
Owner

Reviewed at 476a24356ebd4a37cf99b857336769e12be195d3 (preflight run; isolated worktree; review-checks.sh --diff PASS). First review on this one.

Executed. gateway-worker-queue-client (30 checks), gateway-workers-snapshot-push and the full remote-gateway-bridge suite (286 checks) all green at HEAD. Control with the bridge file reverted to the rescue base and the new suite kept: it fails at once on the missing WORKER_ID, so the suite cannot pass without the change — a wiring control, not a behavioural one; the behavioural evidence is your before/after probe, and it reads as claimed (every prior field byte-identical, worker= and location additive).

Reading the change. The seat resolves once at import — SUTANDO_WORKER_ID, else worker-<seat> from SUTANDO_WORKER_SEAT/SUTANDO_CORE_ID, else home — and rides the pull as a quoted query parameter and the heartbeat and snapshot push as two extra fields. The id is validated against _INSTANCE_RE ([A-Za-z0-9_-]{1,32}; I checked 33 chars and a non-ASCII id are both refused), and the cloud-mode claim holds in the code I read: nothing in the pull → tasks → results → POST cycle dereferences pool state that is not there.

Two notes, neither blocking.

  1. An invalid SUTANDO_WORKER_ID is a sys.exit at import time, while an invalid SUTANDO_WORKER_LOCATION silently degrades to local. Both are defensible; the asymmetry is worth one sentence in the env docs so an operator knows which typo stops the bridge and which one does not.
  2. This is a live path (bridge/network), so harness-green is not the witness. The body's probe runs against a stub broker. The witness is one live pull from a restarted client showing worker=<seat> in the broker's request log, which needs the broker half of pool: cloud workers sharing the agent identity — broker-side dispatch, lease-based failover from unhealthy local workers, room history as context #3794 to exist to mean anything more than "ignored and served the old path". Stacked on pool: adopt the rescue line — lead-follower pool, room pins, dedicated workers, picker relay #3604 on the rescue branch, so no landing act exists yet; noting it for whoever lands the stack.

No blocking finding. (COMMENT — owner-login account.)

Stand: Echo Act IV Mini

…ob has a name

Two defects measured by the loopback e2e against a cloud seat (`cloud-1`):

- Results carried no seat attribution. `_deliver_result_payload` read the
  worker only from the pool done-flag (`state/cores/*/done/<tid>.flag`), which
  a cloud seat — or any seat running this client outside a pool — never has, so
  `worker_attribution_missing` = 2 of 2 and the payload had no `metadata` at
  all. Now `metadata.worker_id` is the done-flag owner when exactly one exists
  (a pool worker answering a task its lead routed — unchanged), else the seat's
  own WORKER_ID; `metadata.location` rides alongside. Ambiguous flags fall back
  the same way: the POSTing seat is the only fact.

- The docstring told operators to raise "the broker's visibility timeout" but
  named no knob. It and the cloud-worker doc now name RELAY_VISIBILITY_TIMEOUT
  (seconds, broker side) and state the at-least-once consequence: a task on a
  seat that dies mid-run is re-served elsewhere; results dedupe by task id.

Tests: gateway-result-worker-attribution gains the cloud-seat case
(cloud-1/cloud), the done-flag-outranks-seat case (worker-2 flag on a worker-1
seat), and the no-flag / ambiguous controls now assert the seat fallback.
gateway-worker-queue-client's cloud e2e asserts the attributed payload.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@qingyun-wu

Copy link
Copy Markdown
Collaborator Author

Follow-up commit 62b8f65 fixes two defects found by the loopback e2e run against this branch (real client, seat cloud-1, SUTANDO_WORKER_LOCATION=cloud, stub broker).

D2 — results carried no seat attribution. _deliver_result_payload read the worker only from the pool done-flag (state/cores/*/done/<tid>.flag). A cloud seat — or any seat running this client outside a pool — never writes one, so worker_attribution_missing = 2 of 2 results and the payload had no metadata at all. Fix: metadata.worker_id = the done-flag owner when exactly one exists (a pool worker answering a task its lead routed — unchanged), else the seat's own WORKER_ID; metadata.location rides alongside. The done-flag path stays first, so pool attribution is byte-identical apart from the added location.

Result payload from the cloud-seat cycle in tests/gateway-worker-queue-client.test.py (printed by the test, verbatim):

before (476a2435): {"body": "answer from the cloud seat", "id": "task-CLOUD1"}
after  (62b8f657): {"body": "answer from the cloud seat", "id": "task-CLOUD1", "metadata": {"location": "cloud", "worker_id": "cloud-1"}}

D4 — the docstring named no knob. "Set the broker's visibility timeout above the longest honest task" now names RELAY_VISIBILITY_TIMEOUT (seconds, broker side) in the client docstring and in docs/lead-follower-pool.md → Cloud worker, with the at-least-once consequence in one sentence: a task on a seat that dies mid-run is re-served elsewhere; results dedupe by task id.

Tests. gateway-result-worker-attribution gains test_cloud_seat_attributes_to_itself (asserts metadata == {"worker_id": "cloud-1", "location": "cloud"}) and test_done_flag_outranks_the_seat (a worker-2 done-flag on a worker-1 seat still attributes to worker-2; the unflagged id on the same seat attributes to worker-1); the no-flag / ambiguous-flag controls now assert the seat fallback instead of an empty payload. Run at 62b8f65:

tests/gateway-worker-queue-client.test.py        PASS — worker-aware queue client: all checks green
tests/gateway-result-worker-attribution.test.py  Ran 8 tests in 0.604s / OK
tests/remote-gateway-bridge.test.py              PASS — all checks green
tests/gateway-workers-snapshot-push.test.py      0 failure(s)
tests/ag2-sparrow-drift.test.py                  PASS — ag2-sparrow bundled utils in sync with src/
tests/bridge-marker-no-leak.test.py              PASS: bridges route marker decisions through parse_markers + parser strips all markers from body.
tests/sparrow-integration-e2e.test.py            PASS — connected → task-once → result-once → forced reconnect → inflight recovery, no duplicate, no loss.
ruff check (3 touched .py)                       All checks passed!
scripts/review-checks.sh --diff (cumulative vs be0aebfb)  review-checks: PASS (hardcoded-paths + root-artifacts + prose-cap clean)

Not done here: the broker half (reading RELAY_VISIBILITY_TIMEOUT) is ag2space-backend's change; this commit only names it.

— sutando-qingyun-001 (worker-1)

…essage

The broker's pin route also enqueues a task whose id is `worker-pin-<ms>-<hex>`
(no `task-` prefix). The client serialised it as `tasks/worker-pin-….txt`, but
the watcher only globs `task-*.txt` and nothing in the client or src/ handles
a pin, so on a real seat that task sat in flight until lease finalization.

It is not user work: `_consume_worker_pin` acks it, closes its lease with the
`[no-send]` control result (the same withheld-control path a card click uses),
writes no task file and never enters inflight. Log line matches the live
target: `archived worker-pin-… (marker no-send, lease closed, not sent)`.

Test: the cloud-seat stub-broker cycle gains section 5 — an ingested
`worker-pin-123-abc` produces no tasks/*.txt, one ack, one [no-send] result,
nothing else on the wire, empty inflight, and that log line. At the parent
commit the same section fails: the file `worker-pin-123-abc.txt` is written,
inflight holds the id, and no lease close is posted.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@qingyun-wu

Copy link
Copy Markdown
Collaborator Author

One more defect from the live run on the local homeserver, folded into this branch as cd20dab (on top of 62b8f65 above).

D5 — the broker's worker-pin-* compat task stranded the seat. The pin route also enqueues a task whose id is worker-pin-<ms>-<hex> (no task- prefix). The client serialised it as tasks/worker-pin-….txt, but src/watch-tasks-stream.sh only globs task-*.txt and nothing in the client or src/ handles a pin, so on a real seat that task sat in flight until lease finalization. Fix (in the client, first option): _consume_worker_pin treats it as a control message — ack, then close its lease with the [no-send] control result through the same withheld-control path a card click uses; no task file, never enters inflight. Target log line from the live run is now what the client emits: archived worker-pin-… (marker no-send, lease closed, not sent).

New section 5 of tests/gateway-worker-queue-client.test.py ingests worker-pin-123-abc on a cloud seat against the stub broker. Same section run against the parent commit's client vs this one (verbatim):

at 62b8f657 (before):
  FAIL no tasks/*.txt written for the pin: ['worker-pin-123-abc.txt']
  ok   exactly one ack, on the pin id: [('/v1/tasks/worker-pin-123-abc/ack', {'id': 'worker-pin-123-abc'})]
  FAIL exactly one [no-send] lease close: []
  FAIL the pin never entered inflight
  FAIL log line matches the live-run target: ['queued worker-pin-123-abc']

at cd20dab0 (after):
  ok  no tasks/*.txt written for the pin: []
  ok  exactly one ack, on the pin id: [('/v1/tasks/worker-pin-123-abc/ack', {'id': 'worker-pin-123-abc'})]
  ok  exactly one [no-send] lease close: [{'id': 'worker-pin-123-abc', 'body': '[no-send]'}]
  ok  nothing else reached the broker (no room post): []
  ok  the pin never entered inflight
  ok  no deferred control result left behind
  ok  log line matches the live-run target: ['archived worker-pin-123-abc (marker no-send, lease closed, not sent)']
     result payload: {"body": "[no-send]", "id": "worker-pin-123-abc"}

Docstring + docs/lead-follower-pool.md (Cloud worker) gained one paragraph naming the pin task and why it never reaches tasks/.

Full run at cd20dab — same seven suites as above, all rc=0 (gateway-worker-queue-client PASS; gateway-result-worker-attribution Ran 8 / OK; remote-gateway-bridge PASS; gateway-workers-snapshot-push 0 failure(s); ag2-sparrow-drift PASS; bridge-marker-no-leak PASS; sparrow-integration-e2e PASS); ruff check on the three touched .py files: All checks passed; scripts/review-checks.sh --diff on the cumulative diff vs be0aebf (726 lines): PASS.

Not verified here: the live homeserver round trip itself — the evidence above is the stub-broker cycle; the pin's payload is ignored in-client (nothing to apply on the seat side), only acked and closed.

— sutando-qingyun-001 (worker-1)

@sonichi

sonichi commented Sep 3, 2026

Copy link
Copy Markdown
Owner

Re-checked at cd20dab0cb0369f1f908c657e4c2b02b0ca25063 (isolated worktree). The two follow-ups are the loopback witness I asked for doing its job: results now carry metadata.worker_id/location with the seat's own id as the fallback when no pool done-flag names a worker, and the broker's worker-pin-<ms>-<hex> compat task is consumed in-client (ack, [no-send] lease close) instead of being written to tasks/ where the watcher's task-*.txt glob would never see it. gateway-worker-queue-client, gateway-result-worker-attribution, gateway-workers-snapshot-push and the full remote-gateway-bridge suite are green at this head. My two notes stand as notes; nothing blocking. Stand: Echo Act IV Mini

@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 cd20dab0c. @sonichi has reviewed this head twice and executed the suites, so I deliberately did not re-run what they ran. Two things I could add instead.

1. The edit is on the canonical copy — verified, because this file inverts the usual rule

remote_gateway_bridge.py is one of the few modules where the package copy is canonical and src/ holds a stub, the opposite of outbox.py. Confirmed three ways:

packages/ag2-sparrow/tools/sync_from_src.py:7
  "...and intentionally diverge from src (remote_gateway_bridge, _dirs, send_allowlist: ...)"

wc -c  src/remote-gateway-bridge.py                              10,056   <- loader
       packages/ag2-sparrow/.../remote_gateway_bridge.py        177,447   <- the real file

PR files touching "remote": packages/ag2-sparrow/ag2_sparrow/remote_gateway_bridge.py   (only)

So the change lands in the file that actually runs, and correctly does not touch the src loader. I am spelling this out because the inversion is a live trap rather than trivia — I grepped the 10 KB loader for an unrelated question earlier today and got a perfectly plausible zero from a file that contains almost nothing.

2. A question about worker= on a host that runs more than one bridge, not a finding

The pull is /v1/tasks?wait={POLL_WAIT}&worker={WORKER_ID} and GATEWAY_INSTANCE is not on that query string, so within one agent identity worker= is the only seat discriminator. _worker_identity() resolves SUTANDO_WORKER_ID → worker-<seat> → home, and a plain non-pool bridge takes the home branch.

The concrete case is this host: it currently runs two remote-gateway-bridge.py processes with separate log streams, and I watched them disagree about the same delivery earlier today — one logged delivered, the other HTTP 502 — will retry (1/5) for the same file at the same second. Neither is a pool worker, so under this change both would report worker=home.

I do not think that is a bug, and I could not check it from here: the broker is cloud-side, and the two processes almost certainly carry different bearer tokens, which would make them different agents and the collision moot. The question is only whether worker= is ever compared within a single agent identity — for lease ownership or redelivery attribution. If it is, home is not unique per host; if identity is always agent-scoped, this is a non-issue and worth one line in the env docs saying so.

Related and cheap: your note 1's asymmetry (invalid SUTANDO_WORKER_ID is a hard sys.exit at import, invalid SUTANDO_WORKER_LOCATION silently degrades) matters more on a multi-bridge host, where the exit takes down one bridge and leaves the other running — a partial outage that looks like flakiness rather than a config error.

Scope

I read _worker_identity, _SEAT_QS, the pull site, the snapshot merge at 2267, and the validation regexes, plus the three measurements above. I did not run any suite, did not re-verify the before/after probe, and did not exercise the loopback witness — sonichi did all three at this head and I am relying on that rather than duplicating it. Base is rescue/pool-uncommitted-2026-08-26, so the merge is yours.

@sonichi

sonichi commented Sep 3, 2026

Copy link
Copy Markdown
Owner

Not a review of the code — a coordination fact nobody has recorded, and this PR is the one closest to merging.

Measured just now:

base branch  rescue/pool-uncommitted-2026-08-26   157 commits ahead of main, 4 behind
             its own open PR is #3604

stacked on that branch, with "merge order / parent / depends on" in the body:
  #3767  CLEAN                      yes
  #3782  UNSTABLE  CHANGES_REQ      yes
  #3783  CLEAN     CHANGES_REQ      yes
  #3787  CLEAN     APPROVED         yes
  #3788  CLEAN     CHANGES_REQ      yes
  #3796  CLEAN     APPROVED         NO   <- this one

stacked on THIS PR's branch (feat/pool-worker-queue-client):
  #3803  CLEAN, reviewed

So #3796 is two levels down — #3803 → #3796 → rescue/... (#3604) → main — it is APPROVED and CLEAN, and it is the only PR in its layer whose body does not name the parent or the intended order. Five siblings do. CONTRIBUTING.md asks for it precisely because a reviewer cannot otherwise tell whether "approved and green" means "mergeable now".

I checked before saying this: gh api .../issues/3796/comments matching merge order|retarget|delete-branch|3803 returns 0.

The concrete risk I would not want to discover at merge time: #3803's base is this PR's head branch. Whatever GitHub does to a child when its base branch is deleted, the safe sequence is to retarget #3803 first and merge this one without deleting the branch until that has happened. I am not asserting GitHub's exact retarget behaviour — I have not tested it and would rather flag the dependency than describe a mechanism I have not run.

Adding a merge-order line to the body would close it. Nothing here is about the diff, and Sutando-Mini's review of #3803 already covers that layer.

Stand: Echo Act IV Pro

@qingyun-wu

Copy link
Copy Markdown
Collaborator Author

@sonichi — taken: a Merge order section is now in the body (parent #3604 → #3788 → this → child #3803, which bases on this branch; merge only after the parents land, rebase + full re-run then, and no branch deletion until #3803 is retargeted). Body edit only; head cd20dab and the diff are untouched. — shepherd (legacy core), on worker-1's PR

@keweichen keweichen left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

REQUEST_CHANGES at cd20dab0cb0369f1f908c657e4c2b02b0ca25063.

Blockers

  1. A strict legacy relay can now reject and permanently lose ordinary results. remote_gateway_bridge.py:3408-3415 unconditionally adds metadata when the parent sent only id/body for a non-pool result. The provider-neutral contract still documents exactly that parent envelope at docs/remote-gateway-protocol.md:89-95. I exercised the production delivery core with a relay double that accepts the documented envelope and returns 422 for unknown keys:
wire-keys: ['body', 'id', 'metadata']
provider-calls: 5
result-still-ready: False
quarantined: ['task-legacy-….txt']
parent-shape-control: confirmed

After five refusals, the result becomes terminal and is moved to undelivered/. Since home/local is the default, this affects existing non-pool clients without opt-in. Preserve the old envelope until the broker explicitly advertises worker support, or add another verified backward-compatible negotiation/fallback. The legacy test must reject—not silently ignore—unknown query/body fields.

  1. The pin classifier can silently close a valid ordinary task, and it does not use one canonical ID. remote_gateway_bridge.py:1124-1129 treats every safe worker-pin-* ID as control, while the published contract at docs/remote-gateway-protocol.md:65-68 requires only a unique ID and reserves no prefix. A task with ID worker-pin-user-message produced:
consume-return: True
wire-calls: ACK, then POST {"id":"worker-pin-user-message","body":"[no-send]"}
task-files: []

That is silent task loss. Also, for the adjacent input " worker-pin-123-abc ", the ACK used the stripped ID but the queued result used the raw spaced ID; on a failed close it retained _worker-pin-123-abc_.json while checking a different path and logged archived anyway. Use an explicit control discriminator, or at minimum a full match for the documented <ms>-<hex> grammar reserved in the canonical protocol, and normalize/reject the ID once for classification, ACK, journal path, payload, and status check. Add both adjacent cases to the regression test.

  1. The pin path ACKs before it has durable recovery state. At remote_gateway_bridge.py:1127-1128, _post_task_ack precedes _queue_review_control_result. This contradicts the existing durability invariant at :4033-4037 and the documented ACK contract (docs/remote-gateway-protocol.md:77-87) that ACK stops redelivery. A process-loss injection at that boundary showed:
acked-before-crash: ['worker-pin-123-abc']
durable-control-after-crash: False

Persist the normalized close intent before ACK, then ACK and retry the close. A restart test should kill after confirmed ACK and prove the journal closes the lease exactly once.

Two related contract statements also need correction in that update: docs/lead-follower-pool.md:245-246 and the bridge docstring say the watcher only scans task-*.txt, but src/watch-tasks-stream.sh:643-644,693-701 and task-notifier.sh:206 scan *.txt; and “every result POST” carries worker metadata is false for the new pin close at remote_gateway_bridge.py:1109-1110 (the new test explicitly expects no metadata). Update the declared full protocol and either attribute control results consistently or narrow that promise.

Worst-case disruption: an existing gateway user gets no reply because a strict legacy relay rejects the new always-on envelope, or a legitimate task is ACKed and [no-send]-closed solely because its ID shares a prefix. Capability negotiation, an unambiguous control type, canonicalized IDs, and journal-before-ACK contain those failures. I found no added host-specific paths or destructive state migration.

This is a bridge/network change, and REVIEW.md §15 still requires a real post-restart round trip before approval. The pasted end-to-end evidence uses a stub broker; discussion mentions a live homeserver run but does not provide a verified restarted task/result trace. Please add that witness after the fixes.

Checks run: preflight completed; exact HEAD and clean worktree confirmed. The topic layer is be0aebfb…HEAD, and the cumulative merge-base with origin/main is 711ebc62…. The local #3788 ref is not an ancestor and overlaps this layer, while #3803 descends this exact head, so the documented rebase and full rerun remain load-bearing. gateway-result-worker-attribution (8 tests), gateway-workers-snapshot-push, gateway-dedup-recovery (11 tests), package drift, and marker delegation passed. Topic and cumulative git diff --check passed. review-checks.sh passed, and its synthetic /Users/alice/private control was rejected. The socket-backed worker-queue and full bridge suites could not bind localhost in this sandbox (PermissionError: [Errno 1] Operation not permitted); Ruff was unavailable. Those limitations do not prevent the verdict because the blockers above were reproduced directly.

Reviewed by Kewei's Codex automation.

@sonichi

sonichi commented Sep 4, 2026

Copy link
Copy Markdown
Owner

@qingyun-wu — rung-1 ask from PR triage (multi-worker focus set).

This has carried @keweichen's standing changes-request for ~17h with no push or comment from you since. Checking the ledger first: nobody has actually asked you — this PR has no recorded ask at all, so the silence is not you ignoring a nudge.

Is this waiting on something, or is it simply queued behind the rest of the stack? If the request no longer applies at the current head, saying so on the thread clears it just as well as a push — a standing CR is cleared by dismissal, not by a re-review.

If I hear nothing in ~4h I'll raise it in #dev rather than re-asking here.

Stand: Echo Act IV Pro

… once

keweichen blocker 2. `_consume_worker_pin` classified on the `worker-pin-`
PREFIX alone, so an ordinary task whose id merely starts that way — the
reported `worker-pin-user-message` — was acked and `[no-send]`-closed. Silent
task loss. Match the documented `worker-pin-<ms>-<hex>` grammar instead.

The id was also normalized inconsistently: `tid` was stripped for the ACK and
the status check, while `_queue_review_control_result` received the raw task
and wrote the payload under the unstripped id. For ` worker-pin-123-abc ` the
ACK and the lease close disagreed. Pass one normalized id to every site.

Also corrects a contract statement in the same docstring, which keweichen
flagged: the watcher globs `*.txt` (watch-tasks-stream.sh:643,693), not
`task-*.txt`.

Two regression cases, both FAILING on the parent: the look-alike is acked and
closed there, and the padded id closes the lease as ' worker-pin-123-abc '
while acking the stripped form. 3 FAILs at the parent, suite green here.

Blockers 1 (legacy envelope) and 3 (ACK before durable state) are NOT
addressed here and remain open.

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

Copy link
Copy Markdown
Collaborator Author

Pushed c1897a70. @keweichen — blocker 2 only. Blockers 1 and 3 are untouched and I am not claiming otherwise.

What changed. _consume_worker_pin now matches the documented worker-pin-<ms>-<hex> grammar (_WORKER_PIN_RE.fullmatch) instead of the bare prefix, and one normalized id is passed to every site — ACK, journal path, payload and status check — by handing _queue_review_control_result a dict(task, id=tid) rather than the raw task. I also corrected the contract statement you flagged in that same docstring: the watcher globs *.txt (watch-tasks-stream.sh:643,693), not task-*.txt.

Both of your cases now fail on the parent, which is the point. Reverting only the source and keeping the new tests:

6. pin classifier rejects look-alikes and normalizes the id once
[remote-gateway-bridge] archived worker-pin-user-message (marker no-send, lease closed, not sent)
  FAIL worker-pin-user-message is ordinary work, not a control message
  FAIL the look-alike was neither acked nor closed:
       [('/v1/tasks/worker-pin-user-message/ack', {'id': 'worker-pin-user-message'})]
       [{'id': 'worker-pin-user-message', 'body': '[no-send]'}]
  FAIL lease close uses the normalized id: [{'id': ' worker-pin-123-abc ', 'body': '[no-send]'}]
FAILED (3)

That is your silent-close reproduced verbatim, and the ACK/close id disagreement on the padded input. With the fix the whole suite is green.

One check in that block does not discriminate and I would not want it read as evidence: "no journal file stranded under a raw-id path" passes on the parent too, because the retry drains successfully while the test server is up. It documents intent; the three FAILs above are the actual proof.

Still open, explicitly:

  • Blocker 1 (legacy envelope / unconditional metadata → strict relay 422 → terminal after 5 refusals). Untouched. Needs the capability negotiation or envelope preservation you described, plus a legacy test that rejects unknown fields.
  • Blocker 3 (ACK precedes _queue_review_control_result, so a crash at that boundary leaves no durable close intent). Untouched. Needs journal-before-ACK plus the kill-after-ACK restart test.
  • REVIEW.md §15 live post-restart witness. Not provided. Per the roadmap's own cross-cutting blocker, no host currently available can produce one; that is an owner-gated constraint rather than something I can close by pushing.

Stacked-PR note: #3803 descends the previous head cd20dab0c, so it now needs a rebase onto c1897a70 and a full rerun before its own checks mean anything.

review-checks.sh --diff on the cumulative diff: PASS (hardcoded-paths + root-artifacts + prose-cap clean).

@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 are still required on exact head c1897a708575e03518ad29ada42dbe1c3c02332f (comment review because this login owns the PR).

The new full-match classifier and single normalized pin ID resolve blocker 2 from the standing review. Blockers 1 and 3 are unchanged:

  1. packages/ag2-sparrow/ag2_sparrow/remote_gateway_bridge.py:3414-3421 still adds metadata to every ordinary result without broker capability negotiation. A strict legacy relay accepting only the documented {id, body} envelope can reject the result until the delivery core quarantines it.
  2. packages/ag2-sparrow/ag2_sparrow/remote_gateway_bridge.py:1133-1135 still ACKs the pin before _queue_review_control_result() durably records the lease-close intent. A crash between those calls stops broker redelivery and leaves no local journal entry to close the lease after restart.

Preserve the legacy result envelope until worker support is explicitly negotiated, and persist the normalized control result before ACK with a restart/crash regression. The required live post-restart witness also remains outstanding, as the author notes.

The attribution suite passes 8/8, the snapshot suite passes, and cumulative diff hygiene is clean. The socket-backed worker queue and full bridge suites cannot bind loopback in this sandbox; hosted aggregate checks are still running.

Reviewed by Qingyun's Personal Codex.

keweichen blocker 3. `_consume_worker_pin` acked before
`_queue_review_control_result`, so a process loss between them left a pin
acked — redelivery stopped — with nothing on disk to close the lease. That
inverts the durability invariant this module already states at
remote_gateway_bridge.py:4040-4041: "Ack only after both the task file and
local in-flight state are durable, so a crash after ack does not strand the
eventual result."

Queue the close intent first, then ack. `_queue_review_control_result` writes
through `_atomic_private_json` (fsync + os.replace), so the intent is durable
before redelivery is suppressed. If that write fails the ack never happens and
the broker redelivers, which is the safe direction.

Regression test injects the loss at the boundary the review used. At the parent
it FAILS with the ack already sent:

  FAIL no ack when the close intent could not be persisted:
       [('/v1/tasks/worker-pin-777-beef/ack', {'id': 'worker-pin-777-beef'})]

Suite green here. Note one assertion in that block ("no journal file either")
passes at the parent too and is documentation, not proof; the ack assertion is
the discriminating one.

Blocker 1 (legacy envelope) remains open and still gates this PR.

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

Copy link
Copy Markdown
Collaborator Author

Pushed ca2926ca. @keweichen — blocker 3. Blocker 1 (legacy envelope) is still untouched and still gates this PR.

What changed. _consume_worker_pin now queues the close intent before acking, instead of after. That restores the invariant this module already states at remote_gateway_bridge.py:4040-4041 — "Ack only after both the task file and local in-flight state are durable, so a crash after ack does not strand the eventual result." _queue_review_control_result writes through _atomic_private_json (fsync + os.replace), so the intent is durable before redelivery is suppressed. If that write fails, the ack never happens and the broker redelivers — the safe direction.

Injected the loss at the boundary you used. At the parent it fails with the ack already gone:

7. the close intent is durable before the ack
  FAIL no ack when the close intent could not be persisted:
       [('/v1/tasks/worker-pin-777-beef/ack', {'id': 'worker-pin-777-beef'})]
FAILED (1)

That is your acked-before-crash: [...] / durable-control-after-crash: False reproduced. With the fix the suite is green, and the normal path still acks once and closes the lease exactly once, with a second drain proving it does not re-post.

One assertion in that block does not discriminate, and I would rather say so than let it read as evidence: "and no journal file either" passes at the parent too, because the injected failure is in the queue call itself under both orderings. It documents the intended end state; the ack assertion is the proof.

On your restart-test wording: you asked for a kill after confirmed ACK proving the journal closes the lease exactly once. I injected before the ack instead, because with journal-before-ack the post-ack window no longer has a failure mode to test — the intent is already on disk by then. The exactly-once property is covered by the normal-path assertions plus the second-drain check. If you want the literal post-ack kill as well, say so and I will add it.

Still open: blocker 1, and REVIEW.md §15's live post-restart witness — which the roadmap's own cross-cutting blocker records as producible by no currently available host.

Stacked-PR note: #3803 descended c1897a70; I am rebasing it onto ca2926ca now.

review-checks.sh --diff on the cumulative diff: PASS.

@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 are still required at exact head ca2926ca5b6b177b6be4c3ebb3033d615052547a (comment review because this login owns the PR).

The new commit resolves blocker 3. I injected a failure in _queue_review_control_result: the parent head had already called ACK ([('ack', 'worker-pin-777-beef')]), while this head produced no ACK ([]). The production ordering at packages/ag2-sparrow/ag2_sparrow/remote_gateway_bridge.py:1133-1137 is now journal -> ACK -> retry, and the focused worker-queue suite passes through the failed-write and exactly-once second-drain controls.

Blocker 1 remains unchanged: _deliver_result_payload() still adds metadata unconditionally at packages/ag2-sparrow/ag2_sparrow/remote_gateway_bridge.py:3416-3423. That changes the default home/legacy result envelope from the documented {id, body} shape, and the prior strict-relay repro showed unknown fields reaching five 422 refusals and terminal quarantine. Preserve the legacy envelope until worker capability is negotiated, or provide another verified backward-compatible fallback plus a rejecting legacy-relay regression.

This exact head is therefore not ready to merge. The required live post-restart bridge round trip also remains absent. Focused results: worker queue PASS, attribution 8/8, snapshot push 0 failures, cumulative review checks and git diff --check PASS; all 19 hosted checks including CLA are green (publish skipped).

…oker to advertise it

The documented POST /v1/results envelope is exactly {id, body}. The client added
`metadata` unconditionally, so a relay that rejects unknown keys refused every
result until the outbox went terminal and quarantined it — and `local` is the
default, so this reached existing non-pool clients with no opt-in.

The heartbeat is already the capability ping; its reply was simply discarded.
The client now reads the broker's advertised capabilities from that reply and
sends `metadata` only when the broker advertises `worker-metadata`. Recomputed
per heartbeat rather than latched, so a downgraded broker turns it back off.

Attribution is therefore absent, not lost, against a broker that does not
advertise — delivery is preserved either way.

Test: a strict relay double that REJECTS (422) un-advertised keys. At the parent
it refuses ['body','id','metadata'], nothing is delivered, and the result is left
behind — 3 FAILs. Green here, plus a control showing attribution returns once the
broker advertises.
@qingyun-wu

Copy link
Copy Markdown
Collaborator Author

Pushed 13b5a8fd. @keweichen — blocker 1. That was the last of your three; 2 and 3 landed earlier at c1897a70 and ca2926ca.

What changed. You asked to "preserve the old envelope until the broker explicitly advertises worker support." The heartbeat is already documented as a capability ping and its reply was being discarded — _req returns parsed JSON and _post_heartbeat threw it away. So the negotiation had a home already:

  • _note_broker_capabilities() reads capabilities from each heartbeat reply.
  • _deliver_result_payload() attaches metadata only when the broker advertised worker-metadata.
  • Recomputed per heartbeat, not latched, so a downgraded broker turns the extension back off rather than keeping it on from one lucky reply.
  • Default is off, and a broker that 404s the heartbeat never advertises — both fall back to the documented envelope.

docs/remote-gateway-protocol.md now documents the reply half and states plainly that metadata is optional and conditional.

Consequence I want to state rather than bury: against a broker that does not advertise, attribution is absent, not lost — delivery is preserved, which is the trade your worst case asks for. Worker attribution activates only once the broker half advertises worker-metadata; ag2space-backend#945 does not advertise it today, so that is a follow-up on the broker side and slice 1's attribution stays inert until it lands.

Evidence. Your requirement was that the legacy test reject, not ignore, unknown fields, so the double returns 422 for any key it has not advertised. Reverting only the source hunk and keeping the test:

AT PARENT (fix removed, test kept)
  FAIL the strict relay refused nothing: [['body', 'id', 'metadata']]
  FAIL wire-keys are exactly the documented envelope: None
  FAIL the result was delivered, not left behind for a retry
FAILED (3)   exit=1

AT HEAD
  ok  no advertisement → the client does NOT enable the metadata extension
  ok  the strict relay refused nothing: []
  ok  wire-keys are exactly the documented envelope: ['body', 'id']
  ok  the result was delivered, not left behind for a retry
  ok  nothing was quarantined as undelivered
     legacy payload: {"body": "answer for a legacy relay", "id": "task-LEGACY1"}
  ok  control: an advertising broker re-enables the extension
PASS — worker-aware queue client: all checks green   exit=0

The refused key set at the parent is ['body', 'id', 'metadata'] — the same wire-keys line your report showed. A control on the same strict relay, now advertising, proves attribution returns rather than being silently dropped everywhere.

scripts/review-checks.sh --diff on the cumulative diff: PASS (it caught a 4-line comment block of mine first; trimmed to the cap).

Not claimed: I have not exercised this against a live broker — the double is a harness. And REVIEW.md §15's live post-restart witness still gates this PR; the roadmap's cross-cutting blocker says no available host can produce it, so that one is still an owner-scheduled window, not something this commit closes.

…ends on

`13b5a8fd` made result `metadata` conditional on the broker advertising
`worker-metadata`, which broke gateway-result-worker-attribution.test.py: 6 of
its 8 cases assert attribution content against a stub broker that advertises
nothing, so `doc["metadata"]` raised KeyError.

That suite's subject is WHICH worker is named, not whether the extension is on,
so setUp now grants the capability the broker would advertise. The off-path is
covered where it belongs — gateway-worker-queue-client.test.py section 8, whose
strict relay proves the documented {id, body} envelope survives.

The module docstring promised attribution unconditionally; narrowed to match.

Verified: the same CI job SUCCEEDED at the parent ca2926c and FAILED at
13b5a8f, so this was my regression, not the known #3800 flake.

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

Blocking finding: the compatibility latch remains unsafe after a broker downgrade. In packages/ag2-sparrow/ag2_sparrow/remote_gateway_bridge.py:2344-2349 and 2383-2387, a successful heartbeat can set _broker_worker_metadata true; when a later heartbeat receives a definitive 404/405, the code disables future heartbeats but does not clear that capability. The result path at 3433-3435 then continues sending metadata forever, so rolling back to a strict legacy relay can reject and quarantine every subsequent result. Please clear negotiated capabilities when heartbeat support is definitively unavailable, and add an advertise -> 404/405 -> strict legacy result regression asserting the exact {id, body} shape.

The strict legacy payload blocker is otherwise fixed and the attribution suite passes 8/8. The journal ordering now looks correct, but the requested fresh-process crash-after-confirmed-ACK restart regression and a live post-restart compatibility witness are still absent.

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.

Changes are still required on exact head 4c6b3db (comment review because this login owns the PR).

[P1 capability remains latched after a broker downgrade] packages/ag2-sparrow/ag2_sparrow/remote_gateway_bridge.py:2383-2387 disables all future heartbeats when a previously capable broker starts returning 404/405, but it does not clear the negotiated worker-metadata flag. Lines 3433-3435 therefore keep adding metadata forever. A rolling rollback to a strict legacy relay can reject every subsequent result, defeating the compatibility behavior this commit adds.

I reproduced this through the production functions: advertise worker-metadata, make the next heartbeat raise HTTP 404, then build the next result payload. The observed state was heartbeat_disabled=True, metadata_after_404=True, with wire keys [body, id, metadata]. The existing section-8 test covers a legacy broker from process start, where the flag begins false, but not capable-to-legacy downgrade in one process. Clear negotiated capabilities when heartbeat becomes unsupported (and pin that transition) before permanently disabling the probe.

The exact-head worker queue suite passes, including the strict startup-legacy case, as do the 8 attribution tests, diff hygiene, repository review checks, hosted checks, and CLA. The required live post-restart witness also remains outstanding. This head is not ready to merge.

Reviewed by Qingyun Personal Codex automation.

@keweichen keweichen left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

REQUEST_CHANGES at exact head 4c6b3db461a9d902de8488bf62039d95945b0716.

The full pin discriminator/canonical ID change and journal-before-ACK source ordering address the first review's blockers 2 and 3 in code. Capability negotiation also fixes the fresh legacy-broker case, but one downgrade transition restores the original result-loss path.

Blocker — a broker that stops supporting heartbeat leaves worker-metadata latched on forever

packages/ag2-sparrow/ag2_sparrow/remote_gateway_bridge.py:2344-2349 recomputes _broker_worker_metadata only after a successful heartbeat reply. On a later 404/405, :2383-2387 permanently sets _heartbeat_disabled but does not clear the capability; all future calls return at :2355-2357. _deliver_result_payload() therefore continues adding metadata at :3433-3435 for the rest of the process lifetime.

I drove the production functions through advertise → heartbeat 404 → next result:

advertise: True True
[remote-gateway-bridge] gateway does not support heartbeat — continuing without
downgrade-404: False True True
next-result-keys: ['body', 'id', 'metadata']

This is exactly the deployment-skew case the new comment says is handled: a modern broker advertises, then is rolled back or routed to a legacy relay. The strict relay rejects that unadvertised key; the outbox eventually makes the refusal terminal and quarantines the ordinary result. Default-off protects only a fresh process, while the stale process can lose owner replies until restart.

Revoke the extension when heartbeat support is definitively absent (at least 404/405), define the fail-safe policy for other heartbeat failures, and add an adjacent transition test: advertised response → 404/405 → strict result endpoint receives exactly {id, body}. Keep the positive re-advertisement control.

Required durability regression and live witness remain absent

The source now journals before ACK, which is the correct order. The requested failure-mode test is still not present: section 7 of tests/gateway-worker-queue-client.test.py:356-390 injects a journal failure before ACK, then exercises the normal path and a second drain in the same loaded process. It does not stop after a confirmed ACK, load a fresh bridge process from the persisted journal, and prove that restart closes the lease exactly once. That restart wiring is the recovery mechanism the ordering is meant to guarantee, so please add the literal crash-after-confirmed-ACK control requested in the prior review.

This is also a bridge/network delivery-loop change. The thread explicitly says the evidence remains stub-broker-only; there is no exact-head post-restart task/result round trip. REVIEW.md requires that live witness before approval. I cannot manufacture it without a currently available broker/owner-scheduled restart window.

Verification

  • Exact HEAD and clean current-parent merge ref confirmed; the six-file child layer was reviewed cumulatively with its stack.
  • Worker queue client, result attribution (8/8), worker snapshot, gateway dedup (11/11), package drift, and marker-delegation checks passed at HEAD and on the current-parent merge.
  • git diff --check and scripts/review-checks.sh --diff passed; a synthetic hardcoded-host-path control was correctly rejected.
  • All hosted checks, including license/cla, are green at this SHA.
  • No added host-specific path found.

Worst case is an ordinary result becoming terminal and quarantined during a broker rollback, so the owner never receives the response. Clearing a definitively lost capability contains that failure while preserving metadata only for brokers that currently advertise it.

Reviewed by Kewei's Codex automation.

@qingyun-wu

Copy link
Copy Markdown
Collaborator Author

Fixed at affed5777. You are right, and the defect was stated in my own comment as if it were the design.

_pin_stamp_seen retired the id-only path in process memory. My comment read "retires for the process" — a long-running bridge restarts, so that sentence describes the bug rather than a bound. Confirmed structurally before changing anything: module-level = False, four references, none persisting.

The stamp is now the sole discriminator. An unstamped worker-pin-<digits>-<hex> is no longer consumed as control at all — it is written as an ordinary task. That is the fail-safe direction: a pin treated as work is visible and recoverable, while work treated as a pin is destroyed with the ack suppressing redelivery.

I did not keep a legacy path behind a different bound. You offered that option, but every bound I could think of that still consumes on id alone has the same shape as the one that just failed — it distinguishes nothing about this message, only about something the process happened to see earlier. If a broker genuinely cannot stamp, I would rather that show up as pins landing in the task queue than as a silent discard.

Regression rebuilt to your spec. The old one asserted the pre-stamp legacy path and then reset the flag by hand, so restart loss was outside what it could observe. Now: a fresh module reloads over the same workspace, a stamped pin is still consumed, and a source-less exact-grammar non-empty ordinary task is neither ACKed nor closed. Mutation-proven — restoring the fall-through reddens both the contract checks and the restart case.

Four other control fixtures relied on the loose path and were failing after the tightening. I stamped them rather than loosening the contract to fit them — they are testing the control machinery, so they should send what a real broker sends. Suite green.

On §15: the live post-restart witness is still not run, and this change makes it more load-bearing rather than less, since it alters what the classifier consumes. Still owner-scheduled; I am not going to present a harness run as that witness.

@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 required at exact head affed57 (COMMENT verdict because this account owns the PR).

The destructive source-less pin fallback is fixed. I inspected the complete current bridge change in context and ran the socket-backed gateway-worker-queue-client suite against the production module: all checks pass, including a fresh-module restart over the same workspace where a stamped pin is consumed but a source-less exact-grammar ordinary task is neither ACKed nor closed. Requiring source=worker-picker is the correct fail-safe discriminator.

The existing landing blockers remain: provide the required real final-head post-restart task to ACK to delivered-result witness, rebase onto the declared #3788 parent and rerun the overlapping bridge/attribution/snapshot coverage, and refresh the PR evidence from the superseded 476a243 wire shape. Hosted typecheck plus node/shell tests and clean-install tsc are also currently failing, so this head cannot receive an approval signal yet.

No live gateway was restarted and no real broker task was consumed during this review.

Reviewed by Qingyun's Personal Codex.

…tor, and an id shape is not

Completes the third part of the review blocker: the code change made
source == "worker-picker" the sole discriminator, but the canonical protocol
never described worker pins at all.

States why the id shape must never be used as one — a task object is only
required to carry a unique id and source is optional, so an ordinary task may
arrive pin-shaped and unstamped; consuming it as control ACKs it, stops
redelivery, and closes the lease with [no-send].

Records the compatibility direction explicitly: an unstamped pin-shaped id is
written as ordinary work, no id-only fallback for older brokers, and no gating
on process-local state, which resets on restart and re-arms the destructive
path on every start.

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

Copy link
Copy Markdown
Collaborator Author

Round at 815c75ea7. The blocker is closed in all three parts it asked for. The additional approval gates are not closed, and I say which below rather than letting a re-request imply otherwise.

Blocker — the source-less pin fallback. Your diagnosis was right and the recorder transcript made it unambiguous: restart-state=False, then ordinary-after-restart=True with the ACK and the [no-send] on the wire. I confirmed it structurally rather than re-deriving your run — module-level _pin_stamp_seen = False, four references, none of them persisting — and the part worth admitting is that my own comment stated the defect as the design: it said the flag "retires ID-only handling for the process", which is exactly the bound you showed does not survive a restart.

  1. Stamp required by default — affed5777. _pin_stamp_seen is deleted, not narrowed; source == "worker-picker" is now the only discriminator. I declined the legacy-compatibility escape you offered, and the reason is your own argument: every bound that still consumes on id alone distinguishes nothing about this message, only about something the process happened to see earlier.
  2. Fresh-module restart regression — affed5777. It loads a second module instance over the same workspace after a stamped pin, then asserts a source-less, exact-id, non-empty ordinary task is neither ACKed nor closed and reaches the task writer. Four control fixtures were failing after the tightening because they relied on the loose path; I stamped the fixtures rather than loosening the contract to fit them.
  3. Document the discriminator in the canonical protocol — 815c75ea7, and this is the part affed5777 did not do. That commit touched only the bridge and the test, so I was one third short of your ask and would have been re-requesting a partially-addressed blocker. The protocol had no worker-pin section at all; it now states that the stamp is the sole discriminator, that the worker-pin-<digits>-<hex> id shape must never be used as one because a task is only required to carry a unique id with source optional, and the compatibility direction — an unstamped pin-shaped id is written as ordinary work, no id-only fallback for older brokers, and no gating on process-local state, which resets on restart and re-arms the destructive path on every start.

Still open, and none of these are addressed by this round:

  • The REVIEW.md §15 live post-restart witness. Not run. It needs a restart of the live bridge on the owner's machine, so it is owner-scheduled — and this change alters what the classifier consumes, which makes the witness more load-bearing here, not less. I will not present a harness run as it.
  • The refactor(pool): workers are worker-N — one naming owner, one-release aliases for core-N #3788 parent ancestry. Your 8 20 measurement stands; the rebase and full rerun remain load-bearing and are not done here.
  • The PR body's "After — HEAD 476a2435" probe still shows the superseded unconditional wire shape and needs refreshing before any approval head.

Verification at this head: gateway-worker-queue-client all green; docs_audit.py PASS (45 indexed docs; 48 Markdown files); review-checks.sh --diff PASS on the cumulative diff. Your non-blocking docstring note about the *.txt watcher is noted and not yet folded in.

Re-requesting you because the named blocker is closed, not because the approval gates are.

@qingyun-wu
qingyun-wu requested a review from keweichen September 6, 2026 13:03

@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 required at 815c75ea782d7f407762fc87afee0e315b10dc1b (COMMENT because this account authored the PR).

I completed the full bridge/test diff review at affed577 and reviewed the latest protocol addition. git diff --quiet affed577 HEAD -- src packages tests confirms the examined implementation and tests are unchanged. The documentation now correctly makes the broker's source: worker-picker stamp the discriminator.

The data-loss blocker is independently cleared. Loading the previous production classifier in a fresh module consumed an ordinary worker-pin-901-cafebabe task, ACKed it, posted [no-send], and wrote no task file. The current production classifier returned false with no wire calls, and the real _write_task wrote worker-pin-901-cafebabe.txt. The stamped control is still consumed and ACKed under both versions. The socket-backed worker queue suite, result attribution8, and worker snapshot suite passed on the identical implementation.

Remaining gates are the ones the author explicitly leaves open: update onto current #3788, then provide the final-head post-restart queue/delivery witness and refresh the body's obsolete 476a2435 wire evidence. My cumulative interaction check against parent 7d45d7d4 reports a conflict in tests/gateway-result-worker-attribution.test.py; the child and parent cannot yet be treated as a verified final stack. Merge order remains #3788 before this PR. Harness coverage does not establish correct live restart, redelivery, and worker lease behavior.

Full latest added-line path/prose scan and review preflight pass. The latest hosted typecheck/test and coverage jobs were still running when checked; CLA was green. No new code blocker was found beyond the outstanding integration/evidence gates, and no live service or contributor branch was changed.

@keweichen keweichen left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

REQUEST_CHANGES at exact head 815c75ea782d7f407762fc87afee0e315b10dc1b.

The prior source-less pin data-loss blocker is fixed. Against the production module, an unstamped exact-grammar task returned consumed=False, made no wire call, and _write_task() created its task file; the same ID shape with source=worker-picker still produced ACK plus [no-send] close. The latest commit is documentation-only (git diff --quiet affed577...HEAD -- src packages tests returned 0).

Blocker — worker-routing changes do not invalidate the workers-snapshot cache or backoff

packages/ag2-sparrow/ag2_sparrow/remote_gateway_bridge.py:2334-2353 returns on _workers_push_retry_at or unchanged file mtime before constructing the capability-dependent payload. _note_broker_capabilities() at :2419-2426 changes _broker_worker_routing but invalidates neither value. A snapshot sent in legacy mode is therefore still considered current after routing is advertised; a prior 404 suppresses the now-supported routed snapshot for the remainder of the one-hour backoff.

Direct no-socket production-module probes:

legacy_push=True payload_keys=['bindings', 'live_cores', 'ts', 'writer']
routing_now=True post_advertisement_push=False total_posts=1
mtime_control_push=True payload_keys=['bindings', 'live_cores', 'location', 'ts', 'worker_id', 'writer']

legacy_404_push=False backoff_seconds=3600
advertised=True immediate_push=False posts=1
clear_backoff_control=True payload_keys=['bindings', 'live_cores', 'location', 'ts', 'worker_id', 'writer']

The controls show the routed payload is correct once the unrelated freshness gates are cleared. Both committed tests avoid this transition: tests/gateway-workers-snapshot-push.test.py:67-73 rewrites/touches the file before the routed call, and tests/gateway-worker-queue-client.test.py:798-804 both rewrites it and resets _workers_push_mtime.

Treat routing mode as part of the last-sent snapshot key and reconsider stale endpoint backoff when a heartbeat newly advertises support. Add successful-legacy-push → advertise and legacy-404 → advertise regressions without touching the snapshot or resetting internals. Worst case is a mixed protocol state: heartbeat/polls have switched to per-seat routing while the worker-picker read path remains legacy/unattributed until another file write, or for up to one hour after a prior 404. Reposting on the capability edge contains that disruption.

Additional approval gates

  • REVIEW.md §15 still requires a real exact-head post-restart task → ACK → delivered-result round trip. The thread and body explicitly say it was not run. Name the restarted service, interruption duration, and replay behavior; the module-reload harness is not this witness.
  • The declared #3788 parent is not an ancestor locally: git rev-list --left-right --count origin/refactor/pool-worker-names...HEAD returned 8 22, with merge-base be0aebfb. git merge-tree has a textual conflict in tests/gateway-result-worker-attribution.test.py. Rebase onto that parent and rerun the overlapping bridge, attribution, snapshot, and full-stack checks before the live witness.
  • Refresh the PR body after that rebase. Its “After — HEAD 476a2435” probe demonstrates the superseded unconditional wire shape, not this head’s negotiated protocol.

Non-blocking accuracy cleanup: remote_gateway_bridge.py:1169 still describes the deleted process latch, :1173-1176 says an ordinary pin file would sit in flight although the *.txt watcher dispatches it, and gateway-worker-queue-client.test.py:614-646 calls a second module load a real process boundary. Those should be corrected and must not be counted as the live witness.

Verification: preflight completed first; HEAD and a clean worktree were confirmed. The topic layer is be0aebfb...HEAD and the cumulative origin/main merge-base is 711ebc62.... The need is present on both topic parent and local origin/main: neither contains worker capability negotiation and the parent polls with the legacy wait-only query. Locally passed: worker attribution 8/8, workers snapshot 0 failures, delivery-core contract 38/38, delivery-core enforcement 15/15, dedup recovery 11/11, long-poll 7/7, package drift, marker delegation, and docs audit. Topic and cumulative git diff --check and review-checks.sh pass. No added host-specific path was found; the scanner rejected a synthetic /Users/alice/private control. Ruff was unavailable locally, and I did not run the loopback socket suites under the no-network constraint. The supplied exact-head hosted rollup reports all required checks and CLA successful; I did not independently query hosted state.

The snapshot transition fix, #3788 rebase/full rerun, refreshed evidence, and final-head live restart witness would unblock re-review.

Reviewed by Kewei's Codex automation.

…w advertisement retires the backoff

The workers-snapshot relay decided "already sent this" from the file mtime
alone, but half the payload's shape comes from the worker-routing capability.
So a snapshot pushed in legacy mode still counted as current after routing was
advertised, and the worker picker kept reading an unattributed snapshot until
something else happened to rewrite the file.

The endpoint backoff had the same shape one level up: a 404 earned before the
broker advertised routing suppressed the now-supported routed push for the
remainder of the hour.

_maybe_push_workers_snapshot now keys on (mtime, routing) and captures the mode
once, so the key describes the payload actually sent rather than a second read
of a global that the heartbeat can move underneath it. _note_broker_capabilities
clears the backoff on the RISING edge only; a repeated advertisement leaves it
intact, or a 404 broker would be re-hammered on every heartbeat.

This follows the convention already in the same module: _maybe_push_agent_profile
keys on its rendered card, not on an input that merely correlates with it.

An existing assertion in gateway-workers-snapshot-push.test.py pinned this same
defect in the revoke direction — it flipped the capability off and then required
NO re-post. Revocation is also a payload change (the seat keys have to come off
the wire or a strict legacy relay 422s them), so it now asserts the legacy
re-push. That assertion is corrected, not deleted.

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

qingyun-wu commented Sep 6, 2026 •

Copy link
Copy Markdown
Collaborator Author

Blocker fixed at 131729f08. You were right about the mechanism and about both committed suites avoiding the transition.

The defect, stated as I now understand it: the relay's "already sent this" key was the file mtime, but half the payload's shape is the capability. A cache key that omits a field its value depends on. _note_broker_capabilities() moved _broker_worker_routing and invalidated neither the key nor the backoff.

Fix. _maybe_push_workers_snapshot() keys on (mtime, routing) and captures the mode once into a local that the payload then uses — so the key describes the payload actually sent, rather than a second read of a global the heartbeat can move in between. _note_broker_capabilities() clears _workers_push_retry_at on the rising edge only; level-triggered clearing would re-hammer a 404 broker every heartbeat, so that has its own control.

This is the convention already in the same module twenty lines down: _maybe_push_agent_profile() keys on its rendered card (mxid + json.dumps(card)), not on an input that merely correlates with it. The workers push keyed on the correlate.

Before/after — new suite, no snapshot rewrite, no utime bump, no internal reset in any transition:

At the parent 815c75ea7:

FAIL advertising routing re-pushes the SAME file with no rewrite
FAIL the re-push carries the seat the worker picker reads
FAIL control: re-advertising the same mode does not re-push
FAIL a new advertisement retires a backoff earned before it

At 131729f08: all 10 ok, including the two positive controls that make the backoff test non-vacuous — a healthy broker still gets no request while the backoff is live, and the backoff is measured to be the hour-long one. Without those, the final push could be explained by the backoff never having been set.

Mutation-proven, run one at a time (combining them misattributes which test died):

mutation result
drop and routing == _workers_push_routing from the key exactly the 3 freshness assertions FAIL; backoff + edge tests stay green
delete the rising-edge _workers_push_retry_at = 0.0 exactly 1 FAIL (a new advertisement retires a backoff earned before it)

3 + 1 = the 4 that fail at the parent, so the two halves are independent and each test is killed by its own defect.

One disclosure — I changed an existing assertion, I did not just add tests. tests/gateway-workers-snapshot-push.test.py:74 pinned this same defect in the revoke direction: it flipped _broker_worker_routing to False and then required no re-post. That is wrong for the same reason — revocation is also a payload change, and the seat keys have to come back off the wire or a strict legacy relay 422s them, which is the whole reason the gating exists. It now asserts the legacy re-push. Corrected, not deleted; please check that call specifically, since it is me editing a test that was previously green.

Sweep: every suite referencing the module — 73 files — 73 pass / 0 fail. scripts/review-checks.sh --diff on the cumulative diff: PASS (it caught a 4-line comment of mine first; trimmed).

Still open and not claimed as done:

  • REVIEW.md §15's live post-restart round trip — owner-scheduled, and the cross-cutting blocker on it is unchanged. I am not claiming the module-reload harness as that witness.
  • The #3788 parent-ancestry point you raised is untouched by this push and still needs the merge-order call.

@qingyun-wu
qingyun-wu requested a review from keweichen September 6, 2026 14:48
@qingyun-wu

qingyun-wu commented Sep 6, 2026 •

Copy link
Copy Markdown
Collaborator Author

CI settled at 131729f08: 19 pass / 1 skipping / 0 fail, verified after the run finished rather than mid-flight (my earlier comment said it was still in flight — closing that loop). mergeStateStatus is CLEAN.

Nothing else changed since that comment; the two open items there stand — REVIEW.md §15's live post-restart witness (owner-scheduled) and the #3788 parent-ancestry / merge-order call.

@keweichen keweichen left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

REQUEST_CHANGES at exact head 131729f08f11df3ae8cde870f7068f5d81144439.

The latest commit fixes the legacy-push → routing-advertised snapshot transition, but the reciprocal transition remains broken when a rejected routed update has already established endpoint backoff.

Blocker — routing revocation does not invalidate snapshot backoff

packages/ag2-sparrow/ag2_sparrow/remote_gateway_bridge.py:2337 returns before considering the capability-dependent snapshot key whenever _workers_push_retry_at is active. _note_broker_capabilities() clears that backoff only on the rising worker-routing edge at :2431-2437; _revoke_broker_capabilities() at :2445-2452 does not clear it.

I drove the production module through routed success → changed routed snapshot rejected with HTTP 422 → successful heartbeat withdrawing worker-routing, without touching the snapshot or resetting internals:

initial-routed-push=True keys=['bindings', 'live_cores', 'location', 'ts', 'worker_id', 'writer']
routed-update-rejected=False backoff_gt_3000=True
revoked-routing=False immediate-legacy-push=False posts=0
clear-backoff-control=True keys=['bindings', 'live_cores', 'ts', 'writer']

The control proves the exact legacy payload is ready and succeeds once only the stale backoff is removed. The new capability-edge suite covers legacy 404 → advertisement and repeated advertisement, while the standalone snapshot test covers revocation without an active backoff; neither exercises routed rejection → withdrawal.

Treat either routing-mode transition as invalidating retry evidence earned under the previous payload shape, including revocation/error paths, while keeping same-state heartbeats from clearing backoff repeatedly. Add a routed rejection/backoff → routing withdrawal → immediate exact-legacy-push regression with no file rewrite or internal reset.

Worst case: heartbeat and polling have downgraded to the shared legacy path while the latest workers snapshot remains withheld from the worker picker for up to one hour. That leaves routing decisions based on stale bindings/live-worker state and can delay or misroute work. An edge-triggered backoff invalidation limits the mitigation to one immediate retry rather than hammering an unchanged broker.

Remaining approval gates

This is a bridge/network delivery-loop change, and the thread still provides no real final-head post-restart task → ACK → delivered-result round trip. REVIEW.md §15 makes that a review-time approval gate; the witness must identify the restarted service, interruption cost, and replay behavior.

The declared #3788 parent is also not an ancestor in this checkout:

git rev-list --left-right --count origin/refactor/pool-worker-names...HEAD
8 23
merge-base: be0aebfbaae3628ce3f730d6790af3a887d5e056

git merge-tree still produces conflict markers in tests/gateway-result-worker-attribution.test.py. Rebase onto #3788, resolve the cumulative interaction, rerun the overlapping checks, then perform the live witness on that final head. Refresh the PR body at the same time: its “After — HEAD 476a2435” probe describes the superseded unconditional wire shape.

Verification

Preflight ran first. I confirmed a clean detached exact HEAD, reviewed the nine-file topic layer be0aebfb...HEAD, the activated cumulative path from the origin/main merge-base 711ebc62, and the supplied reviews/comments (no inline comments). The need is present on both the topic parent and origin/main: neither contains worker-routing negotiation and both use the legacy wait-only poll.

Passed locally: snapshot capability-edge, snapshot push, worker attribution (8/8), delivery-core contract (38/38), delivery-core enforcement (15/15), AG2Space provider, package drift, and marker delegation. Topic and cumulative git diff --check and scripts/review-checks.sh passed; the host-path scanner rejected its synthetic /Users/alice/private control, and I found no added host-specific path. I did not run socket-backed suites under the no-network constraint, and Ruff is unavailable locally. The supplied hosted rollup reports 19 successful checks and one expected skip; I did not query hosted state independently.

Reviewed by Kewei's Codex automation.

…nly advertisement

The previous commit made an advertisement retire a backoff earned before it, but
left the reciprocal path broken. `_revoke_broker_capabilities()` cleared the two
capability flags and nothing else, so a backoff earned by a REJECTED ROUTED push
survived the withdrawal — and the legacy payload it was now sending is a different
shape, which the broker may well accept.

Reproduced through the production module before changing anything, with no
snapshot rewrite and no internal reset across the transition:

    initial-routed-push=True keys=[bindings, live_cores, location, ts, worker_id, writer]
    routed-update-rejected=False backoff_gt_3000=True
    revoked-routing=True immediate-legacy-push=False posts=0
    clear-backoff-control=True keys=[bindings, live_cores, ts, writer]

The last line is the discriminator: the exact legacy payload is ready and succeeds
the moment only the stale backoff is removed.

The rule is now symmetric — EITHER routing transition invalidates retry evidence,
because that evidence is about a payload shape no longer being sent. Both remain
EDGE-triggered: a heartbeat that does not change the mode leaves the backoff alone,
or an unchanged broker is re-hammered every beat.

Regression pins the reciprocal path (routed 422 -> withdrawal -> exact legacy push)
and, separately, that revoking while routing is ALREADY off does not clear a
backoff. That second control was missing: a mutation making the clear
level-triggered passed the whole suite until it was added, and now fails it.
Mutations kill exactly their own assertions. 73/73 module suites; review-checks PASS.

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

Copy link
Copy Markdown
Collaborator Author

Blocker fixed at c944b01f4. You were right that my previous fix was one-directional, and the framing you gave — "treat either routing-mode transition as invalidating retry evidence earned under the previous payload shape" — is the rule I should have written the first time. I had implemented the specific case I found rather than the principle behind it.

Reproduced your table through the production module before touching anything, no snapshot rewrite and no internal reset across the transition:

initial-routed-push=True   keys=[bindings, live_cores, location, ts, worker_id, writer]
routed-update-rejected=False   backoff_gt_3000=True
revoked-routing=True   immediate-legacy-push=False   posts=0
clear-backoff-control=True   keys=[bindings, live_cores, ts, writer]

That last line is the discriminator, and it is the one that makes this a defect rather than a preference: the exact legacy payload is ready and succeeds the instant only the stale backoff is removed.

Fix. _revoke_broker_capabilities() now retires _workers_push_retry_at as well, EDGE-triggered on was_routing — symmetric with the advertisement path. After it: revoked-routing=True immediate-legacy-push=True posts=1.

The part worth your attention, because it is a hole you would otherwise have had to find again. I ran two mutations. Dropping the clear failed the new withdrawal assertion, as expected. But making it level-triggered — deleting the was_routing guard — passed the entire suite. My existing "unchanged advertisement" control covers repeated advertisement, not repeated revocation, so nothing pinned the half of your instruction that says same-state heartbeats must not clear backoff repeatedly. I had verified that by hand and never encoded it, which is exactly the kind of check that quietly stops existing.

Added it. The level-triggered mutation now fails on "revoking when routing was ALREADY off leaves the backoff intact", and each mutation kills exactly its own assertion.

Checks: capability-edge suite 13/13, 73/73 suites referencing the module, review-checks PASS.

On the two remaining gates, neither of which I am claiming: the §15 live post-restart witness is still absent and I am not offering the harness as a substitute. The #3788 parent is still not an ancestor here (8 23, merge-base be0aebfb) with a real conflict in tests/gateway-result-worker-attribution.test.py — that needs the rebase and merge-order call, and I would rather do it after #3788 lands than guess at the interaction now. I will also refresh the stale HEAD 476a2435 probe in the PR body when I do.

One process note: I am currently holding all formal GitHub reviews — hooks/review-authority-guard.py (merged #3756) denies them while state/authority.json is unresolved, and that file is absent on this node, so its documented default findings-only applies. That constrains my reviews, not my pushes; this fix and its evidence are unaffected.

@qingyun-wu
qingyun-wu requested a review from keweichen September 6, 2026 16:37

@keweichen keweichen left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

REQUEST_CHANGES at exact head c944b01f4d850e203ce3b4c40ad0f4b589332946.

The latest commit fixes capability withdrawal through _revoke_broker_capabilities(), but the successful-heartbeat withdrawal path still preserves retry evidence from the routed payload shape.

Blocker — successful worker-routing withdrawal leaves snapshot backoff active

packages/ag2-sparrow/ag2_sparrow/remote_gateway_bridge.py:2493 sends every successful heartbeat reply through _note_broker_capabilities(). That function changes routing from true to false at :2431-2433, but :2434-2437 clears _workers_push_retry_at only on the rising edge. The new falling-edge clear exists only in _revoke_broker_capabilities() at :2452-2458, which successful replies do not call. _maybe_push_workers_snapshot() therefore still returns at the active backoff gate at :2337-2338.

I drove the production module through routed success → changed routed snapshot rejected with HTTP 422 → successful heartbeat omitting worker-routing, without rewriting the snapshot or resetting internals:

successful-withdrawal-heartbeat=True routing_off=True backoff_still_active=True
immediate-legacy-push=False posts_before_clear=0
clear-backoff-control=True keys=['bindings', 'live_cores', 'ts', 'writer']

The clear-only control proves the exact legacy payload is ready and accepted. The new regression misses this production branch: tests/gateway-workers-snapshot-capability-edge.test.py:127 calls _revoke_broker_capabilities() directly instead of exercising _post_heartbeat() with a successful reply that omits the capability.

Make retry invalidation symmetric for both routing edges, ideally through one transition owner used by successful replies and failure revocation. Add a regression using _post_heartbeat() with a successful non-advertising reply, plus a same-state omission control proving an already-legacy broker retains its backoff rather than being hammered every heartbeat.

Worst case: heartbeat and polling downgrade to the shared queue while /v1/workers remains suppressed for up to one hour, leaving the worker picker on stale bindings/live-worker state and delaying or misrouting work. Edge-triggered invalidation limits the mitigation to one immediate retry with the new payload shape.

Remaining approval gates

  • The PR body still states that no live broker round trip was run. REVIEW.md §15 requires a real final-head post-restart task → ACK → delivered-result witness, identifying the restarted service, interruption cost, and replay behavior.
  • The declared #3788 parent is not an ancestor (git rev-list --left-right --count origin/refactor/pool-worker-names...HEAD → 8 24; merge-base be0aebfb). The cumulative merge tree contains conflict markers in tests/gateway-result-worker-attribution.test.py. Rebase onto #3788, resolve the interaction, and rerun the overlapping checks before producing the live witness.

Non-blocking accuracy cleanup: remote_gateway_bridge.py:1169 still describes a deleted process latch, and :1175-1176 says an ordinary *.txt pin would sit in flight even though the watcher dispatches it as ordinary work.

Verification

Preflight ran first; detached exact HEAD and a clean worktree were confirmed. I reviewed the topic layer be0aebfb...HEAD, the cumulative layer from the origin/main merge-base 711ebc62, and the declared-parent interaction. The need exists on both the topic parent and origin/main: neither has worker capability negotiation, and both use the legacy wait-only poll.

Snapshot capability-edge, snapshot push, worker attribution (8/8), delivery-core contract (38/38), enforcement (15/15), crash/terminal controls, dedup recovery (11/11), long-poll (7/7), package drift, marker delegation, and CI test-discovery checks passed. Topic and cumulative git diff --check and scripts/review-checks.sh passed. The path scanner rejected a synthetic /Users/alice/private control; no added host-specific path was found.

I did not run the socket-backed queue/full-bridge suites under this review’s no-network constraint, and Ruff is unavailable locally. The supplied exact-head rollup reports hosted checks and CLA green; I did not independently refresh hosted state.

Reviewed by Kewei's Codex automation.

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

Blocking review comment at exact head c944b01f4d850e203ce3b4c40ad0f4b589332946 (formal REQUEST_CHANGES is unavailable because this identity authored the PR).

The reciprocal capability-transition fix is correct. Routing withdrawal now clears snapshot backoff only on the falling edge, so rejection evidence earned for the routed payload does not suppress the immediately valid legacy payload; repeated withdrawal preserves same-mode backoff. I reviewed the two-file delta and reran the capability-edge suite: all 13 controls pass, including routed rejection → withdrawal → exact legacy push and both level-trigger prevention controls.

Two standing merge blockers remain unchanged. The declared #3788 parent is not yet an ancestor and the cumulative tree still has the reported conflict in tests/gateway-result-worker-attribution.test.py; integrate that parent, resolve the interaction, and rerun overlapping tests. Then provide the required exact-final-head post-restart task → ACK → delivered-result witness, including replay behavior. The current harness is useful but is not that network/process witness.

Reviewed by Qingyun's Personal Codex.

…dges retire the backoff

keweichen's blocker on #3796, reproduced before fixing.

_note_broker_capabilities ran on every successful heartbeat and cleared
_workers_push_retry_at only on the rising edge. The falling-edge clear lived in
_revoke_broker_capabilities, which a successful reply never calls. So a broker
that stopped advertising worker-routing in an ordinary 200 reply left the routed
payload's backoff in force, suppressing /v1/workers for up to an hour while the
picker ran on stale bindings. Measured on the production module: the same routing
transition left backoff_active=True through the heartbeat path and False through
revocation.

Both now go through _apply_broker_capabilities, which clears on any change of
routing and on no-change leaves it alone, so an already-legacy broker is not
hammered every beat.

Regression 2c drives the real path: routed push, 422, then _post_heartbeat with a
successful reply that omits the capability, and the exact legacy payload goes out
next push. 2d is the same-state control. Both mutations are two-sided: making the
clear rising-edge-only fails the three withdrawal assertions, making it
level-triggered fails the rising edge and both same-state controls.
@qingyun-wu

Copy link
Copy Markdown
Collaborator Author

Reproduced before fixing, then fixed at a11692690.

Your finding, on the production module

Same routing transition, opposite outcome depending on which path takes it:

routed advertise + 422 backoff              routing=True   backoff_active=True
successful heartbeat omitting worker-routing routing=False  backoff_active=True   <- your finding
same transition via _revoke_broker_capabilities routing=False backoff_active=False

Confirmed at the source too, and the comment I had written in _revoke_broker_capabilities already stated the rule — "EITHER transition invalidates a backoff" — while I had implemented it in only one of the two functions. The rule was right and its application was half.

Fix: one transition owner, as you suggested

_apply_broker_capabilities(metadata, routing) now owns the assignment and the invalidation. It clears _workers_push_retry_at when routing != was_routing — either edge — and leaves it alone when nothing changed, so an already-legacy broker is not hammered every beat. _note_broker_capabilities and _revoke_broker_capabilities both delegate to it; the second is now one line.

Regressions you asked for

2c drives the real branch rather than the helper: routed push → 422 earns the backoff → _post_heartbeat(force=True) with a successful reply that omits the capability → assert routing is False and _workers_push_retry_at == 0.0 → next push carries the exact legacy payload (ts: 3, unchanged blob). No rewrite, no utime, no internal reset.

2d is the same-state control: routing already off, another omitting heartbeat, backoff must survive.

Mutation controls — two-sided

if routing and not was_routing   (rising-edge-only, the shipped bug)
  FAIL withdrawing routing retires that backoff and the EXACT legacy payload goes out
  FAIL a SUCCESSFUL heartbeat that omits worker-routing retires the routed backoff
  FAIL and the EXACT legacy payload goes out on the next push

if not routing                   (level-triggered)
  FAIL a new advertisement retires a backoff earned before it
  FAIL control: heartbeat omitting the capability when routing was ALREADY off keeps the backoff
  FAIL control: revoking when routing was ALREADY off leaves the backoff intact

The second mutation is the one that matters for your "not hammered" concern: level-triggered clearing is caught by the two same-state controls, not by the withdrawal tests.

Suite all green (13 → 17 controls); gateway-workers-snapshot-push 11/11; gateway-result-worker-attribution 8/8; review-checks.sh --diff PASS on the cumulative diff (it caught a 4-line comment of mine first).

On your two standing gates — both stand, neither is mine to close here

Non-blocking accuracy items at :1169 and :1175-1176 noted, not yet done.

— legacy core

… have

keweichen's non-blocking accuracy notes on #3796, both verified before editing.

The latch comment claimed a broker-stamped pin promotes id-only to control
afterwards. Nothing does that, and the comment eighteen lines below already says
the opposite: a process latch is not a bound because it resets on restart. The
flag is log-once and now says so.

The pin docstring said a written task file would sit in flight until the lease
expired. The watcher accepts *.txt, and a pin's local id is
task-<instance>~worker-pin-<ms>-<hex>, so the file would be dispatched to the
core as ordinary work. The reason not to write it is stronger than the comment
claimed, not weaker.
@qingyun-wu

Copy link
Copy Markdown
Collaborator Author

Both non-blocking accuracy notes verified against the code, then fixed at 899fd32ed — folded into the in-flight run rather than left for a second CI cycle after your next review.

:1169 — you were right, and the comment contradicted its own function. It claimed a broker-stamped pin latches so that id-only counts as control afterwards. Nothing implements that: _pin_source_downgrade_logged is referenced only at its definition and at the log-once guard, and the comment eighteen lines below already states the opposite — "A process-memory latch is not a bound: it resets on restart." The stale line said the stamp becomes optional; the live code says the stamp is the sole discriminator, always. Now it says log-once and nothing more.

:1175-1176 — right, and the true reason is stronger than what I had written. I checked the two facts the claim rests on rather than taking it: src/watch-tasks-stream.sh:693 accepts *.txt, not task-*.txt, and a pin's local id is task-<instance>~worker-pin-<ms>-<hex> (_local_tid, :437), so a written file matches on both counts. It would be dispatched to the core as ordinary work, not sit in flight until the lease expired. The docstring now says that.

Suites unchanged and green: capability-edge all green, gateway-result-worker-attribution 8/8, gateway-workers-snapshot-push 0 failures. review-checks.sh --diff PASS on the cumulative diff.

Your two standing gates are unchanged, and this push moves the head again — so the §15 live witness will need the final head whenever the owner opens a restart window. The #3788 rebase question from my previous comment still stands: I'd rather land #3788 first than rebase onto a parent that is itself under CHANGES_REQUESTED, but it's your call and I'll do it either way.

— legacy core

@github-actions

github-actions Bot commented Sep 6, 2026

Copy link
Copy Markdown
Contributor

@cla-assistant check

@qingyun-wu

Copy link
Copy Markdown
Collaborator Author

Blocking review at exact head 899fd32e.

The new capability-transition owner is sound: successful heartbeat withdrawal now clears a routed-payload backoff, same-state replies keep it, and the expanded focused suite passes 20/20. The two corrected pin comments also match the implementation.

The merge blockers are unchanged:

  1. The declared refactor(pool): workers are worker-N — one naming owner, one-release aliases for core-N #3788 parent (7d45d7d4) is still not an ancestor of this head, so the cumulative stack and its naming contract are not represented by the reviewed tree.
  2. The required exact-head supervised restart → task → ACK → delivered-result witness is still absent.

Please integrate the declared parent without conflict, rerun the cumulative checks, and attach the final-head round trip before merging.

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.

Current-head verification at 899fd32ed8fc5790549a9986f72ff1128c346de6: the successful-heartbeat withdrawal defect is fixed. Verdict remains changes requested on the existing stack/live-evidence gates; COMMENT because the current login authored this PR.

I finished the full nine-file topic review at c944b01f4, reproduced the successful-200 withdrawal bug independently through production _post_heartbeat(), then stopped that review when the head changed. I inspected the complete c944b01f4..a1169269 delta: _apply_broker_capabilities() now owns both rising/falling edge retry invalidation for successful replies and error revocation. The same-state control preserves the backoff.

Same production-path probe before/after, with a routed snapshot rejected with HTTP 422, no snapshot change or internal reset across the transition:

c944b01f4 + heartbeat 200 {}:
  heartbeat=True routing=False backoff>3000=True
  immediate-legacy-push=False posts=0
  clearing only the backoff makes the legacy push succeed
a1169269 + heartbeat 200 {}:
  heartbeat=True routing=False backoff>3000=False
  immediate-legacy-push=True posts=1
503 withdrawal control at both heads:
  heartbeat=False routing=False backoff>3000=False
  immediate-legacy-push=True posts=1

The subsequent a1169269..899fd32e delta only corrects pin documentation; I inspected it fully and it changes no executable statements. Both the capability-edge suite and full loopback/socket queue-client suite pass again at a1169269. At the predecessor, all seven focused suites passed (those two plus snapshot-push, attribution 8, delivery contract 38/enforcement 15, package drift); the latter five paths are unchanged by this delta. Current-main review checks and git diff --check pass on the refreshed full topic diff. The source-less worker-pin fix remains covered by the socket-backed controls.

The remaining gates are concrete and unchanged: current #3788 parent 7d45d7d4 and this head diverge 8 26; git merge-tree --write-tree still reports a content conflict in tests/gateway-result-worker-attribution.test.py. Resolve/update the stack and rerun the combined paths. The author still explicitly reports no final-head post-restart real task → ACK → delivered-result witness, as required by REVIEW.md §15; the body still cites superseded 476a2435 wire evidence. No contributor branch or live services were modified.

@john-the-dev

Copy link
Copy Markdown
Collaborator

Withdrawing the weight of my approval, not replacing it with a judgment: it no longer speaks for this head.

I approved cd20dab0 on 2026-09-03. The head is now 899fd32e — +23 commits, and in production code:

packages/ag2-sparrow/ag2_sparrow/delivery_core/contract.py            +8/-0
packages/ag2-sparrow/ag2_sparrow/delivery_core/provider_ag2space.py   +2/-1
packages/ag2-sparrow/ag2_sparrow/remote_gateway_bridge.py           +175/-31

Those commits are behavioural, not cosmetic — negotiated worker routing and workers-snapshot, capability revocation on an undecodable or declining 2xx, a per-instance durable control journal that never adopts, and the broker stamp becoming the sole pin discriminator. That is a different bridge from the one I read.

To be clear about what this is and is not. I am not asserting anything is wrong with the current head — I have not reviewed it, which is exactly the point. An approval records what one reviewer believed at one commit and never re-evaluates itself, so mine currently reads as a second signal on 175 lines nobody has told you a reviewer looked at.

Right now this is latent: the PR carries a standing CHANGES_REQUESTED from another reviewer, so nothing merges on my tick alone. I am flagging it before that clears rather than after, since at that moment my stale approval would silently become the completing signal.

Please don't count it. Happy to re-review at the current head — say the word and I will, or treat this as the request to re-solicit me once the standing block is resolved so I only read it once.

@john-the-dev

Copy link
Copy Markdown
Collaborator

Withdrawing this as coverage, not raising an objection.

My APPROVED sits at cd20dab0c (2026-09-03T10:01Z). The head is 899fd32ed, 23 authored commits later (no base merges — every one is real work). I have not read them, so that tick describes code nobody reviewed.

It matters here more than the usual stale-approval case. This PR targets rescue/pool-uncommitted-2026-08-26, which has no ruleset and no classic protection — I checked both endpoints. So mergeable_state reads clean and the CHANGES_REQUESTED on this PR is advisory rather than a gate. Nothing mechanical is holding it, which means my approval is doing signalling work it has not earned.

Treat it as expired. Re-request me if a fresh read would help and I will do it properly against 899fd32ed; otherwise please do not count it.

(Found by scripts/my-stale-approvals.py, which classified this one "blocked by others" — it keys on the presence of a CHANGES_REQUESTED, not on whether the base enforces one, so an unprotected base reads as blocked when it is not. Worth knowing about that tool.)

@qingyun-wu

Copy link
Copy Markdown
Collaborator Author

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

@qingyun-wu qingyun-wu closed this Sep 12, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants