# Internal: Manifest ## Purpose Explain the session-progress model implemented by `internal/manifest`. Physical manifest placement belongs in [Operations](../operations.md#local-state-layout). ## Session Manifest `manifest.Manifest` records: - required `schema_version: narratio.session-manifest.v1` - identity (`session_id`, `campaign`, `run_id`) - local path metadata (`local_workdir`, `local_spool_dir`) - remote identity metadata (`s3_bucket`, `s3_session_prefix`, `s3_run_prefix`) - `inputs` records - durable `artifacts` records - per-stage `stages` map, including the ordinary cleanup stage record Session, campaign, and run identities in local and downloaded manifests must be portable opaque segments. Unsafe legacy identities are rejected with migration guidance rather than being normalized into a different workspace or remote namespace. Decoding rejects unknown typed fields, including informational fields inside `effective_config` and its nested selection. The schema version remains `narratio.session-manifest.v1`; there is no compatibility reader that silently drops retired fields. See [overlay migration](../operations.md#migrating-manifest-overlay-provenance) for deliberate migration of retired selection provenance. Cleanup progress uses only the cleanup entry in `stages`. Every persisted session manifest has a non-nil `stages` map. Each entry is non-nil and its embedded `name` exactly matches its map key; loading never invents either marker. A missing or unsupported schema version, or incomplete stage identity, is unsupported local state: move it aside and regenerate it, or restore a verified current committed snapshot when that is authoritative. Prepare records independent `party` and `players` input checksums. In canonical party mode, the party record retains its campaign source identity while the players record uses `derived_from_party`; raw roster content is never embedded in manifest metadata. Both records remain the durable authority for consumers of their prepared input source IDs. The model admits these stage states: - `pending` - `running` - `succeeded` - `failed` - `skipped` - `stale` - `interrupted` ### Transcribe-owned speaker state A `transcribe` stage record may carry `transcribe_state_version: 1` and a `transcribe_speakers` map keyed by the existing derived speaker ID. The version is the authority marker: a versioned empty map means that no earlier speaker completion is reusable. Absence of both fields is accepted as legacy aggregate-only state so current Narratio can read older manifests. Each map entry represents one completed speaker only. It binds a versioned fingerprint to the session-relative audio path, audio SHA-256 and size, one canonical `transcript_raw` output with its path, SHA-256 and size, and the producer run ID. Paths must be canonical and session-relative, checksums must be lowercase SHA-256 values, and the record and output producer identities must agree. Other stages cannot carry this transcribe-owned state. Audio size is nonnegative; WhisperX determines whether that audio can be transcribed. Output size remains positive and the transcript must be valid JSON. Transcribe reconciles this collection against the current prepared audio set and observable WhisperX semantics. Missing or corrupt regular outputs become work; unsafe or non-regular evidence is rejected. It saves the reconciled collection before HTTP dispatch and saves a detached candidate after each validated completion. Only after a save succeeds is the candidate installed as live authority. Canonical bytes produced before an unsuccessful save are not a completion record. Aggregate transcribe status still describes the whole invocation. A failed, running, stale, or interrupted aggregate may therefore retain reusable speaker records, while aggregate outputs are declared only after the current collection is complete. Ordinary retry reconciles and reuses those records. Force first checkpoints an empty current collection, preventing later retries from reviving pre-force results. Older aggregate-only transcribe success is conservatively regenerated once when transcribe is selected because it does not bind individual outputs to the audio bytes used. The session-manifest schema identifier remains `narratio.session-manifest.v1`; this additive state is accepted by current Narratio, but older strict-decoding binaries may reject manifests after these fields have been written. ### Analyze-owned artifact state Every `analyze` stage record carries `analyze_state_version: 1` and an `analyze_artifacts` map keyed by normalized configured artifact key. The version is the authority marker: version 1 with no entries is a valid evaluated empty set. An absent or unsupported version is unsupported local state and is rejected rather than being treated as aggregate-only authority. Each analyze artifact record has one disposition: - `current`: the configured artifact is available and carries a versioned fingerprint plus a complete output record and separate output size; - `stale`: the recorded semantic identity is no longer current; - `missing`: no validated current result exists; - `failed`: the attempted work failed and carries a bounded diagnostic; or - `unselected`: the artifact was intentionally outside the evaluated set. Records bind their normalized key and dependencies, fingerprint contract when evaluated, canonical session-relative output identity when current, producing Narratio run, update time, and bounded non-secret Scriptorium provenance and diagnostic paths. A current output includes its configured source ID, contract, checksum, and positive byte size. Non-current records cannot carry an output, so an older file is not advertised through stale, missing, failed, or unselected state. Family-produced records additionally retain optional `family` and `character_id` provenance supplied by configuration resolution. These fields do not replace the concrete configured key or infer family membership from a name, so older records without them remain valid. The session-stage collection is the reconciled authority across invocations. These records remain analyze-owned data inside the fixed stage; they are not dynamic stages or generic subtasks. Analyze checkpoints one complete validated candidate collection after each successful artifact and before exposing that artifact to later work. It saves a detached manifest candidate and installs its version, collection, and updated timestamp into live state only after the store reports success. Aggregate stage outputs are ordinary result outputs rebuilt in configured-key order from current session records only. Successful incremental execution replaces only evaluated artifact records and preserves valid unrelated current records. Rebuilt outputs are compared by bytes and contract: an unchanged identity permits an unselected dependent with the same recomputed fingerprint to remain current, while a changed identity removes output authority from every unselected transitive dependent by marking it stale. A partial analyze invocation can therefore succeed while unrelated configured records remain stale. Existing canonical files never create current records without validated execution and a successful checkpoint. Aggregate analyze status is deliberately coarser than this collection. Resume validation may skip a succeeded aggregate record when the selected artifact closure is current even if unrelated records are stale. Conversely, a stale aggregate record may cross the ordinary runner boundary and perform zero Scriptorium calls when reconciliation proves every selected artifact current; ordinary successful stage completion then restores the aggregate status. On an artifact error, analyze installs a complete retry collection on the live manifest before returning. The active record is failed without output, current transitive dependents are stale, unrelated current records survive, and only earlier checkpointed completions remain current. The runner's ordinary failure path marks aggregate analyze failed, clears aggregate outputs, invalidates delivery dependents, and saves that collection. A terminal session-save failure is joined with the original adapter, validation, filesystem, cancellation, or checkpoint cause. If an artifact checkpoint fails, analyze rolls its working collection back to the last committed collection before constructing the active failed record. Newly materialized canonical bytes therefore remain incidental and cannot become current authority. A later ordinary run reconciles the durable records, reuses earlier current artifacts, and resumes with the failed or stale work. The same recovery applies after process interruption: the aggregate record may be non-successful, but every completed artifact checkpoint remains reusable. ## Remote Commit Manifest `artifacts.RemoteCommitManifest` is a separate, versioned remote snapshot contract. It is not a serialized session manifest and contains no local post-publication assertion such as `current_pointer_written`. A remote commit identifies one campaign, session, and run and declares its immutable artifact set. Each artifact has a typed source, immutable destination key, SHA-256 checksum, size, and storage generation. `current/commit-pointer.json` is the sole mutable selector for the new contract. It identifies exactly one run-scoped `runs/{run_id}/commit.json` and binds that object by checksum, size, and generation. Readers strictly reject unknown fields, version mismatches, pointer/commit identity mismatches, and objects that do not match their declaration. An absent commit pointer means no remote state is published. Republish to establish a current immutable snapshot. ## Persistence Semantics `manifest.LocalStore`: - accepts only the exact supported session-manifest schema version; - rejects unknown fields and multiple JSON values; - validates stage-map identity without synthesizing missing maps, records, or names; - retains only representation-preserving collection normalization; - writes through a sibling temporary file and atomic replacement visibility; - updates `updated_at` on save. ### Checkpoint persistence boundary Analyze and transcribe share one private checkpoint operation in `internal/stage`. Their typed callers still decide when to checkpoint, validate their collections, populate stage-owned fields, and serialize access. The shared operation copies the manifest and its checkpoint-relevant mutable collections, applies the typed update to the copied target record, and calls the existing `manifest.Store` exactly once. Only after that save succeeds does it promote the saved target record and manifest `updated_at` into live state; it preserves an existing target record's identity and leaves unrelated live stage records in place. The copy boundary includes stage records, input and artifact slices, aggregate output/log/configuration slices, and the typed analyze and transcribe collections that callbacks or `LocalStore.Save` may modify. Metadata and other pointer fields remain shared read-only because neither checkpoint callback nor the store mutates them. This is a bounded execution helper, not a general manifest clone or a transaction across transcript/artifact files. Manifest replacement provides atomic visibility, not a crash- or power-loss durability guarantee. Callers reload after a returned save error before retrying. A transcribe or analyze checkpoint error leaves the prior manifest collection as authority. The runner then attempts to persist the aggregate failure; if that terminal save also fails, its error is joined with the originating failure and the next invocation must reconcile from the last readable manifest. ## Execution Semantics Planning and execution share one pure pre-execution decision over the recorded stage status, force selection, and normalized resume-validation result. That decision identifies run versus reuse and any required stale and dependent invalidation transition. Planning applies it only to a cloned manifest. Execution remains the sole owner of observed stage results and persistence. For each selected stage, the application runner computes semantic evidence, performs any eligible resume validation, applies the pre-execution decision, and persists required invalidation before either reusing the existing success or beginning an attempt. The same one-stage execution owner marks the attempt running before invoking the stage and then records and persists its failed, skipped, or successful terminal state before a later stage may start. On success it records outputs, logs, generated configuration references, metadata, and—when the stage implements the optional contract—a versioned semantic-configuration fingerprint. Artifact records may include optional contract and external provenance objects; old manifests remain compatible when those fields are absent. A successful forced rerun marks only succeeded transitive dependent session-stage records stale. The application owns a fixed dependency relation distinct from execution order; dependents are returned in canonical order. Render and extract therefore never stale one another, while either can stale analyze, publish, notify, and cleanup. Starting an execution clears the current session-stage record's prior outputs, logs, generated configuration references, metadata, and semantic fingerprint. Failed and skipped transitions enforce the same clearing rule directly, while success repopulates only fields returned by the new result. Marking a record stale does not clear those details because resume validation and diagnosis may still require them before execution begins. Aggregate lifecycle clearing deliberately preserves both the transcribe-owned speaker collection and the analyze-owned per-artifact collection. This lets later reconciliation replace only non-current entries without erasing durable completions. Other stages retain aggregate-only lifecycle behavior and are forbidden from carrying either collection. A stage may explicitly return a skipped disposition and stable reason. The runner persists that outcome in the session manifest, clears older outputs, logs, generated configuration references, and metadata, then applies any bounded details from the current skip and continues. This self-skip is distinct from deciding not to execute an already-succeeded stage and is reconsidered on later runs. Skipped results cannot contain outputs. An intentional self-skip records the current semantic fingerprint because it is a completed, reusable stage result; failed or interrupted work never promotes one. When an already-succeeded stage is skipped, the session manifest deliberately retains its existing succeeded record because it remains the cross-invocation progress authority. If a stage supplies semantic configuration evidence, reuse first requires the persisted positive schema version and lowercase SHA-256 digest to match the current resolved stage semantics. Missing semantic evidence, malformed evidence, or a mismatch makes the stage and its fixed transitive dependents stale. The existing stage-specific resume validator runs only after this semantic check succeeds; both checks are required. Extraction, analyze, and cleanup have resume validators and may reject an otherwise eligible skip when their selected durable evidence is obsolete; the runner marks the aggregate record stale and executes it. Analyze's validator can still accept a partial selection when only unrelated artifact records are stale. Implemented reuse coverage is deliberately split between aggregate semantic evidence and focused durable validators: | Work | Reuse authority | Focused owners | | --- | --- | --- | | prepare | aggregate semantic fingerprint | [prepare](stage-prepare.md) | | transcribe speakers | aggregate semantic fingerprint plus per-speaker input/output reconciliation | [transcribe](stage-transcribe.md), [WhisperX](../integrations/whisperx.md) | | merge | aggregate semantic fingerprint | [merge](stage-merge.md), [Seriatim](../integrations/seriatim.md) | | polish | aggregate semantic fingerprint | [polish](stage-polish.md), [Audita](../integrations/audita.md) | | normalize | aggregate semantic fingerprint | [normalize](stage-normalize.md), [Seriatim](../integrations/seriatim.md) | | trim | aggregate semantic fingerprint | [trim](stage-trim.md), [Scriptorium](../integrations/scriptorium.md), [Seriatim](../integrations/seriatim.md) | | render | aggregate semantic fingerprint | [render](stage-render.md), [Seriatim](../integrations/seriatim.md) | | extract | aggregate semantic fingerprint plus reference/output validator | [extract](stage-extract.md), [Notarius](../integrations/notarius.md) | | analyze artifacts | per-artifact fingerprint, reconciliation, and output validator | [analyze](stage-analyze.md), [Scriptorium](../integrations/scriptorium.md) | | publish | aggregate semantic fingerprint plus immediate lock/commit checks | [publish](stage-publish.md), [storage adapter](adapters.md) | | notify | aggregate delivery-mode fingerprint | [pipeline overview](overview.md), [configuration](../config.md#notifications) | | cleanup | aggregate semantic fingerprint plus publish-evidence and target-absence validator | [cleanup](stage-cleanup.md), [workspace](workspace.md) | These contracts record resolved choices Narratio can observe, not operational runner tuning. External model, module, prompt, profile, and configuration-file contents that a tool privately loads remain outside the contract when their configured identifier is unchanged; operators must force the affected work after such a private content change. The session manifest is the authoritative stage-progress ledger across invocations. It retains the most recently resolved invocation's bounded configuration provenance. It identifies the selected overlay name and source (`explicit` when selected) plus the effective configuration digest, but never a raw secret or overlay content. This provenance is informational: it does not participate in stage resume or cache decisions. An overlay change therefore invalidates only stages whose semantic configuration changed. When a private external-tool model, module, prompt, or profile changes behind an unchanged configured identifier, use `--force` for the affected work. `session plan` computes the same current fingerprint and applies the shared decision and invalidation rules to a cloned manifest. It predicts the runner decision without persisting session state. The semantic fingerprint owner hashes deterministic JSON from stage-owned typed structs; stage providers must exclude secrets, complete effective-configuration dumps, and operational values that cannot affect canonical results. Concrete coverage is owned by the focused stage and integration documents linked above. Before an explicitly bounded execution starts after `prepare`, the application reads the session manifest and accepts only `succeeded` or `skipped` for every excluded canonical prefix stage. The first other status or absent record fails the request before layout mutation, adapter initialization, session-manifest writes, or run-directory creation. Excluded prefix records are not passed to resume validators. Records after the selected end are not prerequisites and may be made stale by selected work without being scheduled. After a publish commits remotely, any configured local cleanup is first recorded as a session-manifest obligation before deletion begins. Each target becomes complete only after its confined deletion (or safe absence check) and a successful manifest save. An incomplete obligation is retried when publish executes again and retains the committed run and remote identity that authorized it; an invocation that does not execute publish does not perform cleanup. Each invocation derives campaign, session, run, local-path, and remote-prefix metadata from the validated resolved configuration as one projection. A persisted session manifest must agree on campaign and session identity before execution; the current projection is refreshed for every invocation while stage progress, inputs, and durable artifacts remain session history. For handled failures after the session identity transition, the runner records the failure on the session ledger and atomically attempts its save while preserving the originating error. Abrupt process death remains an accepted case where the last session transition can require operator interpretation. ## Invariants - stage resume/skip decisions are session-manifest driven. - semantic fingerprint comparison precedes stage-specific resume validation. - only successful and intentional-skipped results promote current semantic evidence. - running, failed, and self-skipped stages do not retain result payloads from an earlier success. - stale stages retain prior details until replacement execution starts. - force reruns stale succeeded stages in the fixed dependency relation. - the session manifest is the only durable local progress ledger. - remote commitment is established by a verified current pointer and remote commit relationship, never by a mutable session-manifest boolean. ## Implementation And Tests - Models and transitions: `internal/manifest/manifest.go` - Remote commit model and readers: `internal/artifacts/remote_commit.go`, `internal/artifacts/current_state_commit.go`, - Persistence and validation: `internal/manifest/store.go` - Transcribe speaker state: `internal/manifest/transcribe_state.go` - Package tests: `internal/manifest/*_test.go` - Assembled execution behavior: `internal/app/runner_test.go`, `internal/app/run_stage_test.go`