Skip to content

Durable run capture

alignmenter run now creates its durable execution records before calling a chat provider. Each returned answer, its observed context/usage, and its successful attempt are committed together before the progress callback runs. A later generation, scorer, or reporter failure leaves that committed work available.

Inspect and recover captured transcripts

Use the run directory printed on a handled failure, or the directory under your chosen --out path after a hard kill:

alignmenter status reports/<run-directory>
alignmenter status reports/<run-directory> --json
alignmenter export-transcripts reports/<run-directory> --out recovered.jsonl
alignmenter export-transcripts reports/<run-directory> --stream compare --out comparison.jsonl

These commands read saved records without invoking providers or scorers. Export refuses to replace an existing file unless --force is supplied. A partial export contains only committed records; it may end with a user turn whose assistant answer was never captured. It never substitutes an old dataset answer for a failed generation.

status reports the last committed state, not process liveness. An abrupt kill can leave a run and its active attempt marked running; the JSON result includes liveness: "not_checked". Handled interruptions finalize the run as interrupted. A legacy provider exception leaves its attempt unknown_outcome because that interface cannot prove whether the external action stopped. Returned but invalid responses are classified separately as failed attempts.

Recorded inputs and adapters with explicit recovery contracts now support capture-only resume. Legacy providers do not acquire retry or session-replay guarantees automatically. Re-running run creates a new directory and starts fresh work. Successful execution means the pipeline completed; quality thresholds remain separate from execution status. Capture-only completion has status captured.

Saved records

Artifact Meaning
run.sqlite3 Authoritative manifest, planned inputs, attempts, observations, committed transcript records, and state history
manifest.json Human-readable snapshot of the versioned manifest
run.json Existing report metadata, with a reference to the durable database
transcripts/*.jsonl Derived completed or partial transcript exports
Existing score/report files Outputs from the current scoring and reporting pipeline

The database uses transactions and SQLite synchronous=FULL. Small source and observation payloads are stored inline in this first implementation, so they share the same commit boundary. A generated answer and the successful attempt cannot be committed separately. The process-kill tests exercise this boundary through the real runner.

Source snapshots preserve parsed dataset records in canonical JSON and persona bytes when available. The dataset digest identifies those record values and their original order, rather than the source file's whitespace. Changing or deleting the source files after run creation does not replace the saved inputs.

Primary and comparison streams have separate identities and transcript files, including when both use the same model identifier. Every generated turn gets a unique local attempt ID. Compatible retries retain the original request ID. The legacy provider receives its existing message interface; local IDs alone do not establish app-side request echo or idempotency guarantees.

Evidence and provenance

An observation stores the provider's exact text; the legacy transcript retains its existing whitespace trimming. Missing context is None with context_status: missing. An explicitly returned empty context is {} with context_status: provided. A context containing excerpts: [] remains distinct from both. None of these legacy captures asserts complete evidence: evidence_completeness is unknown.

Regenerating an answer replaces its old context, usage, generation identity, and baseline metadata. Case metadata is preserved, and baseline_text records the answer actually being replaced. Missing usage remains unavailable in the observation. Provider responses must contain string text and JSON-compatible context/usage; invalid responses are recorded as failed attempts rather than successful empty answers.

The manifest explicitly lists the configuration and capability gaps in legacy providers and scorers. It does not introspect provider objects or store credentials. Failure records store the exception class and classification; callers still receive the original exception. The include_raw option continues to control the extra legacy raw.json file, while transcripts and durable capture remain part of every run.

This change does not alter the existing scorers, judge budgets, threshold semantics, or breakdown computation. The new durable rubric path adds versioned verdicts and pure aggregation separately. Legacy scorer migration, qualified judges, stateful recovery, and physical-device leases remain subsequent delivery slices.

Python API

from pathlib import Path
from alignmenter.storage import RunStore

store = RunStore(Path("reports/<run-directory>"))
summary = store.summary()
observations = store.observations()
attempts = store.attempts()
committed_records = store.transcripts("primary")
manifest = store.manifest()
original_dataset = store.source_artifact(manifest.dataset_digest)

Runner.run_dir is available once a run directory is created, including when execute() later raises. Execution models live in alignmenter.schemas.execution; their schema_version is currently 1; new databases use version 2. Version 1 databases remain readable but are not reopened for writes. Unknown schema/database versions are rejected rather than silently migrated. The typed JSON contract requires Pydantic 2.5 or newer.