RFC-0016: Durable Derivation Commits
Status: Partially implemented; durable extraction work, journaled plans, and fenced atomic commits; explicit plan revision and retired-stage collection implemented Date: 2026-09-12 Depends on: RFC-0001, RFC-0002, RFC-0003, RFC-0004, RFC-0014
Problem and evidence
An immutable source alone cannot reproduce nondeterministic model output. An in-memory retry loop cannot survive a process exit. Retrying graph creation with fresh UUIDs can duplicate facts; compensating rollback can delete a replacement whose prior fact was already retired. An expired worker can also race its successor unless the database rejects its final write.
Current fault-injection work has established these prerequisites:
- Source events and deferred raw NOTE jobs commit together on both backends. Local deferred processing prepares graph nodes and durable vector payloads within the pass budget, then atomically replaces its bounded lexical document batch. Only after that commit are individual source jobs acknowledged. Failed vector writes retain their own failures; failed lexical batches fall back to individual replacements. Cancellation or process exit before acknowledgement leaves replayable work. This batches raw indexing, not graph publication or LLM derivation, and does not make a pass one database transaction.
EXTRACTION_VALIDATEDoperations preserve the first grounded model output and its source hash, owner, scope, model, schema and grounding versions. Indexing retries reuse that output. The record survives restart and abrupt process exit.- Local vector metadata and numerical payloads commit together. Startup repairs unsaved vectors and stale snapshot deletions without inference.
- Named replacements commit as a batch. Normal materialization failures roll back tracked artifacts while preserving previous facts.
- Native storage operations retain their locks until their worker threads finish after cancellation. Failed engine startup releases resources.
- In-process materialization records completed node/edge writes before caller cancellation propagates. Cleanup waits for queued index writes and withstands repeated cancellation. A final replacement that already committed is retained. These cleanup guarantees do not make intermediate graph writes invisible or recover a process killed before cleanup; the batch commit protocol is still needed.
These prerequisites motivated replacing compensating cleanup with journaled
plans and atomic graph publication. Normal _materialize() now uses that path;
the older interleaved graph/index writer is no longer used by ingestion.
Implemented commit primitive
Internal DerivationPlan records now preserve source identity, fixed artifacts,
existing-node snapshots, complete replacement edges, numerical embeddings and
lexical inputs. Both event stores journal the first plan with a checksum and
reject changes to the same plan identity. The serialized plan is stored as a JSON
string inside the operation payload so PostgreSQL JSONB normalization cannot
change numerical details such as signed zero.
GraphStore.commit_derivation() verifies the exact journaled plan inside its
transaction, checks dependencies, and commits all nodes, edges, replacements and
one immutable receipt together. PostgreSQL writes prepared vectors in that same
transaction; DuckDB requires matching durable vector payloads staged beforehand.
Retry returns the existing receipt and does not reactivate subsequently archived
nodes. Dependency comparison normalizes timestamp offsets, so moving a pack
between database timezones does not falsely invalidate unchanged nodes.
Tests cover failures after every node/edge position, independent readers, concurrent replay, changed dependencies, source/scope invariants, hypothetical replacement rejection, and DuckDB process exits during node insertion, after replacement, and after commit before acknowledgement. Restart completes the same plan without partial graph state or new model calls. These are component tests.
VectorIndex.stage() now durably binds a prepared node identity to its source
text and numerical payload. Identical retries reuse the key and repair a missing
native vector without inference; conflicting inputs are rejected. Startup rebases
the key sequence beyond recovered metadata, fixing a reproduced process-exit case
where DuckDB recovered a payload but reused its key on the next insertion.
LexicalIndex.stage() compares exact stored fields under the directory writer
lock and commits only missing documents as one batch. It never deletes existing
documents to retry. Tests cover cancellation, lost commit acknowledgements,
concurrent in-process retries, restart and abrupt exits at both index boundaries.
Plan journaling now reserves each new node ID for its exact prepared-operation identity in the same transaction as the plan and work binding. Reservations are global across sources and tenants; concurrent plans cannot allocate a shared ID, and even a new revision reusing the old plan UUID cannot reuse its artifacts. Losing concurrent planners for the same source/revision still converge on the first saved plan before reserving identities.
The derived derivation_artifact_owners and derivation_registered_plans tables
are incrementally rebuilt from checksummed journal records at startup, in pages
of 128 records. Historical overlapping allocations become ambiguous (NULL owner)
instead of making the pack unreadable or assigning ownership arbitrarily. Those
identities cannot be newly reserved. Invalid historical plan records are left
unregistered with a warning containing only their operation ID; source reads
remain available. Any future collector must disable reclamation when registration
is incomplete and must retain ambiguous identities. Registration alone is not authorization to delete: the external-stage fence
below must also prevent obsolete workers from recreating reclaimed entries.
Managed DuckDB index staging now holds a work-row transaction on an independent
cursor over the same database for the duration of each native write. It verifies
the exact current saved plan, its input membership and the claim before writing,
then revalidates the lease afterward. Touching the work row prevents a concurrent
connection from advancing its generation or plan revision while the native call
is in flight. The index’s primary connection can commit durable vector payloads
normally. An expired or cancelled in-flight operation may leave plan-owned staged
inputs; ownership cannot transfer until that operation finishes. Cancellation
keeps index and connection locks held until the native operation returns.
Standalone index stage() calls without this fence remain an unmanaged component
API. PostgreSQL stages prepared indexes in its fenced graph transaction.
index_compaction collects up to 500 staged node identities per pass from
explicitly replaced revisions. It verifies their saved plan/source, requires
unique ownership and absence from the graph, and scopes them through the original
event owner. Current failed, pending and running plans remain retryable. Ambiguous
legacy allocations are retained; unregistered or invalid journals block this
stage cleanup and appear in job details. No age-based abandonment is inferred.
Lexical deletion commits before vector metadata removal. A failed deletion or
process exit retains that vector staging row as a retry anchor; native removal
errors propagate rather than discarding the metadata. Sources, immutable plans
and permanent identity reservations are retained. Existing archived graph nodes
remain eligible for ordinary index compaction. Rebuild must not run concurrently
with a derivation publication.
The normal ingestion pipeline now prepares a scoped in-memory graph overlay, batches embedding inference, journals the first complete plan, stages external indexes and calls the atomic commit. Reused entities are referenced without re-indexing. Unrelated scanned entities are not commit dependencies. Explicit older-effective updates remain historical facts without retiring later state. Retries load the saved plan before calling either provider. An existing receipt skips staging and publication, including after archival. Existing unjournaled derived nodes are rejected as requiring explicit migration rather than duplicated.
Public-ingestion fault tests cover post-plan/index writes, every graph write kind, post-commit failure, cancellation and concurrent attempts on both backends. DuckDB subprocesses exit after plan save, each index stage, a graph insertion, and commit. Explicit retry after reopening uses the same plan without inference.
Source append now atomically queues durable extraction work alongside raw
indexing. Owner-scoped extraction_status, retry_extraction, and
process_extractions are available through the engine, sync client, HTTP, MCP
and CLI. Explicit processing discovers unfinished jobs after restart; retrieval
never invokes it and no daemon is installed. Budgets apply between jobs.
Failure reporting preserves provider/schema categories before traversing lower implementation causes. In particular, a provider TimeoutError must not become CancelledError merely because asyncio enforced the timeout by cancelling its inner call. Caller cancellation retains the separate Cancelled work code. Unknown failures retain bounded class names; exception chains are cycle-safe and bounded. Provider messages and response bodies are not recorded. Public blocking ingestion exposes the sanitized reason_code alongside its persisted event_id. The same code survives restart in extraction status on either backend. Deferred raw-materialization failures and serialized write-job failures also log bounded categories instead of exception messages or tracebacks. A failed job’s original exception still reaches its caller; formatting a provider exception is not required for the queue to continue processing healthy jobs. This is scoped to those failure paths, not a claim that all application logging is sanitized.
Built-in extraction transports source messages literally. Grounding input stays
in async task-local validation state instead of Instructor’s prompt context,
because Instructor applies that context as a Jinja template to every message.
User code containing {{ ... }} or {% ... %} must never execute as a prompt
template, change source bytes, or leak validation state across concurrent calls.
Claims increment a persistent generation and attempt count. Heartbeats renew live leases, while extraction journaling, plan binding and graph publication check ownership inside their transactions. Completion and its receipt commit together. Pending/running work follows append order within an owner and scope; terminal failures allow later work to proceed. Retry retains attempt history, and cannot preempt active work or repeat completion.
PostgreSQL uses row locks. DuckDB touches the work row inside each publication transaction so a concurrent claim conflicts and rolls back. Mutable work fields must remain unindexed: a local two-connection test reproduced an indexed-status UPDATE rewriting rows and allowing both transactions to update. Schema startup drops the former status index. A fault test blocks graph publication across lease expiry, attempts takeover from a separate connection, and verifies rollback before the successor can publish on both backends.
Tests also cover abrupt exit immediately after admission, provider outage and
restart recovery, cancellation, heartbeat renewal, empty extraction, owner
isolation, and stale workers attempting all three durable write boundaries.
Explicit retry_extraction(..., replan=True) now queues a new plan revision from
the same grounded extraction. It advances the work generation and journals
DERIVATION_REPLAN_REQUESTED in one transaction, preserving the previous plan.
It cannot preempt a live worker or revise completed work. Prepared operation IDs
include the revision after v1; the event still has one completion receipt.
Existing v1 checksums remain unchanged when the newly defaulted revision field
is absent from historical payloads. Processing may recompute embeddings, but
does not call extraction again once that output is saved. Automatic replanning
and model-output/grounding-policy revisions are not implemented.
DuckDB fresh schemas include the revision column directly. Older work tables checkpoint after adding it: a subprocess test reproduced DuckDB 1.4.4 failing to replay the ALTER from WAL after abrupt exit. The migrated-pack test now verifies both preserved legacy work and recovery after subsequent plan journaling.
Legacy sources are not automatically enrolled in extraction work. Unmanaged or ambiguous staging remains conservatively retained; collection only covers the explicitly replaced, uniquely owned revision protocol above.
Historical plans retain their original behavior and checksums. Fresh built-in
inference and plans record speech_act_v14. V14 retains the v13 singular-form
source matching and adds bounded explicit correction matching and per-message
caller-declared first-person owner references. Saved v13 records still prepare
v13; v11/v12 records still prepare v12. Negated updates can target their own object
without a replacement field; a bounded switch-to rule and unique whole-word
shorthand can identify an old value. Older-effective corrections cannot retire
later-effective claims. The immutable plan captures the complete subject
snapshot, prior claims and fixed replacement edges, and the existing fenced graph
transaction publishes new claims, retirement, validity closure and receipt
atomically. It does not retire raw notes or migrate old pronoun identities. See
MEMORY-CORRECTIONS.md.
All current policies preserve these earlier guarantees: relationship
outputs become source-cited FACT nodes and normal subject/object association
edges, unresolved personal references remain event-local, and claim polarity and
explicit conditions are preserved in node metadata. A fact can also carry one
source-bound decimal quantity. Its quantified phrase must occur in the claim
object and evidence, its decimal must match the supported source notation, and
its unit or symbol remains verbatim. Invalid optional quantity output is dropped
without discarding the grounded claim. V7 can recover the exact decimal token
from grounded source text after a provider emits a JSON float, but never converts
or trusts that float; surrounding approximation and range cues fail closed.
V8 can also recover a model-omitted exact dimensionless
score/count/rating/level from a complete user-authored sentence by creating its
source-literal concept subject; normal evidence, quantity and reference checks
still apply. V9 can derive one currency or bounded verbatim measurement unit
from an already grounded object and can recover a complete first-person
conditional quantified action from a user sentence. These paths retain the
same exactness, evidence, condition and reference validation. V10 suppresses
conditional fallback when a validated fact already has the same evidence,
predicate, polarity and quantity identity, even if its valid condition is a
different substring of that evidence. V11 can recover one leading exact measure
from a user-authored first-person completed action in a bounded verb lexicon.
The action remains sentence-level and passes the same evidence, quantity and
closed-reference validators; modals, negations, examples, questions,
conditions, approximations and ranges do not enter this path. Saved v10 through
v6 records also prepare v12. Type-qualified object
references avoid arbitrary namesake links. Conditional claims start with an
unknown condition state and stay out of DEFAULT retrieval until confirmed.
Literal first-person attempts and intentions must retain that non-completed
speech act in their predicate; they and relationships between components inside
the attempted action cannot become current-state relations. The extraction
contract also requires the attempted action target when a count or failure
detail is emitted; those details cannot substitute for the target relation.
When the model omits that relation, recovery is limited to a literal nonactual
clause and the first exact model-returned entity after its action verb. Explicit
if/unless conditions remain conditional. It does not search memory, resolve
pronouns, or infer an unreturned entity.
Each new fact also sets valid_from to its resolved source-effective time. A
valid newer replacement closes the referenced prior interval in the same graph
transaction. The commit skips interval closure when the boundary precedes a
legacy stored start, avoiding an inverted range while still publishing lifecycle,
supersedence, edge, and receipt atomically.
Repeated claims (#209). With enable_claim_merge (default on), a new
speech_act_v12 plan merges a claim that exactly repeats an active claim on the
same resolved subject entity: the same memory, epistemic and source type,
normalized predicate and object, object entity link, polarity and resolved date.
Normalization is NFKC, case folding, and one space for underscores and
whitespace runs; similarity never counts. The plan still creates the new node,
gives it the copies’ evidence references in source order, their highest scores
and the most established lifecycle state, lists their IDs in merged_claim_ids,
and retires them as ordinary replacements in the same commit. The new node keeps
its own text, valid_from and event_time, so each copy’s interval closes at
the restatement like any other replacement. Quantities (their sums across
statements are meaningful), conditional claims, unresolved relative dates,
pinned or expiring nodes, and claims that are not observed or asserted never
merge. A copy whose effective time is later than the new statement stays
active, so a late-arriving older statement gains no authority over it. One
message that states the same claim twice produces one node. The policy label
stays speech_act_v12: merging only uses the existing node and replacement
primitives and does not change how model output is read, and merged_claim_ids
marks every merged record. Saved plans and plans under older policies are
unchanged, and setting the option to false restores one node per extracted claim.
Saved v5 records prepare missing plans under v11, v4 under v10, v3 under v9, and v2 records
prepare them under v8. Extraction records whose omitted policy defaults to source_passage_v1 prepare
a missing plan under temporal_validity_v7; recovery cannot relabel old model
output as v8. Existing source_passage_v1, typed_references_v2,
event_local_references_v4, claim_qualifiers_v5,
grounded_quantities_v6, temporal_validity_v7, speech_act_v8, speech_act_v9, speech_act_v10, and speech_act_v11 plans remain readable and
replay their saved artifacts unchanged. Recovery never regenerates an existing
plan under the current policy implicitly; historical committed edges are not
migrated.
Required behavior
Acknowledged ingestion must retain both the immutable source and the request to extract it. Model failure must leave inspectable, retryable work. Once a complete derivation plan exists, recovery must reuse its identities, timestamps, policy decisions and numerical embeddings without calling a model again.
A graph derivation becomes visible as one database transaction: all new nodes, edges, valid replacement transitions, and its completion receipt commit together. Replaying a committed plan returns the existing receipt. It must not create new facts, repeat replacements, alter scores, or reactivate retired knowledge.
The transaction must also verify that the worker still owns the current work generation and plan. Checking a lease in application code before writing is insufficient: ownership can change between that check and the commit.
Durable records
Extraction work
A mutable work table records event_id, state, attempt count, next-attempt time,
lease expiry, monotonically increasing fencing generation, current plan ID, and
a sanitized last error. Ownership and scope come from the source event. An
append-time sequence orders eligible work independently of user-supplied event
timestamps. Raw indexing remains a separate job and status.
Claims and explicit plan-revision transitions advance the generation. The final graph transaction must lock/check the work row and reject an expired or superseded generation. Heartbeats extend a live lease; they do not substitute for commit fencing. If event-loop starvation expires an uncontested lease after provider return, the rejected worker attempts one immediate database reclaim under a new generation. It reuses a saved extraction or plan when one reached its durable boundary; otherwise the provider may run again. Failure to reclaim falls back to the ordinary retry policy, and the expired generation never publishes. Failure during provider I/O must not hold graph/database locks.
Saved extraction
The existing ExtractionRecord is the durable model-output boundary. It contains
grounded structured output rather than the raw SDK response. Provider or schema
changes require an explicit revision identity; they must not overwrite the
existing EXTRACTION_VALIDATED operation. A cache hit is not graph completion.
Prepared derivation
An immutable DERIVATION_PREPARED operation records:
- Source/revision identity, content hash, owner and scope.
- Plan schema, grounding, materialization and scoring-policy versions.
- Complete new node/edge values, including their fixed IDs and timestamps.
- Referenced existing entities and the scope/lifecycle conditions for reuse.
- Explicit replacement targets and their expected state.
- Exact embedding values, dimension, model/version, and indexed-content hash.
- Lexical documents associated with the newly created nodes.
The plan is persisted before external index writes begin. Canonical serialization and a content checksum detect accidental plan mismatches. A retry of the same plan ID with a different payload is an error, not an upsert. Node defaults must not be re-evaluated during replay.
The planner reads an overlay of staged nodes/edges and durable graph data. It
does not write through WriteQueueGraphWriter as it resolves entities or facts.
Reused entities must not be re-indexed with changed descriptions before commit.
Entity matching remains scoped and conservative; preserving an ambiguous
duplicate is preferable to silently joining unrelated people.
Commit receipt
DERIVATION_COMMITTED is appended in the graph transaction with the plan ID,
generation and resulting node/edge IDs. The mutable work row becomes complete in
that same transaction. A lost client acknowledgement can then be resolved by a
scoped status read. Existing committed operation payloads are never overwritten.
Backend execution
DuckDB, USearch and Tantivy
- Claim work and load/extract its durable model output.
- Prepare and journal the complete plan, including embeddings.
- Idempotently stage vectors and flush lexical documents for new, invisible node IDs. Native vector payloads are durable before graph commit.
- In one fenced DuckDB transaction, insert nodes and edges, validate/apply the replacement set, append the receipt, and complete the work row.
- Retire obsolete index entries after commit; normal graph eligibility filters must already prevent those entries from resurfacing.
Candidate generation must continue joining staged index IDs to visible, owned graph nodes. A losing worker must never compensate by deleting another worker’s committed nodes or shared index entries. Index staging therefore needs immutable, plan-specific write identities, not blind delete-and-reinsert behavior. Compaction may collect abandoned staging records only when no live plan or committed graph references them.
PostgreSQL
Compute embeddings before opening the transaction. Use one connection and one
transaction for the work-row fence, graph insertions, embedding/lexical columns,
replacement transitions, immutable receipt and completion state. The existing
standalone PgVectorIndex.index() cannot stage a vector for a missing node.
It must not be reused as though it provided that behavior.
Lock referenced existing rows in a consistent order. Unique receipt identity and work-row fencing must make concurrent retries converge on one commit.
Temporal and concurrency constraints
Within an owner/scope, resolve stateful replacements in durable work order. Source event time and knowledge time remain separate. An older effective event does not gain authority to retire a later assertion merely by arriving last. Unqualified coexisting values and hypothetical changes do not imply replacement.
A prepared plan can become stale because an organizer or explicit API operation changed a referenced node. The commit must reject that stale plan atomically. Replanning creates a new immutable plan revision and advances the work generation; it does not edit the old plan or allow its worker to commit later.
Developer-facing contract
Expose extraction work status and bounded, owner-scoped processing separately
from raw NOTE processing_status()/process_pending(). Status should distinguish
waiting for inference, prepared, complete, and actionable failure. A saved
extraction or a list of partially derived nodes is not a completion signal.
Retrieval should not unexpectedly start paid model work for unrelated tenants. Explicit processing uses a cooperative total budget and a provider-call timeout; neither is permission for a stale worker to publish results. Every failure retains the event ID and a sanitized reason. Complete records and plans remain inspectable through the source owner’s boundary.
Acceptance tests before enabling automatic recovery
- Kill a process after source append, extraction journaling, plan journaling, each index stage, and database commit before acknowledgement. Reopening either completes the same plan or reports pending work; committed fact IDs/counts stay unchanged and inference is not repeated once the required output exists.
- Inject a failure at every node, edge and replacement position. No partial graph derivation or retired prior fact becomes visible after transaction failure.
- Expire a lease while its worker runs, let a successor commit, then resume the old worker. The old generation cannot commit or delete its successor’s artifacts.
- Retry concurrently on both backends; verify one immutable receipt, scoped node/edge identity, and correct behavior after a lost acknowledgement.
- Reject source, scope, plan-checksum, embedding-model/dimension and expected-state mismatches before exposing graph changes. Test existing-entity reuse separately.
- Exercise provider outage/recovery, cancellation, empty extraction, late effective events, interdependent updates, and schema/policy revisions through public APIs.
- Rebuild the covered derived state from saved plans in a fresh pack and compare identities, values, provenance, lifecycle and retrieval under the same config.
Limits of the replay claim
This protocol covers newly journaled ingestion derivations. Direct store()
requests use the simpler source/initial-node journal and materialization job in
RFC-0002; they do not use derivation leases or fenced graph commits. Those jobs
recover the initial typed node and indexes, without replaying optional post-store
reinforcement, supersedence or QA pairing. Version-2 direct-store records may
carry a caller-supplied retrieval projection distinct from the immutable event
content; recovery reuses the checksummed node snapshot and does not regenerate
that projection. Existing organizer and manual graph mutations also need
complete operation payloads before PRME can
claim full-state replay. Legacy packs need an explicit baseline snapshot for
state that was never recorded; nondeterministic historical model output cannot
be retroactively recovered from a raw event alone. No implementation milestone
or synthetic test substitutes for that evidence.
QA pairing is disabled by default because the current heuristic depends on an in-process previous-turn cache and does not use this protocol. Enabling it is an explicit experimental choice until it has source-complete provenance, atomic publication, restart recovery, and matched answer-quality evidence.
Direct-store vector snapshot cadence (2026-09-12)
Direct store() and deferred raw-source materialization rely on the vector
backend’s committed numerical payload and metadata before completing work.
They do not require a complete USearch snapshot rewrite for every event.
Local indexing persists both rows in one DuckDB transaction before adding the
native vector; startup restores keys missing from a snapshot without calling
an embedding provider. The configured vector_save_interval controls periodic
snapshot writes, and engine close still flushes the derived snapshot. PostgreSQL
persists its vector row in its existing index transaction.
This removes redundant whole-index writes from direct materialization. Lexical commit and failure accounting are unchanged. A failed scheduled snapshot still propagates through the index operation and leaves work retryable. Abrupt-exit checks cover completed public stores with no snapshot and with an older partial snapshot, preserving owner isolation and requiring no embedding inference during reopening. The source, graph node and work status remain durable independently of the derived snapshot’s cadence.
Synchronous-client interpreter shutdown (2026-09-14)
MemoryClient tracks live instances with weak references and installs one
process-wide cleanup hook in Python’s pre-thread-shutdown phase. Forgotten
clients therefore close their engine and flush derived indexes before
concurrent.futures disables the executor used by storage cleanup. Explicit
close() remains authoritative and removes the instance from that registry.
The fallback on runtimes without the early hook is best effort. This changes
shutdown ordering only; the event, direct-store journal, and materialization
completion boundaries above remain unchanged.
Entity-profile preparation recovery
The separate profile publication primitive now journals PROFILE_PREPARED inputs
before local staging, reuses exact prepared requests and atomically records
replacement of pending preparations. It shares the global artifact ownership
registry with derivations. A changing unindexed work epoch fences local native
writes against both replacement and publication, including caller cancellation.
The completed profile work row commits with its publication receipt.
Scoped Python profile_jobs, resume_profile and process_profiles expose
explicit recovery without inference. Startup reconstructs missing queue and
reservation rows from checksummed preparations and validated publication or
replacement receipts, including when registration markers already exist.
Operational attempt diagnostics reset on queue reconstruction. Invalid profile
journals disable retired derivation collection as well, because global artifact
ownership can no longer be established. Profiles expose explicit owner-scoped abandonment and fenced abandoned-stage
collection. Discard and collection receipts preserve their history; collection
retains ambiguous or mismatched inputs and never deletes a published graph node.
They have no automatic scheduler. See RFC-0015
and the profile guide for the public contract.
Source: docs/RFC-0016-Durable-Derivation-Commits.md in PRME at 58caddb1.