DUNIN7 · LOOMWORKS · RECORD
record.dunin7.com
Status Current
Path change-requests/cr-2026-120-actor-kind-read-contract-and-commit-gate-v0_1.html

CR-2026-120 — Actor-Kind Read Contract + Commit-Gate Enforcement

Version 0.1  ·  Date 2026-06-29
Repo loomworks-engine  ·  Base main @ d32c99b
Room Memory — Pipeline-walk Finding 1 of 3
Author Claude.ai  ·  Executor Claude Code (DUNIN7-M4)  ·  Operator Marvin Percival

Plain-language summary

The Companion writes memory the system then cannot read back. The write side accepts an actor kind of companion; one read serializer does not, so any assertion the Companion authored crashes the Memory room with a 422. That is why E0060's Memory room shows "Couldn't load this room" — all five of its assertions are Companion-authored.

This CR makes read match write, introduced once as a shared ActorKind alias so the two sides cannot drift apart again. The same inspection found a security-relevant hole the display fix would otherwise make more reachable: the commit and discard gates block non-humans by checking kind == "agent", but a companion kind is not "agent", so it slips through. The seed commits Loomworks to Operator-authority — the machine surfaces and signals; the Operator approves. This CR closes the gate so a companion actor cannot commit or discard, making that principle structural.

Two changes, one contract: companion is a first-class actor kind we display honestly, and precisely because it is a distinct kind, we enforce that it is a non-committing one.

Seed alignment

In scope

  1. Introduce a canonical ActorKind alias — a single Literal["contributor","agent","person","companion"] referenced everywhere the driver-kind is constrained, so the set cannot drift across duplicated inline declarations again.
  2. Add companion to the read-side serializers (the live crash plus siblings of the same bug class):
    • ActorRefResponse.kind (schemas.py:2525) — the live 422/500.
    • DecidedBySchema.kind (schemas.py:3756)
    • DecidedBySchema.actor_kind (schemas.py:3788)
    • BoundaryRecordResponse.raised_by_actor_kind (schemas.py:3864)
    • ActorRefSchema.kind (schemas.py:4292) — also nested in responses
  3. Fix the stale ORM CheckConstraint at memory/events.py:79 (3-value, shadowed by live migration 0057) so a schema regen / create_all cannot silently re-narrow the DB.
  4. Close the commit/discard gate hole. Change the human-only gates from "block == "agent"" to "block any non-human kind," explicitly excluding companion, mirroring credit/authorization.py:27 (_AGENT_KINDS = {"agent","companion"}):
    • engagement/assertions.py:204 (commit), :491 (discard)
    • engagement/creation.py:748, :802 (engagement commit)
    • api/deps.py:1044 (HTTP commit gate)

Out of scope — Room-1 residue, recorded not actioned

  • compositions.py:73 — silently relabels a companion-triggered composition as contributor. Display-correctness on a path neither probe exercises. Note as residue.
  • The two-queue commit handshake (Finding 2) and the "Logged" false-success (Finding 3) — separate CRs, later in the Room-1 sequence.

Step-0 inspect-first (mandatory before edits)

Before any change, CC reads each listed site at current HEAD and confirms the line still matches this CR's quoted state (d32c99b may have advanced). Two-pass discipline: inspect-and-report first, edit after confirmation. If any site differs, halt and report the divergence rather than editing blind. Specifically confirm:

Execution steps

Stage by explicit path. Per-step commits. Push is a separate, explicitly-authorized instruction — do not push until the Operator says so.

Step 1 — Canonical alias

Introduce ActorKind = Literal["contributor","agent","person","companion"] in a shared location (co-locate with domain ActorRef in memory/base.py, or a types module importable without cycle — CC's judgment, report the chosen home). Reference it at ActorRef.kind so the alias is anchored to write authority. Commit. Suite green.

Step 2 — Read-side serializers

Repoint the five read-side Literals to include companion (via the alias where the field is the full driver-kind set; DecidedBySchema.kind also carries rule — preserve that, add companion to the actor portion). Sites: schemas.py 2525, 3756, 3788, 3864, 4292. Commit. Suite green.

Step 3 — Stale ORM constraint

Update memory/events.py:79 CheckConstraint to the 4-value set matching live migration 0057. No new migration (DB already correct); this aligns the ORM so a regen cannot re-narrow. Commit. Suite green.

Step 4 — Commit/discard gate enforcement

At assertions.py:204, :491; creation.py:748, :802; deps.py:1044 — change the non-human rejection from kind == "agent" to reject any kind in the non-human set ({"agent","companion"}, ideally importing or mirroring _AGENT_KINDS so commit-gate and spend-tier share one definition of "non-human actor"). A companion actor must be rejected with the same error a human-only gate gives an agent. Commit. Suite green.

Step 5 — Tests

Commit. Suite green.

HALT. Report terminal SHA and suite count. Await explicit push authorization.

Verification — runtime, exists is not works

After merge, restart the dev server (serializer + gate logic load in-process), then verify against both probes.

E0060 — repair

  1. Open E0060 Memory room. Confirm 200 (not 422) and the five held Companion-authored assertions render with kind: companion shown honestly.
  2. Confirm /assertion-groups and /contributors/me for E0060 no longer 500.

E0089 — clean loop + reproduction-then-fix

  1. Add one note through the Companion (writes a companion-authored held assertion — the shape that crashed E0060 from clean).
  2. Confirm E0089's Memory room now displays it.

Gate enforcement — the security fix holds at runtime

  1. Confirm a companion-attributed actor cannot commit or discard through the gate — attempt the commit path with a companion ref and confirm rejection.

Pass condition: E0060 loads and renders; E0089 displays a freshly-added Companion note; companion ref is rejected at the commit/discard gate. Only then does Room 1 advance to Finding 2.

Risk notes