- Status: Canonical
- Owner ADR: ADR-0080
- Surface: Shared — the engine (
packages/core), the run store (packages/db), and both dispatch producers.
This is the one canonical home for what the engine promises about external side effects, the identities it
uses to say it, the journal's state machine, and what a resumed run or session does with what it finds. The
table's DDL is not here — run_effects lives in database-schema.md with every other
table, and this document links to it rather than restating it.
What ships today, stated once so no section below has to be read as a promise. Both halves are live as of 2026-08-18. The durable RECORD: every effectful dispatch is bracketed by a
preparebefore the call and asettleafter it, a failure past the prepare is never node-retried, and a duplicate identity is refused. The READ side: the resume gate (§4) refuses a run whose prior attempt left an effect unresolved, a session discloses instead of blocking (§8), and retention sweeps onlycommittedrows of a run that can no longer be resumed (§9). Session privacy and disclosure follow ADR-0098: a session row holds no result and never replays; resume/reseat discloses incomplete or unattributable committed effects before sweeping the exact captured rows. The file-byte guarantee is conditional on a successful WAL checkpoint (§11).Two things in this document are still specified but unoccupied, and both say so where they appear: tiers 1 and 2 (no shipping capability offers an idempotency key or a receipt lookup — §1), and the operator command that RESOLVES a
needs_attentionrow as accepted or discarded (§8). Because no reconciler exists, a tier-1 or tier-2 row currently takes tier 3's refusal — the conservative direction, named here rather than left to be discovered.
An effectful tool dispatch is bracketed by a durable prepare before the effect and a durable settle after it. The guarantee the engine makes depends on what the target supports, so it is tiered, and only tier 1 may use the words "exactly once".
| tier | precondition | guarantee | claimed by, today |
|---|---|---|---|
| 1 | the target honours a caller-supplied idempotency key | safe retry under the same key — effectively exactly-once | nothing |
| 2 | the target's outcome is queryable after the fact | exactly-once after reconciliation from a receipt | nothing |
| 3 | opaque, non-idempotent, no receipt | at-most-once dispatch attempt, never auto-retried | every effectful tool that ships |
Tiers 1 and 2 are reserved and specified, not occupied. No shipping capability offers a receipt lookup and no tool injects an idempotency key. A document that says otherwise is wrong, not aspirational.
Collapsing any two of these is how this design goes wrong — the phase document's original single key did it and became unimplementable. They are separate concepts with separate lifetimes.
Which run/node or session/turn an effect belongs to. A discriminated union, mirroring the invariant the run-event
envelope already enforces at runtime (exactly one of runId/sessionId):
type EffectCorrelation =
| { readonly kind: 'run'; readonly runId: string; readonly nodeId: string; readonly attempt: number }
| { readonly kind: 'session'; readonly sessionId: string; readonly turn: number };The attempt is the node-retry attempt (ADR-0040),
carried for audit only. It is deliberately not part of the gate lookup — see §4.
Which effect within one correlation. A zero-based ordinal over all tool calls in one model turn, in the order the provider returned them. It disambiguates two effects in one turn, which correlation alone cannot.
It is stable only within one model response. A replay that regenerates the response may produce a different number of calls in a different order, so a slot from before a crash is not comparable to one after it. That is why the gate is at node granularity (§4) and not at slot granularity.
EffectCorrelation with attempt dropped, plus EffectSlot, plus toolId. This is the table's UNIQUE
constraint. Its job is concurrency, not replay: two processes preparing the same effect collide on it, so one
loses and learns another attempt exists. It is not claimed to be reproducible after a model replay.
The audit identity of one occurrence: the node-retry attempt, the provider attempt, the tool-call id, and the owning (ownerId, generation) fence from
ADR-0079. Never used for dedup —
it is deliberately unstable, because its question is "which occurrence was this?".
On the session path, dispatch forwards the actual provider attempt and engine-assigned call id
per call; prepare accepts that occurrence after its optional target idempotency key. It overrides
the wiring-time audit fields, preserving run node/fence fields. The same engine id appears in
the structural transcript, enabling a join across
all historical completed turns, including compacted/trimmed ones. Provider/model-chosen ids
are never part of a session's durable attempt. The core reference journal records these per-call
fields too. Run callers without this override retain their existing wiring-time audit behaviour.
This id is load-bearing for the session disclosure join, never for dedup. A legacy wiring
placeholder (session, home, agent-run) cannot establish completion and is disclosed conservatively.
Tier 1 only. Supplied to the target, generated once at prepare and stored on the row so a retry reuses it verbatim. Never derived from model output, because model output is what a replay changes.
A dispatch is journaled when it can change state outside this process and duplication of it is not benign.
journaled ⇔ toolPerformsExternalMutation(def, validatedArgs, resolvedTarget) ∧ ¬def.duplicationBenign
Three rules make this precise:
- It is a predicate over the resolved call, not a policy class.
governedActionis a security classification and is deliberately wider — a read-only egress and a clipboard read are governed but mutate nothing. Journaling those would put two durable writes on every web search and halt a run on a crashed GET. - Two tools decide per dispatch, not per definition.
http_requestis journaled for a non-GET method and not for a GET;write_fileis journaled whenappend: true(appends compose) and not for a whole-file overwrite (naturally idempotent — the file is the receipt). duplicationBenignis a trust-bearing declaration and may only be set by first-party code. It is a property of a built-inToolDefin this repository. It may never be set from MCP tool metadata, a discovered tool descriptor, or any other bytes originating outside the engine — the same rule that keeps MCP annotations from raising a tier (§5). Today its only member isnotify.
Local-filesystem writes are journaled: "external" means outside this process, not outside this machine.
On resume, for a correlation with no terminal node record, the engine reads every prior non-benign effect record for that correlation and resolves each by tier before the node may run again.
The lookup key is the correlation with attempt dropped. The node-retry attempt resets to 1 both on a
crash-resume and on a budget approval, so an attempt-scoped lookup would miss the very row it exists to find.
| record state | tier | behaviour |
|---|---|---|
committed, result retained |
any | RUN only: re-deliver the stored result; do not re-execute |
committed, result not retained |
1 | retry under the stored target idempotency key |
committed, result not retained |
2 | reconcile from a receipt lookup, then decide |
committed, result not retained |
3 | needs_attention |
prepared / dispatched / ambiguous |
1 or 2 | reconcile, then decide |
prepared / dispatched / ambiguous |
3 | needs_attention |
This table and blocksResume govern runs. A matching session prepare is always refused, even
if an older writer left a result; neither SQLite nor the core reference journal serializes a session
result or returns one from its readers. Sessions use the disclosure contract in §8.
A committed run row is not a green light. If the journal did not retain enough to re-deliver the result, it
blocks the node exactly as an unresolved row does. This is the window an earlier draft of ADR-0080 left open:
settle succeeds, the process dies before node:completed persists, and a gate that only examined unresolved
rows would wave the re-run through.
The table has two enforcement points, and knowing which is which is the difference between reading this document and being able to find the code.
- Re-delivery is decided at the dispatch, inside
prepare(@relavium/db'screateEffectJournalStore). It returns a verdict:replaywhen the identity, the args digest and a retained result all match, and the registry then skips the call entirely and re-delivers the projections the original produced — the model-facing value, its truncation flag, its summary, and itsoutput_mappingresult. They are RECORDED, not re-derived: re-deriving ranoutput_mappingover the truncation preview, so a node whose result exceeded the bounding ceiling put a different value into workflow state on the replay than on the original. A replay is also refused when the node'soutput_mappingconfiguration differs from the one the effect ran under — a workflow edited in the crash window is not a call we can answer from the record. It is host-side because only the host can compute the digest the comparison needs — the engine is platform-free. A committed row that does not match is a refusal, not a replay: different args at the same slot means the model asked a different question on the re-run, and answering it with the old answer would be a silent wrong result. - Refusal is decided BEFORE anything is scheduled, by the engine's pre-flight gate (
RunExecution), over every node the checkpoint leaves re-runnable. It exists becausepreparealone is not enough:prepareonly fires if the re-run happens to reach the same tool at the same slot, and a model that answers differently sails straight past it, letting the run complete "successfully" with an ambiguous real-world effect from its own prior attempt still unresolved.
A gate read that fails is a refusal, not a pass — the same answer ADR-0075 gives for an unreadable event log, and for the same reason: resuming a run whose external effects are unknown is the one outcome this contract exists to prevent.
Tiers 1 and 2 currently take tier 3's row. Their reconcilers do not exist, and treating "we have not built it" as "proceed" would be the fail-open this contract rejects. The refusal names the tier, so the follow-up stays visible.
Discovered MCP tools carry no usable annotations, and any that existed would be attacker-controlled bytes from
the very server the hostile-MCP class defends against. An annotation may never raise trust. Tier 3 is not a
temporary state pending better metadata; it is the correct terminal answer for a tool whose semantics are
declared by an untrusted party. The same rule governs duplicationBenign (§3.3).
stateDiagram-v2
[*] --> prepared: durable write, BEFORE the effect
prepared --> dispatched: the call left the process
dispatched --> committed: a result came back
dispatched --> ambiguous: no answer — timeout, abort, crash
prepared --> ambiguous: resume found it here
ambiguous --> needs_attention: tier 3, or reconciliation failed
ambiguous --> committed: tier 2 reconciliation found a receipt
committed --> [*]
needs_attention --> [*]: an operator resolved it
prepared and the settle (committed | ambiguous) are the two durable writes. dispatched is a derived
reading of prepared when no settle followed — it is not a third write, because a write between the prepare and
the call would be a second crash window rather than fewer.
The order is fixed, and every failure after the prepare is kept off the node-retry path — a retryable failure after an effect has left the process is the duplicate this document exists to prevent.
| # | step | failure ⇒ |
|---|---|---|
| 1 | validate args, enforce policy, resolve approval | ordinary refusal; nothing journaled |
| 2 | durable prepare (fenced, its own BEGIN IMMEDIATE) |
refuse the dispatch; the effect never happens |
| 3 | dispatch to the target | settle ambiguous; never node-retryable |
| 4 | durable settle immediately | the row stays prepared; the run stops with needs_attention — never retried |
| 5 | output mapping, result bounding, event emission | the effect stands; settle already landed; the node fails non-retryably |
Step 5 matters as much as step 3: a post-dispatch abort, an output-mapping error, a bounding/spill failure or an
ActionGuard receipt error all occur after the effect. Classifying any of them as retryable reopens the hole.
An unresolved tier-3 effect is surfaced, never retried. The two surfaces differ deliberately.
A run terminates as run:failed carrying a dedicated ErrorCode of effect_needs_attention. Three
shapes were weighed and two rejected:
- a new terminal event — rejected for the reason ADR-0079 §5 rejected
run:fenced: it widens ADR-0036's exactly-one-terminal set and forces every surface, schema and checkpoint fold to handle a new variant; - a new
RunStatus— rejected becauseruns.statusis a derived projection the event fold writes, so the status would have to be invented inapplyDerivedrather than carried by the event that causes it; - a typed
ErrorCodeon the existing terminal — chosen. It reuses the closed error taxonomy every surface already switches on, andrun:failedis honest: the run did not complete.
It does NOT report durability: 'uncertain', and that is a correction to an earlier draft of this
section. Terminal durability and final local effect health are independent. After all admitted
work has joined, the CLI selects the final disposition and remedy according to
commands.md; an unchanged completion/cancellation terminal can still
have an effect that needs attention. The disposition means exactly one thing
(ADR-0078 §5): did this run's
terminal reach the durable log. Here it did — the run recorded its failure correctly, and the only thing in
doubt is what a target did. Overloading the disposition would route the run to exit 5, whose documented
remedy is "held in the outbox and retried on the next start"; nothing is pending, nothing will drain, and a
script following that advice waits forever instead of looking at the target. For this recorded
failure, the discriminator is the terminal's ErrorCode; final admitted-effect health is also
observed independently when the terminal has a different outcome.
The CLI maps that code or final effect attention to exit 7 unless required-money uncertainty
takes priority, recorded in commands.md, for the reason
ADR-0079 took one — a caller must be able to tell "a human must look at this" from an ordinary failure, and
from the transient "another process owns this" of exit 6. Required-money uncertainty has its own
final disposition and also forbids blind retry; the canonical priority and remedies live in
commands.md.
The run is not resumable past the unresolved effect; resuming it re-enters the gate in §4 and stops
again.
A session discloses and does not block. Initial chat-resume, standalone /models reseat and Home
reseat read one snapshot in an owned read transaction: content-free effect metadata and every strictly
decoded historical transcript row, including rows before compaction/trim. Atomic completed-turn writes make
the engine-assigned structural call ids completion evidence only within a completed exchange, as selected
by the shared completedSessionTurns(..., false) projection. A strictly decoded call alone cannot prove
completion: missing-terminal, orphan and abandoned exchanges disclose conservatively. Duplicate IDs and
mismatched names anywhere in history remain attribution anomalies; completed-turn counts are not a join key.
- Every unresolved state is disclosed, including negative-slot
!commands; these rows remain evidence. - A committed non-negative-slot effect whose valid engine id is absent from the historical transcript is disclosed as landed in a turn that did not complete. A match within a completed exchange is silent. Corrupt or mismatched attribution and legacy placeholders are disclosed conservatively as possibly incomplete.
- A committed negative-slot
!command is silent: it is not a model turn.
The active TTY/Home transcript receives the notice; plain/JSON modes use stderr. A standalone TTY driver
activates disclosure after passive terminal-input setup and an initial render flush, separately from opening
the session. A layout commit or render() returning alone does not establish a usable transcript. TTY/Home
await the disclosure render flush, then recheck active ownership before retention (§9); input stays gated
until activation completes. Component lifetime and the observed Ink exit/error guard the standalone driver;
Home also observes its renderer lifetime. The acknowledgement uses the exact stdout from Ink context,
checks that it can write before and after the flush and observes output close/error. An Ink flush promise
alone can resolve for unwritable output and is not sufficient. Failed setup, read, notice delivery or
render acknowledgement, and callbacks after cancellation, teardown, exit or a session swap, prevent deletion. Notice insertion and
render acknowledgement share terminal ownership with suspension: publication waits for terminal reclaim,
and a stop during publication waits for acknowledgement. An inline Static notice must not be consumed by a
suspended renderer. A flush adapter that omits publication or discards an asynchronous delivery failure cannot
permit deletion. Chat and Home explicitly configure Ink's interactivity from the CLI's resolved output mode:
Ink's ambient CI predicate must not buffer a selected interactive transcript until unmount. Home also resets
the inline Static cursor when a reseat replaces the view store, while retaining the root input owner and
already-printed terminal scrollback. A new store's notice cannot inherit the previous store's printed-item count.
Acknowledgement also requires the exact newly published notice to be included in the rendered transcript.
Inline mode confirms its actual Static child; full-screen mode confirms every wrapped notice row in the
committed viewport, bounded by measured layout and terminal clipping. A newly mounted viewport may need
its measured follow-up frame. A flushed footer, an older identical notice, a clipped window or a replaced
store cannot acknowledge disclosure. If a resize leaves insufficient space, retention is refused and the
evidence remains for a later activation; activation still completes so input and exit remain usable.
Chat driver selection and its transcript/approval ownership use the same existing CI/output predicate.
Plain/JSON await the native stderr write callback on the exact output sink, checking its state
and handling error/close. Returning from write() alone only acknowledges enqueueing. Failed stderr delivery
retains evidence; stdout remains the event stream. Ctrl-C disarms ownership before releasing the
headless activation wait, so a blocked native write cannot delay command teardown. Its late success or failure
stays observed and cannot sweep or access a closed database. EOF still waits for delivery. The command does
not destroy its externally owned stderr or claim that queued bytes were flushed. The bin assigns
process.exitCode; queued native stdio can still delay natural process exit until the reader drains,
as it did before W7. Command/session/database/MCP teardown and natural process exit are separate guarantees. Home disarms activation as
soon as the session stops or its teardown starts, even while the same chat remains mounted during MCP close.
A discarded Home build cannot consume evidence. A committed incomplete/legacy row is disclosed once after a successful
sweep; a crash between notice and sweep may disclose it again. Unresolved rows are disclosed on later resumes
until resolved. No earlier effect is auto-retried. The user must check its target before repeating the message.
An operator resolves a row as accepted (the effect landed; treat it as committed) or discarded (it did not; the node may run again). The resolution is written to the row with the actor and a timestamp. The CLI command that performs it is a named follow-up, not part of this contract's first landing — the journal must exist before anything can resolve rows.
- Unresolved rows (
prepared,dispatched,ambiguous,needs_attention) are never swept by age. They are the record an operator needs, and they outlive their run deliberately — the row carries no foreign key toruns, because a purge is exactly when the record matters most. committedrows are swept only once their correlation can no longer be resumed. Sweeping a committed row while its run is still resumable would delete the evidence the gate in §4 reads, reintroducing the duplicate. Both sweeps ship: a run's committed rows go when the run reaches a terminal (it can no longer be resumed —resumeFromCheckpointreturns a closed handle for one). A resumed session sweeps only the committed rows captured by its successful §8 read after active-surface disclosure. There is no completed-turn-count bound. One ownedBEGIN IMMEDIATEtransaction, with busy retry, checks each captured physical id together with its scope/slot/tool address and its current committed state. All chunks commit together; actual deletions are counted. A new commit after the read, a changed state, or the reuse of an id at a different effect address survives. Reads and sweeps refuse an outer transaction, whose uncommitted view cannot prove durable completion.
agent run is never resumable and has no transcript to disclose. Its teardown sweeps owned committed rows on
success, failure, abort and setup/teardown unwind, while retaining unresolved rows. Ownership begins only after
its one-shot turn-key reservation succeeds; a refused reservation cannot sweep another session's evidence.
Each session sweep checkpoints the WAL after commit, including an empty sweep (§11).
- Growth is bounded by resolution, not by time. Unresolved rows accumulate until an operator clears them; that is a deliberate trade against silently discarding an ambiguous external effect, and the quota/archive mechanism for a neglected queue is a named follow-up.
Process-crash durability, not power-loss. history.db runs synchronous = NORMAL
(database-schema.md), which survives a process death but not an OS crash or a power cut.
CR-12's failure statement is process death; claiming more by silence is the class of overclaim this contract
exists to remove.
A journal row never stores argument bytes. It stores a digest of a redacted projection of the effective
args — every key named in secretArgKeys is excluded before hashing, not hashed and hidden.
The reason is that a digest is a permanent equality oracle. A low-entropy secret (a short token, a URL with an
embedded key) is recoverable from its digest by dictionary attack, and on the CLI path history.db may be
unencrypted at rest. "A digest, not the bytes" is therefore not by itself a sufficient argument, and this
contract does not make it. The hash is SHA-256 over a canonical JSON serialization (sorted keys, no insignificant
whitespace) of the redacted projection, using a vetted implementation — never a hand-rolled one.
The current persisted form is v5: followed by the SHA-256 hex digest. The version
prefix is outside the hash and separates current full-value projections from legacy
unversioned digests, v2:/v3: projections with incomplete credential masks and
v4: projections that could omit own public data keys; a JSON wrapper alone could
itself equal arbitrary old tool args.
The digest consumes the full surviving non-secret values, including long string tails.
Nested own enumerable data keys, including __proto__, remain data members in the
projection rather than invoking inherited setters. Top-level tool-argument admission
exclusions remain unchanged. The bounded toolInput display preview is not a journal
input, and its clipping follows full recognized-value projection
(tool-registry.md). Equal-length
values can differ beyond its retained prefix. Every unversioned,
v2:, v3: or v4: legacy row refuses matching prepare, including unchanged small
arguments: its original projection cannot be proven from the digest alone. There is
no fallback to the old digest or automatic retry permission. Secret and
inline-media redaction remain deliberate projection equality; raw credential bytes are never
added to the digest to distinguish calls.
Recognized URL userinfo is masked through the authority's last @, preserving the host,
path, query and fragment. Recognized opaque URL-query values are masked through their delimiter, including values
longer than 512 characters. Quoted secret-key values honor escaped quotes and backslashes
through the actual closing quote. A recognized value must not leave a credential suffix
in the projection. This remains a finite shape detector, not a claim to identify arbitrary
unlabelled secrets. Existing old digests are not retroactively erased or rewritten.
A recognized structural credential key masks its entire value before traversal. A partial
inner text match does not make the remaining value public. Only an anchored Bearer, Basic
or Token scheme survives as its canonical label followed by [redacted]; no original
credential remainder or casing is retained. Credential flags in an argument vector mask
the whole following element or the complete value in --token=value form. Only the
ambiguous -p spelling retains the existing port-shaped exception; explicitly named
credential flags have none. Unrecognized public fields and ordinary arguments still
contribute their complete surviving values to identity. These projections do not change
the arguments dispatched to the host.
A session row's result_json is always SQL NULL; even a supplied result whose serializer throws is never
examined. A matching prepare refuses rather than replaying, and session audit readers omit the stored
result column entirely; run readers retain replay results.
A run row retains its bounded result until the terminal sweep, preserving §4's replay contract.
Open-time high-water backfill isolates invalid legacy identity/history evidence to its affected session; reservation and existing-session committed-row retention both refuse until the identity history is trustworthy. Before deletion, the same initializer runs inside the sweep transaction; its floor and all deletion chunks commit together. Invalid evidence remains intact and the active surface shows a fixed warning that effect-turn allocation remains blocked. Row-less orphan privacy erasure is preserved. Evidence is never guessed or removed by backfill. Healthy sessions remain usable. The exact initialization and allocation contract lives in database-schema.md.
The Node client opens with secure_delete = ON. After schema migration and high-water initialization,
an owned BEGIN IMMEDIATE data update clears every legacy session result, including unresolved, orphan and
one-shot rows, without changing their other fields. The clear and every session sweep are followed after
commit by wal_checkpoint(TRUNCATE). After a successful checkpoint, bytes freed from the upgrade onward
survive in neither the main database nor WAL; acceptance scans both files while the connection remains open.
The CLI surfaces a fixed stderr warning when opening maintenance reports a deferred checkpoint, as session retention already does. A diagnostic sink failure does not revoke a successful database open or repeat maintenance. Later opens and sweeps retry the checkpoint even when no row changes; logical suppression alone cannot certify that an earlier reader released sensitive WAL bytes.
A busy reader defers physical erasure (old bytes can remain in the main file and WAL) until the next
successful open/sweep checkpoint. Pages freed before the upgrade are not retroactively zeroed; no VACUUM
is performed. These two accepted residuals remain in deferred-tasks.md.
User text, @ file content and ! output, and run tool results, remain sensitive data at rest;
OS key storage does not make history.db content-free.
Each is an acceptance point, not a suggestion:
- kill before the prepare commits · 2. after the prepare, before the target call · 3. after the target
completes, before the settle · 4. after the settle, before the tool-result event · 5. after the tool result,
before
node:completed· 6. two effects in one node · 7. two processes preparing the same identity · - a settle write that fails · 9. tier 1 retry, tier 2 reconcile, tier 3 attention · 10. session
disclosure-once and a concurrent
chat-resume· 11. the committed-retention sweep boundary.
Session acceptance additionally covers errored/aborted/crashed turns with a committed effect; all-history joins after compaction/trim and empty finals; legacy disclosure once; negative command slots; read/notice/ activation failure preserving evidence; concurrent post-read commits and id replacement; atomic chunk rollback and outer-transaction refusal; owned one-shot unwind; and live main/WAL byte scans before/after clear or sweep, with a blocked reader and a successful later empty checkpoint as separate controls.
A budget pause becomes a paused outcome, and an approval resets the node to pending and re-dispatches it
from the start — replaying every provider call and every tool call the turn already made. That makes the
budget path a duplicate-effect generator, independently of any crash.
The rule: from a turn's second provider egress onward, a budget verdict that would pause instead fails the node closed with the budget error, replaying nothing and issuing no further egress. The first egress still pauses normally — nothing external has happened yet, so a replay costs one provider call.
The two alternatives lose for opposite reasons: completing the loop past the cap spends money the user capped, and pausing-then-resuming is the replay. Failing closed neither overspends nor duplicates, and is deliberately the more disruptive of the two honest options.
The long-term answer — checkpointing the continuation (provider messages, tool call/result pairs, round index) so an approved pause resumes mid-loop instead of restarting — is a new durable artifact of a different shape. Trigger: a user who must resume a partially completed tool loop rather than fail it.
- The session path has no ownership guarantee; concurrent resume remains unsupported. ADR-0079 is
runs-only. The durable effect-turn allocator gives each host a distinct key, so two
chat-resumeprocesses can both dispatch the same user request under different identities. UNIQUE prepare refuses reuse of the same journal identity; it does not grant session ownership or detect these independent turns.!commands restart their negative slot ordinal under a newly allocated key, including after reseat or transcript-write failure. See the allocation contract. Trigger to revisit: the first supported concurrent-resume flow. - Physical erasure is conditional. Busy checkpoints defer it; pre-upgrade freed pages are outside the
secure-delete guarantee (§11). No automatic
VACUUMor encryption change is implied. - Credential-only changes deliberately do not distinguish redacted digests. Declared secret keys and recognized credential shapes are excluded or masked before hashing, so the digest is not a credential equality oracle. A changed surviving non-secret value at an existing slot refuses replay (§4); it does not grant a fresh effect identity or permission to repeat the operation.
EffectSlotis not stable across a model replay, which is why the gate is at node granularity. A design that later needs slot-granular resume needs a durable record of the model response, which is CR-95's long-term continuation checkpoint, not this.