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

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

Version: v0.2 Date: 2026-08-21 Status: Drafted, not pre-flighted. Markdown primary — the consumer is Claude Code. Changes from v0.1: §3 rewritten. v0.1's carrier was wrong — it specified populating the session on the ActorRef at its construction site, and the shaping agent's ActorRef is built once at bootstrap and outlives every session it acts under. The correction is preserved at §11 rather than smoothed. §3.3 withdrawnRetrievalAgent has no session in scope and v0.1 asked CC to pass a value that does not exist. A third population is named at §3a: agent contributors acting over the HTTP API, which hold no session and cannot. Acceptance and gates updated. v0.1 stands as sibling. 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. It widens no authority: every value written is one already in scope at the act, and no new act becomes possible. Grounded on: inspection-briefs/loomworks-w6-step-0-inspection-findings-v0_1 (engine 22bec5d); CC's Gate 1 pre-flight halt, 2026-08-21. 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); standing-notes/loomworks-standing-note-a-test-that-pins-a-literal-cannot-follow-a-ruling-v0_1; DR-02, DR-18; O1, O2, O9, O10; R1. Operator rulings carried: separate field, not capability_ref (2026-08-21); the session travels with the act, not the actor (2026-08-21).


Plain-language summary

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

The first draft got the carrier wrong in a way worth stating plainly, because the mistake is the same one the arc has now made twice. An agent's actor reference is built once when the agent starts and used for every job it ever runs. A session is minted per job. So a session id cannot be set on the actor — by the time a session exists, the actor is already built, and frozen. Attaching a per-action value to a per-process object would have attributed one run's session to every later run.

The fix is that the session is bound at the moment of the act, not at construction: the write site takes the long-lived actor and derives a per-action copy carrying that job's session. Same field, same serialization, no new table, no migration — but the value is bound where it is true.

A third population turned up in pre-flight that neither the inspection nor the first draft knew about: agent contributors acting over the HTTP API, fifteen-plus sites, holding no session and unable to. They account for most agent events in the record. If they simply write nothing, the record fills with blanks that a reader cannot distinguish from a session that was lost. §3a proposes making that absence explicit rather than empty, and flags the scope question it rests on as the Operator's.

Two decisions are held for the Operator: §3a (the API population) and §7 (where a refusal is recorded). Gates 1 and 2 run without them.


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.

| Commitment | State at 22bec5d | |---|---| | 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 |

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

Not urgent, stated honestly. Four agent-room calls in the engine's history, all predating W1; zero rows in agent_sessions. The permanently-unattributable set is empty today.

2. What this change does not do

3. Step 1 — the session is bound at the act

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

3.1 — the field. Add to ActorRef (memory/base.py, alongside capability_ref at :200):


session_id: UUID | None = None

Document it as: the agent session under which this specific 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. Bound per action at the write site, never at actor construction — see below.

3.2 — bound at the write, not at construction. ActorRef is frozen=True, and on the shaping path it is built once at shaping_bootstrap.py:107 and registered in an in-process registry. produce_shape then runs many jobs, each minting its own session, and writes that same long-lived actor at shaping.py:773 and :802.

So there is no construction site at which a session exists. The actor is per-process; the session is per-job.

Bind it where the act happens, by deriving a per-action copy:


actor_for_this_act = self.agent_actor.model_copy(update={"session_id": session_id})

Frozen Pydantic models support model_copy; the original is untouched and the shared registry instance never acquires a session. Apply at shaping.py:773 and :802, and at any other write on a path where produce_shape has minted a session.

Why this and not a threaded parameter into append_event. Widening the signature touches 55 files. The actor reference is already the structure every recorded event carries, and deriving a copy at the write keeps the value bound where it is true. The field lives on the type; the value belongs to the act.

The failure mode to guard. A write site that forgets to derive the copy produces session_id: None silently, and a green suite proves nothing — the literal-pinning note's lesson applies directly. So:

3.3 — withdrawn. v0.1 instructed passing session_id into AgentDecision at agents/retrieval.py:136. RetrievalAgent has no session anywhere in its scope. The field stays present, unwritten and unread until something gives retrieval a session, which is not this change's business. Withdrawn rather than deleted so the record shows it was asked and why it was refused.

3a. The third population — and the legibility problem

Held for the Operator. Gate 1 does not depend on it.

Pre-flight found 29 agent-ActorRef construction sites across 22 files. Beyond the shaping path and the known render path, a third population: agent contributors acting over the HTTP API — 15+ router sites plus api/deps.py:1156, each converting an agent contributor row reached by bearer token. Plus the seed-induction agent at seed_induction.py:215 and creation.py:575, which now holds a seeded identity and still mints no session.

They are most of the record. 221 actor_kind='agent' memory events across 79 distinct actor ids in dev, against 14 agent contributor rows.

The problem is not that they lack sessions. It is that their absence would not be legible. After Step 1, an agent event with session_id: None could mean this agent acts on a path that has no sessions, by design or this agent should have carried one and the write site forgot. A reader cannot tell them apart, and neither can a test. An absence that is not legible as deliberate is the failure mode this arc keeps finding — it is A4's control that reports success without acting, in a different costume.

The engine already has the sanctioned answer to exactly this. LEGACY_UNRESOLVED_ACTOR_ID exists because one path genuinely cannot name an identity, and the file's own comment gives the reasoning: a reader can tell "we do not know who" from "this specific person", which a fresh uuid4() could never do. It is called a sentinel, not a fabrication.

Proposed, not ruled: a documented sentinel — UNSESSIONED_ACTION — written into session_id on agent writes that occur on paths where no session exists by design. Three states become distinguishable: a real session id, a deliberate sentinel, and None as a defect that a test can then actually catch.

What this rests on. The sentinel records an exclusion; it does not justify one. The prior question is the Operator's: are API-acting agent contributors inside the accountability commitment's scope?

Either way the sentinel is the right mechanism, which is why it is proposed here rather than deferred: it is correct under both answers and disappears naturally under the first.

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 (credit/models.py:115), so 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 is invented for it. gated_room_complete writes no flow row when key_source != "system", so own-key agent work leaves no spend record. Do not manufacture a zero-cost flow row. A credit flow records spend; writing one for work that spent nothing would falsify the credit record to complete the audit record.

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. Say this in the code comment so a later reader does not treat the flows table as the attribution source.

5. Step 3 — session lifecycle events

Four transitions with a real runtime act and no writer:

| 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 already pre-registered in _ANCHOR_PRIORITY. Do not add registry entries.

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

Ordering, per the W7 precedent and the confirmed flow.id defect: write the event after the row is durable, never before. mint_session uses a raw INSERT; confirm commit or flush before appending.

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

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

Fold it in, because leaving it out means the tightening lane — the mechanism the whole arc demonstrates — has no audit trail. A freeze that stops an agent and leaves no record is the mirror of a control that reports success without acting.

Label it W2-lane debt discharged by W6, not W6's own scope. The inspection was right to refuse the miscount; the record carries the correction rather than absorbing it.

7. The refusal — held for the Operator

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 a row in agent_sessions. A refusal is not a session. Filing it there is the same category error as putting a session in capability_ref, and it would corrupt "which agents are running right now" — which reads ended_at IS NULL — with rows that never started.

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

Buys: every grant decision has one home; a refusal becomes a first-class recorded event rather than an absence; the granted case gains a durable decision→session link. It also gives check_grantability — kept to exactly one caller by W2 — exactly one place to record its verdict.

Costs: a migration and a new table in an otherwise migration-free change. The rejected cheaper option — nullable columns on agent_sessions — is recorded here rather than omitted.

Do not start Gate 3 until this is ruled.

8. agent_identity_minted — referred, not written

No runtime moment exists 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(...) in src/.

W6 does not give it a writer. 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.

9. Acceptance

  1. A shaping-path agent action is recorded with both its actor and the session that authorized it, reachable from the record without inference.
  2. provenance.wasAttributedTo.session_id carries the session that job minted — asserted against the actual value, not against non-null. capability_ref is unchanged and still means delegation.
  3. The shared registry ActorRef still has session_id is None after a job runs. The copy is a copy.
  4. Mint, close and silent each write their pre-registered event kind, after their row is durable.
  5. A freeze writes agent_identity_frozen.
  6. Own-key agent work is attributable through its memory events, with no credit flow row invented for it.
  7. A refusal is recorded — location per §7's ruling.
  8. 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

Explicit path staging; no git add -A; halt before push at every gate.

11. Correction from v0.1

Preserved rather than smoothed.

v0.1 §3.2 said: populate session_id wherever an agent's ActorRef is constructed on a path that holds a session.

Why it was wrong: no such site exists. The shaping agent's ActorRef is built once at bootstrap, frozen, and reused across every job; sessions are minted per job. At construction no session exists, and by the time one does the actor is built.

The lesson, and it is a repeat. This is the same error as the W1 finding that five CR versions claimed the commit path threads session_id — a field specified onto a structure whose lifecycle cannot carry it. Both were caught by someone attempting the work, not by reading the draft. The discriminating question, worth asking of any future attribution field: what is the lifetime of the thing I am attaching this to, and is it the same as the lifetime of the value?

v0.1 §3.3 asked CC to pass a session id that does not exist in RetrievalAgent's scope. Withdrawn at §3.3.


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