State store#
The material lifecycle and the SQLite store that makes a run resumable.
The coordination plane: the funnel state machine + the SQLite StateStore (2.1).
Everything the orchestrator knows about “where each material is” lives here, in two parts:
The state machine – the fixed lifecycle a candidate material moves through, encoded exactly once (
MaterialState,next_state,is_terminal,can_transition) so there is a single definition of “what comes next” and “what is finished”. The full funnel:NEW -> COMPOSED -> STRUCTURED -> SCREENED -> CALCULATED -> EXTRACTED -> DONE
plus two terminal states outside the linear chain, each reachable from any live state:
FAILED(a hard failure) andEXPANDED(a seed or parent retired after fanning out into children – terminal but not a failure, soFAILEDkeeps meaning genuine failure in every downstream query).CALCULATED(Phase 6) is the output of the optionalcalculatestage: a run that skips it walks SCREENED straight to EXTRACTED viacomplete’s explicittarget_state– which stages a run executes is run configuration, not lifecycle physics (thefinalizedoctrine).The store – one concrete SQLite-backed
StateStore(no ABC: this is a single-user tool; the contract lives in docstrings and tests, and an interface gets extracted only if a second database ever actually arrives). SQLite runs in WAL mode with abusy_timeout, and every read-then-write sequence executes inside aBEGIN IMMEDIATEtransaction, which is what makesclaiman atomic lease even across separate store instances on the same file.
Three durability properties the store guarantees (the restart-safety thesis):
Atomic leasing.
claimselects unclaimed rows and stamps them inside one immediate transaction – two claimers can never be handed the same item.Idempotent progress.
completeis keyed on a(material_id, stage)completions ledger: replaying a completion after a crash advances nothing twice.enqueue,expand,clear_joband terminalfailare likewise safe to replay.The in-flight job ledger.
record_jobpersists every submitted backend handle before any polling, so a restarted run re-attaches to its live jobs instead of resubmitting them. A material referenced by a live record is never swept byrelease_stale_leases– the recorded handle owns it untilclear_job. One live job per material is enforced by the schema itself (the link table’s primary key).
This module never imports the config layer, the CLI layer, or a backend: the
handle it stores is an opaque JSON-safe dict, converted from/to the compute
plane’s Handle at the orchestrator boundary (2.2).
On the ``# nosec B608`` markers. Several statements below build their SQL
with an f-string. In every one of them the interpolated part is only a run of
? placeholders (or a fixed WHERE fragment assembled here) – never a
value, never a caller-supplied identifier; the values themselves are always
passed to execute as bound parameters. That is the standard way to express
a variable-length IN (...) in sqlite3, and it is why bandit’s SQL-injection
heuristic is suppressed at those exact lines and nowhere else.
- class matmelt.core.state.MaterialState(value)[source]#
-
The lifecycle states of one candidate material.
A
strenum: members compare equal to their plain-string values and persist as text (state.valueinto a column,MaterialState(text)back out) with no custom conversion.The seven forward states are listed in funnel order;
FAILEDandEXPANDEDsit outside the chain, reachable from any live state.CALCULATEDbelongs to the optionalcalculatestage – runs that skip it never park a material there.
- matmelt.core.state.LIFECYCLE: tuple[MaterialState, ...] = (MaterialState.NEW, MaterialState.COMPOSED, MaterialState.STRUCTURED, MaterialState.SCREENED, MaterialState.CALCULATED, MaterialState.EXTRACTED, MaterialState.DONE)#
The forward funnel, in order. The successor of
LIFECYCLE[i]isLIFECYCLE[i + 1];DONE(last) has none.FAILED/EXPANDEDare intentionally absent – they are sinks, not chain members.
- matmelt.core.state.is_terminal(state)[source]#
Say whether a state is an end state.
- Parameters:
state (
MaterialState) – The state to classify.- Returns:
TrueforDONE,FAILEDandEXPANDED. A terminal material has left the funnel: it has no successor and must never be claimed or advanced again.- Return type:
Examples
>>> is_terminal(MaterialState.SCREENED), is_terminal(MaterialState.EXPANDED) (False, True)
- matmelt.core.state.next_state(state)[source]#
Give the state that follows when a stage completes a material.
- Parameters:
state (
MaterialState) – The material’s current state.- Returns:
Its successor in
LIFECYCLE.- Return type:
- Raises:
ValueError – For a terminal state – asking for the successor of
DONE,FAILEDorEXPANDEDis a programming error, not a recoverable condition.
Examples
>>> next_state(MaterialState.SCREENED) <MaterialState.CALCULATED: 'CALCULATED'>
- matmelt.core.state.can_transition(current, target)[source]#
Judge whether a stage-driven move is legal.
The store consults this before every write, so an out-of-order replay can never corrupt state.
- Parameters:
current (
MaterialState) – Where the material is now.target (
MaterialState) – Where the caller proposes to move it.
- Returns:
Truefor the three legal moves: the single forward step tocurrent’s successor (success); any live state toFAILED(hard failure); and any live state toEXPANDED(retired after fan-out). Everything else – skipping a stage, going backwards, leaving a terminal state – isFalse.- Return type:
Notes
Two moves deliberately sit outside this function:
finalizeandcomplete’s explicittarget_state. Which state ends a run, and which stages a run executes, are run configuration rather than lifecycle physics.
- exception matmelt.core.state.StateError[source]#
Bases:
ExceptionA coordination-plane contract violation.
Raised for an unknown
material_id, an illegal transition (rejected bycan_transition), a bad query filter, or a ledger-invariant violation (empty material set, second live job for a material, unserialisable handle). Never used for “no work available” – that is an empty list.
- class matmelt.core.state.MaterialRecord(material_id, state, claimed_at=None, parent_id=None, attempts=0, error=None, fail_stage=None, fail_reason=None, completions=<factory>, created_at=0.0, updated_at=0.0)[source]#
Bases:
objectOne material’s snapshot as handed back by
claim/query/get.Frozen: advancing a material happens only through
complete/fail/expand, never by mutating what a reader was handed.- state#
Where it is in the funnel.
- Type:
- claimed_at#
Epoch seconds when the current lease was taken, or
Nonewhen unleased. The basis forrelease_stale_leases.
- parent_id#
Lineage: the material this one was fanned out from,
Nonefor a root seed. A plain back-pointer, recorded atenqueue.
- error#
The latest attempt’s failure reason, set on every
failincluding requeues, so a requeued material still shows why its last try died.
- fail_reason#
Why it died. Also reused by
expandto carry its note, since a second note kind did not warrant another column.
- class matmelt.core.state.InFlightJob(job_key, stage, backend_name, handle, material_ids, submitted_at)[source]#
Bases:
objectOne submitted-but-unfinished backend job, as persisted by
record_job.- job_key#
Caller-chosen unique identity, by convention
"<backend_name>:<job_id>". Re-recording a key replaces the record.- Type:
- handle#
The backend
Handleas an opaque JSON-safe dict. The orchestrator converts at the boundary, so this module never imports the compute plane.- Type:
- matmelt.core.state.FINALIZE_STAGE = 'finalize'#
Reserved completions-ledger stage name for the orchestrator’s stop-stage bookkeeping transition (
finalize). Not a real stage; a stage must never use it as its own name.
- class matmelt.core.state.StateStore(db_path, *, busy_timeout_ms=5000, read_only=False)[source]#
Bases:
objectThe durable coordination plane, backed by one SQLite database file.
One connection per instance, shared across threads behind an internal lock (
check_same_thread=False);isolation_level=Nonegives explicit transaction control so read-then-write sequences run underBEGIN IMMEDIATE. Cross-instance safety (two stores on one file) comes from SQLite itself: the immediate transaction takes the write lock up front, andbusy_timeoutabsorbs brief contention instead of raising.- Parameters:
db_path (
str|Path) – Path to the database file, created if absent. Opening an existing file is a safe no-op re-apply of the schema, so a run resumes against the same file it was interrupted on.busy_timeout_ms (
int, default:5000) – How long SQLite waits for a contended write lock before raising.read_only (
bool, default:False) – Open an observer store – themonitorseam. The schema script is skipped (it is a write) andPRAGMA query_only=ONmakes SQLite itself reject every write on the connection, so the no-corruption guarantee is enforced by the engine rather than by caller discipline. Mutating methods additionally raise up front, for a friendlier failure thanSQLITE_READONLY.
- Raises:
StateError – When
read_onlyis set and no database exists atdb_path. An observer must never silently create an empty database.
Notes
query_onlyis chosen over the URI?mode=rodeliberately: a WAL reader may need to create the-shmfile – after the last writer died uncleanly, for instance – whichmode=roforbids andquery_onlyallows.- close()[source]#
Close the underlying connection.
Safe to call more than once. A closed store is not reusable; open a new one against the same file, which is exactly what a resumed run does.
- Return type:
- enqueue(material_id, parent_id=None)[source]#
Add
material_idto the funnel in stateNEW. Idempotent.- Parameters:
- Raises:
StateError – When the store is read-only.
- Return type:
Notes
Re-enqueueing an existing id touches nothing: its current state, attempts, and recorded
parent_idall stand, first write wins. That is what lets a replayed seed or a re-run feeder never knock a material backwards or rewrite its lineage.
- get(material_id)[source]#
Fetch one material’s full snapshot. Read-only.
- Parameters:
material_id (
str) – The id to fetch.- Returns:
The snapshot, or
Nonewhen the store does not know the id.- Return type:
- query(filters=None)[source]#
List materials matching
filters, oldest first. Read-only.No leasing, no writes.
- Parameters:
filters (
dict[str,Any] |None, default:None) – Field/value pairs, AND-combined. Keys must be one ofmaterial_id,stateorparent_id– whitelisted, so a caller-supplied field name is never interpolated into SQL unchecked. AMaterialStatevalue is coerced to its stored text.Noneor empty returns every material.- Returns:
Matching snapshots, ordered by creation time then id.
- Return type:
- Raises:
StateError – For a filter key outside the whitelist.
- count_by_state()[source]#
Tally the materials per state. Read-only.
- Returns:
{state: count}from oneGROUP BY. States holding no materials are simply absent; callers zero-fill if they need the full set.- Return type:
- completion_counts_since(cutoff)[source]#
Count completions ledgered at or after a cutoff. Read-only.
The rolling-window primitive behind the throughput and speed displays: the caller picks the window and divides.
- Parameters:
cutoff (
float) – Epoch seconds. Pass0.0for all-time counts, ortime.time() - window_sfor a rolling window.- Returns:
{stage: count}. ReservedFINALIZE_STAGErows appear under their own name, leaving the caller to decide whether finalizations count as stage work. Stages with no completions in the window are simply absent.- Return type:
- recent_failures(limit=10)[source]#
List the most recently failed materials, newest first. Read-only.
- Parameters:
limit (
int, default:10) – How many to return. Zero or less returns nothing.- Returns:
Ordered by
updated_atdescending. A terminal row is never written again, so itsupdated_atis exactly when it failed, and the record carries it so callers can render “failed at”.- Return type:
- recent_successes(limit=10)[source]#
List the most recently finished materials, newest first. Read-only.
The success mirror of
recent_failures.- Parameters:
limit (
int, default:10) – How many to return. Zero or less returns nothing.- Returns:
DONEmaterials ordered byupdated_atdescending. A terminal row is never written again, soupdated_atis exactly when the material finished andupdated_at - created_atis how long its whole ride took; the monitor renders both.- Return type:
- claim(state, limit, *, among=None)[source]#
Atomically lease up to
limitunclaimed materials instate.Selects the oldest unleased rows and stamps
claimed_atinside one immediate transaction, so two concurrent claimers can never be handed the same material.- Parameters:
state (
MaterialState) – Which input state to claim from.limit (
int) – The most materials to lease. Zero or less claims nothing.among (
Collection[str] |None, default:None) – Restrict the claim to these ids – the selective-run seam, where the orchestrator precomputes the eligible lineage and leases nothing outside it.Noneis unrestricted; an empty collection claims nothing.
- Returns:
The leased snapshots, oldest first. Empty when nothing is available.
- Return type:
- Raises:
StateError – When
stateis terminal – claiming one is a programming error – or when the store is read-only.
Notes
The
amongids ride an SQLINclause, which very old SQLite builds cap at 999 entries. That is fine at v1 scale; chunking is a later concern.
- complete(material_id, stage, *, target_state=None)[source]#
Advance
material_idforward in the funnel and release its lease.Idempotent on the
(material_id, stage)completions-ledger key: if this stage already completed this material the call is a no-op, so a crash-replay never advances twice. Otherwise the material moves to the target, the lease clears, the retry budget resets (attempts = 0,error = NULL) and the ledger row is written – all in one transaction.- Parameters:
material_id (
str) – The material to advance.stage (
str) – The stage that completed it, which is also the ledger’s idempotency key.target_state (
MaterialState|None, default:None) – Where to move it.Nonetakes the single step to its lifecycle successor. An explicit value moves it to any strictly forward chain state, which is how a run that skips an optional stage (saycalculate) walks its consumer’s completion over the skipped state – SCREENED straight to EXTRACTED. Likefinalize, the jump sits deliberately outsidecan_transition: which stages a run executes is run configuration, not lifecycle physics.
- Raises:
StateError – For an unknown material; for one already terminal; for a backward, same-state, or off-chain (
FAILED/EXPANDED) target; or when the store is read-only.- Return type:
- fail(material_id, stage, reason, *, max_attempts=1)[source]#
Spend one retry attempt; requeue while budget remains, else fail terminally.
Increments
attemptsand recordsreasonverbatim inerror.- Parameters:
material_id (
str) – The material that failed.stage (
str) – Where it failed, recorded infail_stagewhen the failure turns terminal.reason (
str) – Why, stored verbatim. Shown bystatusandmonitor.max_attempts (
int, default:1) – The stage’s retry budget. The default of 1 makes the first failure terminal; the orchestrator passes the configured per-stage value.
- Returns:
The material’s state after the call: unchanged when it was requeued (lease cleared, so the next claim re-runs the same stage), or
FAILEDwhen the budget ran out. A failed row stays in the database – failed candidates are still data.- Return type:
- Raises:
StateError – For an unknown material, one terminal as
DONEorEXPANDED, or a read-only store. Calling on an already-FAILEDmaterial is idempotent: a no-op that spends no attempt.
- reset(material_id, target_state)[source]#
Un-fail a terminal
FAILEDmaterial back to a live, claimable state.The recovery primitive behind
matmelt retry. The lease clears and all failure bookkeeping is wiped (attempts = 0, anderror,fail_stage,fail_reasonall set to NULL), so the nextrunre-claims the material and re-runs that stage.The completions ledger is left untouched: those earlier stages really did complete. Preserving the timeline is what lets the caller compute the correct
target_statein the first place, and it keeps lineage and provenance intact.- Parameters:
material_id (
str) – The failed material to revive.target_state (
MaterialState) – Where to put it back – the state it was claimable from before it failed, i.e. itsfail_stage’s input state. The caller derives this from the material’s completions timeline, because the state layer holds no stage-to-state map.
- Raises:
StateError – For an unknown material; for one that is not
FAILED, since this is a recovery primitive and not a general “set state”; for a terminaltarget_state; or for a read-only store.- Return type:
- expand(material_id, reason)[source]#
Retire
material_idasEXPANDED– it fanned out into children.Terminal like
FAILEDbut not a failure. Keeping them distinct is what letsFAILEDgo on meaning genuine failure in every downstream query. The lease clears.- Parameters:
- Raises:
StateError – For an unknown material, one terminal as
DONEorFAILED, or a read-only store. Calling on an already-EXPANDEDmaterial is idempotent.- Return type:
- finalize(material_id)[source]#
Retire a live material as first-class terminal
DONE.The orchestrator’s bookkeeping move, not a stage transition: a material that completes the run’s last stage has no consuming stage left, so this retires it as “went as far as this run was asked to go” and records a completions-ledger row under the reserved stage name
FINALIZE_STAGE. The lease clears and the retry budget resets.It sits deliberately outside
can_transition, which describes stage-driven moves only: finalize is legal from any live state, because which state is “the end” is run configuration rather than lifecycle physics.- Parameters:
material_id (
str) – The material to retire.- Raises:
StateError – For an unknown material, one terminal as
FAILEDorEXPANDED, or a read-only store. Calling on an already-DONEmaterial is idempotent.- Return type:
- release_stale_leases(timeout_s)[source]#
Clear leases older than
timeout_sseconds.Run at startup and between passes: a lease older than the timeout belonged to a run that died mid-stage, and clearing it returns the material to its queue with its state unchanged.
- Parameters:
timeout_s (
float) – Lease age, in seconds, past which a lease is considered abandoned.- Returns:
How many leases were freed.
- Return type:
- Raises:
StateError – When the store is read-only.
Notes
Only live, currently leased rows are touched, and a material referenced by a live in-flight job is never swept whatever its lease age: the recorded handle owns it until
clear_job. Releasing underneath a running job is exactly the double-submission hole the ledger closes.
- record_job(job_key, stage, backend_name, handle, material_ids)[source]#
Persist one submitted backend job: job row plus material links, atomically.
Called immediately after
backend.submitreturns and before any polling – this record is what survives a crash. Re-recording an existingjob_keyreplaces the record and its links, so a replayed submit leaves exactly one record.- Parameters:
job_key (
str) – The job’s unique identity, by convention"<backend_name>:<job_id>".stage (
str) – Whose work the job carries.backend_name (
str) – Which configured backend the handle resolves against.handle (
dict[str,Any]) – The backendHandleas a JSON-safe dict.material_ids (
Sequence[str]) – Every material the job will produce a result for. Duplicates are dropped, caller order preserved.
- Raises:
StateError – When
material_idsis empty; when the handle is not JSON-serialisable; when a material is already linked to a different live job, since the schema allows one live job per material; when a material is unknown to the store; or when the store is read-only.- Return type:
- list_jobs(*, stage=None, backend_name=None)[source]#
List the live in-flight jobs, oldest first. Read-only.
- Parameters:
- Returns:
Ordered by submission time then key, each carrying its material links.
- Return type:
listofInFlightJob
Notes
The check half of the pass loop lists everything; the submit half filters by
backend_nameto count live jobs against that backend’smax_parallel.
- clear_job(job_key)[source]#
Remove one in-flight record, making its materials sweepable again.
Called after the job’s results were attributed –
completeorfailper material – and the backend handle released.- Parameters:
job_key (
str) – The record to remove. Clearing an unknown or already-cleared key is a no-op, so restart-time cleanup is always safe.- Raises:
StateError – When the store is read-only.
- Return type: