# State file reference Every Hankweave run leaves a durable record on disk: which codons ran, what they cost, where they checkpointed, and how they ended. That record is `state.json`, and this page is its field-by-field reference. Read it when you need to debug a failed run, build tooling on top of persisted state, or verify what the runtime believes happened. The page moves from the file's location and write behavior, through its top-level shape, down into run, codon, plan, checkpoint, sentinel, and budget records, and ends with validation, recovery, and safe-reading recipes. ## How to read this page Use `state.json` to inspect persisted runs and codon attempts. A codon is one sealed agent task in the sequence. For event payloads rather than saved state, see [Events](/0.10.0/files/reference/events). Two source files define everything on this page: `types/state-types.ts` defines the shape, and `state-manager.ts` defines persistence behavior. There is no published JSON schema for `state.json`; the published schemas cover Hankweave configuration. The source excerpts below retain the interface fields and comments verbatim, with runtime behavior explained alongside them. A few conventions apply throughout. Illustrative JSON examples use documented model shortcuts rather than dated snapshot identifiers. The whole-file example below is the exception: it is real `state.json` captured from a minimal-single-provider run. Its completed codon retains both `currentCost` and `finalCost`; use `finalCost` for the completed-codon view. Since-markers come from lock history: budget fields are marked `0.6.1`, legacy-provider plan migration is marked `0.10.0`, and `extensionCount` has no marker. For related concepts, see [Codons](/0.10.0/files/concepts/codons), [Checkpoints](/0.10.0/files/concepts/checkpoints), [Loops](/0.10.0/files/concepts/loops), [Sentinels](/0.10.0/files/concepts/sentinels), [Budgets](/0.10.0/files/concepts/budgets), [Events](/0.10.0/files/reference/events), and [Client and exported types](/0.10.0/files/integrate/client-and-exported-types). The capture below shows the whole structure at once – one completed run with a single codon – before the later sections take each part apart. Its paths are normalized for portability. Notice how the run-level fields (`runId`, `gitBranch`, `status`, timestamps) wrap the codon execution record, and how the flattened `executionPlan` sits beside the run history. ```json { "runs": [ { "runId": "1788337916070-aqhfn", "runFolder": "~/.hankweave-executions/1788337915373-x194-4a876b/.hankweave/runs/1788337916070-aqhfn", "gitBranch": "run-1788337916070-aqhfn", "startingConditions": { "type": "fresh" }, "codons": [ { "codonId": "summarize-notes", "startTime": "2026-09-02T08:31:56.104Z", "status": "completed", "claudePid": 952404, "claudeLogPath": ".hankweave/runs/1788337916070-aqhfn/summarize-notes-claude.log", "claudeSessionId": "83b52eaa-5147-4130-aef4-818c9c663c77", "currentCost": 0.01050655, "currentTokens": { "inputTokens": 25, "outputTokens": 654, "cacheCreationTokens": 683, "cacheReadTokens": 56488 }, "assistantMessageCount": 3, "extensionCount": 0, "endTime": "2026-09-02T08:32:08.079Z", "exitCode": 0, "finalCost": 0.01050655, "finalTokens": { "inputTokens": 25, "outputTokens": 654, "cacheCreationTokens": 683, "cacheReadTokens": 56488 }, "resultMessageReceived": true, "completionCheckpoint": "4ae2cf9d1a341fe1108487ddc2cea91ae4dd71ef" } ], "status": "completed", "startTime": "2026-09-02T08:31:56.071Z", "serverPid": 52624, "endTime": "2026-09-02T08:32:10.187Z" } ], "currentRunId": null, "executionPlan": [ { "codon": { "type": "codon", "id": "summarize-notes", "name": "Summarize the notes", "promptFile": "/fixtures/minimal-single-provider/prompts/summarize.md", "model": { "providerId": "anthropic", "modelId": "claude-haiku-4-5", "name": "Claude Haiku 4.5 (latest)", "attachment": true, "reasoning": true, "tool_call": true, "temperature": true, "cost": { "input": 1, "output": 5, "cache_read": 0.1, "cache_write": 1.25 }, "limit": { "context": 200000, "output": 64000 }, "modalities": { "input": [ "text", "image", "pdf" ], "output": [ "text" ] }, "knowledge": "2025-02-28", "release_date": "2025-10-15", "last_updated": "2025-10-15" }, "continuationMode": "fresh", "checkpointedFiles": [ "summary.md" ], "outputFiles": [ { "copy": [ "summary.md" ] } ], "maxExtensions": 100 }, "codonId": "summarize-notes" } ] } ``` | id | category | journaled | sentinelRouted | payloadFields | receipts | | -------------- | ------------ | --------- | -------------- | ------------------------------------------------------------------------------------------------ | -------------------------------------------------------------------------------------------------------------------- | | state.snapshot | server-state | true | true | currentCodon?, completedCodons, fileTree, totalCost, totalTime, recentFileAccess?, isRollingBack | schemas/event-schemas.ts:550, schemas/event-schemas.ts:963, schemas/event-schemas.ts:1233, hankweave-runtime.ts:1422 | The table row appended to the capture is an event-catalog cross-reference. Its payload fields belong to [Events](/0.10.0/files/reference/events), not to the `state.json` roots described in the next section. ## Where state.json lives and how it's written `state.json` lives at `/.hankweave/state.json`. It is plain JSON written with two-space indentation, so any JSON parser can read it without a special library. The runtime joins `executionPath` with `.hankweave` and writes the file there. For debugging, follow `state.json` to the recorded `claudeLogPath`, then to the [journal](/0.10.0/files/integrate/event-journal) and [checkpoint](/0.10.0/files/concepts/checkpoints). Read the recorded log path; its base and naming rules belong to [Observe and debug](/0.10.0/files/operate/observe-and-debug). The file intentionally has no version field and no denormalized cost totals: costs are computed from runs when needed. The related directory layout is documented in [Execution directory](/0.10.0/files/reference/execution-directory). ### Save the complete state atomically State changes enter a fire-and-forget queue processed serially: validate the transition, apply it, save the complete state, then emit `stateChanged`. Each save copies `state.json` to `state.json.bak`, writes the complete new state to `state.json.tmp`, and renames the temporary file to `state.json`, retrying the rename. The backup is made before every write, so `.bak` is the last successfully saved state if the replacement is interrupted. A leftover `state.json.tmp` means the rename did not run. Startup tries `state.json`, then `state.json.bak`, and cleans up neither file. If both fail, recovery starts fresh in memory and logs `Backup also corrupted, starting fresh`; nothing reaches disk until the next save. See [Resume, rollback, and retry](/0.10.0/files/operate/resume-rollback-and-retry) for the recovery workflow. ### Use transitions to change state The StateManager's transitions are the only supported way to modify state. Readers may read `state.json`; writers request transitions through Hankweave CLI or WebSocket commands. Do not edit the file directly. The command catalog belongs to [Protocol](/0.10.0/files/integrate/protocol). ## Read the top-level fields With the file's mechanics established, the rest of the page walks its contents from the outside in. `HankweaveState` has four roots: `runs: Run[]`, `currentRunId: RunId | null`, optional `initialCheckpoint: string`, and required `executionPlan: ExecutionCodonEntry[]`. The source slice below defines all four, plus the `Run` and `StartingConditions` shapes that the next section uses. ```ts export interface Run { /** * Unique identifier for this run. * Also used as git branch name. * * Used by: State lookups, folder naming, git branches */ runId: RunId; /** * Absolute path where run files are stored. * Example: "/project/.hankweave/runs/1234-abc" * * Used by: Log file storage, cleanup operations * Edge case: Folder might not exist if run failed early */ runFolder: string; /** * Git branch name for this run. * Usually same as runId, but explicit for flexibility. * * Used by: Checkpoint system */ gitBranch: string; /** * How this run started - fresh or continuation. * Immutable after run creation. * * Used by: UI to show run relationships, rollback tracking */ startingConditions: StartingConditions; /** * Ordered list of codon executions in this run. * Append-only - new codons added as they start. * * Used by: Progress tracking, cost calculation * Invariant: Only one codon can be non-terminal at a time */ codons: CodonExecution[]; /** * Overall run status. * - running: Currently executing * - completed: All codons done successfully * - failed: Stopped due to codon failure * - crashed: Detected on recovery * * Used by: Run selection, cleanup decisions */ status: "running" | "completed" | "failed" | "crashed"; /** * When server started. Never changes. */ startTime: string; /** * When server stopped. Set when status becomes terminal. */ endTime?: string; /** * Server process ID for lock file validation. * * Used by: Detecting stale lock files, crash recovery * Edge case: Process might not exist anymore */ serverPid: number; } /** * How a run started - fresh project or continuation. */ export type StartingConditions = | { type: "fresh"; initialCheckpointSha?: string; // SHA of the initial checkpoint commit } | { type: "continuation"; source: { /** * Which run we're continuing from. * * Used by: Building run relationships tree */ runId: RunId; /** * Which codon to continue after. * null means start from beginning of that run. * * Example: "codon-2" means start from codon-3 * Used by: Determining next codon to execute */ afterCodon: CodonId | null; /** * Git commit SHA we restored to. * This is the exact state we're continuing from. * * Used by: Verifying correct restoration */ checkpointSha: string; }; /** * Human-readable reason for continuation. * Optional metadata for UI/analytics. * * Used by: Understanding user patterns */ reason?: "retry" | "rollback" | "continue"; }; // ------------- // Top-Level State // ------------- /** * Root state object for Hankweave. * Stored in .hankweave/state.json. * * Design decisions: * - Single file instead of per-run for simplicity * - No version field per user request * - No denormalized costs - computed when needed */ export interface HankweaveState { /** * All runs, newest first. * Append-only - runs are never removed from history. * * Used by: History UI, cost calculations, rollback sources * Scaling: May need pagination/archival eventually */ runs: Run[]; /** * Currently active run ID. * null when server not running. * * Used by: State queries, preventing multiple servers * Invariant: Only one run can be "running" status */ currentRunId: RunId | null; /** * Initial checkpoint SHA from git repository initialization. * This is the empty commit created when the checkpoint system starts. * Represents the project's clean state before any codons have executed. * * Used by: Rollback to clean state, project-level rollback commands */ initialCheckpoint?: string; /** * Current execution plan with all loop iterations expanded. * This is the flattened plan that represents the actual execution sequence. * Rebuilt on server start but persisted for crash recovery and debugging. * * Note: May be empty array during initialization before first run starts, * but the field itself is always present. * * Used by: Codon execution, execution thread analysis, crash recovery */ executionPlan: import("../execution-planner.js").ExecutionCodonEntry[]; ``` `runs` is newest first, append-only, and never removes history. `currentRunId` is the persisted current-run pointer: `RunStarted` sets it, and `RunCompleted`/`RunFailed` clear it. It is not a live-process indicator; after an unclean process exit, the file can still contain a non-null `currentRunId` and a `running` status until recovery records the crash. `executionPlan` may be `[]` before the first run. Load-time structure validation requires `runs` to be an array, `currentRunId` to be `null` or a string, and `executionPlan` to be an array. `initialCheckpoint` is the empty commit created when the checkpoint repository initializes. It represents the project's clean state before any codon and is set by the `InitialCheckpointSet` transition. On load, `google` and `openai` entries remain unchanged. `opencode` entries are re-validated when they use a bare id and a registry is available; otherwise they fall through to the pi reverse-map. The retired `pi` pseudo-provider encoding is split on the first slash in `modelId` to recover the real provider and model, while a bare id gets an inferred provider. These rewrites set no `harnessOverride`, so a legacy plan that forced an Anthropic model onto Pi can resume on the Claude Agent SDK; the runtime logs each codon id and old/new provider-model pair. Continuation runs reuse the persisted plan without re-validation, so this normalization occurs while state is loaded. Before the first run, the history and plan are empty, the current-run pointer is `null`, and the optional initial checkpoint may identify the clean repository state. That minimal file looks like this: ```jsonc { "runs": [], "currentRunId": null, "initialCheckpoint": "abc123...", "executionPlan": [] } ``` See [Checkpoints](/0.10.0/files/concepts/checkpoints) and [Model resolution](/0.10.0/files/reference/model-resolution) for the related operations. ## Follow run entries Each entry in `runs` records one server lifecycle and its relationship to the codon attempts inside it. The `Run` and `StartingConditions` shapes appear in the source slice under [Read the top-level fields](#read-the-top-level-fields). A run contains `runId`, `runFolder`, `gitBranch`, `startingConditions`, `codons`, `status`, `startTime`, optional `endTime`, and `serverPid`. `runId` identifies the run. In 0.10.0, the runtime stores the checkpoint branch as `gitBranch: run-`; read that explicit field rather than deriving a branch name. The captured run pairs `runId` `1788337916070-aqhfn` with `gitBranch` `run-1788337916070-aqhfn`. `runFolder` is an absolute path whose folder name is the run id. `codons` is append-only, with at most one non-terminal codon at a time. `status` is `running`, `completed`, `failed`, or `crashed`; `RunCompleted` and `RunFailed` set `endTime` and clear `currentRunId` to `null`. `StartingConditions` is either `{ type: "fresh", initialCheckpointSha?: string }` or `{ type: "continuation", source: { runId, afterCodon, checkpointSha }, reason?: "retry" | "rollback" | "continue" }`. A `null` `afterCodon` means continuation from the beginning. Otherwise, the referenced codon must have status `completed`; the restore point is that codon's `completionCheckpoint`, or the first codon's `rigSetupCheckpoint` when `afterCodon` is `null`. Two mechanisms turn a stale `running` record into an honest one. On startup, a `running` run whose `runId` is not the current one has its `serverPid` probed with `kill(pid, 0)`. A dead PID changes the run to `crashed` and its non-terminal codon to `failed`, with exit code `-1` and failure reason `{ type: "unknown", retriable: false, message: "Server crashed" }`. Read that structured reason with the exit code: `-1` alone does not identify this crash case. The runtime lock also guards a stale current run. Lock acquisition reads `.hankweave/runtime.lock`; if its PID is dead or its heartbeat is stale beyond two minutes, the runtime unlinks the lock and emits `RunCrashed` for `lockInfo.runId`. This can reclaim a run still named by persisted `currentRunId`; `detectCrashedRuns` checks only runs whose id differs from `currentRunId`. A continuation record ties these fields together. The pseudodata below identifies the source run, the completed codon after which continuation begins, the checkpoint to restore, and the reason. ```jsonc { "type": "continuation", "source": { "runId": "run-previous", "afterCodon": "normalize", "checkpointSha": "def456..." }, "reason": "retry" } ``` See [How a run works](/0.10.0/files/start/how-a-run-works) and [Resume, rollback, and retry](/0.10.0/files/operate/resume-rollback-and-retry). ## Codon execution entries Inside each run, the `codons` array holds one record per codon attempt. A codon execution record is a status-discriminated union: its `status` selects the shape, fields accumulate during progress, and terminal records become immutable. The eight statuses are `preparing`, `starting`, `initializing`, `running`, `completing-sentinels`, `completed`, `failed`, and `skipped`. The source slice below defines each variant's fields, followed by the enforced transition table. ```ts /** * Codon execution status progression. * * Normal flow: preparing → starting → initializing → running → completing-sentinels → completed * Can skip to "failed" or "skipped" from any non-terminal state. * * Intent: Track granular progress for better crash recovery and user feedback. */ export type CodonStatus = | "preparing" // Rig setup running (copy files, run commands) | "starting" // Spawning Claude process | "initializing" // Process started, waiting for session ID | "running" // Claude is working (have session ID) | "completing-sentinels" // Completing sentinel work (draining queues) | "completed" // Success - terminal state | "failed" // Failed - terminal state | "skipped"; // User skipped - terminal state /** * Base properties shared by all codon states. * These are set when the codon starts and never change. */ interface BaseCodon { /** * Which codon configuration this execution is for. * References the codon in hank.json. * * Used by: UI to show codon name, state queries for codon history */ codonId: CodonId; /** * When this codon execution started. * ISO 8601 timestamp. * * Used by: Duration calculations, UI timeline display */ startTime: string; /** * Loop context if this codon is part of a loop iteration. * Used for resuming loops after interruption and rollback. */ loopContext?: { loopId: CodonId; // ID of the loop this codon belongs to iteration: number; // Which iteration (0-indexed: 0 = first, 1 = second, etc.) codonIndexInLoop: number; // Position within loop.codons array }; } /** * Codon is preparing rig (running rig setup operations). * * Next states: * - starting: Rig setup succeeded * - failed: Copy failed, command failed, etc. * - skipped: User skipped during prep */ export interface PreparingCodon extends BaseCodon { status: "preparing"; // No Claude info yet - process not started // No costs yet - Claude not running } /** * Spawning Claude process. * * Next states: * - initializing: Process started successfully * - failed: Spawn failed (Claude not found, etc.) * - skipped: User skipped during startup */ export interface StartingCodon extends BaseCodon { status: "starting"; /** * Git commit SHA after rig setup completed. * Only set if codon config has rigSetup operations. * * Used by: Rollback to know exact state after setup * Edge case: May be undefined if no rig setup configured */ rigSetupCheckpoint?: string; /** * Sentinels loaded for this codon. * Set after sentinels load during starting state. */ sentinels?: { loaded: SentinelState[]; totalCost: number; }; } /** * Claude process running but no session ID yet. * Waiting for init message from Claude. * * Next states: * - running: Got session ID from init message * - completed: Process exited cleanly but init message failed validation * - failed: Process crashed before init * - skipped: User skipped during init */ export interface InitializingCodon extends BaseCodon { status: "initializing"; rigSetupCheckpoint?: string; /** * Claude process ID for monitoring/cleanup. * * Used by: Process manager to kill on skip/shutdown * Edge case: Process might already be dead */ claudePid: number; /** * Path to Claude's JSONL log file. * Relative to .hankweave directory. * Example: "runs/1234-abc/codon-research-claude.log" * * Used by: Log parser, debugging, cleanup */ claudeLogPath: string; /** * Session ID from previous codon if continuing. * Only set if codon has continueFromPrevious: true. * * Used by: Claude CLI --resume flag */ previousSessionId?: SessionId; /** * Sentinels loaded for this codon. * Optional field added during starting state, carried forward to initializing. */ sentinels?: { loaded: SentinelState[]; totalCost: number; }; } /** * Claude is actively working. * This is where most time is spent. * * Next states: * - completed: Claude process exited cleanly * - failed: Timeout, API error, crash * - skipped: User skipped */ export interface RunningCodon extends BaseCodon { status: "running"; rigSetupCheckpoint?: string; claudePid: number; /** * Claude's session UUID from init message. * Required for continuation in later codons. * * Used by: Continue functionality, logs correlation */ claudeSessionId: SessionId; claudeLogPath: string; previousSessionId?: SessionId; /** * Accumulated cost so far in USD. * Updated on each token usage message. * * Used by: Cost display, cost limits (future) * Edge case: May be stale if messages delayed */ currentCost: number; /** * Accumulated token counts. * Updated on each assistant message with usage. * * Used by: Token display, rate limit tracking */ currentTokens: TokenUsage; /** * Number of assistant messages received. * Used to determine if Claude has established a conversation. * Initialized to 0 when codon enters running state. * * Used by: Continue functionality to check if session is valid */ assistantMessageCount: number; /** * Number of extensions performed so far. * 0 initially, increments with each extension. * Used for tracking progress and enforcing maxExtensions. */ extensionCount: number; /** * Sentinels loaded for this codon. * Updated in-place during execution. */ sentinels?: { loaded: SentinelState[]; totalCost: number; }; } /** * Codon is completing sentinel work. * Transient state between agent completion and final state. * * This state indicates: * - Main Claude agent has finished (process exited) * - Sentinel queues are being drained * - All pending LLM calls are completing * - Output files are being finalized * * Next states: * - completed: All work done successfully * - failed: Checkpoint creation failed * - skipped: Should not normally happen from this state */ export interface CompletingSentinelsCodon extends BaseCodon { status: "completing-sentinels"; rigSetupCheckpoint?: string; claudePid: number; claudeSessionId: SessionId; claudeLogPath: string; previousSessionId?: SessionId; /** * Current cost accumulated while Claude was running. * Will become finalCost when transitioning to completed. */ currentCost: number; /** * Current token counts. * Will become finalTokens when transitioning to completed. */ currentTokens: TokenUsage; /** * Number of assistant messages received. */ assistantMessageCount: number; /** * Extension count from the running state. * Preserved during sentinel completion. */ extensionCount: number; /** * Sentinels being completed. * States are updated in-place as work completes. */ sentinels?: { loaded: SentinelState[]; totalCost: number; }; } // ------------- // Terminal States - Immutable once reached // ------------- /** * Codon completed successfully. * This is a terminal state - no further transitions possible. * * Immutability: All fields are final. To retry, start a new run. */ export interface CompletedCodon extends BaseCodon { status: "completed"; /** * When codon completed. Used for duration calculation. */ endTime: string; // Claude integration details claudeSessionId: SessionId; claudeLogPath: string; previousSessionId?: SessionId; /** * Always 0 for successful completion. * * Used by: Success detection */ exitCode: 0; /** * Final cost from result message or last token update. * This is the authoritative cost for this codon. * * Used by: Billing, cost reports * Edge case: May be from token updates if result message timed out */ finalCost: number; /** * Final token counts. * * Used by: Usage analytics, model comparison */ finalTokens: TokenUsage; /** * Whether we got Claude's result message before timeout. * False means costs might be slightly off. * * Used by: Cost accuracy warnings */ resultMessageReceived: boolean; /** * Final count of extensions performed. * Used for reporting and debugging. */ extensionCount: number; // Checkpoints rigSetupCheckpoint?: string; /** * Git commit after successful completion. * Always created for successful codons. * * Used by: Rollback target points */ completionCheckpoint: string; /** * Sentinels that executed during this codon (final state). * Field renamed from 'loaded' to 'executed' when codon completes. */ sentinels?: { executed: SentinelState[]; totalCost: number; }; /** Present when codon was force-completed due to budget limit. */ budgetExceeded?: BudgetExceededData; } /** * Codon failed with error. * Terminal state - must start new run to retry. */ export interface FailedCodon extends BaseCodon { status: "failed"; endTime: string; /** * Which state we were in when failure occurred. * Helps understand how far we got. * * Used by: Error analysis, retry strategies * Example: "preparing" means rig setup failed * Note: Can include "completing-sentinels" if checkpoint creation fails during that codon */ failedDuring: "preparing" | "starting" | "initializing" | "running" | "completing-sentinels"; // Claude info - only set if we got that far claudePid?: number; claudeSessionId?: SessionId; claudeLogPath?: string; previousSessionId?: SessionId; /** * Process exit code. 0 means clean exit (shouldn't happen for failed). * Common codes: * - 1: General error * - -1: Killed by signal * - 130: Ctrl+C * * Used by: Debugging, retry decisions */ exitCode: number; /** * Structured failure information. * * Used by: UI error display, retry logic */ failureReason: FailureReason; /** * Costs accumulated before failure. * Will be 0 if failed before Claude started. * * Used by: Partial cost tracking */ partialCost: number; partialTokens: TokenUsage; /** * Number of extensions performed before failure. * Only present if codon reached running state and attempted extensions. */ extensionCount?: number; // Checkpoints rigSetupCheckpoint?: string; /** * Error checkpoint if created. * On error branch in git. * * Edge case: Might not exist if git operations failed */ errorCheckpoint?: string; /** * Sentinels that executed before failure. */ sentinels?: { executed: SentinelState[]; totalCost: number; }; } /** * Codon was skipped by user. * Terminal state - represents user choice to skip. */ export interface SkippedCodon extends BaseCodon { status: "skipped"; endTime: string; /** * Which state we were in when skipped. * * Used by: Understanding skip patterns */ skippedDuring: "preparing" | "starting" | "initializing" | "running"; // Claude info - only set if we got that far claudePid?: number; claudeSessionId?: SessionId; claudeLogPath?: string; previousSessionId?: SessionId; /** * Partial cost accumulated before skip. * Usually 0, but may have accumulated costs if skipped while running. * * Used by: Cost calculations, continuation logic */ partialCost: number; /** * All zeros - no tokens used for skipped. */ partialTokens: TokenUsage; /** * Number of assistant messages received before skip. * Used to determine if session can be continued. * * Used by: Continue functionality */ assistantMessageCount?: number; // Checkpoints rigSetupCheckpoint?: string; /** * Skip checkpoint if any files were being tracked. * Even empty commits are created for skip markers. * * Used by: Skip history in git */ skipCheckpoint?: string; /** * Sentinels that executed before skip. */ sentinels?: { executed: SentinelState[]; totalCost: number; }; } /** * Union of all possible codon states. * Use discriminated union on `status` field for type narrowing. */ export type CodonExecution = | PreparingCodon | StartingCodon | InitializingCodon | RunningCodon | CompletingSentinelsCodon | CompletedCodon | FailedCodon | SkippedCodon; ``` ```ts export const CodonTransitions: Record = { preparing: ["starting", "failed", "skipped"], starting: ["initializing", "failed", "skipped"], initializing: ["running", "completed", "failed", "skipped"], running: ["completing-sentinels", "completed", "failed", "skipped"], "completing-sentinels": ["completed", "failed", "skipped"], completed: [], // Terminal - no transitions failed: [], // Terminal - no transitions skipped: [], // Terminal - no transitions }; ``` Every record has `codonId`, an ISO 8601 `startTime`, and optional `loopContext` with `loopId`, zero-based `iteration`, and `codonIndexInLoop`. The transition table is enforced on every write; an illegal transition raises `InvalidTransitionError`. A rig – the sandboxed workspace preparation before the agent process starts – executes shell commands and file copies and can produce `rigSetupCheckpoint`. * `preparing` is minimal. `starting` adds optional `rigSetupCheckpoint` and `sentinels`. A sentinel is a per-codon observer whose state is recorded alongside the codon. * `initializing` adds `claudePid`, `claudeLogPath`, and optional `previousSessionId`. * `running` adds `claudeSessionId`, `currentCost`, `currentTokens`, `assistantMessageCount`, and `extensionCount`, which is zeroed on entry. `completing-sentinels` has the same structure as `running`. * Cost and token updates mutate a codon only while its status is `running`. * Terminal records are immutable. `completed` carries `endTime`, literal `exitCode: 0`, authoritative `finalCost` and `finalTokens`, `resultMessageReceived`, an always-created `completionCheckpoint`, `extensionCount`, and optional `budgetExceeded`. * `failed` carries `failedDuring`, an exit code, stored `failureReason`, `partialCost`, `partialTokens`, optional `errorCheckpoint`, and optional `extensionCount`. `preparing` points to a rig problem, `initializing` to process startup, and `running` to an agent or API problem. Failure can occur during `completing-sentinels` if checkpoint creation fails there. * `skipped` carries `skippedDuring` as `preparing | starting | initializing | running`, `partialCost`, and `partialTokens` copied from accumulated `currentTokens`; `assistantMessageCount` and `skipCheckpoint` are optional. The interface comment claiming skipped tokens are always zero is stale; the reducer copies accumulated `currentTokens`. `completing-sentinels` is not a legal `skippedDuring` value, although the transition map lists a skip edge from that status because the target type has no corresponding `skippedDuring` value. * A retry appends a new record for the same `codonId`; the failed record remains terminal history. The last record for that id is the current attempt. * Non-terminal records store sentinels as `{ loaded: SentinelState[], totalCost }`. Every terminal record renames `loaded` to `executed`. `failureReason` stores `type`, `retriable`, and optional `message`, `sentinelRefs`, and `retryAfterMs`. The stored shape: ```ts // Failure reason schema const failureReasonSchema = z.object({ type: z.enum(["timeout", "rate-limit", "api-error", "sentinel-load-failure", "unknown"]), retriable: z.boolean(), message: z.string().optional(), sentinelRefs: z.array(z.string()).optional(), // Which sentinels failed (for sentinel-load-failure) /** * How long the provider asked us to wait before retrying, in milliseconds, * when its error carried an explicit hint (a Retry-After header value, a * "retry after 30s" phrasing, etc). Parsed by classifyApiErrorText and * consumed by computeRetryDelayMs, which prefers it over computed backoff — * the provider knows its own limit window better than we can guess. */ retryAfterMs: z.number().optional(), ``` The failure classification taxonomy belongs to [Errors and exit codes](/0.10.0/files/reference/errors-and-exit-codes), not this page. `claudeLogPath` is recorded in `state.json`; its base and naming belong to [Observe and debug](/0.10.0/files/operate/observe-and-debug), so a reader never infers a log filename. The record below is illustrative pseudodata for a codon mid-flight. It pairs live agent identifiers and usage counters with a `loaded` sentinel collection; terminal records preserve final totals and rename that collection. ```jsonc { "codonId": "normalize", "startTime": "2026-01-01T12:00:00.000Z", "status": "running", "claudePid": 12345, "claudeLogPath": "runs/run-1/normalize.log", "claudeSessionId": "session-1", "currentCost": 0.0234, "currentTokens": { "inputTokens": 1500, "outputTokens": 800, "cacheCreationTokens": 0, "cacheReadTokens": 500 }, "assistantMessageCount": 5, "extensionCount": 0, "sentinels": { "loaded": [], "totalCost": 0 } } ``` See [Codons](/0.10.0/files/concepts/codons), [State machine](/0.10.0/files/contribute/state-machine), [Errors and exit codes](/0.10.0/files/reference/errors-and-exit-codes), and [Observe and debug](/0.10.0/files/operate/observe-and-debug). ## Execution plan entries Alongside the run history, `executionPlan` records what the runtime intends to execute. An execution-plan entry identifies the inner codon configuration and its runtime position in a loop. It has `{ codon, codonId, loopContext? }`; `codon` is always the inner codon configuration, never a loop wrapper. Loop iterations receive runtime ids such as `review#0` and `review#1`. ```ts export interface ExecutionCodonEntry { // The actual codon config to execute (always Codon, never Loop) codon: Codon; // Runtime-generated ID (e.g., "review#0", "review#1" for loop iterations) codonId: CodonId; // If from a loop, track context for resume/rollback loopContext?: { loopId: CodonId; // ID of the loop this codon belongs to iteration: number; // Which iteration (0-indexed: 0 = first, 1 = second, etc.) codonIndexInLoop: number; // Position within loop.codons array // Loop's own budget config (if declared), carried for Budget class to use loopBudget?: { maxDollars?: number; maxTimeSeconds?: number; allocation?: AllocationMode; shares?: Record; onExceeded?: "complete" | "fail"; }; }; } ``` Loops expand lazily: the initial plan contains only iteration 0 for each loop. After a codon completes, the next iteration is appended and the runtime decides whether the loop terminates. `loopContext.loopBudget?` carries `{ maxDollars?, maxTimeSeconds?, allocation?, shares?, onExceeded? }`; the budget release marker is `0.6.1`. The plan is rebuilt for a fresh, non-continuation run. It is persisted for crash recovery and debugging. A continuation run reuses the persisted plan verbatim. The entry below shows the flattened shape: it keeps the inner codon configuration, assigns `review#0` as its runtime id, and records its first loop iteration and position. ```jsonc { "codon": { "id": "review", "name": "Review" }, "codonId": "review#0", "loopContext": { "loopId": "review-loop", "iteration": 0, "codonIndexInLoop": 0 } } ``` > **VersionNote:** Resuming a pre-0.10.0 execution normalizes retired `pi` and `opencode` plan encodings back to real provider ids. Because legacy entries do not preserve a harness override, an Anthropic model that was forced onto Pi can resume on the Claude Agent SDK (the harness that runs the codon). See [Loops](/0.10.0/files/concepts/loops), [Model resolution](/0.10.0/files/reference/model-resolution), and [Budgets](/0.10.0/files/concepts/budgets). ## Record checkpoint fields Checkpoint fields record the git commit SHA associated with a codon-state milestone. The codon record can contain these fields: | Field | Type | Stored when | | ---------------------- | --------- | ----------------------------------------------------------------------------- | | `rigSetupCheckpoint` | `string?` | After rig operations; back-patched while the codon is in the matching status. | | `completionCheckpoint` | `string` | On `completed`; always created and back-patched by `CheckpointCreated`. | | `errorCheckpoint` | `string?` | On `failed`; may be absent if git operations failed. | | `skipCheckpoint` | `string?` | On `skipped`; back-patched by `CheckpointCreated`. | `CheckpointCreated` back-patches a matching field only while the codon has the matching status. This page inventories the fields. The checkpoint repository, rollback mechanics, and corrected `GIT_DIR` recipes belong to [Checkpoints](/0.10.0/files/concepts/checkpoints) and [Resume, rollback, and retry](/0.10.0/files/operate/resume-rollback-and-retry). A completed codon that ran rig setup carries both SHAs: ```jsonc { "status": "completed", "rigSetupCheckpoint": "abc123...", "completionCheckpoint": "def456..." } ``` See [Checkpoints](/0.10.0/files/concepts/checkpoints). ## Track sentinel state `SentinelState` records one sentinel's model, activity counts, cost, and unload state. ```ts export interface SentinelState { id: string; model: string; loadedAt: string; unloadedAt?: string; llmCallCount: number; failedLLMCalls: number; lastLlmCallAt?: string; totalTriggers: number; totalCost: number; status: "active" | "unloaded"; unloadReason?: "codon-complete" | "fatal-error" | "consecutive-failures"; } // ------------- // Codon Execution States - Discriminated Union // ------------- ``` A non-terminal codon stores its sentinel collection as `loaded`; every terminal state stores it as `executed`. Sentinel cost is stored in `sentinels.totalCost`, separately from codon cost. The runtime's `getCodonCost` rollup excludes sentinel cost. The pseudodata below uses a model shortcut and records activity counts and cost for an active sentinel. ```jsonc { "sentinels": { "loaded": [ { "id": "quality", "model": "sonnet", "loadedAt": "2026-01-01T12:00:05.000Z", "llmCallCount": 2, "failedLLMCalls": 0, "totalTriggers": 3, "totalCost": 0.0012, "status": "active" } ], "totalCost": 0.0012 } } ``` See [Sentinels](/0.10.0/files/concepts/sentinels) and [Sentinel configuration](/0.10.0/files/reference/sentinel-config). ## Interpret budget fields State records a budget-limit outcome, not the run-time budget limits themselves. On a completed codon, `budgetExceeded?` has `{ currency, limit, used }` and is present when the codon is force-completed at a budget limit. Budget fields are marked since `0.6.1`. Budget limits resolve from configuration at run time and are not stored in `state.json`. State persists the completed-codon marker and the loop budget configuration carried by plan entries. Budget semantics, currencies, allocation, and the `budget.summary` event belong to [Budgets](/0.10.0/files/concepts/budgets) and [Events](/0.10.0/files/reference/events). A codon force-completed at a one-dollar cost limit stores the marker like this: ```jsonc { "status": "completed", "exitCode": 0, "budgetExceeded": { "currency": "cost", "limit": 1.00, "used": 1.02 } } ``` See [Budgets](/0.10.0/files/concepts/budgets) and [Events](/0.10.0/files/reference/events). ## Validation and recovery Load-time validation reports the emitted errors and warning, then recovery tries the primary file, its backup, and a fresh in-memory state. The shipped validator emits: | Severity | Type | Meaning | | -------- | ----------------- | -------------------------------------------------------------- | | error | `corrupted_data` | Invalid structure; loading throws and follows the backup path. | | error | `missing_run` | `currentRunId` is not present in `runs`. | | warning | `orphaned_folder` | A run folder exists on disk without a state entry. | The type unions also name `invalid_codon`, `missing_checkpoint`, and `cost_mismatch`, but the shipped validator emits none of them. They are reserved names in declarations, not live diagnostics. Recovery uses this ladder: 1. Try `state.json`. 2. If it fails, try `state.json.bak`. 3. If both fail, start fresh and log `Backup also corrupted, starting fresh`. During load-time recovery, the fresh state remains in memory; nothing is written to disk until the next save. The explicit last-resort `recover()` API instead writes the fresh empty state immediately and returns the result below – illustrative pseudodata that makes the fresh-recovery data loss explicit. ```jsonc { "success": true, "method": "fresh", "dataLoss": true, "message": "Started with fresh state" } ``` See [Errors and exit codes](/0.10.0/files/reference/errors-and-exit-codes) and [Observe and debug](/0.10.0/files/operate/observe-and-debug). ## Reading state safely Resolve runs and codon attempts by their identifiers and status, never by an assumed array position. Tool builders can read the plain JSON directly or typecheck against the published `hankweave/types` export, which includes `HankweaveState`, `Run`, all eight codon variants, `SentinelState`, `StartingConditions`, `getCodonCost`, `getCodonTokens`, and `isTerminalCodonStatus`. The order-safe rules are: * Resolve the current run with `runId === currentRunId`, not by array position. * Treat `runs[0]` as the newest run. * For a codon id, take the last record, because retries append records and retain failed attempts. * The currently running codon is the single record whose status is not `completed`, `failed`, or `skipped`. > **Pitfall:** A first-match read can return a failed attempt's stale terminal record after a retry succeeds. Match the run by id and take the last record for the codon id. The commands below put those rules into practice: current-run selection, latest-attempt selection, failed-codon triage, and the last successful checkpoint, all without first-match codon lookup. ```sh jq '. as $state | .runs[] | select(.runId == $state.currentRunId)' /.hankweave/state.json jq '[.runs[0].codons[] | select(.codonId == "normalize")] | last' /.hankweave/state.json jq '. as $state | .runs[] | select(.runId == $state.currentRunId) | .codons[] | select(.status == "failed") | {codonId, failedDuring, failureReason, exitCode}' /.hankweave/state.json jq '[.runs[0].codons[] | select(.status == "completed")] | last | .completionCheckpoint' /.hankweave/state.json ``` The first filter binds the root state before comparing `runId` with `currentRunId`. The second takes the latest `normalize` attempt. The third selects failed codons for that current run. The fourth returns the last successful checkpoint in the newest run. **Cost aggregation.** Runtime-faithful codon cost is `finalCost` for `completed`, `partialCost` for `failed`, `currentCost` for `running` and `completing-sentinels`, and zero for `skipped` and never-started codons. A skipped record may still store `partialCost`; the rollup counts that status as zero. Sentinel costs are summed separately. Token totals use `partialTokens` for failed and skipped records and the `TokenUsage` shape `{ inputTokens, outputTokens, cacheCreationTokens, cacheReadTokens }`. This is the runtime's persisted-state view, not a provider invoice; it does not add `token.usage` event totals or require `modelUsage`. See [Client and exported types](/0.10.0/files/integrate/client-and-exported-types), [Observe and debug](/0.10.0/files/operate/observe-and-debug), [Events](/0.10.0/files/reference/events), and [Execution directory](/0.10.0/files/reference/execution-directory).