Version: v0.2
Date: 2026-08-21
Status: Drafted, not pre-flighted. Markdown primary — the consumer is Claude Code.
Changes from v0.1: v0.1 contained a contradiction — §4.3 excluded a construction while §7.1 forbade any construction carrying None. An excluded site carries None. Both could not hold. §2a resolves it by stating the category the exclusions were reaching for, which CR-2026-231 §2 had already ruled once at a single site and this CR failed to carry forward. Count corrected from 29 to 24; orchestration/routers/composition.py:167 struck as a wire schema rather than the domain type; engagement/composition_creation.py:252 added and dispositioned; Group A's "15+" is exactly 15. The correction is preserved at §10. v0.1 stands as sibling.
Implements: CR-2026-231 §3a, **ruled in scope by the Operator 2026-08-21.**
Operator ruling carried: an agent-kind ActorRef either attributes an act or names an agent; only the first can be missing a grant (2026-08-21).
Independent of: CR-2026-231 §6, which remains unruled and which this does not touch.
Runtime direction screen: No conflict — this records an existing state more legibly and widens nothing. No new act becomes possible; no actor gains authority. It converts a silent gap into an enumerated one.
Grounded on: CC's AST re-derivation at engine 9b103d8, 2026-08-21 — not the earlier grep at 22bec5d, which over-counted (see §10).
Cites: standing-notes/loomworks-standing-note-agent-accountability-v0_1 (STANDING) §1, §6; standing-notes/loomworks-standing-note-a-test-that-pins-a-literal-cannot-follow-a-ruling-v0_1; foray-reference/loomworks-contribution-credential-event-log-bypass-v0_1; CR-2026-231 §2, §3a; O1, O2, O5, O10; R1.
Most agent actions in the record are not the shaping agent. They are agents acting over the HTTP API with a bearer token, plus the seed-induction agent and the render specialists. None holds a session, so after CR-2026-231 they write a blank where the grant should be.
The Operator has ruled these agents in scope of the accountability commitment. They are not excluded; the commitment simply is not met for them. This change request does not meet it — that needs grants at the API boundary, which is separate work. What it does is stop the gap being invisible: a sentinel in place of the missing session, and a baseline of the sites that write it, with a test that fails when a new one appears. The list may shrink and must not grow.
The first draft got the boundary wrong, and the correction is the more useful half of this version. Not every place that builds an agent reference is an action. Some of them just name an agent — a constant, a step definition saying which specialist to use, a field recording which agent produced a shape. Writing this action had no grant onto those would assert something false about something that is not an action. So the rule is stated once, as a category: an agent reference either attributes an act or names an agent, and only the first can be missing a grant.
Settled: API-acting agent contributors are inside loomworks-standing-note-agent-accountability-v0_1 §1. Every agent action carries a responsible human and that responsibility is auditable — for them too.
Therefore, stated as a system property rather than managed as an exposure: the accountability commitment is currently unmet for agent actions taken over the HTTP bearer-token path, the seed-induction path, and the render path. Those actions record who acted and do not record what authorized them.
Not attempted here: meeting it. A grant at the API boundary is separate work with its own design question — what a session means for an agent arriving per-request holding a token.
Why the ruling matters more than the mechanism. Out of scope, the sentinel would have been an exclusion written permanently into the record. In scope, it is a recorded absence carrying its own removal condition. That is the difference between an exception and a debt, and only debt gets paid.
The responsible human behind an API agent is whoever holds the bearer token. That token is a contribution credential — and the credential lifecycle writes via raw SQL into its own table, reaching neither the event log nor any FORAY reserved location.
So the chain from token to human is unrecorded at both ends. The action does not record its grant; the grant's issuance is not recorded either. Same gap, two directions. Neither is closed here, and saying so stops them being discharged separately and half-way.
Ruled 2026-08-21. This is the boundary of everything below.
An agent-kind ActorRef is built for one of two reasons:
wasAttributedTo, a spend at the gate, a decision record.
Only an attributing construction can be missing a grant, because only an attributing construction is an action. Writing UNSESSIONED_ACTION_ID into a naming construction would assert this action had no grant about something that is not an action — a false statement in a field that means something, which is the same error as putting a session id in capability_ref.
This was already ruled once and not carried forward. CR-2026-231 §2 left shaping.py:785 alone on the grounds that shaping_agent_ref is a pointer to the producer, not an attribution of the act. That is this category, discovered at one site. v0.1 treated it as a site-specific exception instead of a rule, which is why v0.1's §4.3 and §7.1 contradicted each other.
The discriminating question: if this reference were wrong, would the record say the wrong agent did something — or would the system use the wrong agent? The first is attribution. The second is naming.
Naming constructions carry session_id=None, and that is correct. They are outside the baseline, outside the sentinel, and outside §7's acceptance.
Observation, deliberately not acted on here. If the category is real, it is currently invisible in the type — both uses share ActorRef, and the distinction lives in a document. A future change might give naming its own type, or the test might discriminate structurally rather than by enumeration. Recorded as an observation, not scoped into this change request, which should stay an enumeration.
3.1 Add to memory/base.py, beside LEGACY_UNRESOLVED_ACTOR_ID and following its documentation pattern:
UNSESSIONED_ACTION_ID: UUID = UUID("00000000-0000-0000-0000-0000000005e5")
Document it as: written into ActorRef.session_id when an agent attributes an act on a path where no session exists. Not an exclusion — a recorded absence. These agents are inside the accountability commitment (Operator ruling 2026-08-21); the commitment is unmet for them. A reader can tell "this path has no grant yet" from "this write lost one", which a bare None cannot. It never resolves to an agent_sessions row, by construction — any query joining session_id to that table must exclude it explicitly. Naming constructions do not carry it (§2a). When the enumerated baseline reaches empty, this constant has no writer and it and the baseline are removed together.
The precedent is exact: LEGACY_UNRESOLVED_ACTOR_ID is a sentinel, not a fabrication, and carries its own removal condition.
3.2 At each enumerated attributing site in §4, construct with session_id=UNSESSIONED_ACTION_ID.
3.3 — None on an attributing construction becomes a defect. Three states are then distinguishable: a real session, the sentinel, and None. On an attributing site, None is now a bug rather than a silence. On a naming site it remains correct.
4.1 — allowlist-as-data, per R1. A module-level frozen structure, not conditionals across the routers. R1's reasoning in mirror image: SANCTIONED_SYSTEM_WRITERS is data because adding to it is an authority grant; this is data because adding to it is an admission that another path writes without a grant. Neither should be reachable by a one-line edit nobody reads.
4.2 — attributing sites, grouped by blocker. The three populations are stuck on different things and a flat list would hide that.
| Group | Sites | Why it has no session | Discharged by |
|---|---|---|---|
| A — HTTP bearer-token agents | 15 routers + api/deps.py:1155 | The actor is an agent contributor row, not an agent_identity. There is nothing to mint against | Grants at the API boundary — unscoped |
| B — seed induction | engagement/seed_induction.py:214, engagement/creation.py:574 | It holds a seeded identity (seed_induction, migration 0113) and simply does not mint | Smallest of the three. It could mint today |
| C — render specialists | render_specialist_bootstrap.py ×4 | Per-binding identity from render_specialist_binding, which per-role session minting cannot express | standing-notes/loomworks-scoping-per-binding-session-minting-v0_1 |
Group B is not blocked. It has an identity and minting is one call. It is listed as debt because this CR's job is enumeration, not closure — but it is the one that could be discharged next.
4.3 — naming constructions, by the §2a category. Not exclusions with individual reasons; one category with three members.
| Site | What it names |
|---|---|
| credit/specialists.py:74 | A module constant, constructed at import |
| engagement/composition_creation.py:252 | specialist_ref in a CompositionStepDefinition — which specialist a step should use |
| shaping.py:785 | shaping_agent_ref on the persisted ShapeEvent — which agent produced the shape. Already ruled at CR-2026-231 §2 |
4.4 — not constructions at all, recorded so a later census does not re-litigate them: orchestration/routers/composition.py:167 builds ActorRefSchema, the wire schema, not the domain type. commit_orchestration.py:103/153 create contributor rows via register_contributor(kind="agent").
5.1 An AST scan over src/ for ActorRef(...) with kind="agent", compared against §4's structure. Fails when a construction appears in neither the attributing baseline nor the naming category. A new site must be classified before it can pass, which is the point.
5.2 Every attributing construction carries a real session or the sentinel; never None. Every naming construction carries None; never the sentinel. Both directions asserted — a naming site that acquires the sentinel is as wrong as an attributing site that loses it.
5.3 — pin against the constant, not the literal. assert ref.session_id == UNSESSIONED_ACTION_ID, never the UUID string.
5.4 The scan must distinguish ActorRef from ActorRefSchema. v0.1's count was wrong partly because grep could not (§10).
ActorRef construction carries a real session id or UNSESSIONED_ACTION_ID. None carries None.None. None carries the sentinel.agent_sessions, and any query joining session_id excludes it.
Halt and report at each. Explicit path staging; no git add -A; halt before push.
§4's sites come from CC's AST re-derivation at 9b103d8. Re-derive again rather than trusting this table — the same instruction that produced the correction. If the set has moved, report the delta before writing the baseline.
Preserved rather than smoothed.
The contradiction. v0.1 §4.3 excluded credit/specialists.py from the baseline while §7.1 required every agent-kind construction to carry a session or the sentinel, and §3.3 declared None a bug. An excluded construction carries None. The contradiction was present as written, independent of any count error; a second naming site only made it unavoidable rather than latent.
Why it happened. CR-2026-231 §2 ruled the naming-versus-attributing distinction at one site and gave the reasoning. v0.1 carried the outcome forward as a site-specific exception and left the rule behind. A ruling stated as a one-site exception does not survive into the next change request.
The count. 29 → 24. The 29 came from grep, which counted comments, docstrings, register_contributor(kind="agent") calls, and ActorRefSchema constructions. This is the third time in two days a grep count has been wrong in this arc — the FORAY census over-counted a docstring and under-counted collapsed emits, CR-2026-231 §7 compared strings to sites, and this over-counted five ways. The lesson is the same each time: grep | wc -l is not a census. An AST scan is.
"15+" is now 15. The + was hedging against the grep's imprecision and the AST removed the need for it.
DUNIN7 — Done In Seven LLC — Miami, Florida CR-2026-232 — Unsessioned Agent Actions: Sentinel and Baseline — v0.2 — 2026-08-21