atif-sql · State machines
Section titled “atif-sql · State machines”No entity in this workspace carries a status enum with declared transitions. The enums the workspace
declares aren’t lifecycles either. RecordType (packages/atif-converter/src/atif_converter/domain/fidelity.py:26)
and FidelityGap (:55) are closed classification vocabularies whose members never move into one
another. OutputFormat (packages/atif-cli/src/atif_cli/output.py:61) has one resolution:
resolve_format maps AUTO to TABLE on a TTY and JSON otherwise (:84), but that is an
idempotent function evaluated per emit, a no-op for every explicit format (:82-83), whose result
is never stored and never advances again. One irreversible resolution with no persisted state is not a
lifecycle.
The machines below are durable-state lifecycles instead: the state lives in a row, a sidecar
file, or the presence of a directory, and named functions move an entity between named states. State
names and transition labels are verbatim source text from each machine’s Defined at: file.
Corpus session materialization
Section titled “Corpus session materialization”Every pass over the raw transcript corpus reclassifies each discovered session from scratch, so the
state is not a stored field; it is what the watermark records, whether the session’s artifact
directory exists, and what its meta.json (or the empty-session record) says about the generation
and the source. The classification is the if / elif / else at
packages/atif-corpus/src/atif_corpus/domain/sessions.py:218-223, and the outcome states are the
counters and id tuples of MaterializationReport
(packages/atif-corpus/src/atif_corpus/application/materialize.py:220-287).
skipped_liveis the entry state for a session still being written, not an error state; a session first scanned after it went quiet entersto_materializedirectly. A transcript is appended to many times per turn, and a session qualifies to convert only once its newest source mtime is at leastquiesce_secondsold, 300 by default (packages/atif-corpus/src/atif_corpus/domain/sessions.py:84).forceoverrides staleness but never quiescence, because converting a half-written transcript produces a wrong artifact rather than a stale one (:183-185).failures --> to_materializeis the retry, and it works only because_advance_watermarkretains the failed session’s entries. Staleness is “recorded set differs from scanned set”, so the retained entry is the retry signal; dropping it would classify the sessionup_to_dateforever (packages/atif-corpus/src/atif_corpus/application/materialize.py:1324-1331).up_to_date --> to_materialize: _unmaterialized_session_idsis the crash-recovery edge. It is reachable exactly one way: a pass killed inside the directory swap, after the previous generation was renamed aside and before the new one landed. The watermark records source mtimes only and cannot express a missing artifact directory, so the directory check is what force-replans it (:1025-1031).empty_session_idsisn’t a failure state. A converter that raisesEmptySourceErrorfound a well-formed transcript with nothing to convert, so its watermark entries advance andempty_sessions.jsonrecords the generation it was checked under (packages/atif-corpus/src/atif_corpus/application/materialize.py:1742-1745,:1360-1384). The next pass counts itup_to_dateuntil its source or the converter schema moves.unreadable_session_idsis a parking state, and a session can starve in it. A transient stat failure clears next pass; a permanent one (a side-file left at mode 000) leaves the session never materialized, neverup_to_date, neverskipped_live, and deliberately never marked source-removed, which is why the report carries the ids at all (:253-261).- No state here deletes a session.
removed_session_idslists the sessions whose source vanished this pass:_retain_sourceless_sessionskeeps the corpus session directory and rewrites itsmeta.jsononce withsource_present: falseandsource_removed_at(packages/atif-corpus/src/atif_corpus/application/materialize.py:1255-1260), because the agent expires old transcripts and the corpus holds the only copy left (:46-49). Later passes count the session inretained_count._advance_watermarkdrops its entries, so a source that comes back reads as stale and converts again from the live file (:1333-1336). A retained session whose recorded generation is stale re-converts from its raw source archive, when it has one, and stays marked (:1270-1307). A session whose sources merely could not be read is never marked source-removed (:1202-1208), and a scan that found zero sessions over a non-empty corpus raisesSuspiciousEmptyScanErrorinstead of marking every session (:1661-1667). - Transition sites:
scan_sourcesat:1622;QuiescencePolicyat:1699;_write_sessionat:777;MaterializationFailureat:1740;_retain_sourceless_sessions(it keeps and marks, never deletes);_advance_watermarkat:1778.
Defined at: packages/atif-corpus/src/atif_corpus/application/materialize.py:1497
Embed store schema version
Section titled “Embed store schema version”The vector store’s state is a schema generation recorded in a schema_version.json sidecar next to
the Lance table, written on create and checked on open. A sidecar rather than a table column, because
the version describes the schema and reading it must not require the schema to be readable
(packages/atif-embed/src/atif_embed/infrastructure/lance_store.py:85-89). SCHEMA_VERSION is 2
(:83).
v1andunrecordedhave no incoming edge on purpose. This code never writes either state; it discriminates a store it finds on disk.v1is the five-column shape lackingtext_hashandtruncated;unrecordedis a store whose columns are already current but whose sidecar is absent, whichread_schema_versionreports asNone(:342-355).v1 --> v2is online and leaves the vectors alone.migrate_pre_stamp_tableadds the two columns withALTER TABLE ... ADD COLUMN ... DEFAULTand a literal default, then marks each NOT NULL: no rows dropped, no vectors re-embedded, and readers keep working throughout (:365-401).text_hashbackfills to_PRE_STAMP_SENTINEL, the literal<pre-stamp>(:130), whose angle brackets make it impossible to equal a real blake2b digest; every migrated row therefore mismatches its corpus hash in the discovery anti-join and re-embeds incrementally through the ordinary staleness path (:126-129).v2 --> v2: has_tableis idempotent reopening.migrate_pre_stamp_tablereturns early when nothing is missing (:392-393) and the sidecar stamp is left alone.- The machine has no terminal state in source.
v2is absorbing; the store is never dropped here. A breaking change (a provider or dimension switch) is refused rather than migrated, and stays fail-loud throughembedding_guard(:33-35).
Defined at: packages/atif-embed/src/atif_embed/infrastructure/lance_store.py:421
Retry-queue row
Section titled “Retry-queue row”One row per (pipeline, unit_id) in the retry_queue table of the analytics state.db. The state is
the triple “is completed_at NULL”, “attempts against max_attempts”, and “next_attempt_at
against now”. The transition table is written as source at
packages/atif-analytics/src/atif_analytics/infrastructure/sqlite_state/retry_queue.py:12-16, and the
columns holding the state are declared at :45-56.
- The first
enqueuelands inbackoff, neverdue: it stampsnext_attempt_atat now plus_backoff_delta(1), which is two minutes (:78-81,:123). Backoff is exponential in minutes (2, 4, 8, 16, 32), and_BACKOFF_CAP_MINclamps it at 60 (:43,:80). That clamp is defensive rather than reachable at the shipped default:drainadmits a unit only whileattempts < max_attempts(:162), so withMAX_ATTEMPTS_DEFAULTof 5 (:42) the largest value_backoff_deltareceives is 5 and the longest real wait is 32 minutes. backoffandexhaustedtogether are exactly whatblocked_unitsreturns, anddueis whatdrainreturns; the two are complementary partitions of the live rows (:176-207,:145-173). Together they make the queue the single re-admission gate for a failed unit: a failed unit is not checkpointed, so without theblocked_unitssubtraction the checkpoint path would re-admit and re-bill it on every run (:188-192).exhaustedis terminal in effect and has no outgoing edge.drainnever returns it, so the pipeline never dispatches it, so it never fails again and is never marked done. The guard iskeep -= blockedatpackages/atif-analytics/src/atif_analytics/application/use_cases/classify.py:139, spelledalready |= blockedinpackages/atif-analytics/src/atif_analytics/application/use_cases/friction.py:250, whose unit is a message uuid rather than a session id.exhaustedis a parking state rather than a deleted row: the module issues noDELETE, and raisingmax_attemptson adraincall re-admits it.completed --> backoffis real, and the attempt counter survives it.enqueueclearscompleted_atback to NULL on conflict (packages/atif-analytics/src/atif_analytics/infrastructure/sqlite_state/retry_queue.py:128-133) while reading the priorattemptswith no filter oncompleted_at(:115-118), so a unit that failed four times, succeeded, then failed again re-enters atattempts = 5and isexhaustedimmediately under the defaultMAX_ATTEMPTS_DEFAULTof 5 (:42). Every state name here is a row predicate, not a stored string; the table has no status column (:45-56).
Every pipeline named by PIPELINE_NAMES
(packages/atif-analytics/src/atif_analytics/infrastructure/sqlite_state/checkpointer.py:37-42) fires
the identical call sequence (drain, then blocked_units, then enqueue on failure and
mark_done on success):
| pipeline | drain |
blocked_units |
enqueue |
mark_done |
|---|---|---|---|---|
classify |
packages/atif-analytics/src/atif_analytics/application/use_cases/classify.py:124 |
:133 |
:269 |
:294 |
conflicts |
packages/atif-analytics/src/atif_analytics/application/use_cases/conflicts.py:139 |
:146 |
:268 |
:335 |
user_friction |
packages/atif-analytics/src/atif_analytics/application/use_cases/friction.py:236 |
:244 |
:450 |
:470 |
perceived |
packages/atif-analytics/src/atif_analytics/application/use_cases/perceived.py:164 |
:171 |
:291 |
:340 |
Defined at: packages/atif-analytics/src/atif_analytics/infrastructure/sqlite_state/retry_queue.py:12