Version: v0.2
Date: 2026-08-24
Status: Scoping note. Names the ruling three notes converge on, one hard dependency none of them mentions, and a premise of v0.1 that the data refutes. Rules nothing. Markdown primary.
Changes from v0.1: §7's three prerequisites are answered and folded in. v0.1's premise that there are two per-configuration registers is wrong — there is one register and one embedded field, and §5b was written against the wrong picture (§3a, §5b). rendering has no consumer at all and is removable independently of this ruling (§3b). §7.3 is answered with a refusal to estimate, and the reason is stronger than a number (§4a). A new item at §6a: three Shapings share one agent identity, which is an attribution question nobody has ruled on. v0.1 stands as sibling.
The question: may an agent_identities row be created when an operator declares a Shaping or a render type — or does seeded-never-minted hold without exception?
Grounded on: agents/native_agent_roster.py, agents/shaping.py, agents/shaping_bootstrap.py, agents/render_specialist_bootstrap.py, agents/grantability.py read at engine 7136e34; Test 8's trace, 2026-08-24.
Converging on this: standing-notes/loomworks-scoping-per-binding-session-minting-v0_2; standing-notes/loomworks-scoping-finding-freeze-alert-v0_1 §6; SR-1 on the seed-ratification list.
Loomworks has two registries of agent identity and they do not know about each other.
The roster holds four roles, written into the database at install time. It is the thing sessions are granted against and the thing a freeze acts on.
The per-configuration registers hold an agent for each Shaping and each declared render type — created when an operator declares one, carrying its own id, name, instructions and version. These are the agents that actually do the work.
Test 8 showed what that costs: freezing the roster's rendering identity reported success and the render ran anyway, because the render path never consults the roster at all. The two registries share a word and nothing else.
Three separate pieces of work are now waiting on the same question, which is why this note exists rather than another restatement.
Three ways forward. Let operator-declared identities be created at runtime — one registry, one freeze, one grant path. Or leave the identities where they are and teach the governance mechanisms to look in both places. Or accept that per-configuration agents are ungoverned and say so plainly.
The dependency nobody has named: runtime creation breaks the frozen startup cache the current design relies on, and that cache is what keeps a hot path synchronous. That is a real cost of the first option and it has not been on the table.
And a correction this version carries. v0.1 described two per-configuration registers, symmetric with each other. They are not symmetric and one of them is not a register. Render specialists genuinely have one identity per binding, in a real column. The shaping side is an actor reference embedded in a memory payload — unindexed, disconnected from the roster, and shared between Shapings: seven Shapings in the dev database carry four distinct agent ids between them. That changes what the second option would cost, because it would have to build the thing it was going to read.
The module states its case precisely, and the case has two distinct legs that are worth separating because only one of them survives contact with the question.
Leg one — no runtime minting. A process minting its own governed identity record is authority-shaped and sits against human-only widening (O10, DR-02), so adding a role is a migration — a human act with a review.
Leg two — deterministic ids. A seeded row's UUID is a citation, and a citation that differs per database is not one.
Leg one is about authority. Leg two is about citability. They point the same way for roles and they come apart for per-configuration identities.
A role exists identically in every database, so its id must too — you cite …022701 in a CR and it means the same thing everywhere. An identity belonging to Aldous's coaching-summary Shaping does not exist in another database at all. Its id is already non-portable, because the Shaping is. Leg two does not reach it.
So the exception, if made, is narrower than it first appears: it does not weaken the citability argument, because that argument was never about these.
Leg one is the real question, and it turns on a distinction the module does not make: a process minting its own identity versus a person declaring a thing and the system recording that it exists. The first is authority-shaped. Whether the second is, is the ruling.
The module defers to it explicitly. From its own comment on render specialists:
> NOT on this roster, deliberately … that identity survives restarts, so acceptance 4 already holds for them; but it is per-binding, which a seeded per-role roster cannot express and should not flatten. Whether specialists take agent identities, and at what grain, is a later question.
This is that later question, and the module was right that forcing them onto the roster for tidiness would be wrong.
The registries are not merely separate — nothing bridges them. All 28 files in the render production path contain zero references to check_grantability, mint_session, require_live_session or resolve_agent_identity. The governance surface has three consumers and rendering is not among them.
And the freeze's success report is not a lie. It set the status and ended the zero sessions it had. It never claimed anything about rendering — the claim was supplied by a shared noun. Any option below has to make the noun mean something or stop implying it does.
One thing to verify before ruling (§7.1): the roster's rendering role, …022702, may have no consumer at all. shaping.py mints under shaping; the bootstrap resolves render_dispatch; seed induction resolves its own. If nothing resolves rendering, the roster contains a role seeded in anticipation of wiring that never happened — and Test 8 froze a row that governs nothing by construction rather than by omission.
v0.1 said: the per-configuration registers hold an agent for each Shaping and each declared render type, carrying its own id, name, instructions and version.
True of render specialists. False of shaping.
| | Render specialists | Shaping |
|---|---|---|
| Where the identity lives | render_specialist_binding.agent_actor_id — a real uuid column | payload -> 'executor' -> 'agent_ref' -> 'id' in current_memory_objects, JSONB |
| One per configuration? | Yes — 8 bindings, 8 distinct ids | No — 7 Shapings, 4 distinct ids; three shared across two Shapings each |
| Indexed? | No, but one migration away | No — an expression index on a JSONB path, or promotion to a column |
| Historical versions | In the binding table | In memory_events — 14 shaping rows against 7 current, so which Shaping did this id ever belong to is a wider scan again |
| In agent_identities? | No | No |
So the shaping side is not a register. It is an actor reference embedded in a payload, sometimes shared, unindexed, and disconnected from the roster. One Shaping's executor is even named "Claude (rendering)", which is its own small evidence that nothing is validating these.
What this does to §5b. That option assumed two registers to resolve in. There is one register and one embedded field, so 5b would have to build the second place before it could read it. That is a materially different proposition from the one v0.1 described, and §5b below is rewritten accordingly.
rendering has no consumer — and is removable now
Nothing resolves the rendering role. Three concrete role literals reach the roster's entry points: seed_induction, render_dispatch, shaping. Every other reference is a pass-through variable, and since shaping.py is the only caller of mint_session, session.role can only ever be "shaping". No route, config or request supplies a role string.
Every other "rendering" literal in the codebase is a different concept sharing the word — a room name, a spend-pause room, a prompt.
So Test 8 froze a row that governs nothing by construction, not by omission.
Removal cost: two code sites, one new migration, zero test changes. The binding test iterates the roster generically and asserts uniqueness rather than count. No fixture names the role or …022702. Three documents in loomworks-record describe it and none depends on it.
The one non-trivial part: migration 0113's data literal is history. The row is deleted forward, in a new migration, never by editing 0113.
This is removable independently of the ruling, and under §5c it becomes mandatory rather than optional.
The frozen startup cache depends on minting being forbidden.
The roster loads once at startup into an immutable role → UUID map. CC's own words when it landed: a frozen cache is sound only because the ruling forbids runtime minting — that's the concrete win. It is what kept ensure_render_dispatch_agent_for_engagement synchronous and left both call sites in engagement/render.py untouched; resolve-or-mint would have forced a database read into a sync path or made it async.
Option A breaks this. If identities are created when an operator declares something, the cache is stale the moment they are, and it must either be invalidated or abandoned.
And the invalidation problem is already documented. The roster cache is process-local. Test 8's finding: a freeze reloads the roster in the freezing process, not the server's. That is harmless today because nothing consults it — and it would become a correctness bug the moment anything did, one that passes in tests (one process) and fails in production (many).
So Option A's real cost is not a migration. It is that the cache must become correct across processes, which is a different and larger piece of work than creating rows.
Asked for a size, not a design. The honest answer is that it needs its own scoping note, and §7.3 of v0.1 explicitly invited that answer rather than a number.
The mechanical facts are cheap and certain. The roster is a module global in agents/identity.py, loaded at three sites — api/app.py's lifespan once per server process, and grantability.py after freeze and after unfreeze, in whatever process made the call. reset_agent_identity_roster exists and is called only from tests. Across processes there is no invalidation mechanism at all — no LISTEN/NOTIFY, no TTL, no generation column, no revalidation on read.
Three reasons a number would be wrong:
ASGITransport, with an autouse fixture loading and resetting the roster per test — so a freeze and the check after it always share a cache, and cross-process staleness is structurally invisible. The only multi-process test launches uvicorn to probe /healthz, is skipped by default, and exists to check port binding.That third point is not a task. It is a testing capability the suite has never had — two processes, one mutating, one asserting — with its own design questions.
Why this belongs before the ruling rather than after. If cross-process invalidation is the real cost of 5a, that is an input to choosing between the options, not a discovery to be made once 5a is underway.
An agent_identities row is created when an operator declares a Shaping or a render type.
Gives: one registry. One freeze switch that reaches everything. Per-identity granularity, which is what finding/freeze/alert needs to mean anything. Sessions mintable for every working agent.
Costs: leg one of the roster's reasoning is amended — narrowly, to a person declaring a thing may cause an identity to exist, never a process may mint its own. §4's cache work. And the natural key stops being role alone, so the 2026-08-20 natural-key ruling needs revisiting rather than assuming it extends.
The narrowness is defensible and should be written into the amendment, not left implied: the creating act is a human declaration through a surface, with an actor and a timestamp; the identity is a record that the thing exists, not a grant. Nothing self-registers.
Rewritten in v0.2. v0.1 framed this as two registries, resolved in either. §3a shows there is one register and one embedded field, so this option now reads: governance mechanisms resolve an identity in agent_identities, in render_specialist_binding, or in whatever the shaping side becomes.
Gives: seeded-never-minted holds untouched. Per-identity granularity for render specialists. No cache problem for the roster, which stays exactly as it is.
Costs, and they are higher than v0.1 said:
ActorRef — it acquires state, which makes it a register whether or not it is called one.agent_lifecycle_events.agent_identity_id and agent_sessions.agent_identity_id are foreign keys to agent_identities; either they loosen or a second column appears in both.Worth noticing: this is the same shape as the ruling already made for actors. CR-2026-234 stopped asking which table does this kind name and started asking does this id name a real one in either space. The consistency argument is real — and so is the caution, because that ruling's cost was that the check no longer verifies kind and id-space agree.
Per-configuration agents are ungoverned. The roster governs roles, which today means shaping and seed induction.
Gives: nothing changes. The design stays coherent and small.
Costs: finding/freeze/alert cannot be built — freeze one and alert the class needs a freezable one. The render path stays outside governance permanently, not pending. And the roster's rendering role must be removed or renamed, because a seeded identity that governs nothing while sharing a word with the thing it appears to govern is the misleading part of Test 8, not the ungoverned render path.
5c is not the do-nothing option. Choosing it means deliberately removing an implication, which is work — and §3b now prices it: two code sites, one forward migration, zero test changes.
It is also the only option whose cost is fully known. 5a's cache half is unestimated (§4a) and gated on a note nobody has written; 5b's shaping half must be built before it can be used (§3a, §5b).
produce_shape mints against the shaping role and attributes work to the per-Shaping actor — two ids on the same act, in production, unlabelled. 5a and 5b can remove the ambiguity; 5c must name it instead.render_specialist_binding with a real column. Shaping's agent identity lives on the Shaping's executor block in Memory. 5b requires resolving in it, which means it has to be a place you can look things up — this note has not checked whether it is.rendering (§3, §7.1).New in v0.2, and it is not only an input to §5b.
Seven Shapings in the dev database carry four distinct executor agent ids. Three ids are each shared by two Shapings. Nothing in the system prevents this, nothing records that it is intended, and no ruling covers it.
What it means for the record today: two Shapings' work is attributed to the same agent. Walking that agent's history returns acts performed under two different sets of instructions — because the instructions live on the Shaping, and the identity does not.
That is the natural-key ruling's own concern arriving from the other side. That ruling held identity must not derive from configuration, or a session's story stops reading as one identity the first time an agent's instructions change. Here the identity is stable and the instructions differ concurrently — one identity, two instruction sets, at the same time.
Which is correct is not obvious. Sharing may be deliberate — one agent doing similar work in two places is a coherent thing. Or it may be an artifact of how executors were copied when Shapings were created, in which case the record is misattributing.
This note rules neither. It records that the question exists, that no answer is written down, and that §5b's per-identity granularity is not per-Shaping granularity while ids are shared.
7.1 — Does anything resolve rendering? No. §3b. Removable now, independently of the ruling; mandatory under 5c.
7.2 — Is the per-Shaping identity queryable? Not as a register. §3a. It is an unindexed JSONB path, and the ids are shared. Both sides are scans today, but render_specialist_binding is a narrow purpose-built table one migration from an index, while the shaping side needs an expression index or a schema change first.
7.3 — What would the cache become under 5a? Unestimated, deliberately. §4a. It needs its own scoping note, and that note should be written before the ruling rather than after, because the answer is an input to choosing between 5a and 5b.
It does not choose. It does not amend the SEED ruling or the natural-key ruling. It does not answer SR-1. It does not rule §6a's shared identities. It does not size 5a's cache work — §4a says why an estimate would be wrong. It does not remove the rendering role, though §3b establishes that it could be removed today. It does not design the freeze semantics under any option. And it does not treat 5c as costless: removing an implication is work, and §3b prices it.
DUNIN7 — Done In Seven LLC — Miami, Florida Loomworks — Scoping Note: The Roster Exception — v0.2 — 2026-08-24