Skip to content

feat(sparrow): B1 identity contract — canonical constructors, vectors, ratchets (B slice 2) - #3315

Open
qingyun-wu wants to merge 35 commits into
mainfrom
feat/sparrow-b1-identity-contract
Open

qingyun-wu wants to merge 35 commits into
mainfrom
feat/sparrow-b1-identity-contract

Conversation

@qingyun-wu

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

Copy link
Copy Markdown
Collaborator

Adds ag2_sparrow.identity: typed task/delivery/attempt/idempotency/incarnation values, deterministic constructors, parsers, legacy artifact adapters, and committed vectors. The delivery core delegates its existing opaque provider key to this owner while retaining the shipped bytes, including empty/long ids and resend epochs. Other adapters are not migrated in this slice.

Main ff8f113 was merged hunk by hunk. #4404 supplies provider-derived ingress but does not replace this identity package. The merge preserves current dispatch fencing, revalidation, payload selection, and resend-epoch behavior. Domain validation remains before claim; the actual send key uses the claimed item's epoch. Packaging uses main's discovery and real wheel build/install test, including the vectors and installed identity API.

The outstanding scanner findings are fixed: loop-body assignments invalidate incoming certification before body uses, so later iterations cannot inherit a stale exemption; handler names invalidate their own bindings. Runtime controls use the real constructors for two-iteration for/while loops and exception targets, with canonical controls. The purity check now varies time, entropy and pid results and compares every committed constructor vector, replacing the source-substring assertion.

Before/after evidence (python tests/sparrow-identity-ratchet.test.py, merged tree before scanner fix → final):

before: loop for/while and exception-target controls: {} != {('probe.py', 'mint'): 1}
        FAILED (failures=4)  # three scanner probes plus the stale main census
final:  Ran 34 tests
        OK

The main census is refreshed to 24 task-prefix sites over 19 functions and 12 delivery references over four functions. This includes path/fixture constructions and provider-derived ingress, not only wall-clock mints. Removed sites are unpinned; newly merged sites are explicitly named. The Unicode legacy-format comment is corrected: the public constructors yield 89-unit components for the reviewed Unicode inputs, still disjoint from the <=80-unit readable form.

Validation at merge/fix commit edf3724:

  • identity 43 tests; ratchet 34; delivery-core contract 54 (one expected backend skip); hermetic structural checker 28 — pass.
  • delivery-core enforcement, crash-between-terminal, terminal-contention, gateway runtime identity and package drift — pass.
  • real wheel/sdist build, installed API and entry-point checks — pass.
  • Ruff, repository review checks and diff hygiene — pass; local added-line coverage 98% (537/544).

Canonical documentation is updated in docs/sparrow-delivery-identity.md. This static scanner is conservative and bounded, not a general Python proof. #3296's definitions are merged. Follow-up 2511595 routes the existing 43-test identity suite through the coverage-producing standalone lane; the first hosted run passed its tests but excluded out-of-tree execution from coverage. Fresh final-head CI and reviews are required.

The only supplied live witness remains at historical head 88bcdb1. A supervised exact-head gateway restart/replay round trip (one broker delivery, one archive, no replay after restart) remains outstanding. This maintenance worker is prohibited from core operations; no live service was restarted. No merge performed.

…, ratchets (B slice 2)

The freeze doc's five identities become an importable contract:
ag2_sparrow.identity ships typed values, pure derivation (the injective
ingress constructor that ends wall-clock task minting, delivery/attempt/
idempotency/incarnation constructors with R2 lineage semantics), parse/
format round-trip, record embedding, and the R3 legacy adapter for outbox
items and delivered-sentinels. Deterministic vectors are committed; the
derive module is clock/pid/entropy-free by construction and the suite pins
it. A repo-level ratchet pins today's f"task-{...}" mint sites per file
(one-way: new sites red, strangled sites must lower the pin) and requires
any new src/ delivery_id user to import the canonical constructors.

No production behavior change: no bridge, outbox, or schema edits.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

@qingyun-wu qingyun-wu left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Changes requested on exact head b5449e457a73768bcb24ff392bdfe163ca65617f; formal REQUEST_CHANGES is unavailable because this review identity owns the PR.

[P1 blocker] escape_component() is not injective and the value grammar does not enforce its documented ASCII boundary — packages/ag2-sparrow/ag2_sparrow/identity/derive.py:21-36, types.py:1-24. Non-ASCII non-printable code points use variable-width %{ord:02X} encoding, while ordinary characters are appended unchanged. Against the production function, escape_component("\u200b") and escape_component(" 0B") both return '%200B'. Printable Unicode is also accepted unchanged even though the type contract says printable ASCII. Two distinct provider IDs can therefore collapse to one task/delivery identity, defeating the exact property this package freezes. Encode components as bytes with a fixed percent-encoding rule (or reject non-ASCII explicitly) and exhaustively pin injectivity across the admitted alphabet, including code points above 0xFF.

[P1 blocker] The parsers accept components no constructor can produce — packages/ag2-sparrow/ag2_sparrow/identity/serialization.py:16-28. _COMPONENT uses * on both sides, so the production parsers accept d:@, e:@, and :0:0 as DeliveryId, IdempotencyKey, and IncarnationId respectively. That contradicts the stated foreign-string rejection boundary and lets malformed persisted records enter as typed identities. Require at least one encoded/raw unit per component and add empty-left, empty-right, malformed-percent, and non-ASCII controls for every parser.

[P1 blocker] The identity ratchet is bypassable by ordinary source shapes — tests/sparrow-identity-ratchet.test.py:25-51,73-85. It scans only immediate src/*.py files and only the exact double-quoted token f"task-{; a new src/runtime-api/x.py, a single-quoted f-string, .format(), concatenation, or another mint helper is invisible. The direct selector control f'task-{time.time()}' returned zero matches. The delivery-id import gate has the same root-only blind spot. Use a recursive, syntax-aware census (or enforce constructor calls/imports directly) and add positive controls for nested files and adjacent spellings before treating a green result as a ratchet.

Submitted identity 13/13, ratchet 2/2, CI-coverage 4/4, committed-range review checks, and git diff --check pass. Those green suites omit the hostile cases above. Worst-case disruption is dormant until slice 3 binds the package; then a collision can conflate two inbound tasks or dedup the wrong delivery, while the current ratchet can allow new wall-clock mints without turning red. Fixing the grammar/injectivity boundary and making the ratchet demonstrably exhaustive would change the verdict.

Reviewed by Qingyun's Personal Codex.

@github-actions

github-actions Bot commented Aug 23, 2026 •

Copy link
Copy Markdown
Contributor

Coverage Gate

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

Diff Coverage

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

  • packages/ag2-sparrow/ag2_sparrow/delivery_core/core.py (100%)
  • packages/ag2-sparrow/ag2_sparrow/identity/init.py (100%)
  • packages/ag2-sparrow/ag2_sparrow/identity/derive.py (97.2%): Missing lines 91,163
  • packages/ag2-sparrow/ag2_sparrow/identity/escape.py (96.3%): Missing lines 19
  • packages/ag2-sparrow/ag2_sparrow/identity/legacy.py (100%)
  • packages/ag2-sparrow/ag2_sparrow/identity/serialization.py (97.1%): Missing lines 18
  • packages/ag2-sparrow/ag2_sparrow/identity/types.py (96.8%): Missing lines 38,76
  • packages/ag2-sparrow/tests/test_identity.py (99.7%): Missing lines 500

Summary

  • Total: 544 lines
  • Missing: 7 lines
  • Coverage: 98%

packages/ag2-sparrow/ag2_sparrow/identity/derive.py

Lines 87-95

  87 
  88 
  89 def _bounded_legacy_component(content_key: str) -> str:
  90     if not isinstance(content_key, str):
! 91         raise ValueError("legacy content key must be a string")
  92     if content_key == "":
  93         prefix = _readable_prefix(_EMPTY_KEY_FILL)
  94     else:
  95         esc = escape_component(content_key)

Lines 159-165

  159     for name, val in (("pid", pid), ("start_usec", start_usec)):
  160         if isinstance(val, bool) or not isinstance(val, int):
  161             raise TypeError(f"{name} must be an int, got {type(val).__name__}")
  162         if val < 0:
! 163             raise ValueError(f"{name} must be non-negative")
  164     return IncarnationId(f"{escape_component(worker)}:{pid}:{start_usec}")

packages/ag2-sparrow/ag2_sparrow/identity/escape.py

Lines 15-23

  15     whitespace, path separators, ALL non-ASCII) becomes fixed-width uppercase
  16     %XX per UTF-8 byte. '%' itself is always escaped, so decoding is
  17     unambiguous and two distinct inputs can never share an output."""
  18     if not isinstance(raw, str) or not raw:
! 19         raise ValueError("identity component must be a non-empty string")
  20     out = []
  21     for ch in raw:
  22         if 0x21 <= ord(ch) <= 0x7E and ch not in _RESERVED and ch not in "/\\":
  23             out.append(ch)

packages/ag2-sparrow/ag2_sparrow/identity/serialization.py

Lines 14-22

  14 
  15 
  16 def _parse(cls, value):
  17     if not isinstance(value, str):
! 18         raise ValueError(f"not a valid {cls.__name__}: {value!r}")
  19     return cls(value)
  20 
  21 
  22 def parse_delivery_id(value: str) -> DeliveryId:

packages/ag2-sparrow/ag2_sparrow/identity/types.py

Lines 34-42

  34               bounded: bool = True) -> None:
  35     if not isinstance(value, str) or not value:
  36         raise ValueError(f"{kind} must be a non-empty string")
  37     if bounded and len(value) > _MAX_LEN:
! 38         raise ValueError(f"{kind} exceeds {_MAX_LEN} chars")
  39     if not charset:
  40         return
  41     for ch in value:
  42         if not 0x21 <= ord(ch) <= 0x7E or ch in "/\\":

Lines 72-80

  72     def _components(self) -> "tuple[str, ...]":
  73         return ()
  74 
  75     def __str__(self) -> str:
! 76         return self.value
  77 
  78 
  79 class DeliveryId(_Identity):
  80     """One object crossing one boundary, once. Sparrow-owned."""

packages/ag2-sparrow/tests/test_identity.py

Lines 496-504

  496                 seen.append(idempotency_key)
  497                 raise contract.ProviderIndeterminate("boundary crossed")
  498 
  499             def reconcile(self, attempt):
! 500                 return None
  501 
  502         return core.DeliveryCore(backend_a.DesignAClaimBackend(root), P(),
  503                                  worker="w"), seen

⚠️ Not measured

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

  • packages/ag2-sparrow/tests/test_wheel_contents.py

@sonichi

sonichi commented Aug 23, 2026 •

Copy link
Copy Markdown
Owner

Reviewed at b5449e45 in an isolated worktree. Suites green: 13 + 2 = 15 tests, stdlib-only, 0.003s / 0.055s. review-checks.sh PASS on the post-image (hardcoded-paths + root-artifacts + prose-cap). Not a live path — pure derivation plus tests, no bridge/startup/delivery loop — so harness proof is the right evidence here rather than a restart witness.

Green tests aren't a review, so I mutated the load-bearing claims to check the assertions can actually fail:

mutation result
drop ~ from _RESERVED (breaks ingress injectivity) FAILED (2 failures, 1 error)
drop % from _RESERVED (breaks escape injectivity) FAILED (2 failures, 1 error)
import time into derive.py (breaks R1/R3 purity) FAILED (1 failure)
plant f"task-{...}" in src/obsidian-mirror.py FAILED — ratchet catches it

All four controls fire, tree reverts clean each time. Keeping % inside _RESERVED is the detail that makes the escaping injective rather than merely reversible, and the vectors pin it.

One non-blocking finding — the ratchet's population is one directory deep.

SRC.glob("*.py") is non-recursive, so it scans src/*.py only. Measured on this tree: 35 .py files live in src/ subdirectories and none is scanned. Nothing mints there today (f"task-{ → 0 hits below top level, and f'task-{ → 0 anywhere), so the ratchet is correct as shipped — but its guard is narrower than the tree it guards, and the pins are exact, so the gap is silent.

Control, same run:

plant f"task-{1}" in src/obsidian-mirror.py   -> FAILED (caught)
plant f"task-{1}" in src/runtime-api/dispatcher.py -> OK (not caught)

SRC.rglob("*.py") (both call sites) keeps the suite green with no pin churn — I ran it: 2 tests, OK. So it looks like a free widening. Your call whether it belongs in this slice or the next; it doesn't block anything here.

Nice work on test_derive_module_has_no_impure_imports — pinning the absence of an import is the part that usually gets written as a comment instead of a test.

Stand: Echo Act IV Pro

@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 exact head b5449e457a73768bcb24ff392bdfe163ca65617f; this head is not ready to merge.

The need is confirmed on the merge-base/main: ag2_sparrow.identity is absent, while docs/census/d1-identity-census.md:15-19 records wall-clock Discord/Slack task IDs and missing stable delivery/attempt identities. This is one commit directly on merge-base 5cb9795a, not a stack; the referenced #3296 definitions branch is a sibling rather than an ancestor, so definitions-first remains a merge-order condition. I independently reproduced the existing grammar/ratchet concerns and found an additional distribution blocker.

[P1 blocker] The advertised ag2_sparrow.identity API is excluded from the installed wheel — packages/ag2-sparrow/pyproject.toml:32-33, packages/ag2-sparrow/tests/test_identity.py:13-17. Setuptools is explicitly configured with only the root package, while the new API is a subpackage. The repository itself documents this flat-wheel constraint at tests/sparrow-result-guard.test.py:259-263; the identity test masks it by inserting the source tree directly into sys.path. Configuration probe:

configured_packages = ['ag2_sparrow']
identity_package_exists = True
identity_package_configured = False

The publish workflow builds that manifest (.github/workflows/publish-sparrow.yml:79-85). Include the subpackage via discovery or an explicit package entry, and add a built-wheel import smoke test that runs without the source checkout on sys.path.

[P1 blocker] escape_component() is not injective and the documented ASCII boundary is unenforced — derive.py:25-36, types.py:17-24. Against the production functions:

escape('\u200b') = %200B
escape(' 0B')    = %200B
ingress_collision = True

ingress_task_id('💥', 'event') is also accepted unchanged despite the promised printable-ASCII grammar. Reject non-ASCII inputs or encode fixed-width bytes, then pin injectivity across the admitted alphabet, including code points above 0xFF.

[P1 blocker] The public types, parsers, constructors, and record serializer do not share one canonical grammar — types.py:27-57, serialization.py:16-28,60-83, derive.py:64-76. The parsers accept constructor-impossible empty components (d:@, d:@#a1, e:@, :0:0). Conversely, TaskId('not-a-task') is accepted and to_record_fields() writes it, but identity_fields_from_record() raises on that same record. resend_delivery_id(TaskId('task-x'), 1) silently returns DeliveryId('task-x+r1'), which parse_delivery_id() rejects; True and 1.5 ordinals similarly return typed attempts that cannot be parsed. Centralize kind-specific validation, enforce exact input types/integer ordinals, and require every accepted typed value to survive record round-trip.

[P1 blocker] The identity ratchet passes ordinary new mint/private-constructor shapes — tests/sparrow-identity-ratchet.test.py:36,48-75. I ran the actual test methods against temporary mutation fixtures:

top-level double-quoted task f-string -> FAIL  (positive control)
top-level single-quoted task f-string -> PASS
a nested double-quoted task f-string  -> PASS
top-level private delivery_id         -> FAIL  (positive control)
nested private delivery_id            -> PASS

SRC.glob('*.py') omits 35 nested Python files, and the exact-token regex misses concatenation and .format() too. Per-file totals also let removing one old site and adding a new one cancel out. Replace this with a recursive, syntax-aware/exact-site census and pin positive controls for nested files and adjacent construction spellings.

Focused checks at this head:

identity suite                 13/13 OK
identity ratchet                2/2 OK
CI Python-test coverage guard   4/4 OK
census anchors                  3/3 OK
vendored drift check            PASS
git diff --check                clean
review-checks                   PASS

tests/review-checks.test.sh also passed its positive controls; no added host-specific path was found. No production caller imports this package at this head, so this is not yet a live-path change and no restart witness is required.

Worst-case disruption is dormant until the first consumer lands: an installed Sparrow cannot import the contract, or two provider events collapse to one task and replay suppression drops legitimate work; malformed persisted identities can then fail reload while the ratchet remains green. Fixing the wheel manifest, injective grammar, unified runtime validation, and demonstrably effective ratchet before Slice 3 is the mitigation that would change this verdict. Hosted CI/CLA and two current formal maintainer approvals remain merge conditions after an updated head.

Reviewed by Kewei's Codex automation.

@sonichi

sonichi commented Aug 23, 2026

Copy link
Copy Markdown
Owner

@keweichen — your P1 on escape_component refutes a claim in my review above, and I want that on the record rather than left standing.

I wrote that four mutations "prove the assertions can fail", including two for injectivity (dropping ~ and % from _RESERVED, both FAILED). That is true and it does not show what I implied. Those mutations prove the suite catches a removed reserved character; they say nothing about whether the escaping is injective across the alphabet the function actually admits. Your collision is exactly in the gap:

escape('​') = %200B
escape(' 0B')    = %200B

Space → %20, then the literal 0B — and a non-ASCII code point that produces the same bytes. My mutations could never have surfaced that, because every one of them stayed inside the ASCII-reserved set. Mutation testing proves the tests catch the mutations you chose; it is not a proof of the property. I picked mutations from the implementation's own vocabulary, which is precisely how the blind spot survived.

On the ratchet we converged independently and yours is stronger. I measured the non-recursive SRC.glob('*.py') and the 35 nested files, with the same control shape (top-level plant caught, nested plant missed). I then checked f'task-{ → 0 occurrences and called the single-quote variant "not a live gap" — wrong framing: a detection gap does not become acceptable because it is currently unoccupied, and your per-file-total point (a removal and an addition cancelling out) is a hole I did not see at all.

So: my review's finding stands but under-scoped, my injectivity claim was over-scoped, and the three P1s I missed entirely — the wheel excluding the subpackage, the parser/constructor grammar split, and the ASCII boundary — are the substantive ones. Downgrading my earlier assessment accordingly; this head needs your changes, not a second approval.

Stand: Echo Act IV Pro

qingyun-ag2 and others added 2 commits August 23, 2026 09:26
…pped wheel, AST ratchet

Addresses both 2026-08-23 reviews on #3315:

- escape_component now percent-encodes UTF-8 bytes fixed-width (%XX,
  uppercase), so escape('\u200b') = %E2%80%8B can never collide with
  escape(' 0B') = %200B; all non-ASCII is encoded, and the value types
  enforce printable ASCII (0x21-0x7E) so the documented grammar is real.
- One canonical grammar: per-kind patterns live on the types; constructors,
  parsers, and the record serializer all validate through them. Components
  require at least one unit (d:@ / e:@ / :0:0 rejected), TaskId requires
  the task- namespace, constructors enforce exact input types and int
  ordinals (bool excluded) — every accepted value survives record
  round-trip by construction.
- Wheel manifest ships ag2_sparrow.identity (and the also-omitted
  ag2_sparrow.delivery_core) plus vectors.json; new test_identity_wheel.py
  pins package-dir/manifest completeness and imports the API from a
  simulated install with no source checkout on sys.path.
- Identity ratchet is recursive and AST-based: catches either quote style,
  .format, concatenation and %-formatting in nested files; pins are per
  (file, function) so removals cannot cancel new mints; scanner positive
  controls run in-suite. Newly visible pre-existing sites (cron-runner,
  github-webhook, health-check, single-quoted variants) are pinned.

Vectors are additive only; no shipped expected value changed.

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

Copy link
Copy Markdown
Collaborator Author

Both 2026-08-23 reviews addressed at head 6cf6619b (old head b5449e45, plus a merge of origin/main @ 9a164b67). Every "before" below is the actual probe output at b5449e45; every "after" is at 6cf6619b. Per-blocker:

1. Wheel excludes ag2_sparrow.identity (@keweichen) — fixed.
pyproject.toml now ships the explicit list packages = ["ag2_sparrow", "ag2_sparrow.delivery_core", "ag2_sparrow.identity"] + package-data for vectors.json. The completeness test found ag2_sparrow.delivery_core was ALSO silently dropped from the wheel (pre-existing on main since #3252) — same defect class, fixed in the same entry. New packages/ag2-sparrow/tests/test_identity_wheel.py (wired into ci.yml, auto-run by the publish glob) pins (a) every on-disk package dir is configured and (b) the API imports from a simulated install with only the configured packages on PYTHONPATH and no source checkout on sys.path. Witnessed red at the intermediate state:

AssertionError: {'ag2_sparrow.delivery_core'} is not false : package dirs exist but are
absent from pyproject packages (they will be DROPPED from the wheel)

and green at head (Ran 2 tests ... OK).

2. escape_component not injective / ASCII boundary unenforced (both reviews) — fixed.
Escaping is now fixed-width uppercase %XX per UTF-8 byte for everything outside safe printable ASCII, and the value types enforce 0x21..0x7E (no whitespace, no /\\).
Before (b5449e45):

escape('​') = %200B
escape(' 0B')    = %200B
ingress_collision = True
ingress('\U0001F4A5','event') = task-💥~event

After (6cf6619b):

escape('​') = %E2%80%8B
escape(' 0B')    = %200B
ingress_collision = False
ingress('\U0001F4A5','event') = task-%F0%9F%92%A5~event

Injectivity is pinned across the review's collision pair plus code points above 0xFF, emoji, controls, %-prefixed inputs (Injectivity.test_escape_is_fixed_width_over_the_full_alphabet); ASCII-only output and non-ASCII type rejection are pinned too. Vectors added for ​ vs " 0B" and U+1F4A5 — additive only, no shipped expected value changed.

3. One canonical grammar across types/constructors/parsers/serializer (both reviews) — fixed.
Per-kind grammar now lives on the types (types.py); constructors and parsers both validate through it, so a value a constructor cannot produce is unconstructible AND unparseable.
Before (b5449e45):

parse 'd:@' -> ACCEPTED as DeliveryId
parse 'e:@' -> ACCEPTED as IdempotencyKey
parse ':0:0' -> ACCEPTED as IncarnationId
parse 'd:@#a1' -> ACCEPTED as AttemptId
TaskId('not-a-task') -> accepted
resend_delivery_id(TaskId('task-x'),1) -> DeliveryId task-x+r1
  ...which parse_delivery_id REJECTS

After (6cf6619b):

all constructor-impossible strings rejected by every parser
TaskId('not-a-task') -> ValueError
resend_delivery_id(TaskId('task-x'), 1) -> TypeError: predecessor must be DeliveryId, got TaskId
attempt_id(d, True) -> TypeError
attempt_id(d, 1.5) -> TypeError

Components require at least one unit; empty-left/right, malformed/lowercase percent, and non-ASCII controls exist for every parser (OneCanonicalGrammar); exact input types and int-not-bool ordinals are enforced; test_every_constructed_value_survives_record_round_trip pins embed→extract equality.

4. Ratchet bypassable (both reviews) — fixed.
tests/sparrow-identity-ratchet.test.py now scans src/ RECURSIVELY with an AST census: f-strings in either quote style, "task-...".format(...), "task-" + expr, and "task-%s" % expr; pins are per (file, enclosing function) so removing an old site cannot cancel a new one; the delivery_id gate is recursive too. The reviewers' exact bypass shapes are now in-suite positive controls run against fixtures on every execution (ScannerPositiveControls: top-level single-quoted, nested file, .format, concatenation, %-format, nested delivery_id). Live mutation controls at head (run, then removed):

src/runtime-api/zz_mutation_control.py (f) has 1 task-mint site(s), pin is 0.  FAILED
src/runtime-api/zz_dc_control.py names delivery_id but does not import ag2_sparrow.identity  FAILED

The stronger scan also surfaced pre-existing mint sites the old regex was blind to (cron-runner 2, github-webhook 1, health-check 2, plus single-quoted variants in the bridges) — all pinned at introduction, so the pin table is now the real census.

Checks at 6cf6619b:

packages/ag2-sparrow/tests/test_identity.py        Ran 20 tests ... OK
packages/ag2-sparrow/tests/test_identity_wheel.py  Ran 2 tests ... OK
tests/sparrow-identity-ratchet.test.py             Ran 5 tests ... OK
tests/ci-covers-every-python-test.test.py          Ran 4 tests ... OK
all 14 packages/ag2-sparrow/tests suites           TOTAL FAILS: 0
tools/test_no_drift.py                             PASS — package in sync with src/
scripts/review-checks.sh --diff <origin/main...HEAD>  PASS   RC=0
git diff --check                                   clean

On merge order: agreed #3296 (definitions) stays a merge-order condition; this PR is not armed for auto-merge and merge remains gated on hosted CI/CLA plus two current formal maintainer approvals on this head.

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

@qingyun-wu qingyun-wu left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Changes still requested on exact head 6cf6619b551d5822f6cf1480ef79f199ef3a4fbe; formal REQUEST_CHANGES is unavailable because this reviewer identity owns the PR.

The grammar/injectivity, wheel-manifest, and recursive task-mint fixes are sound. One ratchet blocker remains:

[P1] The delivery-ID constructor gate is still a text/regex check and can be satisfied by comments or strings — tests/sparrow-identity-ratchet.test.py:125-133. scan_delivery_id_files() marks a file compliant whenever _CANONICAL_IMPORT occurs anywhere in raw text; it never verifies an AST import or that a delivery_id mint resolves to the canonical constructor. Two production-scanner controls both false-green:

comment_spoof= {'runtime-api/private.py': True}
string_spoof= {'private.py': True}

Each fixture defines a private delivery_id() and contains only the text from ag2_sparrow.identity import delivery_id in a comment or string. This leaves the exact constructor-exclusivity property bypassable by ordinary source shapes despite the recursive scan. Make this arm syntax-aware too, prove a real canonical import/binding rather than a substring, and add comment/string/private-constructor positive controls.

Reviewer-run checks otherwise pass: identity 20/20, installed-layout wheel smoke 2/2, ratchet 5/5, diff hygiene, and cumulative repository review checks. Hosted aggregate/coverage checks are still in progress; CLA is green.

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.

Approval signal for 6cf6619b551d5822f6cf1480ef79f199ef3a4fbe.

The new head resolves the blocking identity-contract findings: UTF-8 byte escaping is injective for the demonstrated collision, empty/foreign grammar shapes are rejected consistently by the value types and parsers, the wheel explicitly ships both subpackages and vectors, and the ratchet is recursive with AST-based positive controls and per-site pins. The merge from current main also restores definitions-first ordering.

Focused verification passed locally:

  • identity contract: 20 tests
  • wheel manifest/import: 2 tests
  • identity ratchet: 5 tests

I found no blocking issue in this head. CI was still running when this review was posted. This is a regular review comment because the authenticated account is the PR author.

Reviewed by Qingyun's Personal Codex.

@qingyun-wu
qingyun-wu requested a review from keweichen August 23, 2026 17:11

@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 exact head 6cf6619b551d5822f6cf1480ef79f199ef3a4fbe; this head is not ready to merge.

The need is confirmed at merge-base 9a164b6727a351b2683bf7478a060d919119ad04: the identity package is absent and the Discord/Slack bridges still mint clock-derived task IDs. This is not stacked on #3296—its head is not an ancestor and docs/sparrow-delivery-identity.md is absent—so definitions-first remains a merge-order condition.

[P1 blocker] The delivery-ID exclusivity ratchet still certifies private constructors — tests/sparrow-identity-ratchet.test.py:58-64,125-169. scan_delivery_id_files() searches raw text, so a comment/string satisfies the import requirement, an actual import may be shadowed, and both legacy files are exempt wholesale. Production-scanner controls produced:

scanner says canonical:
comment_spoof.py          True
string_spoof.py           True
real_import_shadowed.py   True
no_import_control.py      False

legacy slack-bridge private constructor:
scanner=False; ratchet failures=[]

Thus the likeliest future delivery work can add a private constructor to slack-bridge.py while the ratchet remains green. Make this arm AST/site-aware, reject shadowed/private bindings, replace whole-file exemptions with exact legacy-site pins, and add the four controls above.

[P1 blocker] The R3 legacy adapter changes the provider idempotency key already used by the delivery core — identity/legacy.py:22-31, delivery_core/core.py:36-39,85-103, identity/derive.py:96-101. The remote gateway invokes that delivery core at remote_gateway_bridge.py:2935-2958, and this PR newly ships both subpackages via pyproject.toml:33. Against the public functions:

delivery_core epoch 0: task-X#0
delivery_core epoch 1: task-X#1
identity canonical:    e:task-X@gw
legacy adapter:         e:task-X@gw

A legacy item attempted under task-X#0 and then parked/left ambiguous would be re-offered under a fresh provider key after adopting this adapter, allowing a duplicate side effect. The exact head also exposes two public owners with contradictory resend semantics. Unblock this by preserving/versioning the shipped key for legacy deliveries and making the delivery core delegate future derivation to the canonical owner; pin an attempted-then-requeued legacy round trip.

[P2 non-blocking] to_record_fields() silently accepts misspelled optional fields — identity/serialization.py:51-59. Name validation occurs after the None short-circuit:

to_record_fields(delviery_id=None) -> {}

Validate the keyword before deciding whether to omit its value so an optional-field typo cannot silently discard identity metadata.

Reviewer-run verification at this head:

identity contract                 20/20 OK
wheel layout                       2/2 OK
identity ratchet                   5/5 OK
CI Python-test coverage guard      4/4 OK
all 14 ag2-sparrow test files        0 failures
delivery-core suites              38 + 15 + 3 OK; design-C/adapter PASS
review-checks + positive controls PASS
git diff --check                  clean

No added host-specific path was found. I inspected the supplied reviews/comments; there are no inline comments. No production caller imports ag2_sparrow.identity yet, so immediate disruption is dormant and a restart witness is not required for this head. Worst case after the first consumer lands is a duplicate user-visible delivery from the legacy key change while the false-green ratchet permits restart-unstable private identities. Fixing the two P1s and demonstrating the hostile controls above would change the verdict; hosted CI/CLA and two current formal maintainer approvals remain merge conditions afterward.

Reviewed by Kewei's Codex automation.

…provider key

Addresses keweichen's REQUEST_CHANGES on 6cf6619.

[P1] The delivery-ID exclusivity arm certified private constructors. It
searched raw text, so a comment or a string satisfied the import
requirement, a real import could be shadowed, and two files were exempt
whole-file. Both halves are now AST-based: the canonical binding must be a
real, public, unshadowed import node, and the legacy exemption is pinned per
(file, enclosing function) site like TASK_MINT_PIN — so a private
constructor added to a legacy file is a new site, not an exempt one. The
reviewer's four hostile fixtures plus a private-alias case and the
legacy-file smuggling control are permanent tests.

[P1] The R3 legacy adapter minted a NEW provider idempotency key for outbox
artifacts that delivery_core had already offered under <item_id>#<epoch>. An
item attempted then parked ambiguous would have been re-offered under an
unrecognisable key — one duplicate side effect per adopted item.
legacy_idempotency_key preserves the shipped bytes, from_outbox_item returns
them (boundary does not enter the key; resend_epoch versions it), and
delivery_core.idempotency_key now delegates to that single owner instead of
formatting its own. An attempted-then-requeued round trip through the real
DeliveryCore pins it.

test_identity.py's LegacyAdapter asserted the defective mapping verbatim
(m1.idempotency_key == canonical). That assertion is inverted here; the
inversion is part of the fix, not collateral.

[P2] to_record_fields validated the keyword after the None short-circuit, so
to_record_fields(delviery_id=None) returned {} instead of raising. The name
check now precedes the short-circuit.

Not stacked on #3296: definitions-first remains a merge-order condition.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@qingyun-wu
qingyun-wu force-pushed the feat/sparrow-b1-identity-contract branch from 7fb6597 to 4f331f2 Compare August 23, 2026 21:29
@qingyun-wu

Copy link
Copy Markdown
Collaborator Author

@keweichen — all three addressed in 4f331f20 (pushed on top of the 6cf6619b you reviewed). Per-item below, with the fail-at-6cf6619b / pass-at-head drives run for real.

Your merge-order finding stands and I am not contradicting it: #3315 is not stacked on #3296 — its head is not an ancestor and docs/sparrow-delivery-identity.md is absent here — so definitions-first remains a merge-order condition.


[P1] Delivery-ID exclusivity ratchet certified private constructors — fixed

Both halves of the arm are now AST-based, and the whole-file exemption is gone.

  • Canonical binding must be a real Import/ImportFrom node for ag2_sparrow.identity, public (a _-prefixed alias is rejected), and unshadowed — if the bound name is rebound anywhere in the file by a def/class/assign/arg/except/import, the binding proves nothing about what the file calls, so it does not count.
  • Legacy exemption is now pinned per (file, enclosing function) site in DELIVERY_ID_LEGACY_SITES, exactly like TASK_MINT_PIN, and ratchets both ways.

Your four controls, plus a private-alias case and the legacy-file smuggling control, are permanent tests (DeliveryGateHostileControls). Same scanners, same fixtures, run against each tree:

                              6cf6619b      4f331f20
scanner says canonical:
  comment_spoof.py            True          False
  string_spoof.py             True          False
  real_import_shadowed.py     True          False
  no_import_control.py        False         False

legacy slack-bridge private constructor:
  6cf6619b: scanner=False; ratchet failures=[]
  4f331f20: ratchet failures=[('slack-bridge.py', '_use_minted')]

I added a negative control for the controls too — a real public unshadowed import (all three spellings) still clears the gate, so the arm cannot pass by being vacuously strict.


[P1] R3 legacy adapter changed a shipped provider idempotency key — fixed

You are right that this is a duplicate-side-effect hole, and right that the head exposed two public owners with contradictory resend semantics. Resolution:

  • identity.legacy_idempotency_key(item_id, resend_epoch=0) reproduces the shipped <item_id>#<epoch> byte-for-byte — components deliberately unescaped, because these are bytes a provider has already seen. IdempotencyKey's grammar admits that shape alongside the canonical one; the two are disjoint (e:...@... contains no #), so admitting it cannot make a key ambiguous.
  • from_outbox_item now returns the preserved key. boundary deliberately does not enter it — the shipped key never carried one, and letting it in would split one side effect across two keys. resend_epoch is the versioning knob you asked for.
  • delivery_core.core.idempotency_key delegates to that single owner instead of formatting its own; it keeps its public name and its exact bytes.
  • from_delivered_sentinel stays canonical, and that asymmetry is now documented: a delivered-sentinel is a local file, so no provider ever saw a key for it and there is nothing to preserve.
                       6cf6619b        4f331f20
delivery_core epoch 0: task-X#0        task-X#0
delivery_core epoch 1: task-X#1        task-X#1
identity canonical:    e:task-X@gw     e:task-X@gw
legacy adapter:        e:task-X@gw     task-X#0     <-- now matches the shipped key

Attempted-then-requeued round trip pinned (AttemptedThenRequeuedLegacyRoundTrip), driving the real DeliveryCore + DesignAClaimBackend over a temp outbox: attempt 1 raises ProviderIndeterminate, the item parks OUTCOME_UNKNOWN, the key the provider actually saw is captured, and the test asserts the adapter's re-offer carries that same key. A companion test pins that a deliberate resend_epoch=1 is the only thing that changes it.

One thing you should know, because it changes how much you trust this suite: test_identity.py's LegacyAdapter.test_outbox_conflation_maps_purely asserted the defective mapping verbatim at 6cf6619b — it required m1.idempotency_key == I.idempotency_key(TaskId(...), boundary), i.e. exactly the key change you flagged. Inverting that assertion is part of the fix, not collateral damage; the replacement test says so in its own docstring.


[P2] to_record_fields() accepted misspelled optional fields — fixed

Name validation now precedes the None short-circuit.

6cf6619b: to_record_fields(delviery_id=None) -> {}
4f331f20: to_record_fields(delviery_id=None) -> ValueError: unknown identity field 'delviery_id'

A known field set to None is still legitimately omitted — pinned in the same test.


Mutation controls

Fix committed first, then each guard reverse-replaced alone, run, restored. Bytecode caching disabled (see the note below).

M1 ratchet: drop shadow/rebind check                       rc=1 RED
M2 ratchet: whole-file exemption instead of site pins       rc=1 RED
M3 legacy adapter mints a new key (the reported defect)     rc=1 RED
M4 delivery_core stops delegating                           rc=1 RED
M5 P2: None short-circuit before name validation            rc=1 RED

post-restore: git status --short = []   git diff = 0 bytes

Worth flagging as a method hazard: my first pass of M5 produced a false RED that survived the restore. The mutation was a pure block reorder, so the file size was unchanged and the rewrite landed in the same integer second — CPython accepted the .pyc built from the mutated source (src mtime=1787519688 size=2622 / pyc mtime=1787519688 size=2622). git status and git diff were both empty and the suite still failed. The same mechanism could just as easily have produced a false GREEN and hidden a non-load-bearing guard, so the whole battery above was re-run with PYTHONDONTWRITEBYTECODE=1.


Gates and CI

gate RC literal verdict
review-checks.sh --diff (merge-base..HEAD) 0 review-checks: PASS (hardcoded-paths + root-artifacts + prose-cap clean) — full PASS, prose-cap had in-scope .py files
lint-sutando-home-path.sh --diff (BASE_REF=origin/main) 0 lint-sutando-home-path: clean (mode=--diff, scanned 9 files)
tests/ci-covers-every-python-test.test.py 0 Ran 4 tests ... OK (no new test file added; edits only)
git diff --check 0 clean
ruff check 0 All checks passed!

Regression sweep at the final head: 35 suites, 0 failures — all 14 packages/ag2-sparrow/tests/, plus delivery-core (38 + 3 + 15), design-C, outbox-adapter/claim-protocol, discord-delivery-provider, and the ag2-sparrow drift guard. identity/ and delivery_core/ are not in sync_from_src.py's MAP, so they are package-canonical and the drift guard stays green.

An intermediate push of mine went red on typecheck + node & shell tests and I want to be explicit about why, since it is a gap in the local gate rather than a flake: I had scoped review-checks --diff to HEAD~1..HEAD, but CI runs it over the full PR diff. My new delivery_core import turned a 2-line run of trailing # noqa: E402 comments into a 3-line block at test_identity.py:17. I reproduced CI's exact finding locally against gh pr diff, regrouped the imports, and re-ran every gate at the CI-equivalent scope. Hosted CI at 4f331f20 is now 18/18 pass (1 skipping: publish).

State: mergeable: true, mergeable_state: behind (1 commit behind main) — I have not run update-branch, and per your review hosted CI/CLA plus two current formal maintainer approvals remain merge conditions. I have not armed auto-merge; merges here are the owner's.

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

Changes requested on exact head 4f331f200f20055af4eada990a35c672bba7c06e; formal REQUEST_CHANGES is unavailable because the connected reviewer identity owns the PR.

The provider-key preservation and optional-field fixes are correct, but one P1 ratchet hole remains at tests/sparrow-identity-ratchet.test.py:180-207. scan_delivery_id_sites() skips an entire file as soon as has_canonical_binding() finds any public, unshadowed canonical import. That does not prove every delivery-ID construction in the file uses that binding.

I ran the production scanner against a file with one valid from ag2_sparrow import identity caller plus a separate private _mint_delivery_id() and consumer. Exact output:

canonical_binding= True
scanner_sites= {}

Thus slice 3 can keep one canonical caller while adding a restart-unstable private constructor elsewhere in the same file, and this ratchet remains green. Please make certification site-aware (or otherwise prove each construction reaches the canonical binding) and add this mixed canonical/private fixture as a hostile control.

Reviewer-run verification otherwise passed: identity 30/30, wheel 2/2, ratchet 8/8, delivery-core 38/38 + 3/3 + 15/15, Design C, outbox adapter, repository checks, and diff hygiene. The definitions-first merge order with #3296 remains a separate gate.

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 still requested on exact head 4f331f200f20055af4eada990a35c672bba7c06e; formal REQUEST_CHANGES is unavailable because this reviewer identity owns the PR.

The provider-key fix is sound: the delivery core now delegates to the identity package while preserving the already-shipped <item_id>#<epoch> bytes, and the legacy adapter carries the resend epoch. Optional-field name validation is also fixed.

[P1] The constructor-exclusivity ratchet still accepts unrelated or renamed imports as proof that every delivery_id site is canonical — tests/sparrow-identity-ratchet.test.py:129-175. _identity_imports() collects every public name imported from ag2_sparrow.identity, and has_canonical_binding() returns true if any one remains unshadowed. It never proves that a delivery_id use resolves to the canonical function. Two production-scanner controls both clear the gate:

from ag2_sparrow.identity import TaskId
def delivery_id(...): ...                         -> canonical=True

from ag2_sparrow.identity import delivery_id as canonical_delivery_id
def delivery_id(...): ...                         -> canonical=True

Because scan_delivery_id_sites() skips the entire file after that boolean, an ordinary unrelated type import or renamed canonical import can hide a private constructor. Resolve each delivery-id use site to its actual AST binding, or at minimum require the exact binding used at the call site and reject a coexisting private constructor; add both controls above.

Focused verification otherwise passed: identity contract 30/30, wheel layout 2/2, ratchet 8/8, and diff hygiene. The adversarial binding controls are false-green, so the exclusivity claim remains unproven.

Reviewed by Qingyun's Personal Codex.

@john-the-dev john-the-dev left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Approving at head 4f331f20. Scope stated up front: I verified @keweichen's two P1 blockers against the current head, not the full 1262-line surface. Both are addressed, and one is resolved better than the patch that was asked for.

@keweichen — both P1s look cleared at 4f331f20 (your CR is on 6cf6619b)

Flagging because nothing notifies you: the head moved at 21:28Z, CI is 20/20 green, and the latch survives the push.

P1-1 — ratchet certified private constructors via raw-text scanning

tests/sparrow-identity-ratchet.test.py went 211 → 414 lines and is now AST-based. Its docstring commits to all three of your asks by name:

AST-based: a comment or string cannot satisfy the import, a shadowed or privately-aliased binding is rejected, and the legacy exemption is per (file, function) site rather than whole-file

import ast at :35, with ast.NodeVisitor walks for imports (:138-146) and a shadowing check that treats a canonical name rebound by a def/class/import as shadowed (:156-166). Your four controls exist as named tests:

test_no_spoof_or_shadow_clears_the_gate
test_a_real_public_unshadowed_import_does_clear_the_gate      <- positive control
test_private_constructor_in_a_legacy_file_reds_the_ratchet    <- your slack-bridge case
test_delivery_id_sites_are_canonical_or_pinned_legacy

The third is the one that matters most: the whole-file exemption you demonstrated as green is now specifically red.

P1-2 — legacy adapter changed a provider key already in flight

Fixed, and the fix is structurally stronger than preserving the bytes by hand. from_outbox_item takes resend_epoch and delegates to legacy_idempotency_key, which emits:

return IdempotencyKey(f"{item_id}#{resend_epoch}")

— the shipped shape, deliberately unescaped, with the reason recorded where the next reader will hit it:

the idempotency key is NOT ours to choose: delivery_core has already offered this item to a provider under <item_id>#<epoch>, so the mapping must reproduce that key or a parked item is re-offered under a name the provider cannot dedupe — one duplicate side effect per adopted item

And the "two public owners with contradictory resend semantics" half of your finding is gone rather than patched: delivery_core.idempotency_key now delegates to identity.legacy_idempotency_key (delivery_core/core.py:45) instead of carrying its own copy. Byte-equality is guaranteed by construction, not by two implementations agreeing — which is CLAUDE.md's own "a shared owner already exists → fix it there" rule, applied to the exact drift you caught.

The artifact path is correctly reasoned separately: a local file no provider ever saw has no shipped key to preserve, so the canonical one applies. That distinction is stated in the docstring rather than left implicit.

Worth refreshing the review state when you get a chance.

Scope and limits of this review

  • I did not independently re-derive your original repros, and I did not run the ratchet suite — verifying it needs the ag2_sparrow package assembled, and the parts I could check statically (AST usage, control test names, key format) all match. CI is 20/20 green at this head.
  • I did not review the other ~1100 lines (identity/types.py, derive.py, serialization.py, vectors.json, the wheel test, ci.yml). Treat this as a targeted blocker-clearance review, not a second full pass.

Worst-case disruption (criterion 5)

The idempotency-key question was the blast radius — a changed provider key on in-flight items means duplicate side effects, one per adopted item — and it is now closed by reproducing the shipped bytes from a single owner. Residual risk sits in the surface I did not review.

MERGEABLE but BEHIND. update-branch on this repo tends to drop license/cla (SHA-bound) — close+reopen is the documented retry if it goes absent.

⚠️ Bot approval — per the main-branch ruleset this still needs human approvals before merge.

qingyun-wu pushed a commit that referenced this pull request Aug 24, 2026
#3316 was DIRTY. The parent (#3315, feat/sparrow-b1-identity-contract) had moved
8 commits ahead of the merge-base, and exactly one file conflicted:

    docs/src-map.md

That file is GENERATED (scripts/gen-src-map.py), so hand-merging it would have
been resolving an artifact instead of rebuilding it. Regenerated instead.

The result is the union of both sides, which is what a merge of two branches that
each added modules should produce:

    vs child:   + sparrowd.py                              (parent's module)
    vs parent:  + ingress_identity.py                      (child's)
                + slack_result_delivery.py                 (child's)

No entry dropped from either side.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@qingyun-wu
qingyun-wu requested a review from keweichen August 24, 2026 20:42

@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 exact head 4f331f200f20055af4eada990a35c672bba7c06e; this head is not ready to merge.

The need is confirmed at merge-base 9a164b67 and current origin/main: ag2_sparrow.identity is absent, while Discord, Slack, and Telegram still contain clock-derived task-{ts} ingress IDs. This PR has no feature-branch parent. The frozen-definition head for #3296 (b83268bb) is a sibling—neither head is an ancestor—so definitions-first remains a merge-order condition.

[P1 blocker] Delegating the shipped provider-key function narrows its accepted item_id domain and can strand a live claim — delivery_core/core.py:37-45,91-99, identity/types.py:29-36,75-80. The parent implementation formatted every string byte-for-byte. The outbox deliberately supports opaque IDs such as a/b through its digest-suffixed safe key (ag2_sparrow/outbox.py:204-212). Against the real current core/backend:

'a/b': parent='a/b#0' head="ValueError: IdempotencyKey contains forbidden character '/'"
publish=True
provider_calls=[]
claim_after_exception=('a/b', 'probe', 'HELD')
second_drain=not_claimed

deliver_one() claims before deriving the key, so validation failure makes no provider call and leaves the long-running process holding the item. Preserve the full previously accepted opaque-string domain in legacy_idempotency_key, and pin a/b through the real DeliveryCore; deriving before claim or reliably releasing on derivation failure would also close the stranded-claim failure.

[P1 blocker] The canonical resend-key rule now contradicts the frozen definitions — identity/derive.py:82-102, tests/test_identity.py:108-116. #3296 at b83268bb states at docs/sparrow-delivery-identity.md:71-75,114-118 that a post-reconciliation resend advances resend_epoch and therefore has a NEW idempotency key. This head still pins the superseded same-key rule:

delivery_ids = d:task-X@gw  d:task-X@gw+r1
canonical_key_signature = (task, boundary)
idempotency_keys = e:task-X@gw  e:task-X@gw  same=True
shipped_legacy_epochs = task-X#0  task-X#1

After #3296 lands, rebase and make the canonical constructor/vectors express the frozen new-key resend semantics rather than preserving the earlier wording.

[P1 blocker] The delivery-ID ratchet still clears an entire file for an unrelated or differently bound import — tests/sparrow-identity-ratchet.test.py:134-207. I drove the production scanner with a positive control and hostile fixtures:

no_import_control:          canonical=False sites={('probe.py', '<module>'): 2}
unrelated TaskId import:    canonical=True  sites={}
renamed canonical + private canonical=True  sites={}
mixed canonical + private: canonical=True  sites={}

has_canonical_binding() accepts any public unshadowed name imported from ag2_sparrow.identity; scan_delivery_id_sites() then skips every site in that file. Resolve each construction/use to the binding it actually calls, and retain hostile controls for unrelated imports, renamed imports, and mixed canonical/private constructors.

This exact head is also a live delivery-loop change: remote_gateway_bridge.py:2935-2958 invokes the modified DeliveryCore on the gateway result path. The supplied evidence contains no real post-restart round trip for this head, and I did not run a live service from the isolated checkout. After the implementation blockers are fixed, a post-restart gateway result round trip is required.

Reviewer-run checks otherwise passed: identity 30/30, wheel layout 2/2, ratchet 8/8, delivery-core contract 38/38, enforcement 15/15, AG2 Space and Discord provider suites, outbox-format controls, CI-coverage guard 4/4, vendored drift, repository review checks including all 79 controls, and git diff --check. No added host-specific path was found.

Worst case today is an outbound result that never reaches its provider and remains held until process death/recovery; after canonical adoption, the stale resend rule can suppress a deliberate resend while the false-green ratchet admits restart-unstable private identities. Preserving the legacy input domain, aligning with #3296, making the ratchet site-aware, and supplying the live witness would change this verdict. Hosted CI/CLA and two current formal maintainer approvals remain merge conditions afterward.

Reviewed by Kewei's Codex automation.

Reviewer P1: delegating delivery_core's shipped key to the identity module
narrowed which item_ids it accepts, and could strand a live claim.

core.idempotency_key documents that "the bytes are unchanged and must remain
so — a provider has already seen them for every item in flight", and
legacy_idempotency_key says components are NOT escaped and the shape is
opaque. But IdempotencyKey ran the canonical charset rule first, so 'a/b'
raised ValueError where the parent returned 'a/b#0'. The outbox deliberately
admits opaque ids (_safe_key sanitizes AND digests), so a path separator does
reach this derivation. _LEGACY_KEY itself also excluded '/', so relaxing the
charset check alone was not enough.

The module's stated reason for banning separators is that ids become
filenames and record keys. That holds for the canonical grammar and not for
this shape: the legacy key is provider-facing, and _safe_key already owns the
filesystem-safe form. So the widening is scoped to the opaque shape via a new
_OPAQUE class pattern — length still bounded, charset rule skipped, canonical
grammar untouched ('e:a/b@gw' and 'e:task X@gw' still raise).

Second half: deliver_one claimed before deriving, so a derivation failure left
the worker holding an item no drain could ever deliver. Derivation now runs
first, which removes the stranding window rather than releasing after it.

tests/test_identity.py asserted the narrowed domain — it encoded the defect,
so it is replaced by one test pinning the shipped domain byte-for-byte and one
pinning that only empty/over-length/bad-epoch raise, plus a control that the
canonical grammar stays strict.

Evidence: identity 32/32 (was 30/30, one test split into three);
delivery-core contract 41/41 with the 3 new LegacyKeyDomain cases registered
in load_tests (an appended TestCase is inert here — load_tests overrides
discovery); 23/23 suites importing ag2_sparrow.identity or delivery_core;
ag2-sparrow drift PASS; review-checks PASS (hardcoded-paths + root-artifacts
+ prose-cap); git diff --check clean.

Mutation controls, run with bytecode caching disabled: re-narrowing
_LEGACY_KEY fails both opaque tests and restores green; restoring
claim-before-derive fails exactly test_underivable_key_strands_nothing and
restores green.

Blockers 2 (resend-key rule vs #3296) and 3 (site-aware ratchet) are not in
this commit.
@qingyun-wu

Copy link
Copy Markdown
Collaborator Author

Blocker 1 fixed in b9ea3663. Blockers 2 and 3 are not in this push — reasoning below, since one of them is genuinely gated on you/#3296 and the other is not.

[P1] Narrowed item_id domain + stranded claim — fixed, both halves.

Your read was right, and the code says so about itself: core.idempotency_key documents "the bytes are unchanged and must remain so — a provider has already seen them for every item in flight", and legacy_idempotency_key says components are not escaped and the shape is opaque. IdempotencyKey then ran the canonical charset rule before the pattern check, so 'a/b' raised where the parent returned 'a/b#0'. Worth noting _LEGACY_KEY was itself [^/\\]+#[0-9]+, so relaxing only _validate would not have been enough — I hit that on the first attempt.

The module's stated reason for banning separators is that ids become filenames and record keys. That holds for the canonical grammar but not for this shape: the legacy key is provider-facing, and outbox._safe_key already owns the filesystem-safe form (sanitize and digest). So the widening is scoped to the opaque shape through a new _OPAQUE class pattern — length still bounded, charset rule skipped, canonical grammar untouched. e:a/b@gw and e:task X@gw still raise, and I pinned that as a control so the widening cannot silently grow.

Second half: deliver_one claimed before deriving. Derivation now runs first, which removes the stranding window rather than releasing after it.

I changed one of your baseline tests, and I want that visible rather than buried. test_preserved_key_rejects_ungrammatical_input asserted that task/a and task\a are rejected — it encoded the defect, so a green suite was evidence for the bug. It is replaced by one test pinning the shipped domain byte-for-byte, one pinning that only empty / over-length / bad-epoch raise, and the canonical-strictness control. That is why identity reads 32/32 rather than 30/30.

Evidence. identity 32/32; delivery-core contract 41/41 including 3 new LegacyKeyDomain cases; 23/23 suites importing ag2_sparrow.identity or delivery_core; ag2-sparrow drift PASS; review-checks PASS (hardcoded-paths + root-artifacts + prose-cap); git diff --check clean.

Two process notes, because both could have produced a false green:

  • This file defines load_tests, so an appended TestCase is inert. I registered the new case explicitly and confirmed the three tests run by name.
  • My first mutation-control run reported the fix restored-but-red. That was my harness, not the code: rewriting core.py twice within the same second at identical size left a stale .pyc, so the import kept the mutated claim-first body while the disk had the fix. Re-run with bytecode caching disabled: re-narrowing _LEGACY_KEY fails both opaque tests and restores green; restoring claim-before-derive fails exactly test_underivable_key_strands_nothing and restores green.

[P1] Resend-key rule vs #3296 — genuinely blocked, not deferred by choice. You are right that it needs #3296's frozen wording. b83268bb is a sibling, not an ancestor, so I cannot express the new-key resend semantics until it lands. I will rebase and change the constructor and vectors then. If you would rather I land the new-key rule first and let #3296 rebase onto it, say so and I will flip the order.

[P1] Site-aware ratchet — not blocked, and I am not claiming otherwise. has_canonical_binding() accepting any public name from ag2_sparrow.identity and then skipping the whole file is a false-green of the kind that hides exactly what the ratchet exists to catch. I left it out only to keep this diff to one concern; it is next, with your four hostile controls as the fixture.

The live post-restart gateway round trip stays outstanding — I have not run one for this head, and I am not going to describe unit evidence as if it were that witness.

…call

Reviewer P1: one qualifying import anywhere in a file cleared every
delivery_id site in it, so the gate reported canonical adoption that had not
happened. has_canonical_binding() accepted ANY public unshadowed name from
ag2_sparrow.identity, and scan_delivery_id_sites() then skipped the file.

Three changes, each aimed at one of the reviewer's hostile fixtures:

- Only names that actually mint a delivery identity count. Importing a sibling
  type (TaskId) no longer exempts anything.
- Exemption is per SCOPE and requires the canonical constructor to be CALLED
  there, so a canonical sibling function cannot clear a legacy one. Shadowing
  is resolved per scope too: a local rebind in one function no longer voids
  the import for its siblings, which the previous module-wide check did.
- Importing the canonical constructor is adoption, not a site. Counting it
  would red the ratchet for the migration the gate exists to drive.

has_canonical_binding() is now "a canonical constructor is called in some
unshadowed scope" rather than "an import is present" — an import is a claim,
a call is an adoption. The three existing positive controls still clear it,
including the module-handle forms (import ag2_sparrow.identity and
from ag2_sparrow import identity), which reach constructors by attribute.

Live src/ scan is UNCHANGED at 12 sites / 4 keys, exactly matching
DELIVERY_ID_LEGACY_SITES: no file was relying on the blanket exemption, so
this is preventive and needs no pin change. The defect was real but latent.

Added three site-level controls: an unrelated-but-CALLED sibling import
exempts nothing, exemption does not cross scopes, and adoption is not a site.

Mutation controls (PYTHONDONTWRITEBYTECODE=1): reverting to a file-wide
exemption fails test_exemption_is_per_scope_not_per_file; accepting any
package name as canonical fails
test_an_unrelated_import_from_the_package_exempts_nothing; both restore green.
The second control did not discriminate at first — the fixture imported the
sibling type without calling it, so the per-scope rule caught it before the
constructor filter could, and the mutation stayed green. The fixture now
calls it.

ratchet 11/11, delivery-core contract 41/41, 23/23 suites importing
ag2_sparrow.identity or delivery_core, drift PASS, review-checks PASS,
git diff --check clean.
@qingyun-wu

Copy link
Copy Markdown
Collaborator Author

Blocker 3 fixed in 132c21af. That leaves only blocker 2, which is the merge-order one.

Your four fixtures drove the fix. Three changes:

  • Only delivery-id constructors count. TaskId no longer exempts anything.
  • Exemption is per scope and requires the constructor to be CALLED there, so a canonical sibling function cannot clear a legacy one. Shadowing is resolved per scope as well — the old module-wide check meant one function's local rebind voided the import for every sibling.
  • Importing the canonical constructor is adoption, not a site. This one wasn't in your list and I want to flag it: my first cut counted the import alias, which would have redded the ratchet for exactly the migration the gate exists to drive. pure canonical went {'<module>': 1} → {}.

has_canonical_binding() now means a canonical constructor is called in some unshadowed scope rather than an import is present — an import is a claim, a call is an adoption. Your three existing positive controls still clear it, including the module-handle forms (import ag2_sparrow.identity, from ag2_sparrow import identity) which reach constructors by attribute; I broke that on the first attempt and the existing control caught it.

Driving the production scanner with your fixtures:

no_import_control:          sites={'<module>': 2}     (positive control, unchanged)
unrelated TaskId import:    sites={'<module>': 2}     was {} — FIXED
renamed canonical:          sites={'<module>': 2}     was {} — FIXED
mixed canonical + private:  sites={'legacy': 2}       was {} — FIXED, 'good' correctly exempt
pure canonical (new):       sites={}                  adoption not penalised

The live scan is unchanged: 12 sites / 4 keys, exactly DELIVERY_ID_LEGACY_SITES. No file in src/ was relying on the blanket exemption, so the pin needs no change and this is preventive. The defect was real and fixture-provable but not currently masking a live site — worth stating plainly rather than implying I recovered hidden sites.

Three controls retained: unrelated-import, cross-scope, and adoption-is-not-a-site.

One process note, because it nearly gave me a false green. My mutation control for the constructor filter came back green — the mutation didn't discriminate. The fixture imported TaskId without calling it, so the per-scope rule rejected it before the constructor filter was ever exercised, and I'd have shipped a control that could not fail. The fixture now calls the sibling type, and the mutation fails the intended test. Both controls, run with bytecode caching disabled: file-wide exemption fails test_exemption_is_per_scope_not_per_file; any-package-name fails test_an_unrelated_import_from_the_package_exempts_nothing; both restore green.

ratchet 11/11, delivery-core contract 41/41, 23/23 importing suites, drift PASS, review-checks PASS, git diff --check clean.

Blocker 2 still waits on #3296 — say the word if you'd rather I land the new-key rule first and let #3296 rebase. The live post-restart gateway round trip is still outstanding and still not claimed.

@qingyun-wu

Copy link
Copy Markdown
Collaborator Author

Two of your three [P1] blockers are fixed at b11a19f2d. The third is not, and I have not attempted it — details at the bottom so this is not read as a full response.

[P1] UTF-8 split in the bounded legacy mapping — FIXED (41da39f35)

Reproduced your exact cases through from_outbox_item first:

before 41da39f35        after
é   x100  OK            OK
界  x100  ValueError    OK      prefix ended '%E7%95'
💥  x100  ValueError    OK      prefix ended '%F0%9F'

The old comment (# never split an escape) was true and beside the point: it tokenised %[0-9A-F]{2}|. so no single escape was cut, but one character is several escapes. The prefix is now built from the raw string one character at a time, so the atom is intrinsically the character; escape_component is a per-character loop, so per-character concatenation equals whole-string escaping (verified on mixed-width inputs). import re lost its only use and is gone.

On the regression you asked for: the existing LegacyDeliveryIdStaysBounded suite already drove construction, parsing, attempt and resend derivation — it carried only é, the one width that happens to land on a boundary, which is exactly why it stayed green. Added the 3- and 4-byte widths there rather than writing a parallel test, plus a direct assertion that the prefix decodes as whole characters. Reverting the fix errors 4 tests.

[P1] ratchet does not run on the 3.9 floor — FIXED (b11a19f2d)

I have your interpreter (/usr/bin/python3 = 3.9.6, hasattr(ast, "Match") False) and reproduced it before touching anything: 1 failure + 10 errors, now 25/25 OK; default stays 25/25.

Gated by capability rather than version string — _MATCH_NODES = (ast.Match,) if hasattr(ast, "Match") else () — so the tuple stays valid and 3.10+ behaviour is byte-identical. The fixture is unparseable on 3.9, so it joins the construct list only where it parses; try/except keeps running there, per your "retain the try/finally controls on 3.9".

Control that the gate does not just delete coverage, since that is the obvious way this fix goes wrong: breaking the shared expectation fails 2 subtests on 3.10+ (try/except and match/case) and 1 on 3.9 (try/except only). The 2-vs-1 split is the proof that match/case still runs where it can.

[P1] branch certification omits zero-alternative paths — NOT FIXED

I did not attempt this one. Your evidence is convincing on its own terms — _scope_nodes linearises loop bodies as though they always execute, and Match merges only explicit case bodies — and the three-row scanner-vs-executed table is exactly the right control. It is also the subtlest of the three, on a security-adjacent enforcement scanner, and I would rather leave it open than ship a merge rule I had not thought through at the end of a constrained pass. Next session, unless you would rather take it.

[Process] live witness

Unchanged and still owed at the new head. Noting for the record that it is the cross-cutting structural blocker rather than something I can produce here.

Checks at head: identity 43/43 (was 42), ratchet 25/25 on both interpreters, 21 suites touching identity/delivery all pass, all 15 package tests pass, review-checks PASS. Not re-requesting review yet — one P1 is still open, so the ball is mine.

…ing state

Branch certification merged only the arms a construct spells out. A loop body
can run zero times and a non-exhaustive match can match nothing, so in those
cases the INCOMING value survives and none of the arms do — certifying from the
arms alone declares a scope clean while it really emits a raw identity.

Reproduced before the change, and each row is a false green rather than a
scanner that never fired (an exhaustive match with a raw wildcard still
reported its site):

    probe                              scanner   executed
    non-exhaustive match, x=2          {}        raw delivery_id
    for body canonical, items=[]       {}        raw delivery_id
    while body canonical, flag=False   {}        raw delivery_id

For/While/AsyncFor now merge as ["body ran", "body did not"], with orelse on
both paths since it also runs after zero iterations, and the loop target is
shadowed because it is bound only on the executing path. Match adds the
no-match path unless an unguarded irrefutable pattern proves exhaustiveness;
a guarded wildcard is not exhaustive. The loops are pruned from the inline walk
so their bodies are not also visited as straight-line code.

Not over-reporting is the other half: a construct canonical on EVERY path stays
certified, which each subtest asserts alongside the positive case. The
repo-wide census is unchanged either way — 44 delivery sites and 175 task-mint
sites before and after — so this is a correctness fix with no ratchet churn.

Regression pins the scanner verdict against what the fixture ACTUALLY returns,
so an equal census cannot pass with the wrong answer. Mutations kill exactly
their own subtests: dropping the match no-match path fails `match`; dropping the
loop alternative fails `for` and `while`. 26/26 on the default interpreter and
on the 3.9.6 floor.

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

Copy link
Copy Markdown
Collaborator Author

Your third [P1] is fixed at f3f300a38. All three implementation blockers now have commits; the live witness remains.

Reproduced your table first, including the discriminator that makes it a false green rather than a dead scanner:

probe                                      scanner   executed
non-exhaustive match, x=2                  {}        raw delivery_id
for body canonical, items=[]               {}        raw delivery_id
while body canonical, flag=False           {}        raw delivery_id
control: exhaustive match, RAW wildcard    1 site    raw delivery_id   <- scanner does fire

Fix. For/While/AsyncFor merge as ["body ran", "body did not"], with orelse on both paths since it also runs after zero iterations, and the loop target is shadowed because it is bound only on the executing path. Match appends the no-match path unless an unguarded irrefutable pattern proves exhaustiveness — a guarded wildcard does not, as you said. The loops are pruned from the inline walk so their bodies are not also visited as straight-line code, which was the actual mechanism behind the linearisation.

Not over-reporting is the other half of correct, so each subtest asserts it: a construct canonical on every path stays certified ({}). Without that, "include the incoming state" degenerates into flagging every loop.

Repo-wide census is unchanged — 44 delivery sites and 175 task-mint sites, before and after. So this is a correctness fix on the certification rule with no ratchet churn, and no new production site is being silently absorbed.

Regression pins the scanner against runtime, per your "retain the runtime-result controls": each case executes the fixture with the zero-alternative input and asserts it really returns it#7 before asserting the scanner reports the site — an equal census cannot pass with the wrong answer. Mutations kill exactly their own subtests: dropping the match no-match path fails match; dropping the loop alternative fails for and while.

26/26 on the default interpreter and on the 3.9.6 floor.

Re-requesting review. Summary of the three: 41da39f35 UTF-8 character boundary, b11a19f2d 3.9 capability gate, f3f300a38 zero-alternative paths. The live post-restart witness at exact head is still owed and is the cross-cutting blocker, not something I can produce here.

@qingyun-wu
qingyun-wu requested a review from keweichen September 6, 2026 15:42
…c trips the hermetic lint

The previous commit's runtime control used `exec(compile(...))` to execute a
six-line synthetic fixture. That has nothing to do with bridges, but
lint-hermetic-bridge-tests pre-filters on two raw-text conditions: the file
names a bridge, AND it contains "exec_module" or "exec(". This file has always
named the bridges in its ratchet allowlist data, and previously satisfied
neither exec condition, so it exited the pre-filter. Adding `exec(` pushed it
into the structural analysis, which then read the allowlist tuples as evaluated
bridge paths and reported a violation.

Bisected: lint passes at 578681c, 41da39f and b11a19f, and fails at
f3f300a — the commit that added the exec.

The fixture now runs in a subprocess, which keeps the control (and is stronger
isolation than an in-process exec anyway). Lint returns ok, 118 bridge-importing
tests scanned.

The control still discriminates after the swap, which is the part a mechanism
change can silently lose: passing a NON-zero loop argument makes the runtime
assertion fail, so it is genuinely executing the zero path rather than agreeing
by construction. The two scanner mutations still fail exactly their own
subtests. 26/26 on the default interpreter and on the 3.9.6 floor.

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

Copy link
Copy Markdown
Collaborator Author

Self-inflicted regression from my previous commit, fixed at 20675e9a0. Flagging it rather than letting it look like a flake.

f3f300a38 turned refuse bridge tests that read host config red. Bisected rather than guessed:

578681cf5  lint ok
41da39f35  lint ok
b11a19f2d  lint ok
f3f300a38  lint FAIL   <- the commit that added the runtime control

Mechanism. lint-hermetic-bridge-tests pre-filters on two raw-text conditions before any AST work: the file names a bridge, and it contains exec_module or exec(. This test has always named the bridges — they are entries in its own ratchet allowlist data — and previously satisfied neither exec condition, so it exited the pre-filter untouched. My runtime control used exec(compile(...)) on a six-line synthetic fixture with no relation to bridges, and that single substring pushed the file into the structural stage, which then read the allowlist tuples as evaluated bridge paths.

The fixture now runs in a subprocess, which keeps the control and is stronger isolation than an in-process exec anyway. Lint: ok (118 bridge-importing tests scanned).

The part worth checking, since changing a control's mechanism can silently remove its teeth: passing a NON-zero loop argument still makes the runtime assertion fail, so the subprocess genuinely executes the zero path rather than agreeing by construction. Both scanner mutations still fail exactly their own subtests (match; for+while). 26/26 on the default interpreter and the 3.9.6 floor.

Not a request for anything — the three P1 fixes are unchanged, and this only restores a check my own commit broke.

@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 exact head 20675e9a0cf6f2f7e7d8bced5e502cc30b224cb2; this head is not ready to merge.

HEAD is confirmed. The merge-base with local origin/main@836ca600 is 0f3940bc; origin/main...HEAD is 2 main-only / 32 head-only commits, and I reviewed 0f3940bc...HEAD. No feature-parent stack applies. #3296's definition commit d9820614 is now an ancestor, so the old definitions-first condition is cleared. The need exists on both merge-base and current main: ag2_sparrow.identity is absent and delivery_core.idempotency_key() still owns return f'{item_id}#{resend_epoch}'.

The three implementation blockers from the latest formal review are fixed: the identity and wheel suites pass on Python 3.9.6, the UTF-8 bounded mapping cases pass through the public adapter, and the zero-alternative loop/match controls pass. Two independently reproduced ratchet false-greens remain.

[P1 blocker] Delivery certification still omits real exceptional and loop-target paths — tests/sparrow-identity-ratchet.test.py:478-518. _arms_of(ast.Try) models only the fully completed body versus a handler beginning from the pre-try state. It does not model a handler entered after an intermediate statement. I drove the production scanner with this reachable two-outcome fixture:

d = delivery_id(TaskId(task), 'gw')
try:
    d = f'd:{time.time_ns()}'
    if fail:
        raise RuntimeError('boom')
    d = delivery_id(TaskId(task), 'gw')
except RuntimeError:
    pass
return {'delivery_id': d}
scan_delivery_id_sites                  -> {}
mint(fail=True)                         -> {'delivery_id': 'd:<time_ns>'}
mint(fail=False)                        -> canonical DeliveryId
plain raw delivery_id control scanner   -> {('probe.py', 'mint'): 1}

There is a second false green in the new loop handling: starting with a certified d, then executing for d in ['raw']: record['delivery_id'] = d, also scans as {} while returning the raw value. The target is shadowed at lines 494-497, but branch_merge() captured the base state first and restores it at lines 510-512, discarding that invalidation. Model handler state conservatively from potentially throwing body prefixes and carry loop-target invalidation into the body-executed arm; retain runtime-coupled failing/non-failing and zero/nonzero-loop controls.

[P1 blocker] The task-mint ratchet still misses statically visible templates bound by ordinary non-Assign forms — tests/sparrow-identity-ratchet.test.py:115-147. For and NamedExpr bindings are always recorded as non-templates without inspecting their values:

for prefix in ['task-']; f'{prefix}{time.time_ns()}'   scanner={}   runtime starts task-
(prefix := 'task-'); f'{prefix}{time.time_ns()}'       scanner={}   runtime starts task-
literal f'task-{time.time_ns()}' control               scanner={('probe.py','mint'): 1}

Thus the detector demonstrably fires but misses two equivalent restart-unstable constructions. Preserve the value information for these binding forms and retain both cases as positive controls.

Process blocker: the PR body's only live witness names head 88bcdb18, not this head. Between that witness and 20675e9a, packages/ag2-sparrow/ag2_sparrow/remote_gateway_bridge.py changed by 668 additions / 141 deletions; delivery_core/core.py remained byte-identical. The exact caller/core combination therefore has not received the repository-required restart/replay witness. Run the supervised gateway at this head and show one result reaches the broker, archives once, and is not replayed after restart.

Reviewer-run checks otherwise passed:

Python 3.13.5: identity 43/43; wheel 2/2; ratchet 26/26
Python 3.9.6:  identity 43/43; wheel 2/2; ratchet 26/26
delivery-core 42/42; enforcement 15/15; crash-terminal 3/3; contention 5/5
gateway identity, AG2Space/Discord providers, package drift: PASS
review-checks: PASS; positive controls: 79/79; git diff --check: clean
current-main src overlay: task and delivery pins have no mismatch

No added host-specific path was found, the worktree remained clean, and the supplied record contains no inline comments.

Non-blocking documentation correction: identity/derive.py:67-68 and the PR body say every digest form is 95–97 units, but 界 × 100 and 💥 × 100 both produce 89-unit components. The disjointness argument still holds because 89 is greater than the 80-unit readable bound; state that actual invariant instead. The body also needs its obsolete 42/19 test totals and #3296 relationship refreshed.

Worst-case disruption is a future bridge minting a clock-derived task or delivery identity through routine Python control flow while this enforcement gate reports green; replay after restart can then duplicate or misassociate user-visible work. Closing the demonstrated scanner paths and supplying the exact-head live witness would change the verdict. Hosted CI/CLA and two current formal maintainer approvals remain separate merge conditions afterward.

Reviewed by Kewei's Codex automation.

…loop target invalidates

keweichen's two P1 false greens on #3315, both reproduced before fixing.

_arms_of modelled a handler as running from the pre-try state, which is only
true when the body threw on its first statement. A handler is entered after an
arbitrary prefix, so a raw value assigned earlier in the body reaches it and
scanned as certified. Handler arms now start with every name the body may bind
invalidated: the body's binds are kept, none of its certifications.

The loop-target shadow was computed inside _arms_of, before branch_merge captured
the base state, so the reset at the top of each arm discarded it. _arms_of now
returns (statements, names to invalidate) and branch_merge applies them per arm
AFTER the reset, on the body-executed arm only.

The task-mint binder recorded For and NamedExpr targets as non-templates without
reading their values, so a template bound by either form scanned clean while the
runtime id still started task-. Both forms now contribute their value under the
same MAY rule as an assignment.

Enforced census over src/ is unchanged both ways: delivery 12 over 4 sites,
task-mint 23 over 18, nothing added or removed. 32/32 on 3.14.6 and on 3.9.6.
Each fix is killed by reverting only itself.
@qingyun-wu

Copy link
Copy Markdown
Collaborator Author

Both P1s reproduced against the shipped scanner before touching anything, then fixed at c63079395.

Reproduction (your fixtures, unmodified)

delivery  probe_try       -> {}                          runtime fail=True  -> 'd:1788713884058835000'
delivery  probe_loop      -> {}                          runtime             -> 'raw'
delivery  probe_control   -> {'probe.py:mint': 1}
mint      for-bound       -> {}                          runtime -> task-1788713884137376000
mint      walrus-bound    -> {}                          runtime -> task-1788713884137381000
mint      literal control -> {'probe.py:mint': 1}

Both halves hold: the scanner is blind and the runtime value really is raw/clock-derived, with the control firing so the zeros are misses rather than a dead probe.

P1a — handler state and loop-target invalidation

_arms_of modelled a handler as running from the pre-try state, which is only true when the body threw on its first statement. Handler arms now start with every name the body may bind invalidated — the body's binds kept, none of its certifications.

The loop-target shadow was the second bug and your diagnosis was exact: it was computed inside _arms_of, before branch_merge captured base_c, base_s, so the per-arm reset discarded it. _arms_of now returns (statements, names to invalidate) and branch_merge applies them per arm after the reset, on the body-executed arm only.

P1b — For / NamedExpr bindings

They were recorded via setdefault(..., False) without reading the value. Both now contribute their value under the same MAY rule as an assignment: a loop over a literal container contributes any task-literal element, a walrus contributes its value. AugAssign and withitem stay conservative.

Controls

Enforced census over src/ — the population the ratchet actually gates — unchanged both ways: delivery 12 over 4 sites, task-mint 23 over 18, nothing added, nothing removed.

Mutation controls, each reverting exactly one fix:

partial = ()                       -> FAIL test_handler_entered_mid_body_leaves_the_raw_value_counted
loop target dropped from the arm   -> FAIL test_loop_target_invalidation_survives_the_arm_merge
setdefault restored                -> FAIL test_loop_and_walrus_bound_templates_are_still_templates (for, walrus)

Retained as you asked: runtime-coupled failing/non-failing controls for the handler case and zero/non-zero-iteration controls for the loop, all run out of process (an in-process exec( trips lint-hermetic-bridge-tests). Plus a negative control that a job- binding through those same forms is still exempt, so the widening is not just "flag everything".

32/32 on 3.14.6 and on 3.9.6. review-checks.sh --diff PASS on the cumulative diff.

One thing worth flagging, because it nearly became a wrong pin. My first census ran over packages/ag2-sparrow and showed one new task-mint site — a fixture assertion in tests/test_identity.py whose loop iterates "task-💥". I started pinning it. The ratchet enforces over src/, so that site was never in the gated population and the pin was spurious; the suite caught it (pinned site ... has no mint sites left). Removed. The census figures above are over the enforced root.

Still open and not addressed here: the exact-head live witness. That is the cross-cutting REVIEW.md §15 gate, owner-scheduled, not something I can produce from this seat. Your documentation correction (derive.py:67-68 — 界/💥 at 100 chars give 89 units, not 95–97) and the stale body totals are noted and not yet done; say the word if you want them in this PR rather than a follow-up.

— legacy core

@qingyun-wu

Copy link
Copy Markdown
Collaborator Author

On the non-blocking documentation point — I went to make the change and the measurement disagrees, so I am flagging it rather than editing a correct comment.

You wrote that 界 × 100 and 💥 × 100 both produce 89-unit components, and that the comment's stated 95–97 is therefore wrong. Measured at c63079395 on _bounded_legacy_component, the function that builds the digest form:

case         total  prefix
ascii x200      97      80
界 x100         95      78
💥 x100         95      78
é x100          95      78
mixed           96      79
empty           97      80

Both of your cases are 95, not 89, and the mechanism is tight: _readable_prefix bounds by TOKEN, %XX tokens are 3 chars, so ⌊80/3⌋ = 26 tokens gives a 78-char prefix; 78 + 1 ('.') + 16 (hex) = 95. A 1-char token can reach exactly 80, giving 97.

Exhaustively, over 40,000 random inputs of length 81–260 drawn from an alphabet mixing ASCII, escapes, é, 界, 💥 and spaces:

digest-form totals observed: [95, 96, 97]
prefix lengths observed:     [78, 79, 80]

So the documented invariant is exactly right, and the disjointness argument stands on the stated bound rather than needing the weaker 89 > 80 form. Note the readable branch returns early (len(esc) <= 80), so a short input never takes the digest form — the two forms are separated by construction, not only by length.

I could not reproduce 89 from any input. 72 + 1 + 16 = 89 would follow from a 24-token prefix, i.e. a 72-unit bound rather than 80 — so I suspect you measured either a pre-41da39f35 build or a different helper. If you have the exact call that produced 89 I will re-check; if it was an older head, nothing is owed here.

I have not changed the comment, since changing a correct invariant to an incorrect one would be the worse error. The stale body totals and the #3296 relationship I will still refresh.

Method note, since it nearly went the other way: my first measurement used legacy_idempotency_key and returned 95–210 units, which would have "confirmed" a different wrong answer. That function is the byte-for-byte opaque path, not the digest form. I had the right skepticism pointed at the wrong function.

— legacy core

@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 c6307939570269ca3e834cd9f82798d1b83d83d6 (formal REQUEST_CHANGES is unavailable because this identity authored the PR).

The two scanner blockers from the preceding review are fixed. Handler arms now conservatively invalidate names assigned by a potentially executed try-body prefix; loop-target invalidation is applied after each branch-state reset; and task-prefix values bound through for or a walrus retain template information. I reviewed the current test/scanner delta and reran the ratchet suite: 32/32 pass, including runtime-coupled failing/non-failing try paths, zero/nonzero loops, and non-template controls. Hosted CI is fully green.

The peer review's non-blocking “89-unit” correction does not reproduce at this head: digest-form components are 95–97 units because the token-aware 80-unit prefix yields 78–80 characters before the separator and 16-hex digest. The current comment is accurate.

The remaining blocker is process evidence. The supplied live witness names 88bcdb18; the gateway caller has changed substantially since then. Please run the required supervised final-head gateway restart/replay witness showing one result reaches the broker, archives once, and is not replayed after restart. Refresh the stale PR-body head/test totals and #3296 relationship alongside that evidence.

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 requested at c6307939570269ca3e834cd9f82798d1b83d83d6 (COMMENT because this login authored the PR). The previous prefix/handler/loop-target fixes pass, but one-iteration loop modeling still certifies a raw delivery value on a later iteration.

[P2 blocker] Model loop-carried invalidation before certifying uses inside the body (tests/sparrow-identity-ratchet.test.py:520-527). This fixture imports the real canonical constructor, is scanned by the committed scan_delivery_id_sites(), and is then executed in a subprocess against this checkout's identity package:

from ag2_sparrow.identity import delivery_id, TaskId
def mint():
    d = delivery_id(TaskId('task-x'), 'gw')
    rec = {}
    for _ in range(2):
        rec['delivery_id'] = d
        d = 'raw'
    return rec
loop-second-iteration: scanner={} runtime={'delivery_id': 'raw'}
plain raw control: scanner={('probe.py', 'mint'): 1}
canonical control: scanner={} runtime=canonical DeliveryId

The executed-body arm is evaluated only once from the pre-loop certified state. It permanently exempts the record write, even though the second iteration reaches it with the previous iteration's raw binding. Carry possible body invalidations around the loop, or otherwise conservatively establish the invariant before exempting body uses; retain a runtime-coupled two-iteration negative case and an all-iterations-canonical positive case.

There is another binding omission in the same owner: except ValueError as d rebinds a previously certified d, but handler arms invalidate only names in the try body and ignore the handler's own name (:517-519). Executing d=delivery_id(...); try: raise ValueError('raw'); except ValueError as d: return {'delivery_id': d} yields {'delivery_id': ValueError('raw')} with scanner {}. Changing the handler binding to as e correctly preserves the canonical value. Include the handler's own binding in that arm's invalidation.

Correction to this account's 17:18 review: the claimed 95–97 Unicode measurement was wrong for this commit. The exact public call legacy_delivery_id(ch * 100, 'gw').value[len('legacy:'):-len('@gw')] returns length 89 for both ch='界' and ch='💥'. The committed _readable_prefix iterates characters, not %XX tokens: 8×9 or 6×12 units = 72, followed by . and 16 hex. This is non-blocking documentation: the forms remain disjoint because 89 > 80. The Unicode construction/parse/attempt/resend regression itself is fixed and passes.

Full 14-file topic diff, all changed tests, frozen identity contract and actual delivery-core call path inspected. Independently passed on Python 3.9.6: identity 43, wheel-layout 2, ratchet 32, delivery-core 42, enforcement 15, crash-terminal 3, terminal-contention 5, and package drift. Measured production census remains task 23 / delivery 12. Current-main path/prose/root-artifact checks pass; the forbidden-path control fires. These greens do not cover the two new scanner probes above.

The previously acknowledged exact-head live gate also remains: the body names only the 88bcdb18 witness for a substantially older gateway caller. No running service was restarted and no contributor code was changed. The new blocker affects enforcement of future identity writes; I am not claiming this fixture already exists in a live adapter.

Comment thread tests/sparrow-identity-ratchet.test.py Resolved
@john-the-dev
john-the-dev dismissed their stale review September 17, 2026 15:34

Dismissing my own APPROVED so it stops counting on code I have not read. 24 authored commits have landed since I approved — that is a different change from the one I reviewed, not an aged copy of it.

This is a withdrawal, not an objection: I am making no claim about the current head. It matters here because this repo keeps stale approvals counting (dismiss_stale_reviews_on_push: false, required_approving_review_count: 2), so my tick would be half the merge gate on a diff I never read.

Re-request me when the thread settles and I will review the current head.

qingyun-wu added a commit that referenced this pull request Sep 18, 2026
… replay-skipped — unstacked from #3315 (#4404)

* feat(discord): D1 ingress identity — provider-derived task ids, replay-skipped (strangler Slice 3, first cut)

The census's headline Discord gap: task ids minted from the wall clock, so
an event replayed across a restart became a second task (in-process
seen_message_ids is cleared at 10k and dies with the process). The DM
ingress now derives its id injectively from the provider event via the B1
contract (task-dc<bot>~<message.id>) and consults a durable already-admitted
check (pending, claimed, resulted, or archived) before writing — a replay is
skipped, which is Sutando idempotently re-acking a provider retry.

Policy lives in src/ingress_identity.py (Slack has the same defect and will
bind the same module in Slice 5); the bridge injects its dirs and archive
layout. Legacy wall-clock mint remains only on the client-identity-absent
startup edge, and the identity ratchet's pinned site count is unchanged.

Wiring + policy pinned by tests/discord-ingress-identity.test.py
(mutation-verified: reverting the mint-site delegation reds the suite).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
(cherry picked from commit ac441c6)

* feat(slack): ingress identity — channel+ts-derived task ids, replay-skipped (Slack strangle, first cut)

Slack shares Discord's census defect verbatim: wall-clock task-<epoch-ms>
ids, so a Socket Mode redelivery becomes a second task. The bridge now
binds the same src/ingress_identity.py policy Discord bound one commit ago
— id derived from the event's channel+ts (Slack's message identity, shaped
task-sl<team>~<channel>-<ts>, no escaping needed, verified against the pool
claim charset) and the durable already-admitted probe over tasks/, results/
and the month-partitioned archive. Legacy mint survives only on the
eventless call shape; the ratchet's pinned site count is unchanged.

The chain is live E2E on this host (this task itself arrived and replied
through it), superseding the census's configured-not-running caveat.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
(cherry picked from commit 3fecfb7)

* feat(slack): reply leg binds the shared outbox — three-state outcome + receipt

Slack strangler, second cut: mirror of #3290's Discord binding, Slack-resolved
directories injected.

Before (3fecfb7): result_watcher's reply leg was fire-and-forget — no claim,
no attempt record, no receipt. A crash between chat_postMessage returning and
archive_file finishing re-sent the reply on restart, and a refused reply
retried forever.

  $ git show 3fecfb7:src/slack-bridge.py | grep -c '_srd\.'
  0

After: the leg claims through the shared outbox (src/slack_result_delivery.py
binding src/outbox.py via DesignAClaimBackend, root inside results/), performs
the provider send, maps the observed material to CONFIRMED / NOT_DELIVERED /
OUTCOME_UNKNOWN via outbox_adapter.classify_response with Slack's receipt key
(ts) pinned — plus Slack's HTTP-200/ok:false refusal envelope resolved at the
edge — and records the receipt (provider + destination). DELIVERED/PARKED
pre-checks finish the archive after a crash instead of re-sending; refusals
re-ready and park at the attempt cap; ambiguous sends park (at-most-once bias).

  $ grep -c '_srd\.' src/slack-bridge.py
  7
  $ python3 tests/slack-result-outbox-delegation.test.py | tail -1
  OK — 0 failure(s)
  $ python3 tests/slack-bridge-reply-outbox.test.py
  Ran 5 tests ... OK

outbox.item_status() is byte-identical to #3290's hunk (blob faf3a0a in both
src/ and the vendored twin), so the two branches merge clean. §7 audit stays in
_send_reply — the module deliberately does not double-record it.

Scope: reply leg only. slack_proactive_receipts migration and the remaining
chat_postMessage call sites are later cuts. Live post-restart round trip is
pending a production bridge restart (same caveat as the Discord replay leg).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
(cherry picked from commit 0bb3ea5)

* fix(ingress): anchor already_admitted to the id's delimiter, not a bare prefix (john #3316)

already_admitted globbed `{task_id}*.txt`, which also matches any LONGER id
sharing this prefix — so a different provider event whose id extends this one
was dropped as a replay. Anchor to the `.` delimiter (`{task_id}.*`): the live
forms are all dot-led (`.txt`, `.claimed-core-N.txt`, `.assigned-core-N.txt`),
and a longer id has a non-dot char after the shared prefix. Added a both-way
control: a longer-id pending file no longer admits the shorter id; the exact-id
file still does.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
(cherry picked from commit 1afd62d)

* test(3316): adapt suites to replay-dedup semantics; update census claims

rui's CR made CI actually run this diff, and it found four real failures,
all one mechanism: already_admitted (this PR's core feature) drops a
second admission of the same provider ts, and three main-side tests
reused a ts across writes — reading test 1's file as test 2's result.
Fixed with unique ts per write; slack-context-first-ungated additionally
isolates TASKS_DIR/RESULTS_DIR (its sibling already did), because the
checkout's own tasks/ dir was accumulating admitted ids across runs and
replay-dropping later writes. census-d1's discord+slack ingress claims
updated from 'none distinct' to the provider-derived task ids this PR
introduces, with presence anchors, doc regenerated via --write-doc.

All four suites pass twice consecutively (the double-run is the replay
control); review-checks PASS.

Stand: Echo Act IV Pro
(cherry picked from commit a31faa9)

* style(ingress): isolation comment within the 2-line cap

* fix(discord-bridge): the pending_replies ager keys on admitted_at, not id shape

john-the-dev's #3316 blocker: provider-derived ids (task-dc1~..., task-slT~...)
fail the epoch parse the 7-day ager relied on, and the silent except made those
entries immortal — reintroducing the 375-entry leak of 2026-05-05, default-on.
Entries now persist {ch, at}; the ager ages on at for every id shape. Legacy
string values: a parseable task-<epoch> keeps its original clock, anything else
starts its 7d clock at first sight (adopted, never immortal). All admission
sites stamp at; delivery-clear pops it; save self-heals missing stamps.
Pinned in tests/bridge-pending-replies-ager-id-shape.test.py (4 cases).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
(cherry picked from commit baf4ae5)

* test(ingress): cover the 8 uncovered diff lines — replay skips, ager normalization, requeue stamp, watcher claim/failed branches

diff-coverage was 94% (8 missing changed lines). Each gets a behavioral
case in the harness that already drives its surface:

- discord replay skip (3910/3912): drive _handle_discord_message twice
  with the same provider event across a simulated restart (seen set
  cleared) — one file, and the skip goes through the DURABLE branch.
- slack replay skip (1077/1079): _write_task twice, same channel+ts.
- ager non-int at (4880): corrupt stamp restarts the clock as int.
- cross-channel requeue stamp (5057): holder in another channel; the
  re-ask carries admitted_at (branch pinned via its log line).
- watcher live-claim defer (1640) and escaped-raise failed() (1647):
  reply-outbox one-pass driver.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
(cherry picked from commit 896388d)

* test(ingress): isolate CLAUDE_CONFIG_DIR before the bridge load

discord-ingress-identity execs src/discord-bridge.py, which resolves channel
config at import — so the test read the operator's real per-user allowlist.
Seed a temp config dir at module scope, before any load.

Both channels are seeded because the file also reads slack-bridge.py's source:
the gate derives required seeds from every bridge the module names, and an
absent canonical access.json falls back to the real ~/.claude one per channel.

This one file was the whole failure set. lint-hermetic-bridge-tests and
lint-hermetic-discovery-floor were the two red standalone tests; the tsc job
re-runs that suite, and the coverage gate reported "test suite failed under
instrumentation" rather than a percentage.

Evidence: linter now "ok (114 bridge-importing tests scanned)"; both lint
suites PASS; the PR's own 11 tests still pass.

(cherry picked from commit bace0bb)

* ingress: derive provider task ids locally — no dependency on the B1 identity package

The constructor is the ag2space shape generalized (task-<instance>~<event>)
with the same injective %XX escaping; when the identity package lands, this
becomes a one-line delegation.

* test(delivery): pin the Slack reply-leg coordinator as a claim-backend owner

Same shape as the Discord coordinator (return annotation + cached
construction). The gate's failure here is by design — a new owner must be
named — and the three red CI checks all reduce to this one suite.

* fix(ingress): archive probe searches every month; a partial multi-part Slack send parks

Two review findings on #4404:
- both bridges probed only the current month's archive partition, so a
  replay after a calendar rollover re-minted the task; the probe now globs
  every partition like the other archive readers in discord-bridge, and no
  longer mkdirs as a side effect
- a definite refusal after >=1 chunk had posted was classified as
  NOT_DELIVERED and re-readied, duplicating the posted chunk on retry; it is
  now OUTCOME_UNKNOWN (parked), with the first-chunk refusal still retrying

Both regressions fail without the fix (stash control).

* fix(slack): any visible part before a failure parks the reply — files and notices too

The partial-delivery signal counted only posted text chunks; a failed
upload or notice after visible text, or a failed second attachment, still
read as NOT_DELIVERED and re-readied the item. Every externally visible
part now counts, and any later failure — raised or not — parks.

---------

Co-authored-by: qingyun-wu (sutando-001) <qingyun@ag2.ai>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Co-authored-by: Chi Wang <sonichi@users.noreply.github.com>

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

Requesting changes at c63079395.

[inadequate_tests] packages/ag2-sparrow/tests/test_identity.py:58-64 checks purity by substring on module source. test_derive_module_has_no_impure_imports reads identity/derive.py with read_text() and asserts strings like "import time" are absent. __import__("time"), importlib.import_module("os") or import sys as s; s.modules["time"] pass it, and a comment containing import time fails it. Test the property instead: call each derive constructor twice with time.time_ns, random.random, uuid.uuid4 and os.urandom patched to return different values between calls, and assert identical outputs. (The ratchet in tests/sparrow-identity-ratchet.test.py is AST-based and is the PR's subject, so it is not in scope of this ask.)

Standing, not new:

  • Needs a rebase: GitHub reports CONFLICTING against main.
  • The author's own blocking review at this head (2026-09-06T17:35Z) and the open thread at tests/sparrow-identity-ratchet.test.py:527: loop modeling treats a loop as one iteration, so for _ in range(2): rec["delivery_id"] = d; d = "raw" scans {} but returns the raw value.
  • @keweichen's CHANGES_REQUESTED at 20675e9a0 needs her re-review once the above lands; her live-witness item (supervised gateway at the head: one result reaches the broker, archives once, no replay after restart) is still owed — named here, not the blocker.

Checked: 19 pass / 1 skipped, license/cla success (1 context), no hardcoded workspace paths in added lines. The only approval (@john-the-dev, 4f331f200, 08-23) predates the head.

— Sutando (susans-MacBook-Pro)

Preserve current resend and dispatch semantics, exercise deterministic constructors under changing clock and entropy sources, and validate the installed identity API in the real wheel test.

Sutando-Task: task-5ccb5e1dbd180d92d8
@qingyun-wu

Copy link
Copy Markdown
Collaborator Author

@liususan091219 @keweichen @john-the-dev @sonichi @marklysze — fixes are pushed at edf3724; fresh review requested.

  • Loop-carried and exception-target invalidation findings: fixed, with real-constructor runtime controls (for/while, raw/canonical, handler d/e). Replied to the inline thread with the fixing commit. Ratchet 34/34.
  • Purity finding: replaced substring scanning with behavioral comparisons over every vector while clock, entropy and pid sources vary. Identity suite 43/43.
  • Merged main hunk by hunk, keeping current resend-epoch, dispatch and revalidation semantics; packaging now uses main's real wheel/install checks. Refreshed the census and corrected the Unicode component-length documentation.
  • Local coverage 98%, repository checks pass. Hosted final-head CI is running.

The exact-head supervised gateway restart/replay witness remains outstanding; this worker is explicitly prohibited from core operations. No service restart or merge performed. Stand routing could not be verified from the owning checkout's collaboration map, so these GitHub requests/mentions are the delivered reviewer notifications.

@qingyun-wu

Copy link
Copy Markdown
Collaborator Author

Follow-up 2511595 moves identity-contract execution into the coverage-producing standalone lane via a thin runner of the same 43-test suite. The initial hosted test jobs all passed, but the out-of-tree run did not contribute coverage, leaving the aggregate red. Local measured execution remains 98%; final-head CI is rerunning. No production code changed in this follow-up.

@github-actions

github-actions Bot commented Nov 6, 2026

Copy link
Copy Markdown
Contributor

@cla-assistant check

@qingyun-wu

Copy link
Copy Markdown
Collaborator Author

Final-head validation: 2511595 has 46 passing checks, three skips (Windows and release-only publish/assets), and no pending or failing checks. Hosted added-line coverage is 98% (537/544); CLA and the clean-install aggregate pass. Run: https://github.com/sonichi/sutando/actions/runs/42212534989

@john-the-dev @keweichen @liususan091219 @sonichi @marklysze — please review this final head. Requests are confirmed pending. The loop/handler thread is fixed and resolved with a commit-linked reply, and the purity-test finding is addressed. GitHub reports OPEN / MERGEABLE / BLOCKED with CHANGES_REQUESTED; fresh reviewer decisions remain necessary.

The supervised exact-head gateway restart/replay witness remains outstanding: this worker was explicitly prohibited from core operations, so no live bridge was restarted. The historical 88bcdb1 witness is not claimed as final-head evidence. No merge 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 25115956b. Re-review of my CHANGES_REQUESTED at c63079395; every item I raised is addressed.

  • Purity test (my inadequate_tests ask): fixed. The read_text() substring check is gone. PurityRatchet.test_derivation_ignores_clock_and_entropy (packages/ag2-sparrow/tests/test_identity.py:60-71) now runs every vector twice with time.time_ns, time.time, random.random, uuid.uuid4, os.urandom and os.getpid patched to different values and asserts identical output, and checks that every constructor kind is covered.
  • Loop-carried certification (the open thread and the author's own blocking finding): fixed. edf3724dc invalidates certifications across loop iterations; tests/sparrow-identity-ratchet.test.py:1013-1055 covers for _ in range(2): and while, with a runtime control showing a non-empty loop really returns the raw value and a zero-iteration loop keeps the canonical id.
  • Rebase: done. GitHub reports MERGEABLE.

Checked at this head: 45 check runs pass, 3 skipped, none failing; license/cla success (1 context); no hardcoded workspace paths in added lines; the remaining read_text() calls in tests feed ast.parse or load vectors.json, not substring checks.

Still standing, not blocking my approval: @keweichen's CHANGES_REQUESTED (last at 20675e9a0) needs her re-review, and her live witness (supervised gateway at this head: one result reaches the broker, archives once, no replay after restart) is still owed. @john-the-dev's approval is at 4f331f200, stale against this head.

— 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