Version: v0.1
Date: 2026-08-22
Status: Scoping note. Names a live defect and the doctrinal question underneath it. Rules neither. Markdown primary — the consumer is Claude Code and change-request drafting.
Occasion: CC's §4 check against loomworks-candidate-discard-and-the-record-v0_2, 2026-08-21, run empirically inside a rolled-back transaction at engine 143fd6b.
Reads with: foray-reference/loomworks-candidate-discard-and-the-record-v0_2; standing-notes/loomworks-standing-note-two-records-and-what-crosses-between-them-v0_1 (STANDING); change-requests/cr-2026-231-w6-writing-the-grant-into-the-record-v0_3 §5, §6; engagement/candidate_discard_policy.py.
Discarding a candidate engagement that ever ran an agent now fails with a database error. Not in an unusual case — in every case, because every grant writes a row recording that it was granted, and that row points at the session the discard is trying to delete.
The immediate fix is one line of schema. Which line depends on a question nobody has answered, and the question is bigger than the defect.
Candidate engagements are deletable because nothing has been committed yet — that carve-out is ruled and built. Grant records are append-only because they are the audit trail of what the system did — that was ruled yesterday. The two rules now meet, and they disagree. A grant refused to an agent working on a candidate is a governance act that genuinely happened, and the candidate being unmade later does not unmake the refusal.
Three schema options. Each is a different answer about how far the pre-commitment carve-out reaches. This note lays out what each one commits to and rules none of them.
Confirmed empirically, not traced: a real session minted against a candidate-state engagement, its granted lifecycle row written by the ordinary mint path, then discard_candidate_engagement called unmodified.
IntegrityError - ForeignKeyViolationError:
update or delete on table "agent_sessions" violates foreign key
constraint "fk_agent_lifecycle_events_session" on table
"agent_lifecycle_events"
No unusual sequence is required. mint_session writes a granted row on every grant, so any candidate that ever held an agent session is undiscardable today. The session-holding path is shaping — the one path H0 governs.
Scope of exposure: small in practice, because agent_sessions held zero rows at the H0 close and agent-room traffic is four calls in the engine's history. The defect is certain and the incidence is currently near zero. That is a reason to rule it carefully, not quickly.
The earlier framing asked whether agent_lifecycle_events.engagement_id was an undeclared foreign key. It is not a foreign key at all — plain column, no constraint targeting engagements.
The dependency is transitive:
agent_lifecycle_events.session_id
→ agent_sessions.id (fk_..._session, ON DELETE NO ACTION)
→ agent_sessions.engagement_id
→ engagements.id (discard policy: delete)
So there was never a missing policy entry. There is no column on agent_lifecycle_events to declare a policy against. Adding one would have been the forced policy the policy file's own header warns about — worse than an undeclared FK, because it passes the fence.
test_every_referencing_fk_has_a_declared_policy scans:
WHERE tc.constraint_type = 'FOREIGN KEY'
AND ccu.table_name = 'engagements'
AND ccu.column_name = 'id'
Direct references to engagements.id. agent_lifecycle_events has no such FK — its two point at agent_identities.id and agent_sessions.id. The fence did exactly what it is built to do.
Third instance of one pattern in this arc, and worth recording as such:
| Contract | Guarantees | Does not see |
|---|---|---|
| Discard policy fence | Every direct FK to engagements.id has a decision | Transitive paths through another table |
| Unsessioned baseline | Every ActorRef construction is classified | References passed rather than constructed |
| FORAY census | Marker strings found by grep | Sites versus strings, collapsed multi-emits |
The general form: a source-contract test guarantees exactly what it scans, and its name always sounds broader than its scan. Each of the three was written well; each has a boundary; each boundary was found by building, not by reading. Whether the discard fence should scan transitively is a question this note raises and does not answer — widening it would pull in a large indirect closure, and that is a design decision.
Two rules meet and disagree.
loomworks-standing-note-two-records-and-what-crosses-between-them-v0_1 §5, ruled 2026-08-21): non-erasure and provenance extend to the substrate operational record on the same terms as Memory.The question: does the pre-commitment carve-out reach the operational record, or stop at Memory?
Why the answer is not obvious. The carve-out's reasoning is that nothing was committed, so nothing is owed. That is persuasive about contents — a candidate's draft assertions are not knowledge anyone relied on. It is much less persuasive about acts. A grant issued, and especially a grant refused, is a thing the system did, at a time, to an identity, for a reason. The candidate being unmade afterwards does not unmake the refusal.
The refusal is the sharpest case and should be the test of any answer. CR-2026-231 exists because a refusal was the least recorded event in the system and the one an audit most wants — a refusal is the moment governance actually did something. An answer that deletes refusals because the engagement was later discarded gives back the thing W6 was built to obtain.
A second consideration, pointing the other way. A candidate is discardable partly so a person can abandon an exploration without leaving a permanent trail. If lifecycle rows survive, a discarded candidate leaves durable evidence that it existed and that agents worked on it. That is a real privacy-shaped property and it deserves to be weighed, not assumed away. It is the same tension the anchor asymmetry already raises — a FORAY anchor outlives local deletion by design.
ON DELETE CASCADE — the carve-out reaches everywhereLifecycle rows die with their session, which dies with the candidate.
CASCADE deletes without any code naming what it removed.ON DELETE SET NULL — the act survives, its subject does not
The lifecycle row remains; session_id becomes null.
session_id is currently non-null for the session-bearing kinds and the schema enforces it — CR-2026-231 §5.2 made that a CHECK constraint deliberately, so the null is legible. SET NULL breaks that constraint and needs a fourth state: null-because-sessionless versus null-because-the-session-was-discarded. Two nulls meaning different things in one column is the failure this arc has ruled against repeatedly — unless a sentinel or a discriminator distinguishes them, which is UNSESSIONED_ACTION_ID's shape again.null policy class — a pointer to an unmade thing goes stale, not away.ON DELETE RESTRICT — the operational record winsThe discard refuses while lifecycle rows exist.
Not an option: adding a discard policy entry. There is no column to declare it against (§2), and a forced policy passes the fence, which the policy header names as worse than nothing.
credit_flows? A candidate's spend rows are deleted today under the same carve-out. Money spent is as much a substrate act as a grant refused. This note has not checked whether the same collision exists there — it should be checked before ruling, not after.loomworks-candidate-discard-and-the-record-v0_2 §2. If a discard writes an operational-record row naming what it removed, 5a becomes far more defensible — erasure with an account of itself is different from erasure. The two questions should be ruled together.The defect is live and the incidence is near zero. Nothing forces a fast ruling, and the arc's own record argues against one — the last three times a schema shape was chosen quickly, the halt came at pre-flight.
Recommended order: check §6.1 first, because if credit_flows has the same collision this is a general question about the carve-out rather than a lifecycle-table fix, and the answer should be general. Then rule §6.2 and §4 together, because whether the discard accounts for itself changes which of the three options is defensible.
It does not choose among 5a, 5b and 5c. It does not add a discard policy or alter any constraint. It does not answer whether the carve-out reaches the operational record. It does not widen the fence. It does not check credit_flows — §6.1 names that as a prerequisite this note has not met. And it does not treat the privacy consideration at §4 as settled either way.
DUNIN7 — Done In Seven LLC — Miami, Florida Loomworks — Scoping Note: What Happens to a Grant When Its Engagement Is Unmade — v0.1 — 2026-08-22