DUNIN7 · LOOMWORKS · RECORD
record.dunin7.com
Status Current
Path change-requests/cr-2026-233-foray-r2-forward-gap-closure-v0_2.md

CR-2026-233 — FORAY R-2: Forward-Gap Closure — v0.2

Version: v0.2 Date: 2026-08-21 Status: Drafted; pre-flighted at engine 70e5639 and halted on one stale premise, corrected here. Markdown primary — the consumer is Claude Code. Changes from v0.1: Site 10's forward list was stale. v0.1 said engagement_id, room, describing the gap as it stood before CR-2026-231 Gate 1 added agent_actor_id and session_id to the same function. Four values, not two — §3a. The origin of the staleness is recorded at §11 rather than smoothed, and the rule it earns is at §7c. Site 17 verified complete by CC's pre-flight and confirmed to add no work (§8). Everything else checked clean at HEAD and is unchanged. v0.1 stands as sibling. Implements: Stage 2 of foray-reference/loomworks-foray-coverage-resolution-proposal-v0_3 §10 — R-2, ruled close them all at D3. Scoped by: foray-reference/loomworks-foray-r2-forward-gap-scoping-note-v0_4, with its census correction at …-v0_4-census-correction-v0_1. Rulings carried: the six registry dispositions at foray-reference/loomworks-anchor-priority-registry-dispositions-v0_1 (Operator, 2026-08-20); the divergent-query fix splits out (§10a Ruling 2). Runtime direction screen: No conflict — every value forwarded is already in scope at its call site, most of them already passed to Loomworks' own audit write one line above. No new act becomes possible, no authority widens, and _foray_reserved_emit remains a no-op stub that discards its payload. Cites: standing-notes/loomworks-standing-note-two-records-and-what-crosses-between-them-v0_1 (STANDING) §6; standing-notes/loomworks-standing-note-migrations-and-the-code-that-runs-at-another-time-v0_2; standing-notes/loomworks-standing-note-a-test-that-pins-a-literal-cannot-follow-a-ruling-v0_1; O1, O5, O7, O10; R1; D3, D6, D8.


Plain-language summary

At seventeen places in the engine a marker sits where a FORAY record will one day be built. At most of them the useful facts — which grant, which engagement, which proposal, who authorized it — are sitting right there in the function, often handed to Loomworks' own audit write one line above, and then dropped. This change forwards them.

Nothing transmits. Nothing anchors. _foray_reserved_emit stays a stub that throws its payload away. The point is that the payload each site can attest is the specification of what it will one day attest, and a site that drops its identifiers would one day produce a record that validates and says nothing.

Three things travel with it because they live in the same functions: a confirmed ordering bug where a database row's id is read before the row exists, the same bug a second time in a four-row loop, and two settings sites carrying byte-identical emit code that should share one builder.

The anchor-priority registry gets the six dispositions the Operator already ruled — one retirement, one rename, three annotations, one held. It does not get entries for the forty-nine live event kinds that lack them; deciding what deserves anchoring is room-posture work, not this.


1. What R-2 is, and what it is not

A forward gap is not missing data. It is data that exists, in scope, at the call site, and does not reach the emit payload. The proposal's own words: in every case the identifier is in scope at the call site, and at the settings sites it is passed into Loomworks' own audit write one line above the FORAY emit and then dropped.

Closing a gap means: pass the in-scope values into _foray_reserved_emit's payload dict. That is the whole change at most sites.

It does not mean: building an emitter, making a network call, constructing or validating or anchoring a record, or changing any production behaviour. The payload argument is passed to a function that discards it.

Why before there is an emitter. The payload is the specification of what each site can attest. Building the emitter against sites that drop their identifiers would produce records that validate and say nothing, and the fix would then be a change to every call site instead of one module.

2. Scope discipline — five things this CR must not do

  1. It does not design the linkage mechanism. Where a forwarded value obviously wants to be a reference — proposal_id at site 11, the site 3/4 token-to-credit relationship, the FIFO lot linkage at site 10 — forward the value and state the wish in a comment. Stage 6's ceiling is gone (R2 ruled PERMIT) but the mechanism is unbuilt and is not R-2's to invent.
  2. It does not present any reference as verified. Permitting a reference is not verifying it. N4 marks the trail leaves FORAY's holdings here — a note for a reader, not a verification result.
  3. It does not mass-add registry entries for the 49 live kinds with none. Assigning anchor priority decides what deserves anchoring, which is room-posture work at Stages 3–5.
  4. It does not clean up the six O7 pre-registrationsagent_session_minted, _closed, _refused, _silent, agent_identity_minted, agent_identity_frozen. A registry entry with no writer is exactly what O7 asks for while the emitter sleeps. §7 reports a consequence about four of them and fixes none.
  5. It does not fix the engagement_committed_divergent query. Ruled split out (§10a Ruling 2). R-2 stays a forward-gap CR.

3. Step 1 — the credit family

credit/flows.py, credit/room_consumption.py, credit/proposal_applier.py. Site numbers as the mapping numbers them.

| # | Event kind | Forward | |---|---|---| | 1 | credit.issuance | grant_id | | 2 | credit.provisioning | authorized_by, when supplied | | 3 | credit.consumption_token (×4) | engagement_id | | 4 | credit.consumption_credit | engagement_id | | 5 | credit.suspension | requested_by, expires_at | | 6 | credit.reactivation | auth_method | | 7 | credit.deletion | deletion_kind | | 8 | credit.balance_zeroing | Nothing. Payload is complete for what the site holds | | 9 | credit.referral_credit | converted_person_id | | 10 | credit.consumption_token / _credit (×3) | Four values — see §3a. engagement_id, pipeline_stage, agent_actor_id, session_id | | 11 | credit.corrective | proposal_id, shape_event_id, applied_by |

Site 11 — forward, and do not build. proposal_id is the thing the correction corrects and reaches neither the payload nor the metadata dict. Forwarding it does not by itself produce a reference: a single-Action record has no component for an F21.ref to resolve against. Comment the wish; leave the mechanism to Stage 6.

Site 9 is unreached code. write_referral_credit_flow has no callers. Close its gap anyway — it is live, correct, and will fire when Phase 48 wires a caller.

Sites 3 and 10 keep the PROVIDER: scheme break, and the comment explaining it stays. Provider token assets are genuinely Anthropic-denominated; claiming LOOMWORKS: over them would be false. D14 is unruled; if it is ruled before this executes, cite it rather than leaving the break unexplained.

3a. Site 10 — four values, and where two of them came from

v0.1 said two values and was wrong at HEAD. CR-2026-231 Gate 1 added _flow_metadata() to credit/room_consumption.py, which builds flow.extra_metadata carrying engagement_id, pipeline_stage, and — new — agent_actor_id and session_id. All four live on flow, the parameter already in scope at _emit(flow, txid). None reaches the FORAY payload, which still forwards only flow_id, transaction_id, asset_id, quantity, from_party, to_party, turn_event_id.

Forward all four. They are exactly what R-2's definition names: in scope at the call site, handed to Loomworks' own write one line above, then dropped. Site 8's nothing dropped precedent does not apply here, because something is dropped.

Two conditions on the shape:

  1. Omit rather than null, matching _flow_metadata's own convention. A key present-and-null would read as an agent whose session went missing — the reasoning CR-2026-231 Gate 1 already applied to extra_metadata itself.
  2. Forward UNSESSIONED_ACTION_ID as-is when that is the value. It is a recorded absence, not a null, and a payload that silently dropped it would lose the distinction the sentinel exists to make. Read the sentinel, do not interpret it.

Read the four from flow.extra_metadata rather than re-deriving them, so the FORAY payload and the credit record cannot disagree about the same act.

4. Step 2 — the governance-shaped family

| # | Event kind | Forward | |---|---|---| | 12 | memory.<event_kind> | object.id, object.object_type, engagement_id, actor.id, actor.kind | | 13 | audit.setting_change (enum) | person_id, engagement_id, current, committed | | 14 | audit.setting_change (numeric) | person_id, engagement_id, current_f, committed | | 15 | conversation.turn | person_id, engagement_id, actor.id | | 16 | audit.setting_change (button) | person.id from the authenticated principal, previous, normalized |

Site 12 is the largest gap in the set and the one worth most. It is Memory's single write path, carrying every memory event kind, and its payload today is event_id and anchor_priority alone. The object id, engagement id, actor id and actor kind are all in scope and none reaches the payload — which is exactly why the mapping had to put the stated placeholder "UNATTRIBUTED" in F3. Closing this is what lets a Memory record name its own subject.

Site 12 is also three rooms' only FORAY path. render_produced, shape_produced, composition_ and shaping_ are live event kinds flowing through it today. Manifestation, Shaping and Rendering have no dedicated call site; until Stage 3 gives them one, this is not a placeholder for those rooms, it is their only path. Do not re-badge site 12 as a Rendering site or invent room-specific emission there — it still emits under memory.<event_kind>.

actor.kind now ranges over five valuesperson, companion, contributor, agent, system. Pin against the constant, not a literal, per the literal-pinning note.

Sites 13, 14 and 16 are one shape three times. The identity is handed to Loomworks' own audit write one line above and not handed to the emit.

5. Step 3 — three defects in the same functions

5a — the suspension-site ordering defect (D8). Confirmed, not suspected. write_suspension_flow calls db.add(flow), emits, then await db.flush(). Its siblings write_reactivation_flow and write_deletion_flow flush first. CreditFlowRow.id is server_default=text("gen_random_uuid()"), so SQLAlchemy has no visibility into it before flush. flow.id is read before the row exists, on every call. Fix the ordering to match the siblings. This is a real bug independent of FORAY.

5b — site 3's flush timing is the same defect again. Four rows db.add-ed, the emit loop reads row.id before any flush. Same server-side default. Fix both.

5c — the duplicated settings emit construction. Sites 13 and 14 carry byte-identical emit calls in two independent functions. room_consumption.py's _emit helper is the pattern. A shared payload builder, not a centralised emit — canon Principle 1 is one emit per business event. The helper builds the dict; each site still calls its own emit.

6. Step 4 — the registry dispositions

Per loomworks-anchor-priority-registry-dispositions-v0_1, which rules all six. This CR is where the code lands.

| Entry | Action | |---|---| | agent_registered | Retire in place, annotated as superseded by agent_identity_minted. Not deleted — the annotation is why it is not silently dropped | | induction_cycle_recorded | Rename the key to induction_cycle, keeping "standard". A live writer already writes that string at engagement/creation.py and persons/personal_engagement.py. Do not rename the writer — the registry moves to match reality | | candidate_engagement_discarded | No action. Held, pending the candidate-discard design question | | contribution_credential_issued / _claimed / _revoked | Keep, annotated: the lifecycle persists via raw SQL into contribution_credentials and never calls append_event, so these acts touch neither record and no reserved location. Point the comment at the filed bypass finding |

The annotation on the credential three should use the standing note's phrasing: not wrong for being outside Memory — wrong for being outside both records.

7. Step 5 — report, do not fix

Two findings to record in the CR's own closing report, neither actionable here.

7a — four pre-registered entries have no destination. _ANCHOR_PRIORITY is consulted by Memory's write path. CR-2026-231 established that grant-lifecycle events go to the operational record, not Memory. So agent_session_minted, _closed, _refused and _silent were registered in good faith against a mechanism that turns out to be the wrong one. Do not delete them and do not implement against them. Refer all four to the dispositions document, joining agent_identity_minted and candidate_engagement_discarded.

That family now has six members, and the pattern is worth stating there: pre-registering an event kind commits to a destination as well as a name.

7b — the 49 unregistered live kinds. Record the count and that the gap is the serious half — real activity, including all of Shaping's and Rendering's, moving through the one FORAY-adjacent site with no priority assigned. Nothing in the code looks wrong, no query fails, no test goes red. A silent gap, stated so it stops being silent. R-2 does not close it.

7c — the rule this CR's own pre-flight earned. Record in the closing report:

> A change request that puts a value in scope at a site carrying a # FORAY_RESERVED_LOCATION marker forwards it to the emit payload in the same change.

Otherwise every CR quietly widens R-2's closure list, and R-2 chases a target that moves whenever anyone touches those seven files. §11 is the instance that earned it. This is a rule about how change requests are written, and it should reach a standing note rather than living only here — but filing that note is not this CR's work.

8. The seventeenth site

agents/grant_lifecycle carries a reserved location added by CR-2026-231 §7. Screened and verified complete at pre-flight: every column record_lifecycle_event's INSERT writes is what its payload carries — agent_identity_id, event_kind, session_id, reason, actor.id, actor.kind, occurred_at, and the row's own id. No gap, no work, no CR growth. It adds a site to the census and nothing to this closure list.

Worth noting for the closing report: site 17 was written under §7c's rule before the rule existed. Site 10, written five commits earlier in the same arc, was not.

On the census number. loomworks-foray-r2-forward-gap-scoping-note-v0_4 §7 reports 16 sites; the correction note establishes there were already 17 markers because the emitter's own docstring carries the format string, and 18 after the lifecycle writer. Sites, not strings: 16 before CR-2026-231, 17 now. Do not re-derive by grep | wc -l — it over-counts the docstring and under-counts the collapsed multi-emit markers. AST or nothing.

9. Acceptance

  1. Every gap in §3, §3a and §4 is closed, or a reason is stated for that site in a comment. Site 8 states its reason: nothing is dropped.
  2. Site 10 forwards all four values, read from flow.extra_metadata, omitting absent keys rather than nulling them, and passing UNSESSIONED_ACTION_ID through unchanged when present. A test asserts the sentinel survives the forward.
  3. _foray_reserved_emit is unchanged and still discards its payload. No production behaviour changes anywhere — asserted by the suite being green with no test's outcome changed except those added.
  4. Site 12 forwards all five of its in-scope values, and a test asserts actor.kind against the constant rather than a literal.
  5. write_suspension_flow flushes before emitting, matching its siblings; site 3's loop likewise. A test asserts the id is populated at emit time.
  6. Sites 13 and 14 share one payload builder and retain two separate emit calls.
  7. The four registry actions in §6 land; the two held entries are untouched.
  8. §7's three findings — including §7c's rule — and §8's screening result are in the closing report.
  9. Suite green with the delta accounted for; ruff clean; mypy against the unchanged baseline; no migration.

10. Gates

Explicit path staging; no git add -A; halt before push at every gate. No migration in any gate — if one appears necessary, halt rather than adding it.

11. Correction from v0.1 — and where the gap came from

Preserved rather than smoothed.

v0.1's site 10 row said engagement_id, room. That was the gap as the mapping recorded it on 2026-08-18. It is not the gap today.

The gap was created by this arc, five commits before the change request that closes it. CR-2026-231 Gate 1 added agent_actor_id and session_id to flow.extra_metadata at a site carrying a FORAY marker, and did not forward them to the payload beside it. So the change written to make agent actions attributable produced a site where attribution is available and dropped — precisely what R-2 exists to close.

Nobody was careless. CR-2026-231 §4 was scoped to the credit record and said so. R-2's mapping was written before those fields existed. Neither document was wrong when written; the gap opened between them. Same shape as the freeze blast radius — a fact living only where two correct pieces of work meet, invisible in either alone.

Found by pre-flighting, not by reading. The site table was confirmed current at e951e60c and became stale at 15d6425 eleven days later. A file-level git log since the confirmation point is what surfaced it — which is the check §7c's rule would make unnecessary.


DUNIN7 — Done In Seven LLC — Miami, Florida CR-2026-233 — FORAY R-2: Forward-Gap Closure — v0.2 — 2026-08-21