Version: v0.3
Date: 2026-08-21
Status: Gate 1 executed and committed on branch cr-2026-231-gate-1 (15d6425), unmerged. Gates 2 and 3 redrafted here. Markdown primary — the consumer is Claude Code.
Changes from v0.2: The destination was wrong for three of five transitions and incidental for the other two. v0.2 treated append_event as the home for grant-lifecycle events with refusal as the exception; freeze has no engagement context either, and no MemoryObject type exists for any of them. §5 replaces them all with one grant-lifecycle record. v0.2's §7 (a refusal table) is subsumed rather than deleted — it was the right table under too narrow a name. Old §6 (freeze) folds out of Gate 2 into the lifecycle gate. The anchor-priority consequence is new at §8. Gate 1 is unchanged and stands as executed. v0.1 and v0.2 stand as siblings; v0.1's carrier error is preserved at §11.
Runtime direction screen: No conflict — W6 implements the recording half of O1. It widens no authority: every value written is one already in scope at the act, no new act becomes possible, and the new table is a record of decisions already being made.
Grounded on: CC's Gate 1 report and Gate 2 halt, 2026-08-21; inspection-briefs/loomworks-w6-step-0-inspection-findings-v0_1.
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; standing-notes/loomworks-standing-note-h0-tightening-guarantee-and-its-scope-v0_1; DR-02, DR-18; O1, O2, O7, O9, O10; R1.
Operator rulings carried: separate field, not capability_ref; the session travels with the act, not the actor; the grant lifecycle is one record (all 2026-08-21).
Gate 1 is done: an agent action on the shaping path now carries the session that authorized it, bound at the moment of the act.
Gate 2 halted, and the halt was right. The plan was to write session events into Memory's event log. Three of the five events cannot go there. Freezing an identity is platform-wide by design, so it has no single engagement to file under. A refusal has no engagement at all. And no memory-object type exists for any of them — the only generic one says in its own docstring that it is test machinery.
The first draft treated those as exceptions to a rule. They are the rule. A grant is not engagement knowledge. It is a fact about an identity and a window, and Memory's event log locks an engagement row and advances its version every time it writes — the wrong machinery for an act that spans every engagement or belongs to none.
So all five go to one place: a grant-lifecycle record. Refused, granted, closed, swept silent, frozen. One home for the whole life of a grant, with no engagement dependency, because a grant does not have one.
Two decisions remain the Operator's: §3a (the API-agent population) and §6 (the tension this creates with Memory).
loomworks-standing-note-agent-accountability-v0_1 §1 is STANDING: every agent action is auditable — the action records who acted and what authorized it — by construction, not by policy.
| Commitment | State | |---|---| | Records who acted | True on the shaping path | | Records what authorized it | True on the shaping path as of Gate 1; false elsewhere | | Session lifecycle recorded | False | | Refusal recorded | False, absolutely | | Freeze recorded | False |
Not repairable later — attribution is excluded from what the operational-identifiers note permits backfilling. Not urgent — zero rows in agent_sessions, four agent-room calls in the engine's history, all predating W1.
On branch cr-2026-231-gate-1 at 15d6425. ActorRef.session_id added; _append_shape_produced derives actor_for_this_act per act; write_room_consumption_flows carries the agent and session into extra_metadata, omitted rather than nulled when absent — a key present-and-null would read as an agent whose session went missing. No zero-cost flow row invented. Suite 3909 → 3918, ruff clean, mypy baseline untouched.
Two things CC did that the CR should record as precedent. Acceptance 3 was pinned two ways — a unit test that a second derivation does not inherit the first, and a source assertion that neither write site still uses self.agent_actor. And model_copy semantics were verified at the installed Pydantic 2.13.0 before a line was written rather than assumed.
shaping.py:785 stays as it is. shaping_agent_ref=self.agent_actor is a field on the persisted ShapeEvent naming which agent produced the shape — a pointer to the producer, not an attribution of the act. Merging it with :773/:802 would collapse two different meanings because they happen to name the same agent.
Recorded, not resolved: one payload now contains the same agent twice, once with a session and once without. That is §3a's legibility problem in miniature. It is correct as written and it will look odd to a reader who does not know why.
Unchanged from v0.2 §3. ActorRef is frozen=True and built once at bootstrap; sessions are minted per job. The write site derives a per-action copy with model_copy. The field lives on the type; the value belongs to the act.
§3.3 remains withdrawn — RetrievalAgent has no session in scope.
29 agent-ActorRef construction sites across 22 files. Beyond shaping and the known render path: agent contributors acting over the HTTP API — 15+ router sites plus api/deps.py:1156 — and the seed-induction agent at seed_induction.py:215 and creation.py:575. They hold no session and cannot.
They are most of the record: 221 actor_kind='agent' events across 79 actor ids in dev, against 14 agent contributor rows.
The problem is legibility, not absence. After Gate 1, session_id: None could mean this path has no sessions by design or the write site forgot. A reader cannot tell, and neither can a test.
Proposed, not ruled: a documented UNSESSIONED_ACTION sentinel, on the precedent of LEGACY_UNRESOLVED_ACTOR_ID — whose own comment gives the reasoning: a reader can tell we do not know who from this specific person.
The prior question is the Operator's: are API-acting agent contributors inside the accountability commitment's scope? If yes, they need grants and W6 is much larger than this change. If no, the exclusion must be said, and a sentinel says it more durably than a document. The sentinel is correct under both answers and disappears under the first.
Unchanged. Own-key work gets no invented flow row: a credit flow records spend, and writing one for work that spent nothing would falsify the credit record to complete the audit record. Attribution rides the memory event instead — the spend row is a convenience copy; the memory event is the record of record.
v0.2 said mint, close and silent have an engagement context and refusal does not. The claim was wrong and the framing was worse.
grantability.py:135 — freeze_agent_identity(*, agent_identity_id, actor, db, now). No engagement_id, by design: identity is per-role and platform-wide, so one freeze ends sessions across many engagements at once. That is the blast radius documented twice in this arc.mint_session returns SessionRefusal before any engagement is settled.MemoryObject type exists for a session event, and append_event requires one. GenericMemoryObject says in its own docstring that it is Phase-1 test machinery; using it in production would be misuse of a type that names itself as unsuitable.append_event locks the engagement row and advances engagement_version — machinery for engagement knowledge, applied to an act that spans every engagement or belongs to none.Mint and close only appeared to fit. They have an engagement in scope because the job does. A session's engagement is a property of the job, not of the grant. Three of five could not go to Memory and two fitted incidentally, which is a rule, not a set of exceptions.
agent_lifecycle_events — migration 0115. One row per grant-lifecycle act.
| Column | Notes |
|---|---|
| id | PK, server_default gen_random_uuid() |
| agent_identity_id | FK to agent_identities. The one thing every event has |
| event_kind | granted, refused, closed, abandoned, identity_frozen |
| session_id | FK to agent_sessions, null on refused and identity_frozen — neither is about a session |
| reason | Non-null on refused and abandoned; the citable verdict text |
| actor_id / actor_kind | Who performed the act. Human on freeze, system on the sweep |
| occurred_at | |
| engagement_id | Nullable, and null is correct — populated on granted/closed where the job had one; null on freeze and refusal because there is none |
Name. agent_grant_decisions was v0.2's proposal and is rejected: a close is not a decision and a sweep is not a decision. The record is a lifecycle, not a decision log. v0.2's table was the right table under too narrow a name — it is subsumed here, not discarded.
CC named three ways past freeze's missing engagement and all three were bad: pick one affected session's engagement (arbitrary and false for the others), write one event per engagement (N records of one act), or write none (defeats the purpose).
The fourth way is that they are different acts. A freeze writes one identity_frozen row. Each session the cascade ends writes its own closed row with reason='frozen'. N+1 rows, none arbitrary, none false: one act of freezing, and N acts of ending.
This is the shape the tightening-guarantee note already implies — freeze ends every live session in the same call. Each ending is a real event and deserves its own row.
| Location | Writes |
|---|---|
| agents/sessions.py::mint_session | granted, or refused on the SessionRefusal return path |
| agents/sessions.py::close_session, ::end_session | closed |
| agents/presence.py::sweep_silent_sessions | abandoned |
| agents/grantability.py::freeze_agent_identity | identity_frozen, plus closed per ended session via the cascade |
Ordering, per the W7 precedent and the confirmed flow.id defect: write after the row is durable, never before. mint_session uses a raw INSERT — confirm commit or flush first.
The refusal is the one that matters most. It is currently the least recorded event in the system and the one an audit most wants, because a refusal is the moment governance actually did something.
Raised rather than absorbed, because it touches what the seed commits to.
The seed commits that Memory accumulates with provenance and is the record. A separate lifecycle table can be read as a second record living outside Memory.
The case that it is not a conflict: memory_events is engagement-scoped knowledge, and the engine already keeps substrate operational fact elsewhere — credit.flows, agent_sessions, MigrationEventRow. A grant lifecycle is operational fact about the substrate, not knowledge about an engagement. This adds no new precedent.
The case that it deserves a ruling anyway: the events being recorded are governance acts, and governance acts are exactly the kind of thing a reader would expect to find in the record. Filing them outside Memory means "walk the record" means two places.
CC found the same shape one table over. The contribution-credential lifecycle — three pre-registered anchor entries, zero writers — writes via raw SQL into its own table and never reaches append_event. Issuing and revoking a contribution credential is an authority act with the same homelessness. This is a missing seam in the substrate, not a W6 detail, and it is already filed at foray-reference/loomworks-contribution-credential-event-log-bypass-v0_1.
Proposed: proceed with the lifecycle table, and file a standing note naming the category — operational lifecycle events that are not engagement knowledge — so the next one has a pattern instead of a fresh argument. Not a request to widen this change request.
Under O7, a new auditable-event layer gets a FORAY reserved location. agent_lifecycle_events is one.
Add a # FORAY_RESERVED_LOCATION marker and a _foray_reserved_emit call at the lifecycle writer, following the existing pattern exactly. No emitter is built; the stub discards its payload. Forward what is in scope at the site — identity, event kind, session, reason, actor, timestamp.
Worth naming precisely, because I got this wrong once already. The R-2 scoping note's v0.1 asserted that H0 had added FORAY call sites, and the census found zero — O7 was satisfied by extending the registry a dynamic site already reads. That was correct then and this is genuinely different: a new write path with no existing dispatch site to absorb it. The census of sixteen becomes seventeen when this lands, and the R-2 note should be told rather than left to rediscover it.
_ANCHOR_PRIORITY is consulted by Memory's write path. If grant-lifecycle events never become memory events, four pre-registered entries have no destination — agent_session_minted, agent_session_closed, agent_session_refused, agent_session_silent. They were registered under O7 in good faith against a mechanism that turns out to be the wrong one.
Do not delete them and do not implement against them. Report the consequence and refer all four to foray-reference/loomworks-anchor-priority-registry-dispositions-v0_1, joining agent_identity_minted and candidate_engagement_discarded in the same family: entries pre-registered against a write path the implementation did not use.
That family now has six members. Six instances is a pattern, and the pattern is worth naming in the dispositions document: pre-registering an event kind commits to a destination as well as a name.
agent_identity_frozen is unaffected in kind — it still has no memory-event destination — and joins the same referral.
ActorRef still has session_id is None after a job runs.refused row with its verdict text. This is the criterion that matters most.granted; its ending writes closed; a swept session writes abandoned.identity_frozen row plus one closed row per ended session, each with reason='frozen'.engagement_id is null on refused and identity_frozen — asserted, so a later reader sees the null is deliberate.0115 round-trips on dev and test.15d6425, branch unmerged.0115, the table, all writers including freeze, and §7's reserved location. Halt and report.
Explicit path staging; no git add -A; halt before push at every gate.
Preserved rather than smoothed.
v0.1 §3.2 — the carrier. Said populate session_id where the ActorRef is constructed on a path holding a session. No such site exists: the actor is per-process, the session per-job. 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. The discriminating question, now twice earned: 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 absent from RetrievalAgent's scope. Withdrawn.
v0.2 §5 — the destination. Said only refusal lacked an engagement context. Freeze lacks one too, by design, and no memory-object type exists for any of the five. The deeper error was framing three-of-five as exceptions to a rule instead of reading them as the rule. A grant is not engagement knowledge.
All three were found by attempting the work. None was found by reading the draft, and two of the three drafts carried a screen line asserting no conflict.
DUNIN7 — Done In Seven LLC — Miami, Florida CR-2026-231 — W6: Writing the Grant Into the Record — v0.3 — 2026-08-21