Context
The A+FU terminal review of PR #16239 (pullrequestreview-4832880658) named one non-blocking follow-up at merged head 1383ebbee3: "fanout() clones only the top-level annotation. Two joined callers therefore share nested delivery values: mutating waiter A changed waiter B, while the cache stayed pristine." The review's explicit guidance: "clone per waiter or narrow the stronger 'any other delivered copy' claim before Related: #16223 and #16224 adopt the family primitive" — the hardening transfers to the next family-adoption lane; it is not deferred #16222 correctness.
The Problem
boundedRetryGate.mjs fanout() delivers each waiter {...annotated, gate: {...}} — a top-level spread. Nested objects/arrays inside the result (e.g. detail, tags) are shared BY REFERENCE across every joined waiter's delivered copy. Two joined callers can therefore mutate each other's received truth (the reviewer's joined-waiter probe: mutating waiter A's nested value changed waiter B's). The gate's internal cache is NOT affected (closed separately — annotate/settle-cache/snapshot all deep-clone), so this is a caller-to-caller alias, invisible while the only production consumer (HealthService) ignores scheduled deliveries. The moment a family consumer (#16223 miniSummary backfill, #16224 TenantRepoSync) consumes tick()/runNow() results, joined callers share mutable state.
The Architectural Reality
ai/services/shared/boundedRetryGate.mjs:
annotate() deep-clones the result body (structuredClone) when building the shared annotated result — ONCE per settle/serve.
fanout(annotated, waiter) then spreads that ONE annotated object per waiter — the per-waiter gate fields (coalesced, demandedKey, demandedGenId) are fresh, but the result body's NESTED values still alias the single annotated object.
The isolation boundary ends one level too early: per-waiter metadata is isolated, per-waiter nested result state is not.
The Fix
In fanout(), structuredClone the annotated result per waiter before attaching the per-waiter gate fields — a waiter can never observe another waiter's mutations. Deliveries are small (health-result-sized); the per-waiter clone cost is negligible and bounded by the waiter count.
Alternative considered and rejected: narrowing the JSDoc isolation claim instead of hardening — leaves the trap armed for exactly the consumers the primitive exists to serve (the family adoption lanes).
Contract Ledger
| Target Surface |
Source of Authority |
Proposed Behavior |
Fallback |
Docs |
Evidence |
boundedRetryGate delivery results (tick() / runNow() resolutions) |
ai/services/shared/boundedRetryGate.mjs fanout() |
Every waiter's delivered result is an independent deep copy; mutating one waiter's nested value never changes another waiter's |
none — isolation is unconditional |
module JSDoc names per-waiter delivery copies explicitly |
joined-waiter nested-mutation spec (below) |
Decision Record impact
none — no ADR governs the retry gate; ADR-0019 unaffected (no config surface touched).
Acceptance Criteria
Out of Scope
- The family adoption itself — #16223 and #16224 adopt the gate in their own lanes; this ticket only discharges the precondition the A+FU review named.
- Any API/annotation shape change (keys, fields, vocabulary).
Related
Source: #16222 (closed by PR #16239) · the A+FU review pullrequestreview-4832880658 · adoption lanes #16223 / #16224 (blocked-by this hardening per the review's guidance) · merged head 1383ebbee3.
Live latest-open sweep: checked latest 20 open issues at 2026-07-31T23:55Z; no equivalent found. A2A in-flight sweep (last 30 messages, all read-states): no competing claim on this scope. KB semantic sweep: unavailable (collection mid-rebuild, count 0) — substituted live GitHub + grep over resources/content/**.
Origin Session ID: 05b5fdc9-1f2b-4b45-a2c9-4b64ed5f15cd
Retrieval Hint: query_raw_memories("boundedRetryGate fanout joined waiter nested alias structuredClone A+FU follow-up")
Context
The A+FU terminal review of PR #16239 (pullrequestreview-4832880658) named one non-blocking follow-up at merged head
1383ebbee3: "fanout()clones only the top-level annotation. Two joined callers therefore share nested delivery values: mutating waiter A changed waiter B, while the cache stayed pristine." The review's explicit guidance: "clone per waiter or narrow the stronger 'any other delivered copy' claim before Related: #16223 and #16224 adopt the family primitive" — the hardening transfers to the next family-adoption lane; it is not deferred #16222 correctness.The Problem
boundedRetryGate.mjsfanout()delivers each waiter{...annotated, gate: {...}}— a top-level spread. Nested objects/arrays inside the result (e.g.detail,tags) are shared BY REFERENCE across every joined waiter's delivered copy. Two joined callers can therefore mutate each other's received truth (the reviewer's joined-waiter probe: mutating waiter A's nested value changed waiter B's). The gate's internal cache is NOT affected (closed separately — annotate/settle-cache/snapshot all deep-clone), so this is a caller-to-caller alias, invisible while the only production consumer (HealthService) ignores scheduled deliveries. The moment a family consumer (#16223 miniSummary backfill, #16224 TenantRepoSync) consumestick()/runNow()results, joined callers share mutable state.The Architectural Reality
ai/services/shared/boundedRetryGate.mjs:annotate()deep-clones the result body (structuredClone) when building the shared annotated result — ONCE per settle/serve.fanout(annotated, waiter)then spreads that ONE annotated object per waiter — the per-waiter gate fields (coalesced,demandedKey,demandedGenId) are fresh, but the result body's NESTED values still alias the single annotated object.The isolation boundary ends one level too early: per-waiter metadata is isolated, per-waiter nested result state is not.
The Fix
In
fanout(),structuredClonethe annotated result per waiter before attaching the per-waiter gate fields — a waiter can never observe another waiter's mutations. Deliveries are small (health-result-sized); the per-waiter clone cost is negligible and bounded by the waiter count.Alternative considered and rejected: narrowing the JSDoc isolation claim instead of hardening — leaves the trap armed for exactly the consumers the primitive exists to serve (the family adoption lanes).
Contract Ledger
boundedRetryGatedelivery results (tick()/runNow()resolutions)ai/services/shared/boundedRetryGate.mjsfanout()Decision Record impact
none — no ADR governs the retry gate; ADR-0019 unaffected (no config surface touched).
Acceptance Criteria
boundedRetryGate.mjsJSDoc names the per-waiter delivery-copy guarantee explicitly.test/playwright/unit/ai/services/shared/+memory-core/suites green at the landing head; no public contract shape change (isolation only).Out of Scope
Related
Source: #16222 (closed by PR #16239) · the A+FU review pullrequestreview-4832880658 · adoption lanes #16223 / #16224 (blocked-by this hardening per the review's guidance) · merged head
1383ebbee3.Live latest-open sweep: checked latest 20 open issues at 2026-07-31T23:55Z; no equivalent found. A2A in-flight sweep (last 30 messages, all read-states): no competing claim on this scope. KB semantic sweep: unavailable (collection mid-rebuild, count 0) — substituted live GitHub + grep over
resources/content/**.Origin Session ID: 05b5fdc9-1f2b-4b45-a2c9-4b64ed5f15cd
Retrieval Hint:
query_raw_memories("boundedRetryGate fanout joined waiter nested alias structuredClone A+FU follow-up")