DUNIN7 · LOOMWORKS · RECORD
record.dunin7.com
Status Current
Path change-requests/cr-2026-231-w6-writing-the-grant-into-the-record-v0_1.md

CR-2026-231 — W6: Writing the Grant Into the Record — v0.1

Version: v0.1 Date: 2026-08-21 Status: Drafted, not pre-flighted. Markdown primary — the consumer is Claude Code. Runtime direction screen: No conflict — W6 implements the recording half of O1 and serves the accountability commitment by writing the authorizing grant at the moment of the act, on paths that already carry it. It widens no authority: every value written is one already in scope at its call site, and no new act becomes possible. Grounded on: inspection-briefs/loomworks-w6-step-0-inspection-findings-v0_1 (engine 22bec5d, dev DB inspected live). Cites: standing-notes/loomworks-standing-note-agent-accountability-v0_1 (STANDING) §1, §3, §6; standing-notes/loomworks-standing-note-operational-identifiers-are-not-audit-trail-facts-v0_1 (STANDING); DR-01 as amended, DR-02, DR-18; O1, O2, O5, O9, O10; R1. Operator rulings carried: separate field, not capability_ref (2026-08-21).


Plain-language summary

H0 built a gate that will not let an agent act without a grant, and then nothing writes the grant down. This change writes it down.

Three things get recorded that are not recorded today. Which run an action happened under — carried on the actor reference that every event already serializes, so no new table and no migration. What a session did over its life — minted, closed, swept as silent. And the one an audit most wants and the system least records: a refused grant, which currently writes nothing anywhere at all.

The refusal is the only part that needs a new place to put something, because a refusal is not a session and must not be filed as one. That decision is flagged at §7 as the one thing in this change request the Operator may want to overrule.

Two entries that looked like this change's work turn out not to be. Freezing an identity is a W2-lane act W2 did not record; it is folded in here deliberately and labelled as such. Minting an identity has no runtime moment at all, because identities are seeded — it goes back to the registry dispositions rather than being given a writer that would have to lie.

Scope discipline: the shaping path only. The render path is ungoverned by sessions and this change does not reach it — closing that is the per-binding scoping item.


1. What is false today

loomworks-standing-note-agent-accountability-v0_1 §1 is STANDING and commits that every agent action is auditable — the action records who acted and what authorized itby construction, not by policy. §6 makes attribution on every agent action a preserved invariant.

Against the inspection:

| Commitment | State | |---|---| | Records who acted | True on the shaping path since CR-2026-227 and CR-2026-228 | | Records what authorized it | False. session_id reaches the gate and is dropped | | Session lifecycle recorded | False. append_event count is 0 across sessions.py, grantability.py, presence.py | | Refusal recorded | False, absolutely. A refused grant writes nothing anywhere |

Why this is not repairable later. The operational-identifiers note permits backfilling values recomputable from the log and excludes attribution explicitly. Which session an action ran under is not recomputable — it existed for a window and nothing outside the moment records the pairing. Actions taken without it never carry it.

Why this is not urgent, stated honestly. Q7 found four agent-room calls in the engine's history, all predating W1, and zero rows in agent_sessions. The permanently-unattributable set is empty today. This change is necessary and is not racing anything.

2. What this change does not do

3. Step 1 — the session id travels on ActorRef

No migration. ActorRef serializes whole into memory_events.provenance.wasAttributedTo, which is JSONB; 472 dev rows already carry the capability_ref key by the same mechanism.

  1. Add session_id: UUID | None = None to ActorRef (memory/base.py, sibling to capability_ref at :200). Document it as the agent session under which this action ran; distinct from capability_ref, which is the delegation assertion authorizing the actor to act for a person. Both may be present; neither implies the other.
  2. Populate it wherever an agent's ActorRef is constructed on a path that holds a session. The gate already has session_id in RoomSpendContext.
  3. AgentDecision.session_id is present, unwritten and unread (engagement/types.py:397-404). Its one construction site — agents/retrieval.py:136 — does not pass it. Pass it.

Why the actor and not the payload. Attribution belongs with the actor reference, which is the one structure every recorded event already carries. Putting it in payloads would make it optional per event kind, which is how a by-construction guarantee becomes a by-policy one.

Test: an agent-kind event written on a path holding a session has provenance.wasAttributedTo.session_id populated. Assert against the constant, not a literal — per the literal-pinning standing note.

4. Step 2 — the spend record names the agent and the run

write_room_consumption_flows receives eight of the ten fields in scope at the gate; agent_actor_id and session_id are not among them. CreditFlowRow.extra_metadata is JSONB, so this needs no migration.

Pass both through and record them in extra_metadata alongside engagement_id and pipeline_stage.

On the own-key gap, and why nothing new is invented for it. gated_room_complete writes no flow row at all when key_source != "system", so own-key agent work leaves no spend record. Do not manufacture a zero-cost flow row to fill it. A credit flow is a record of spend; writing one for work that spent nothing would make the credit record false in order to make the audit record complete.

The attribution for own-key work rides Step 1 instead: the work produces memory events, and those carry the actor with its session. The spend row is a convenience copy; the memory event is the record of record. State this in the CR so a later reader does not read the flows table as the attribution source.

5. Step 3 — session lifecycle events

Four transitions, all with a real runtime act and no writer. Locations from Q4:

| Event kind | Location | |---|---| | agent_session_minted | agents/sessions.py::mint_session | | agent_session_closed | agents/sessions.py::close_session, ::end_session | | agent_session_silent | agents/presence.py::sweep_silent_sessions | | agent_identity_frozen | agents/grantability.py::freeze_agent_identityW2-lane, see §6 |

All four are pre-registered in _ANCHOR_PRIORITY and this change gives them writers. Do not add registry entries; they exist.

Constraint: append_event requires a MemoryObject and an engagement_id. Mint, close and silent all have an engagement context. Refusal does not — §7.

Ordering, per the W7 precedent and 4a's lesson: write the event after the row is durable, never before. mint_session uses a raw INSERT; confirm the row is committed or flushed before the event is appended, and match the pattern of whichever sibling already does it correctly.

6. Freezing is W2-lane debt, paid here, labelled

agent_identity_frozen is not a session event. Freezing an identity is a grantability act built by CR-2026-229, which did not record it.

Fold it in anyway, and say why in the CR: leaving it out means the tightening lane — the mechanism the whole arc is built to demonstrate — has no audit trail. A freeze that stops an agent and leaves no record is the mirror of A4's control that reports success without acting.

Label it as W2-lane debt discharged by W6, not as W6's own scope. The inspection was right to refuse the miscount and the record should carry the correction rather than absorb it.

7. The refusal — the one decision to rule

A refused grant writes nothing anywhere. It returns a SessionRefusal and logs. It is the least recorded event in the system and the one an audit most wants, because a refusal is the moment governance actually did something.

It cannot go through append_event: no MemoryObject, and not necessarily a settled engagement.

It must not be written as a row in agent_sessions. A refusal is not a session. Filing it there is the same category error as putting a session id in capability_ref, and it would corrupt "which agents are running right now" — a query that reads ended_at IS NULL — with rows that never started.

Proposed: a new table, agent_grant_decisions. One row per grantability verdict, granted or refused. Columns: identity, outcome, reason (non-null on refusal), decided_at, and the resulting session_id when granted. Migration 0115.

What this buys. Every grant decision has one home; a refusal is a first-class recorded event rather than an absence; and the granted case gains a durable link from decision to session that agent_sessions alone does not express. It also gives check_grantability — which W2 deliberately kept to exactly one caller — exactly one place to record its verdict.

What it costs. A migration and a table where none existed, in a change that is otherwise migration-free. The cheaper alternative is a nullable-column extension of agent_sessions, which is rejected above on category grounds and is recorded here as the rejected option rather than omitted.

This is the one item in this change request the Operator may want to overrule. Do not proceed past Step 3 without it ruled.

8. agent_identity_minted — referred, not written

There is no runtime moment at which it could be emitted. Identities are seeded by migration 0113 and never minted at runtime, by the SEED ruling; grep confirms no INSERT INTO agent_identities or AgentIdentityRow(...) anywhere in src/.

W6 does not give it a writer. Emitting it would require either migration-time emission or a change to seeded-not-minted, and neither is this change's business.

Refer it to foray-reference/loomworks-anchor-priority-registry-dispositions-v0_1 as a new item, in the same family as candidate_engagement_discarded: an entry whose implementation shape makes it unwritable. Both need a disposition and neither gets one here.

9. Acceptance

  1. An agent action on the shaping path is recorded with both its actor and the session that authorized it, and the session is reachable from the record without inference.
  2. provenance.wasAttributedTo.session_id is populated on agent-kind events written on a session-holding path; capability_ref is unchanged and still means delegation.
  3. Mint, close and silent each write their pre-registered event kind, after their row is durable.
  4. A freeze writes agent_identity_frozen.
  5. A refusal is recorded — location per §7's ruling.
  6. Own-key agent work is attributable through its memory events, with no credit flow row invented for it.
  7. Suite green with the delta accounted for; ruff clean; mypy against the unchanged baseline; migration round-trips on dev and test if §7 adds one.

10. Gates

Halt at each. Explicit path staging; no git add -A; halt before push.


DUNIN7 — Done In Seven LLC — Miami, Florida CR-2026-231 — W6: Writing the Grant Into the Record — v0.1 — 2026-08-21