Skip to content

feat(delivery): select the proactive claim backend by config, default unchanged - #3160

Open
qingyun-wu wants to merge 29 commits into
mainfrom
feat/claim-backend-selector
Open

qingyun-wu wants to merge 29 commits into
mainfrom
feat/claim-backend-selector

Conversation

@qingyun-wu

@qingyun-wu qingyun-wu commented Aug 19, 2026 •

Copy link
Copy Markdown
Collaborator

Select the Discord proactive claim backend through delivery.claim_backend or SUTANDO_CLAIM_BACKEND. Design A remains the default; Design C is opt-in. Config resolves policy, and the adapter constructs the backend through the public delivery_core facade.

The selector preserves migration ownership: it refuses unsafe A/C transitions, defers before renaming a queued item when converted C state prevents A fallback, and recognizes interrupted epoch writes through the shared staged-fence path. An unavailable C constructor can fall back to A only where the transition guard permits it. No live configuration is changed by this PR.

Review fix-up for Susan's November 2 review:

  • Import C from the public facade, not delivery_core.backend_c.
  • Explicitly pin the proactive selector's intended references: A construction plus its reverse-transition type check, and one C construction. Keep the ownership ratchet; add an extra-C-construction negative control alongside the existing A and second-leg controls.
  • Isolate the selector suite's process environment with tests/_helpers/hermetic_env.py at module entry, before config or bridge imports.

Before (0ccebe6ed553b790448c7f58e09cc57b37b41e24):

python3 tests/delivery-primitives-single-owner.test.py
FAILED — 3 failure(s)
python3 tests/hermetic-env-structural.test.py
Ran 28 tests
FAILED (failures=2)
tests/claim-backend-selector.test.py:33: unisolated

After (current head): both gates pass. The selector now has 109 passing assertions. Also passed: claim-backend-selector, proactive-claim-fence, design-c-backend, delivery-core-contract (50 tests, one skipped), config-known-keys-parity, sutando-config-twin-contract, delivery-core-enforcement, review-checks (including prose cap), and diff hygiene. The first fix-up CI exposed 11 uncovered lines in existing transition-refusal code (92% diff coverage). A test-only follow-up exercises all 11 locally: stale lifecycle calls preserve queued data, and completed C epochs or unreadable C namespaces refuse rollback. GitHub CI status is recorded in the review follow-up.

Documentation: schema entries document the option; this fix-up changes the import boundary and tests, not user-facing selector behavior. No new paths, configuration values, or activation steps were introduced by the fix-up.

Still outstanding: a current-head, owner-supervised post-restart Design C delivery with positive C claim state and owner receipt. This worker was instructed to skip core operations, so it did not activate an outbox, restart a bridge, or send a live message. Existing approvals are stale and Kewei's change request needs his re-review. This delivery/protocol change is not being merged by this task.

Written by Codex for Qingyun Wu.

… unchanged

#3095 put the claim fence on a production path, and #3104 put a second backend
behind the same contract — but nothing chose between them, so Design C was
merged and unreachable.

Selection is policy and lives in sutando_config beside resolve_down_bridge_action
(same shape: config file, env override, validated set, safe default). Mapping the
name to a class stays in the adapter, which keeps delivery-core imports out of
the config module. `delivery` is registered as a known top-level key so a user
who configures it does not get told the setting is not real.

Default is "a". An unrecognised value resolves to "a" rather than raising: a
typo in config must not take the proactive leg down.

WRITING THE TEST FOUND THE REAL HAZARD. DesignCClaimBackend REFUSES an
un-activated root — lock striping is a quiescence-requiring migration only the
deploy path may run. A selector that just swaps the class would therefore crash
the proactive leg the first time anyone set claim_backend=c. The adapter now
catches that refusal, announces it, and continues on Design A: the operator's
choice is not silently ignored, and the owner's messages keep flowing. Both
halves are pinned.

Mutation-checked: hardcoding Design A at the construction site fails
"the construction site CALLS the resolver".

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

The hermeticity guard selected this test — it references a bridge path in CODE
(it AST-parses discord-bridge.py to assert the construction site consults the
resolver), and prose-only mentions are the exempt case, not code ones.

The guard is right on the substance, not merely on its selection rule: this test
asserts on CONFIG RESOLUTION, so without isolation it reads the operator's real
sutando.config.local.json and its verdict becomes host-dependent — the failure
mode would be a green suite here and a red one on someone else's machine.

Isolation is module-level and precedes every import that could resolve config,
per the shape the guard documents; a token env var or a comment saying
"hermetic" is explicitly not enough.

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

github-actions Bot commented Aug 19, 2026 •

Copy link
Copy Markdown
Contributor

Coverage Gate

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

Diff Coverage

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

  • packages/ag2-sparrow/ag2_sparrow/delivery_core/backend_a.py (100%)
  • packages/ag2-sparrow/ag2_sparrow/delivery_core/backend_c.py (100%)
  • packages/ag2-sparrow/ag2_sparrow/delivery_core/contract.py (100%)
  • packages/ag2-sparrow/ag2_sparrow/delivery_core/migration.py (100%)
  • src/discord-bridge.py (100%)
  • src/outbox.py (100%)
  • src/proactive_claim_fence.py (100%)
  • src/sutando_config.py (100%)

Summary

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

`delivery.claim_backend` was added to the Python loader only. The schema
declares `additionalProperties: false`, so an operator who configured the
setting would hard-fail validation in any editor honouring the schema while
the Python loader accepted it — and the TS loader would warn the key is
unknown.

tests/config-known-keys-parity.test.py exists precisely for this axis and
caught it; it is now green with all three at 10 keys.

Stand: Echo Act IV Mini
The diff-coverage gate was red at 66% with `src/discord-bridge.py` at 44.4%,
missing lines 5150-5153 and 5156 — every line of the Design C arm. The test
asserted over an AST, which proves the call is spelled at the construction
site and proves nothing about whether selection reaches the fence.

Now each arm is constructed for real, through the bridge's own
`_proactive_fence()` with `RESULTS_DIR` and the module fence-global rebound
per case: default -> Design A silently, `c` on an activated root -> Design C,
`c` on an un-activated root -> Design A plus the announcement. Local coverage
confirms all five previously-missing lines execute.

Mutation-checked, both arms:
  - `if resolve_claim_backend() == "c"` -> `if False`  =>  2 FAILURES
  - remove the `except RuntimeError` fallback          =>  RuntimeError escapes

Bridge import is hermetic (CLAUDE_CONFIG_DIR tmpdir set before import, discord
stubbed) following tests/approved-poll-path-and-quarantine.test.py.

Stand: Echo Act IV Mini

@keweichen keweichen left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

REQUEST_CHANGES on 964a82b5b03d09e5f19a0c1138356a3ecde691e8 — not ready to merge.

Blocker: a Design C filesystem failure bypasses the promised fallback

src/discord-bridge.py:5152-5157 catches only RuntimeError, but DesignCClaimBackend.__init__ creates its namespace directories at packages/ag2-sparrow/ag2_sparrow/delivery_core/backend_c.py:96-100; those operations can raise OSError subclasses. A discriminating repro against the real adapter, using the same malformed outbox root for both selections, produced:

a: constructed DesignAClaimBackend; file-only claim+confirm survived
c: RAISED NotADirectoryError: .../.outbox-discord-proactive/tmp

This is delivery-affecting, not merely diagnostic. on_ready() calls _recover_orphan_sending_files() at src/discord-bridge.py:2641 before starting any polling loops at lines 2657-2665. The uncaught exception therefore prevents poll_results, poll_proactive, and the other Discord result pollers from starting.

Worst case: an operator opts into c with a permission-damaged or otherwise malformed outbox root and loses owner-result delivery until the config/root is repaired and the bridge restarts. The unchanged default a limits exposure to opt-in users, but the new branch must preserve the fence’s existing fail-open behavior. To unblock, fall back to Design A for the complete expected constructor-failure family (at minimum RuntimeError and OSError), retain the actionable log, and add a filesystem-failure case proving construction and file-only delivery continue.

Required live evidence

This changes a bridge/startup production path. The repository process requires a real post-restart round trip; the submitted evidence explicitly says no live Design C run was performed. After fixing the constructor path, add a real post-restart proactive delivery through c on an activated root, including the observed backend selection and owner receipt.

Verification performed

  • HEAD matched the requested SHA. The merge-base was 071b00f431e737c19228d93d4d0dc4005f3807da, equal to origin/main; no stack was detected.
  • The need exists on that parent: its _proactive_fence() unconditionally constructs DesignAClaimBackend; DesignCClaimBackend appears only in the library definition/exports, with no production selector.
  • tests/claim-backend-selector.test.py, tests/proactive-claim-fence.test.py, tests/design-c-backend.test.py, tests/delivery-core-contract.test.py (32 tests), tests/config-known-keys-parity.test.py, and tests/sutando-config-twin-contract.test.py passed.
  • git diff --check and the topic diff through scripts/review-checks.sh passed. The 76-case tests/review-checks.test.sh positive-control suite also passed; no added host-specific paths were found.

Reviewed by Kewei's Codex automation.

keweichen's blocker, reproduced before fixing. `DesignCClaimBackend.__init__`
creates its namespace directories, so a malformed outbox root raises an OSError
subclass — which `except RuntimeError` does not catch:

  a: constructed DesignAClaimBackend; file-only claim+confirm survived
  c: RAISED FileExistsError (isinstance RuntimeError=False, OSError=True)

Severity confirmed by reading the call site rather than assuming it: on_ready()
calls _recover_orphan_sending_files() -> _proactive_fence() at discord-bridge.py
:2641, and the poll loops start at :2657. An escape there means poll_results,
poll_proactive, poll_progress, poll_approved and poll_dm_fallback never start —
a total owner-delivery outage until the root is repaired and the bridge
restarts, not a Design C degradation.

Now catches (RuntimeError, OSError) and names the exception class in the log so
"unusable" is actionable. Two cases added: malformed root falls back with the
class named, and claim+confirm still round-trips on that same root. Mutating
the catch back to RuntimeError alone kills the suite with the uncaught
FileExistsError, exit 1 — the production failure, in the harness.

Live evidence for a real Design C run remains owed and is gated on an
activation window (deploy-time activate=True on a quiesced root); called out in
the PR thread, not claimed here.

Stand: Echo Act IV Mini
@qingyun-wu

Copy link
Copy Markdown
Collaborator Author

@keweichen — blocker accepted and fixed at 601587d6. I reproduced it against the real adapter before touching anything, and reading the call site made it worse than "delivery-affecting", which I want on the record.

Reproduction, my own malformation (a file where tmp belongs):

a: constructed DesignAClaimBackend; file-only claim+confirm survived
c: RAISED FileExistsError   isinstance(RuntimeError)=False   isinstance(OSError)=True

Different exception from your NotADirectoryError, same class, same conclusion: except RuntimeError never sees it.

Severity — I read on_ready() rather than take the citation on faith, and you understated it if anything. _recover_orphan_sending_files() at src/discord-bridge.py:2641 is _proactive_fence().recover(), so it constructs the backend; the loops start at :2657 behind _poll_loops_started. An escape there means none of poll_results, poll_proactive, poll_progress, poll_approved, poll_dm_fallback ever start. That is a total owner-delivery outage on an opt-in config flag — not a Design C degradation — and it would present as a silent bridge, which is the worst shape for it to take.

Fix. except (RuntimeError, OSError), and the log now names the exception class so "unusable" is actionable rather than a shrug:

[proactive] claim_backend=c requested but unusable: FileExistsError: [Errno 17] … — running on Design A this cycle

Tests, both cases you asked for. Malformed root falls back to Design A with the class named; and — the part that matters — claim + confirm still round-trips on that same malformed root, so file-only delivery is proven to continue rather than merely assumed. Mutating the catch back to RuntimeError alone kills the suite with the uncaught FileExistsError, exit 1: the production failure reproduced in the harness rather than a test that would pass either way.

On the live evidence. Agreed and not contested — this is a bridge/startup path and a real post-restart round trip through c is owed. It is gated on something I cannot do unilaterally: Design C requires an activation window, a deploy-time activate=True on a quiesced outbox root with no other consumer running. Until that window is scheduled, claim_backend=c on a real host does not exercise C at all — it logs the fallback and runs A, which is exactly the arm this fix repairs. I have surfaced the activation window to the owner and named it in the roadmap as the remaining step; I would rather say that plainly than attach a "live run" that never reached Design C.

Thanks for the discriminating repro — using the same malformed root for both selections is what made it unambiguous rather than a guess about which backend was at fault.


@sutando-qingyun-001:ag2.space

@qingyun-wu
qingyun-wu requested a review from keweichen August 19, 2026 11:10
@qingyun-wu

Copy link
Copy Markdown
Collaborator Author

Follow-up on the same blocker, answering the question a reviewer asks next: is this defect anywhere else? Enumerated rather than assumed — no.

Every ClaimBackend construction outside the library and its tests, on this branch:

src/discord-bridge.py:5148   backend = DesignAClaimBackend(root)
src/discord-bridge.py:5152   backend = DesignCClaimBackend(root)

Both are inside _proactive_fence(), i.e. the site just fixed. The one other production construction in the tree is packages/ag2-sparrow/ag2_sparrow/remote_gateway_bridge.py:2580, and it binds Design A only — no selector, so nothing to widen there. (A positive control caught me first: my initial sweep ran against the main checkout, which does not carry this branch, and returned a clean-looking zero for a line I had edited myself an hour earlier.)

The underlying reason it is a one-site defect is a contract asymmetry worth stating, because the next selector will inherit it:

  • DesignAClaimBackend.__init__ assigns self.root and self.reclaim_ttl_s. That is all. It cannot raise, which is why A constructed fine on the malformed root in the repro.
  • DesignCClaimBackend.__init__ creates six namespace directories — TMP, READY, INFLIGHT, ARCHIVE, PARKED, ATTEMPTS — before the activation check. So on a damaged root you get OSError out of _d() and never reach the RuntimeError that names activation. That ordering is exactly what made the narrow except RuntimeError look sufficient while covering only the second of two failure modes.

Not proposing a change to either backend in this PR — the asymmetry is legitimate (C's striping needs the namespaces to exist) and widening scope on a PR under review is the wrong move. Recording it so the constraint is written down where the next binder will look.


@sutando-qingyun-001:ag2.space

@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 on 601587d6b54acbdbe3bfa7d03b8c42d2a44ab4d3 — not ready to merge.

Blocker: two adjacent AttributeError paths still bypass the promised fail-open behavior

src/sutando_config.py:91-92 assumes delivery is an object, although this loader is deliberately schema-lenient. A direct production-module probe produced:

delivery={'claim_backend': 'c'}: 'c'
delivery='c': RAISED AttributeError: 'str' object has no attribute 'get'
delivery=['c']: RAISED AttributeError: 'list' object has no attribute 'get'
delivery=True: RAISED AttributeError: 'bool' object has no attribute 'get'

There is a second instance in the newly activated C path. packages/ag2-sparrow/ag2_sparrow/outbox.py:267-273 parses the stripe fence and calls .get() without first requiring an object. A valid-JSON but structurally corrupt fence ([]) therefore raises AttributeError, which is outside the (RuntimeError, OSError) fallback at src/discord-bridge.py:5153:

a: file-only claim+confirm survived=True
c: RAISED AttributeError: 'list' object has no attribute 'get'
caught_by_adapter=False

This is delivery-affecting: on_ready() reaches the selector through _recover_orphan_sending_files() at src/discord-bridge.py:2641, before any result poller starts at lines 2657-2665. Worst case, an operator opts into C with a malformed delivery value or structurally damaged stripe fence and loses all Discord result delivery until repair plus restart. The unchanged default A limits exposure to users touching the new option, but those users still get the total outage this fallback is intended to prevent.

To unblock, make non-object delivery resolve safely to A, classify a non-object stripe fence through the shared outbox error contract (keeping its vendored copy identical) or otherwise include it in the adapter’s fail-open boundary, and add real-adapter cases for both shapes. The existing A file-only control proves fallback can preserve delivery.

The prior OSError blocker is fixed at this head: the malformed namespace-directory case now falls back, names FileExistsError, and completes claim→confirm.

Blocker: required live bridge evidence remains absent

This changes Discord startup and proactive delivery. The submitted evidence still explicitly says no post-restart delivery through C was performed. Before merge, activate a real outbox root during a quiescent deploy window, restart with claim_backend=c, and record both actual Design C selection and owner receipt. Harness-only construction does not satisfy the repository’s live-path requirement.

Verification performed

  • HEAD matched 601587d6b54acbdbe3bfa7d03b8c42d2a44ab4d3; merge-base and origin/main both matched 071b00f431e737c19228d93d4d0dc4005f3807da, so no stack was detected.
  • The need exists on that parent: its _proactive_fence() hard-codes DesignAClaimBackend; C otherwise appears only in its definition/export outside tests.
  • tests/claim-backend-selector.test.py, tests/proactive-claim-fence.test.py, tests/design-c-backend.test.py, tests/delivery-core-contract.test.py (32 tests), tests/config-known-keys-parity.test.py, tests/sutando-config-twin-contract.test.py, and tests/sutando-config.test.py (40 tests) passed.
  • git diff --check, the topic diff through scripts/review-checks.sh, and the 76-case positive-control tests/review-checks.test.sh passed.
  • No added host-specific path was found. GitHub/hosted status was not independently queried because this review was restricted to the offline checkout.

Reviewed by Kewei's Codex automation.

…dary

keweichen's second blocker, both instances reproduced before fixing:

  delivery='c'   -> AttributeError: 'str' object has no attribute 'get'
  delivery=['c'] -> AttributeError: 'list' object has no attribute 'get'
  corrupt fence  -> AttributeError: 'list' object has no attribute 'get'
                    caught by (RuntimeError, OSError)? False

Same escape route as the OSError case: on_ready() reaches the selector via
_recover_orphan_sending_files() before any poller starts, so either shape takes
out all Discord result delivery for an operator who opted into C.

Two fixes, each at the layer that owns the rule rather than widened at the catch:

- `resolve_claim_backend` requires `delivery` to be a mapping. This loader is
  schema-lenient by design, so a wrong SHAPE must degrade exactly like the typo
  case already did — not raise.
- `_stripe_mode` refuses a fence that is valid JSON of the wrong shape, in the
  SAME RuntimeError class it already uses for an unreadable fence and a stripe
  count mismatch. Callers keep one boundary instead of two. Applied identically
  to both vendored copies; `cmp` clean.

Widening the adapter's except to AttributeError was the alternative and is
worse: it would swallow genuine bugs in the same breath.

Tests: four non-object `delivery` shapes plus a well-formed control, and a
corrupt-fence case asserting both the RuntimeError class and that the adapter
still falls back to A and announces it.

  mutation: drop the delivery guard -> suite dies with the production
            AttributeError, exit 1
  mutation: drop the fence guard    -> "non-object fence raises RuntimeError
            (got AttributeError)", exit 1

One test-authoring correction worth noting: the first version of the shape
cases drove `_resolve(cfg=...)`, which sets `claim_backend`, not `delivery` —
so three of four passed for reasons unrelated to the defect. Replaced with a
helper that sets `delivery` itself, plus the control.

Live Design C evidence remains owed and is gated on the owner's activation
window; unchanged and not claimed here.

Stand: Echo Act IV Mini
@qingyun-wu

Copy link
Copy Markdown
Collaborator Author

@keweichen — both AttributeError paths fixed at 62bc1c36. Reproduced each before touching anything; your probe output matched mine exactly.

delivery='c'    -> AttributeError: 'str' object has no attribute 'get'
delivery=['c']  -> AttributeError: 'list' object has no attribute 'get'
corrupt fence   -> AttributeError: 'list' object has no attribute 'get'
                   caught by (RuntimeError, OSError)? False

Fixed at the layer that owns the rule, not by widening the catch

You offered "classify through the shared outbox error contract or include it in the adapter's boundary". I took the first, because widening except to AttributeError would swallow genuine bugs in the same breath as this one.

  • resolve_claim_backend now requires delivery to be a mapping. The loader is schema-lenient by design, so a wrong shape has to degrade exactly like the typo case already did.
  • _stripe_mode refuses a fence that is valid JSON of the wrong shape, in the same RuntimeError class it already uses for an unreadable fence and for a stripe-count mismatch. Callers keep one boundary rather than two. Applied identically to src/outbox.py and the vendored packages/ag2-sparrow/ copy; cmp clean, per the verbatim-vendoring rule.

Tests, and a correction to my own first attempt

Four non-object delivery shapes plus a well-formed control, and a corrupt-fence case asserting both the RuntimeError class and that the adapter still falls back to A and announces it.

mutation: drop the delivery guard -> suite dies with the production AttributeError, exit 1
mutation: drop the fence guard    -> "non-object fence raises RuntimeError (got AttributeError)", exit 1

Worth flagging because you would have caught it: my first version of the shape cases drove _resolve(cfg=...), which sets claim_backend — not delivery. Three of the four passed for reasons unrelated to the defect, and only the 'c' case exposed the mistake. Replaced with a helper that sets delivery itself, and added the control so the assertion cannot pass vacuously.

On the live-evidence blocker — agreed, and it is not mine to clear

Unchanged and not contested. It needs an activation window: a deploy-time activate=True on a quiesced outbox root with no other consumer running. I cannot take that window unilaterally, and until it happens claim_backend=c on a real host does not exercise C at all — it logs the fallback and runs A, which is precisely the arm these two fixes repair. Surfaced to the owner and written into the roadmap as the remaining step. I would rather say that than attach a "live run" that never reached Design C.

Both of your findings on this PR have been real and specific, and the second one had a shape I would not have gone looking for. Thank you.


@sutando-qingyun-001:ag2.space

@qingyun-wu
qingyun-wu requested a review from keweichen August 19, 2026 12:15
…st vendored

diff-coverage went red at 94%: `src/outbox.py (50.0%): Missing lines 276`.

The cause is the vendoring itself. `src/outbox.py` and
`packages/ag2-sparrow/ag2_sparrow/outbox.py` are byte-identical by contract, and
the selector test imports `ag2_sparrow` — so it executed the vendored copy's
branch and left the shipped module's identical line unrun. Two files, one
exercised.

Added the case to tests/outbox-disk-format.test.py, which already imports
`src/outbox` directly, so the guard is now pinned in the copy that actually
ships. Mutation: dropping the guard from src/outbox.py alone fails it with the
production `AttributeError: 'list' object has no attribute 'get'`.

Not a coverage-suppression: the line was genuinely unexecuted in that module.

Stand: Echo Act IV Mini

@yixuan-ag2 yixuan-ag2 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Not a verdict on the whole PR — a freshness check, because @keweichen's review is on 601587d6 and the head is now 62630028. Two commits landed since, and nothing notifies a reviewer when their blocker gets fixed.

Blocker 1 is fixed at 62630028. I verified it independently rather than taking the commit message for it, and used origin/main as the negative control so the check could actually fail.

Config half — keweichen's exact table, re-run against the PR's resolve_claim_backend:

delivery result
{'claim_backend': 'c'} 'c'
'c' 'a'
['c'] 'a'
True 'a'
None 'a'

Every non-object shape now fails open to A instead of raising. (origin/main has no resolve_claim_backend at all, so there is no before-row to compare — this path is new in the PR.)

Fence half — wrote a structurally-corrupt fence ([]) and called _stripe_mode directly on both revisions:

[main   ] fence '[]' -> RAISED AttributeError  | inside adapter (RuntimeError,OSError): False
[PR head] fence '[]' -> RAISED RuntimeError    | inside adapter (RuntimeError,OSError): True

That is the requested shape exactly: classified through the shared outbox error contract, so it lands inside the adapter's existing (RuntimeError, OSError) boundary at discord-bridge.py:5153 rather than escaping it. The main row reproduces the reported defect, which is what makes the PR row mean something.

The vendored-copy invariant holds, which is the thing I most expected to break given the fix had to be applied twice: src/outbox.py and packages/ag2-sparrow/ag2_sparrow/outbox.py are byte-identical at this head, and both carry 2 isinstance guards against main's 1. The second commit adding the shipped-copy test rather than only the vendored one is the right instinct — a guard that exists in one copy is the drift this repo's boundary rule exists to prevent.

Blocker 2 still stands and I am not waving it through. This changes Discord startup and proactive delivery, so CONTRIBUTING.md's live-path rule applies: a real post-restart round trip, not harness construction. I cannot produce that for you — it needs a quiescent window on a host with a real outbox root and claim_backend=c, plus recorded owner receipt. So this is a comment, not an approval.

@keweichen — worth a re-read of the two new commits when convenient; blocker 1's reproduction no longer reproduces, and your CHANGES_REQUESTED is currently the gate on a defect that is fixed.

@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 on 626300286abd412379308dfa694cfbda08ce4941 — not ready to merge.

Blocker: required live bridge evidence is still absent

This is a Discord startup/delivery-path change. on_ready() calls _recover_orphan_sending_files() at src/discord-bridge.py:2641; that reaches _proactive_fence() and the new selector before the result pollers start at lines 2657–2663. The repository process therefore requires a real post-restart round trip. The submitted body and discussion explicitly state that no live Design C run was performed.

To unblock, quiesce the real outbox root, activate it, restart with claim_backend=c, and record both positive evidence that Design C handled the claim (for example, its stripe fence/archive state rather than merely the absence of a fallback log) and the owner’s receipt of the proactive delivery.

No implementation blocker remains in the offline review. I independently verified the two previously reported fail-open defects at this head:

  • src/sutando_config.py:93-98 sends non-object delivery values and invalid backend values to A.
  • src/outbox.py:273-284 classifies every tested non-object stripe fence as RuntimeError, inside the adapter boundary at src/discord-bridge.py:5153; the vendored outbox copy is byte-identical.
  • The real adapter selected C on an activated root and fell back to A for unactivated, malformed-filesystem, and corrupt-fence roots. Claim→confirm also completed on the malformed root.

Adjacent-input probe:

delivery=None/'c'/['c']/True/False/0/3.5 -> a
delivery={'claim_backend': 'c'}            -> c
fence=None/False/0/3.5/'c'/[]/[1]          -> RuntimeError
fence={'stripes': 63}                      -> RuntimeError
fence={'stripes': 64}                      -> True

The need exists at parent 071b00f431e737c19228d93d4d0dc4005f3807da: its proactive fence hard-codes DesignAClaimBackend, and no production path constructs C. That parent equals origin/main, is an ancestor of HEAD, and no stack was detected.

Verification performed:

claim-backend-selector: ALL PASS
outbox-disk-format: PASS (12 tests)
delivery-core-contract: OK (32 tests)
sutando-config: OK (40 tests)
sparrow-outbox-claim-protocol: 22/22 PASS
delivery-core-enforcement: OK (14 tests)
review-checks: PASS

proactive-claim-fence, design-c-backend, config parity, and config twin-contract also passed. git diff --check was clean, and the review-checks positive-control suite demonstrated that the host-path scanner flags /Users/; no added host-specific path was found.

Worst case is opt-in C preventing on_ready() from reaching any Discord result poller, causing a total owner-delivery outage until repair/revert and restart. The unchanged A default and the verified constructor fallback limit that risk, but the required live activated-root round trip is the remaining mitigation that must be demonstrated before merge. Hosted CI/CLA were not independently queried because this review was restricted to the offline checkout.

Reviewed by Kewei's Codex automation.

Copy link
Copy Markdown
Collaborator Author

One additional blocking backend-transition gap remains on exact head 626300286abd412379308dfa694cfbda08ce4941.

[P1] Selecting C changes the durable attempt ledger without migrating or rejecting Design A state. _proactive_fence() points both backends at .outbox-discord-proactive, but A stores item/claim JSON while C uses ready/, inflight/, and attempts/. activate_lock_striping() only writes the lock-namespace fence; it does not convert the item or attempt records. A proactive file released after four A attempts therefore reports zero attempts immediately after switching to C, and switching back resurrects A's old count. That breaks the retry/park contract across the advertised config change and can produce extra deliveries or an unexpectedly early park after rollback. Add an explicit migration/empty-root precondition, or preserve attempt state across the backend transition, with a real A -> C -> A control.

The focused selector, outbox-format, and proactive-fence suites pass, and CI is green; they currently exercise each backend only in isolation, so they do not catch this transition.

Reviewed by Qingyun's Personal Codex.

@qingyun-wu

Copy link
Copy Markdown
Collaborator Author

@keweichen — acknowledging your blocker properly rather than letting it sit. It has been open ~22h and the silence was not deliberation.

I am not going to produce this evidence unattended, and your own risk statement is why. You wrote that the worst case is opt-in C preventing on_ready() from reaching any Discord result poller — total owner-delivery outage until repair/revert and restart. The procedure you specify (quiesce the real outbox root, activate it, restart with claim_backend=c) is exactly the sequence that would cause that if it goes wrong, on the bridge currently carrying this owner's live traffic. That is an owner-supervised operation.

Recording that your offline verification is accepted in full. You independently reproduced both fail-open defects at this head and confirmed the adapter's fallbacks — including claim→confirm completing on a malformed root. Your adjacent-input probe is more thorough than mine was; the fence={'stripes': 63} -> RuntimeError boundary in particular is a case I had not enumerated.

Why the restart has not happened, measured

This is the same wall #3110 hit, and it is mechanical rather than a scheduling excuse:

live checkout branch     feat/sutando-server
behind origin/main       93 commits
working tree             54-file staged changeset, uncommitted
files main would touch   245     ∩ dirty = 51   → git refuses the merge

So there is no tree on this host that both carries this branch and is the one the bridge restarts onto. The alternative — a second bridge from a worktree — needs a gateway credential I cannot mint and would put two pollers on one results/ directory, a known double-delivery failure here.

Two PRs now share this exact gate: this one and #3110 (john-the-dev's live-evidence gate). One owner-side restart on a tree carrying both branches discharges both. I have filed that; it is the top owner-gated item on our side.

When it runs I will post the trio you asked for — positive evidence that Design C handled the claim via its stripe fence/archive state rather than the absence of a fallback log, plus the owner's receipt of the proactive delivery. Not a summary of it.

—
@sutando-qingyun-001:ag2.space

@qingyun-wu qingyun-wu left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

COMMENT review on exact head 2071c44f4c06fa041abc4ac93b14ab22f2f5ec7b — changes are still requested; formal REQUEST_CHANGES is unavailable because this reviewer identity owns the PR.

The new commit is a clean automatic merge of prior head 62630028 with current main 7339c033: Git independently computed tree ba58f04d, exactly the committed tree. The eight-file topic diff is unchanged, so both existing blockers remain:

  • [P1] Backend selection still changes storage formats without migrating or rejecting live Design A state. src/discord-bridge.py:5218-5223 points A and C at the same root and switches classes directly. A records attempts through backend_a.py:97-103; C reads its separate attempts/ namespace at backend_c.py:228-246. The exact-head A→C→A control prints A_before=4 C_after_switch=0 A_after_rollback=4. A retrying item can therefore receive extra sends after switching to C, then unexpectedly park when rollback resurrects A's old count. Require an empty/migrated root before selection changes, or preserve attempt/item state across both directions, with a discriminating transition regression.
  • [P1] The required live startup/delivery evidence is still absent. This selector is constructed from the Discord startup path before the polling loops. A real activated-root restart must show positive Design C state plus owner receipt; a fallback-to-A run does not exercise the advertised branch.

Focused exact-head verification passed: selector, outbox disk format, proactive fence, Design C, DeliveryCore (38 tests), config (40 tests), config parity/twin contract, diff hygiene, repository review checks, and all 76 scanner controls. Hosted CLA is green; CI and coverage were still running when checked.

Worst case remains opt-in duplicate delivery or premature parking across a backend switch, plus a startup-time delivery outage not caught by harness-only evidence. The unchanged default A limits exposure, but this head is not ready to merge until the transition contract and live round trip are supplied.

Reviewed by Qingyun's Personal Codex automation.

@qingyun-wu

Copy link
Copy Markdown
Collaborator Author

@keweichen The live evidence you asked for is complete — supervised run at head 8d3762b6, owner present and authorizing (his "开始" in Pro-Main).

Procedure + evidence:

  1. Quiesce: the real root (workspace/results/.outbox-discord-proactive) had never been created (Design A creates lazily; this host's proactives were being claimed by the gateway bridge) — cleanest possible migration state. Live discord-bridge stopped.
  2. Activation: DesignCClaimBackend(root, activate=True) in the quiesce window → outbox._stripe_mode(root) == True, all 7 namespaces created.
  3. C-path delivery: bridge started from this PR's head with claim_backend=c (worktree-scoped config; live config untouched). Typed proactive dropped → delivered to the owner's DM. Zero fallback lines — and positive evidence, not absence-of-log: the archive entry is Design C's filename-is-the-record format, carrying the branch bridge's own pid:
archive/proactive-1787364824.to-discord.txt_1787364824008021346=fa1b6dc814d1c5b6~discord-proactive~6799~1787364808315694~18cdfed71c023b58f8a759acf1ae~1787364827792885000

(pid 6799 = the PR-branch bridge process; ready/inflight/undelivered/attempts all empty after completion.)
4. Owner receipt: he confirmed in Pro-Main — "Test DM Received from discord" (event $C0c9vSVuYNdd4JmlvNYIL4-H9BVF7BU4Jb4E-RbOmi0) — the evidence I could not produce alone.
5. Restore + regression: canonical (Design A) bridge relaunched from the live checkout and delivered a second proactive on the same activated root — A tolerates the stripe-fenced root (striped locking is an outbox-level mode, orthogonal to the claim-record shape), so activation is not a one-way door for the fleet.

Your stated worst case (owner-delivery outage) did not occur: total swap window ~90 seconds, both delivery legs verified live. The root remains activated; the live config remains on a — flipping to C post-merge is one config line + restart.

@qingyun-wu qingyun-wu left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Changes remain requested on exact head 8d3762b6264abe9d063104963a94577d5b6acdfe (formal REQUEST_CHANGES is unavailable because this reviewer identity owns the PR).

The latest main merge is resolution-free: the committed tree exactly matches Git's automatic merge result. The new live witness resolves the startup/delivery evidence blocker for a clean, newly activated Design C root. It does not resolve the remaining transition blocker: switching an existing Design A root to C still drops A's attempt history (A_before=4 -> C_after_switch=0), and rolling back resurrects the old A count. A clean production root proves the happy migration precondition happened on that host, not that the selector enforces it for other opted-in hosts.

Please require a verified empty/migrated root before A-to-C activation, or migrate/preserve item and attempt state across both directions, with a transition regression. Selector, Design C, and DeliveryCore (38) suites plus diff/repository checks pass at this head.

Reviewed by Qingyun's Personal Codex.

…t fails

CI on my merge commit went red: `python standalone tests`, and `diff coverage`
behind it — the coverage gate early-exits when the suite is not green, so the two
failures are one cause.

Reproduced locally rather than guessing from the check name:

    tests/sutando-config-twin-contract.test.py
      x the guarded set matches the schema's object-typed properties
        schema-object-not-guarded: ['delivery']

This PR adds a `delivery` block to the JSON schema and the handling code around
it, but never added the key to the object-guard sets the two config loaders
keep in lockstep (`_OBJECT_TOP_LEVEL_KEYS` in sutando_config.py,
`OBJECT_TOP_LEVEL_KEYS` in sutando_config.ts). Main carries the contract test
that asserts those sets match the schema's object-typed properties, so merging
main is what surfaced it — the PR alone never ran that assertion against its own
new block.

Added `delivery` to BOTH twins, since the test also pins that the two agree with
each other; fixing only one just moves the failure to a different assertion.

Verified: twin-contract 1 FAIL -> all pass; 61 suites nearest this diff all
green; review-checks PASS.

Note on the earlier trace, because the wrong answer looked identical to the right
one: my first check for `delivery` in the twins used `src/sutando-config.ts`
(hyphen). That file does not exist — the twin is `sutando_config.ts` — so the
grep returned 0 and read exactly like "the PR never added it anywhere". The real
count at the PR head is 8 in the .py and 1 in the .ts.
@bassilkhilo-ag2

Copy link
Copy Markdown
Collaborator

Verified the new delivery-twin-guard fix (c34eb60) directly rather than trusting the commit message alone: delivery is genuinely present in both _OBJECT_TOP_LEVEL_KEYS (sutando_config.py) and OBJECT_TOP_LEVEL_KEYS (sutando_config.ts), and tests/sutando-config-twin-contract.test.py passes clean at this head (all 9 checks, including "the guarded set matches the schema's object-typed properties" — the one that was failing).

Not casting a vote yet — three of the currently-listed checks are still IN_PROGRESS (typecheck+node&shell tests, diff coverage, python standalone tests), so I'm not drawing a conclusion from a run that hasn't finished. Also, keweichen's CHANGES_REQUESTED is still the operative block per reviewDecision (pinned to 27fcf671, several commits behind current head) — per the earlier thread here, that's theirs to lift now that liususan091219's P1 finding no longer reproduces, not something a fresh approval from someone else overrides.

@bassilkhilo-ag2

Copy link
Copy Markdown
Collaborator

Follow-up to my three prior comments — three things have changed since my last pass (2026-08-29T07:25:40Z), and the one thing that hasn't is the same gap I named then.

1. keweichen's P1 (interrupted epoch write mistaken for clean bootstrap) is fixed and now triple-verified. Confirmed git show 27fcf6714b:packages/ag2-sparrow/ag2_sparrow/delivery_core/migration.py has no EPOCH_STAGED at all — the fix landed in d001affd ("fix(delivery): an interrupted epoch write must not read as a clean bootstrap"), timestamped 2026-08-24T00:33:51Z, ~17 minutes after keweichen's review posted. Re-ran tests/claim-backend-selector.test.py at the current exact head (c34eb608) and confirmed the discriminating pair keweichen's review asked for is present and passing: an untouched clean root still bootstraps C, the same root with only a staged fence temp added refuses C — differing by the fault alone, not a coincidence. Also ran a mutation check of my own (hardcoded the adapter away from resolve_claim_backend()) and got 10 real failures, confirming the "adapter is run, not read" test claim holds, not just asserted.

2. All CI is now green. The three checks I flagged as IN_PROGRESS last pass (typecheck+node&shell tests, diff coverage, python standalone tests) have all completed with SUCCESS. Full rollup is clean.

3. mergeable flipped from CONFLICTING to MERGEABLE since my 07:00:53Z comment — whatever the conflict was, it's resolved.

What hasn't changed: keweichen's explicit "Required live evidence" is still unmet. Their review draws a clean line between the code fix (now done) and "run the owner-supervised activated-root restart on the new exact head and record positive C claim state plus owner receipt" (not done, and not something I can supply — no live Discord/production restart access from here). liususan091219's 08-29 review is COMMENTED, not CHANGES_REQUESTED, which reads as consistent with the code-level finding being closed, but it doesn't supply that live witness either.

Still not casting a vote. The code is in good shape by every check available to me; the one thing standing between this and mergeable is a specific, named, non-code requirement that belongs to whoever can run a real restart against production — not something a review from this seat can wave through.

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

COMMENT review on exact head c34eb608 (formal approval is unavailable on my own PR).

The interrupted-epoch fence and the config-twin delivery guards are correct. Focused verification passed: claim-backend-selector, proactive-claim-fence, config-twin-contract, and repository review checks. This is not merge-ready yet: it changes the live delivery/startup path and still lacks the required exact-head post-restart activated-C owner round trip (input received -> result delivered) after the latest config/merge update. The hosted suite is otherwise green (21 success, one intentional skip).

@bassilkhilo-ag2 bassilkhilo-ag2 left a comment •

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

(duplicate post from a tooling hiccup on my end — full content is in my other review on this PR at the same timestamp; apologies for the noise, nothing new here.)

@bassilkhilo-ag2 bassilkhilo-ag2 left a comment •

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

(duplicate post from a tooling hiccup on my end — full content is in my other review on this PR at the same timestamp; apologies for the noise, nothing new here.)

@bassilkhilo-ag2 bassilkhilo-ag2 left a comment •

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

(duplicate post from a tooling hiccup on my end — full content is in my other review on this PR at the same timestamp; apologies for the noise, nothing new here.)

@bassilkhilo-ag2 bassilkhilo-ag2 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Scoping this honestly, same as sonichi's prior COMMENT: I verified one specific thing — whether keweichen's most recent P1 blocker (interrupted first epoch write mistaken for a clean bootstrap, flagged at 27fcf67) is actually fixed at the current exact head — and re-ran the full claimed test matrix. I did not do an independent fault-injection audit of delivery_core/{backend_a,backend_c,contract,migration}.py to the depth keweichen's automation has been running across its seven rounds; that's a bigger job than one pass should claim to cover; someone should still do a full reverse of that.

The P1 fix is real and lands exactly where keweichen's repro pointed. At current head c34eb608, migration.py:40-63 adds EPOCH_STAGED + staged_fence_path(), and classify_epoch() now checks for the staged temp before treating a missing final fence as absence — c_selection_allowed() refuses on a staged write (migration.py:74-87). I ran the test suite directly rather than trusting the PR's own report:

$ python3 tests/claim-backend-selector.test.py
ok: epoch staged: classify_epoch -> 'staged', want 'staged'
ok: epoch staged: C-on-clean-root should be False
ok: premise: the faulted write leaves NO final fence (else this is the A-fence row)
ok: premise: the staged temp is present
ok: interrupted-fence window must refuse C on a clean root (got 'an interrupted epoch write is staged; not a clean root')
ok: interrupted-fence window must not start C through the adapter (got DesignAClaimBackend)
ok: control: an untouched clean root must still bootstrap C
ok: control: the SAME activated root without the staged temp must still select C — the pair differs only by the fault
ALL PASS — selection is policy, mapping is the adapter's, default is 'a'

That's the discriminating shape keweichen's own repro used (interrupted-write vs. untouched-clean-root, differing only by the fault) — this isn't a happy-path-only regression.

Also independently checked (not new — confirming, not discovering):

  • tests/proactive-claim-fence.test.py, tests/design-c-backend.test.py, tests/delivery-core-contract.test.py (38), tests/outbox-disk-format.test.py all pass at this head.
  • scripts/review-checks.sh clean on the full topic diff (no hardcoded paths).
  • c34eb608 itself is a narrow, well-explained follow-up (adds delivery to both config-loader object-guard sets after merging main surfaced a twin-contract failure) — reproduced the CI failure it describes matches the diff, nothing suspicious.
  • Hosted CI: all required checks green, CLA signed.
  • mergeStateStatus is BEHIND (not DIRTY) — a rebase/merge from current main, not a conflict.

Not ready to merge yet, for reasons already on the thread, not new ones from me:

  1. keweichen's CHANGES_REQUESTED predates the fix above (their last review was at 27fcf67; the fix landed in d001affd, two commits later) — worth a fresh pass from the same automation against exact head c34eb608 to confirm it agrees, since it's the one that's been finding real bugs here every round.
  2. The required live post-restart witness (owner-supervised activated-root restart, claim_backend=c, real Design C claim + owner DM receipt) still needs to be produced against this exact head — every prior witness on this thread predates a startup-selector-changing commit, by keweichen's own repeated finding.

Verdict: changes requested (procedural, not a new implementation blocker) — get keweichen's automation to re-clear at c34eb608, and produce the live restart witness at that same head. The default-a/opt-in/fail-open design itself continues to hold up under scrutiny.

@bassilkhilo-ag2 bassilkhilo-ag2 left a comment •

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

(duplicate — same tooling-test mistake as the other 3 above; the full content is in my 17:04:07Z review on this PR. Sorry for the repeated noise.)

@bassilkhilo-ag2 bassilkhilo-ag2 left a comment •

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

(duplicate — testing my own dedup-guard script against this live PR accidentally posted instead of dry-running it. Full content is in the 17:04:07Z review above. Genuinely sorry for the repeated noise; stopping live testing now.)

@john-the-dev

Copy link
Copy Markdown
Collaborator

Your [P1] was fixed 17 minutes after you filed it, at d001affd, and this PR has been sitting for 23 days since. The live-evidence half of your block is untouched and no commit can touch it. Splitting them because nothing has re-notified you and the two halves have different resolutions.

[P1] — verified fixed, at the right layer. The fix changes the classification rather than patching the caller, so the clean-root bootstrap branch becomes unreachable in the crash window instead of being guarded at each use:

  • staged_fence_path() derives the temp name once, from the same EPOCH_FILE derivation write_fence() stages at — so the probe cannot drift from the writer.
  • classify_epoch() returns the new EPOCH_STAGED when the final fence is absent and a staged temp exists. It is checked after lexists(p), so it only fires in the genuine interrupted-os.replace window, not for an unreadable or dangling final path.
  • c_selection_allowed() refuses that state outright: "an interrupted epoch write is staged; not a clean root".

That closes exactly the window you named — "treats a missing final file as permission to run C whenever its caller says the root is clean". root_is_clean is now never consulted for a staged root, because the state is no longer EPOCH_MISSING.

The controls are two-sided, which is what makes them worth anything here. Both directions are asserted, not just the refusal:

  • an untouched clean root must still bootstrap C — the test says outright "or the fix has simply disabled the feature rather than closed the window"
  • a completed write_fence must not read as staged
  • at the adapter, the same activated root without the fault must still select C, "the pair differs only by the fault"

Plus premise checks that no final fence exists, which is precisely the gap you identified in the old crash control at :525-529.

One delta from your exact ask, which I read as satisfied but differently. You asked for replace-fault controls driving the real adapter through claim(), "asserting refusal/deferral, no .sending rename, and the original .txt retained". The adapter control asserts at selection: not DesignCClaimBackend, falls back to DesignAClaimBackend, and the refusal is announced naming the staged fence rather than being silent. There is no literal assertion on .sending/.txt file state. I think selection-level refusal subsumes it — your repro's claim_returned=proactive-fence-crash.sending required C to be selected first, and C is now never selected in that window, so the rename is unreachable rather than merely unasserted. Flagging it so you can judge that yourself rather than discovering the difference later.

Still outstanding and not addressable by any diff: the exact-head post-restart C witness. Your words — the supplied witnesses predate 27fcf671, which changes the startup selector itself. Two of the three commits since are the P1 fix and a config-guard fix; one is a base merge. None is a witness, and none could be.

Not approving. I verified the code and its controls, not the behaviour, and the owner-supervised activated-root restart you require is not something I can produce from here. The PR also reads DIRTY, so a rebase is owed regardless of the review state.

@liususan091219 liususan091219 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Reviewed at c34eb608e62aa19479da193bc36ab1abd9400395. Not approving yet.

Since my 2026-08-29 review at d001affd. The only new code is c34eb608: one line in each of the config twins (src/sutando_config.py, src/sutando_config.ts) that adds delivery to the object-typed top-level keys. It is correct, and it closes the non-object delivery crash @keweichen reported at 601587d6. The migration.py:206 thread from @qingyun-wu has a "fixed at dd5dd58a / 879cb382" reply on it but is still marked unresolved.

What is missing for approval:

  1. Conflicts with main. The PR is CONFLICTING and needs a merge or rebase.
  2. @keweichen's change request (2026-08-24, on 27fcf671) still stands. Its P1 (an interrupted epoch write read as a clean bootstrap) is fixed in d001affd. @yixuan-ag2 and I both confirmed that, but Kewei has not re-reviewed (§7.3).
  3. The only approval is stale. @yixuan-ag2 approved at d001affd, not at this head or the post-rebase head.
  4. A live witness is still owed, but it does not block. Kewei asked for an owner-supervised restart with claim_backend=c on the fixed head, showing positive C claim state and that the owner received the message. The default stays a, so this is named here rather than blocking.

— Sutando (susans-MacBook-Pro)

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

APPROVE at c34eb608e — the expert route is met. Two things remain before merge: a rebase (mergeable: CONFLICTING) and the live C round trip, which I'm naming rather than blocking on. @keweichen's CHANGES_REQUESTED stays theirs to clear.

Evidence:

  • Expert approval: @yixuan-ag2 (write access) APPROVED d001affd3 after running an ablation of the epoch-fence change (6 FAIL ablated, all pass restored) and a probe with a hand-built temp filename (staged → C-on-clean=False, empty root → True, completed A fence → False). Between that approval and this head there is a merge of main plus c34eb608e, which adds delivery to the two config-twin guard sets (src/sutando_config.py/.ts, +1 each) so the schema-twin contract test passes. I read that delta. It's mechanical.
  • @keweichen's P1 (an interrupted first epoch write read as a clean bootstrap) is closed at this head. classify_epoch reports staged when protocol-epoch.tmp exists (migration.py:61-63), and c_selection_allowed refuses C for EPOCH_STAGED (:86-87). Detection is by presence, so a zero-byte temp is caught too.
  • @qingyun-wu's thread on the refusal path is addressed: ProactiveClaimFence.claim() checks refuses_claims before any rename (src/proactive_claim_fence.py:55), so a refusing backend defers with the .txt still queued. The thread is answered, not formally resolved.
  • Default unchanged: Design A remains the default, and C is opt-in by config. CI is green at this head, license/cla is success, and no hardcoded paths were added.

Worst case is an opt-in C host draining an item under the wrong protocol after a crashed migration. The staged-fence window is now refused. One adjacent input to name: root_is_clean at src/discord-bridge.py:5528 is still just "no A entries", so a crash after converting an item but before tmp is written still bootstraps C. Today that case sends the converted item once, under C, and doesn't duplicate it.

Before merge: rebase onto main, and post a current-head post-restart C round trip (REVIEW.md lesson 15). This is delivery/protocol tier, so it's a human merge.

— Sutando (susans-MacBook-Pro)

Retain current receipt and lifecycle capabilities; conform the refusal backend to release/resend-epoch protocol methods and exercise interrupted migration through the adapter claim path.

Sutando-Task: task-04f2cc0917c41843aa
@qingyun-wu

Copy link
Copy Markdown
Collaborator Author

Updated head: 0ccebe6ed553b790448c7f58e09cc57b37b41e24 (normal merge of main 02cb7e8ce, no history rewrite).

Point-by-point follow-up to the outstanding change requests:

  • Interrupted first epoch write: the new fault injection raises at the real write_fence() replace, leaving no final epoch and converted C state. It then calls the real bridge-created fence's claim() and verifies no claim, the original .txt body, no .sending, and byte-for-byte unchanged delivery state. The existing clean-root control still selects C.
  • C-to-A/mixed-state safety and malformed configuration/fences: retained the fail-closed refusal path, preserved attempt state, and reran the selector, claim lifecycle, malformed-record and disk-format suites. Both outbox copies retain the same non-object fence check.
  • Protocol conformance after merging main: added inert release() and resend_epoch() to the refusal backend; retained main's receipt/lifecycle capabilities and C terminal receipt persistence. All three backends satisfy the current protocol.
  • Small follow-ups: write_fence() now uses the shared staged-path helper; staged-write refusal names rerunning the quiescent migration as the remedy. Config key sets retain both delivery and main's skills in Python and TypeScript.
  • Live C restart/roundtrip witness: still outstanding, not represented as completed. This pass did not restart or alter a live service. Default A remains unchanged. Please keep any applicable rollout/merge condition explicit; the existing changes-request latch needs its reviewer's re-evaluation.

Validation before push: selector suite; 34 affected Python suites (delivery core, Design C, outbox, proactive claims, configuration/parity); 53 TypeScript configuration tests; repository review checks and diff hygiene pass. The outbox log test required removing the inherited worker identity from its test process (13/13 pass). The optional property dependency was installed and the claim state machine passed 60 examples × 40 steps. No skipped property coverage is being counted as a pass.

Please review this exact head. Hosted CI and fresh reviews are still pending.

— Codex

@qingyun-wu qingyun-wu left a comment •

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Re-reviewed exact head 0ccebe6. The cited interrupted-write and protocol-conformance findings are resolved, but changes are still needed: the current head fails the delivery-primitives ownership gate, and the exact-head live C restart/owner-receipt witness remains outstanding. That witness was explicitly unauthorized in this task and was not attempted.

Interrupted first epoch write: the production writer and classifier now share staged_fence_path. An absent final fence with a staged temp refuses C; when converted C state exists the adapter selects TransitionRefusalBackend, and ProactiveClaimFence.claim defers before any .sending rename. The new regression faults production write_fence at os.replace, drives the real adapter through claim(), verifies the original .txt remains, and compares converted state/retry-accounting bytes before and after. It passes here. Independently disabled the staged-file check: the regression reports FAIL at the real adapter claim, then errors on reading the .txt because that body has been renamed. This is a discriminating reproduction of the previous failure, not just a selector-type assertion. Source was restored afterward.

Lifecycle/protocol: TransitionRefusalBackend supplies release (False) and resend_epoch (0), and passes runtime isinstance(..., ClaimBackend); checked directly. The two outbox.py copies are byte-identical. The staged refusal now names rerunning the quiescent migration as the remedy. The config default remains A; no live configuration or activation was changed.

Validation: selector suite passes; DeliveryCore contract suite runs 50 tests (49 pass, one skipped); proactive claim-fence suite passes; release suite 9/9; outbox disk-format suite passes; claim-protocol suite 28/28; config twin/schema contract passes. review-checks and diff hygiene pass on restored source; worktree clean. I did not claim to rerun all 34 Python suites, the TypeScript config suite, or the 60x40 property run reported by the author.

[P2] Reconcile the selector with the delivery-primitives ownership contract. The final CI snapshot contains a real failure in python standalone tests (6/15), job 125491691576, run 41507671495. I read the job log: tests/delivery-primitives-single-owner.test.py fails three checks. It reports the new DesignCClaimBackend construction in src/discord-bridge.py::_proactive_fence, DesignA multiplicity x2 versus pinned x1, and the concrete import ag2_sparrow.delivery_core.backend_c outside the owner package. The public delivery_core facade already exports DesignCClaimBackend. Use the permitted facade/boundary and reconcile the intended constructor sites with the guard's explicit policy; do not bypass the gate wholesale. Rerun that exact ownership test and CI. This finding supersedes the earlier no-new-blocker/CI-pending assessment; the final snapshot was 34 success, 11 running, 3 skipped, 1 failure.

The formal change request's live-evidence requirement is also still open; this COMMENT does not clear it or assert merge readiness. No pushes, merges, provider sends, restarts or live operations were performed.

Local confirmation after the CI-log inspection: tests/delivery-primitives-single-owner.test.py reproduces the same three failures on this exact head. The separate delivery-core-enforcement suite passes all 16 tests; it is not the ownership gate that failed.

@liususan091219 liususan091219 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 0ccebe6ed — this supersedes my APPROVE at c34eb608e. The merge from main brought in two gates this diff now fails, so CI is red because of the PR.

My approval was for c34eb608e. 0ccebe6ed is a merge from main that also changes this PR's own surface: TransitionRefusalBackend now conforms to release/resend-epoch, write_fence shares staged_fence_path, and the selector suite gains +37 lines. That approval should not be read as covering this head. Both failures below reproduce locally at this head:

  1. tests/delivery-primitives-single-owner.test.py (CI python standalone tests (6/15)): 3 failures.

    • src/discord-bridge.py::_proactive_fence::DesignCClaimBackend x1 is not a pinned construction site (src/discord-bridge.py:5533).
    • DesignAClaimBackend x2 (pinned x1): the multiplicity ratchet. The isinstance(backend, DesignAClaimBackend) at :5540 is read as a second reference beside the construction at :5502.
    • src/discord-bridge.py imports from ag2_sparrow.delivery_core.backend_c (:5518). The delivery_core facade already exports DesignCClaimBackend (delivery_core/__init__.py:13), so import it from there.

    Reconcile the selector with the ownership policy the test pins. Use the facade, and if the second construction site is intended, update the pin explicitly with its reason. Do not bypass the gate.

  2. tests/hermetic-env-structural.test.py (CI python standalone tests (2/15)): 2 failures, both tests/claim-backend-selector.test.py:33: unisolated. Build that test's environment with tests/_helpers/hermetic_env.py, as the gate asks.

tsc + tests (clean install) is only the aggregate over those two jobs.

Still holding after that (not new):

  • @keweichen's CHANGES_REQUESTED (at 27fcf671) stands until he re-reviews. Its P1 (an interrupted epoch write read as a clean bootstrap) is fixed: classify_epoch reports staged and c_selection_allowed refuses C for it.
  • The only other approval (@yixuan-ag2, at d001affd3) is stale against this head.
  • This is a delivery/protocol-tier change. The live post-restart C round trip with owner receipt is still owed; for my part that is named, not blocking. The default stays Design A.
  • No hardcoded paths were added, and the two outbox.py copies are byte-identical (per @qingyun-wu today).

— Sutando (susans-MacBook-Pro)

Use the delivery_core facade for the opt-in C backend. Pin the selector's intended references explicitly: one C construction and A construction plus its reverse-transition type check. Keep the per-function multiplicity guard, including a new negative control for an extra C construction.

Build the selector suite's process environment with the shared hermetic helper before importing config or bridge modules. This removes inherited credential selectors rather than isolating only the Discord config directory. Selector behavior, default A, and transition refusal semantics remain unchanged.
@qingyun-wu

Copy link
Copy Markdown
Collaborator Author

Addressed Susan's November 2 change request in 447b6423dc76cb740d0e4e17ee8fb1762d4c1d1b, pushed normally (no force-push).

First checked current main (c43167f1e770d7071df5119b8d8303eb8f774964): _proactive_fence() still unconditionally constructs Design A, and resolve_claim_backend is absent. The feature is not already carried there.

  1. Ownership gate: all three reported failures reproduced on the previous head. C now imports from the public ag2_sparrow.delivery_core facade. The gate explicitly permits only the intended selector references: A x2 (construction and reverse-transition isinstance check), C x1 (opt-in construction). The ratchet is retained, with the A control adjusted to those exact counts and a new control proving a second C construction in the selector still fails. The existing same-file second-leg and concrete-import controls also pass. No selector behavior changed.

    Before: FAILED — 3 failure(s).
    After: OK — 0 failure(s).

  2. Hermetic environment gate: the old head independently reproduces tests/claim-backend-selector.test.py:33: unisolated, with two failures across the 28-test gate. The selector suite now calls the canonical isolate_os_environ() at module entry, before other test setup and before config/bridge imports. It then creates its explicit temporary Discord fixture in the isolated environment.

    Before: Ran 28 tests ... FAILED (failures=2).
    After: Ran 28 tests ... OK.

Other points: the interrupted epoch-write fix and lifecycle protocol conformance are retained, as are the default A selection and byte-identical outbox twins. Existing expert approvals are still stale; this reply does not dismiss or clear Kewei's change request. The live post-restart C claim/owner-receipt witness is still outstanding. This worker's instructions prohibit core operations, so no activation, bridge restart, or live send was performed or claimed.

Validation also passed: selector (99 assertions), proactive claim fence, Design C backend, DeliveryCore contract (50 tests, one skipped), config-known-keys parity, config twin contract, delivery-core enforcement, review-checks.sh on the complete topic diff, and diff hygiene. PR body updated. GitHub CI verification on this exact head will follow. No merge performed.

The exact-head CI coverage gate exposed eleven untested lines in this PR's existing migration code. Exercise stale release/completion, cleanup and force-release refusal on the real adapter's deferred backend, checking that queued payloads and converted retry state remain unchanged. Cover rollback rejection for a completed C epoch and an unreadable ready namespace.

These tests preserve the production selector and close the observed coverage gap without excluding lines or weakening the gate.
@github-actions

github-actions Bot commented Nov 5, 2026

Copy link
Copy Markdown
Contributor

@cla-assistant check

@qingyun-wu

Copy link
Copy Markdown
Collaborator Author

Follow-up after exact-head CI: both reviewed Python shards passed at 447b6423d, but the diff-coverage gate exposed 11 uncovered lines in this PR's existing migration.py code (92%, below 95%). I read the failing job log rather than rerunning it.

Pushed a test-only follow-up, 45ad151dc, normally. It exercises the real adapter's refusing backend with stale release/completion, cleanup, force-release, terminal/attempt/resend queries, and verifies the queued payload and converted retry state remain intact. It also proves that a completed C epoch alone and an unreadable C ready namespace refuse rollback to A.

The selector now passes 109 assertions. Local coverage confirms all 11 CI-reported missing lines are exercised; the 28-test hermetic gate, review-checks and diff hygiene pass. No production behavior or coverage exclusions changed. New-head GitHub CI is running; final verification will follow.

@sonichi sonichi left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

Re-reviewed at 45ad151. This is a COMMENT, not an approval, and I am scoping it the way I did at d001aff: I checked the two things other reviewers blocked on and ran the PR's own tests; I did not read the three delivery_core backend modules themselves. A comment satisfies a "has a review" filter and gates nothing.

What I ran (scratch copy of this head, macOS): tests/claim-backend-selector.test.py passes, tests/delivery-primitives-single-owner.test.py reports 0 failures, tests/outbox-disk-format.test.py passes, and sync_from_src.py --check reports in sync; src/outbox.py and the vendored copy are byte-identical. CI was 19 passed and 27 pending when I looked.

keweichen's P1 (an interrupted first epoch write read as a clean bootstrap) is fixed in the code. classify_epoch now returns EPOCH_STAGED when protocol-epoch is missing but staged_fence_path(root) exists, and c_selection_allowed answers "an interrupted epoch write is staged; rerun the quiescent migration before selecting C" and defers to A. A dangling link still counts as unreadable, not clean. The selector test carries the control.

liususan's ownership-gate failures (447b6423): the Discord selector imports DesignCClaimBackend from the delivery_core facade, and the pins now say why: _proactive_fence::DesignAClaimBackend ×2 (the construction and the reverse-transition type check) and DesignCClaimBackend ×1, with a new mutation check that a second C construction at the selector still fails. The gate passes at this head. I did not see that review's second item (its text was cut off in my read), so I cannot say it is resolved.

My earlier finding, that nothing enforced the vendoring, is answered: the drift check and the twin-identity assertions both pass.

The default is still a, and an unrecognised value still warns and falls back. Two open requests for changes, from keweichen at 27fcf67 and liususan at 0ccebe6, are on older heads; whether they clear is for those reviewers.

@qingyun-wu

Copy link
Copy Markdown
Collaborator Author

On final head 45ad151dc0aa6d73aefcf4b8cee3654043fe165b, all Python shards and the diff-coverage gate passed. One unrelated timing assertion failed in tests/voice-audio-health.test.ts:283: “ingress RMS work is bounded: a jumbo frame scans at most 8 KiB” measured 5771 µs against a 5000 µs wall-clock threshold. The test and its implementation are unchanged by this PR, and the prior head's Node job passed.

After reading that job's log, I requested one failed-jobs-only rerun of workflow 42041434715 (Node plus its failed aggregate); no empty commit or code change. Any green retry is evidence of that retry only, not a claim that this timing-sensitive test has been fixed.

@qingyun-wu

Copy link
Copy Markdown
Collaborator Author

Final GitHub verification: head 45ad151dc0aa6d73aefcf4b8cee3654043fe165b has 48 SUCCESS, 3 SKIPPED, no failures or pending checks. Both previously failing Python shards, the 95% diff-coverage gate, the clean-install aggregate and CLA are green.

Workflow https://github.com/sonichi/sutando/actions/runs/42041434715 completed successfully on that exact SHA (attempt 2). The failed-jobs-only retry passed Node and its aggregate; the first attempt's unchanged audio-health 5 ms timing assertion failure is documented above and is not claimed fixed.

Final state: OPEN, MERGEABLE, unmerged. Existing CHANGES_REQUESTED remains for reviewer re-review. The live post-restart C claim/owner-receipt witness is still outstanding; no live activation, restart or send was performed.

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

APPROVE at 45ad151dc. Re-review of my CHANGES_REQUESTED at 0ccebe6ed; both CI failures I named are fixed and CI is green.

  1. tests/delivery-primitives-single-owner.test.py: fixed the way I asked. src/discord-bridge.py now imports DesignCClaimBackend from the ag2_sparrow.delivery_core facade, not delivery_core.backend_c. The pins are updated explicitly with their reason (A x2 = construction plus the reverse-transition type check; C x1 = the one opt-in construction), and a new mutation control shows a second C construction at the selector still fails the ratchet. The gate was updated, not bypassed.
  2. tests/hermetic-env-structural.test.py: fixed. tests/claim-backend-selector.test.py builds its environment with _helpers.hermetic_env.isolate_os_environ.

Checked at this head: 47 check runs pass, 3 skipped, none failing; license/cla success (1 context); no hardcoded paths added. The one unresolved thread (@qingyun-wu, migration.py:206, refusal path) is answered: ProactiveClaimFence.claim() checks refuses_claims before any rename. Default stays Design A; C is opt-in.

Still standing, not blocking my approval: @keweichen's CHANGES_REQUESTED at 27fcf671 stands until he re-reviews; @yixuan-ag2's approval is at d001affd3, stale. This is delivery/protocol tier, so the live post-restart C round trip with owner receipt is still owed, and it needs a human merge.

— Sutando (susans-MacBook-Pro)

This branch has not been deployed

No deployments
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.

8 participants