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 debugging a failed run, building tooling on top of persisted state, or verifying 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.

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 (against hankweave@0.10.0 ). Its completed codon retains both currentCost and finalCost; use finalCost for the completed-codon view. Since-markers come from lock history; extensionCount carries no marker. For related concepts, see Codons, Checkpoints, Loops, Sentinels, Budgets, Events, and 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": "<workspace>/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"
    }
  ]
}
Scroll to explore the table →
idcategoryjournaledsentinelRoutedpayloadFieldsreceipts
state.snapshotserver-statetruetruecurrentCodon?, completedCodons, fileTree, totalCost, totalTime, recentFileAccess?, isRollingBackschemas/event-schemas.ts:662, schemas/event-schemas.ts:1077, schemas/event-schemas.ts:1347, hankweave-runtime.ts:1676

The table row appended to the capture is an event-catalog cross-reference. Its payload fields belong to 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 <execution>/.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 and checkpoint. Read the recorded log path; its base and naming rules belong to 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.

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 cleans up neither file. Startup tries state.json, then state.json.bak. 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 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.

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, declared-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 a persisted current-run pointer: RunStarted sets it, RunCompleted and RunFailed clear it to null, and RunCrashed leaves it set. It is not a live-process indicator: after a hard kill the file can still hold a non-null currentRunId with a running status and no server, and after a run completes a live server sits at null. 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 declared on the root state, and the state manager would set it from an InitialCheckpointSet transition, but no code emits that transition at this release. The field is never written, and a real state.json does not contain it; the checkpoint repository's initial empty commit is found with Git, not in state.json. The same holds for initialCheckpointSha in the fresh StartingConditions variant: declared, never assigned.

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. Only that retired pi branch leaves harnessOverride unset, 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 and the current-run pointer is null. The declared initialCheckpoint field is not written, so it does not appear in a real file:

JSONC
{
  "runs": [],
  "currentRunId": null,
  "executionPlan": []
}

See Checkpoints and 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. A run contains runId, runFolder, gitBranch, startingConditions, codons, status, startTime, optional endTime, and serverPid.

runId identifies the run. The runtime stores the checkpoint branch as gitBranch: run-<runId>; 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" }. These are the run tree's parent pointers. A null afterCodon means continuation from the beginning of the source run. A rollback continuation records the checkpoint the rollback selected: afterCodon is the target codon for any non-rig-setup checkpoint – including a failed codon's error checkpoint or a skipped codon's skipped checkpoint – and checkpointSha is that selected checkpoint. A rig-setup rollback writes afterCodon: null, which need not be the first codon.

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 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 and 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, and fields accumulate during progress. 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<CodonStatus, CodonStatus[]> = {
  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 workspace preparation (file copies and shell commands) executed before the agent process starts – can produce rigSetupCheckpoint. Copy operations are constrained at load (the source must exist and must not be the hank directory itself), but a command operation's run string is an ordinary shell invocation in its working directory and is not sandboxed by those copy-path validators.

  • 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.
  • A terminal record's status is final, but the record is not fully immutable: CheckpointCreated still sets completionCheckpoint, errorCheckpoint, or skipCheckpoint on a terminal record, and AssistantMessageCountUpdated updates a skipped record's assistantMessageCount. Both find the record by the first match on codonId, so after a retry they can patch the earlier attempt. completed carries endTime, literal exitCode: 0, authoritative finalCost and finalTokens, resultMessageReceived, an always-created completionCheckpoint, extensionCount, and optional budgetExceeded. A persisted completed record also retains the source running-state fields (claudePid, claudeSessionId, claudeLogPath, currentCost, currentTokens, assistantMessageCount, extensionCount), so a real file shows both currentCost and finalCost; read finalCost.
  • failed carries failedDuring (the state the codon was in when it failed), exitCode (0 or 1 from the harness, or -1 written by the runtime for a force stop, a run crash, or a failure before any runner ran; nothing writes 130, and a graceful Ctrl+C leaves the interrupted codon running with no exit code), the stored failureReason, partialCost, partialTokens, optional errorCheckpoint, and optional extensionCount. A checkpoint-creation failure records the pre-drain status, normally running; failedDuring: "completing-sentinels" is written only by a force stop or a crash that reads the live status during the sentinel drain. For diagnosis: preparing points to a rig problem, initializing to process startup, and running to an agent or API problem.
  • 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 (from starting onward, including completing-sentinels) store sentinels as { loaded: SentinelState[], totalCost }. A terminal record stores them as { executed, totalCost } when a failed or skipped transition's source record was running or completing-sentinels, and when a crash's source record was running; a codon that fails or is skipped before running, or crashes outside running, ends with no sentinels field at all.

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, not this page. claudeLogPath is recorded in state.json; its base and naming belong to 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 that keep sentinels rename that collection to executed.

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, State machine, Errors and exit codes, and 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<string, number>;
      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 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 – which is why the legacy-plan normalization described under Read the top-level fields runs at load.

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
  }
}

See Loops, Model resolution, and Budgets.

Record checkpoint fields#

Checkpoint fields record the git commit SHA associated with a codon-state milestone. The codon record can contain these fields:

Scroll to explore the table →
FieldTypeStored when
rigSetupCheckpointstring?After rig operations; back-patched while the codon is in the matching status.
completionCheckpointstringOn completed; always created and back-patched by CheckpointCreated.
errorCheckpointstring?On failed; may be absent if git operations failed.
skipCheckpointstring?On skipped; back-patched by CheckpointCreated.

CheckpointCreated back-patches a matching field only while the codon has the matching status, and its checkpointType is one of the four stored types (rig-setup, completed, error, skipped). start and end are rollback selectors, not stored types; that contract belongs to Checkpoints. This page inventories the fields. The checkpoint repository, rollback mechanics, and corrected GIT_DIR recipes belong to Checkpoints and Resume, rollback, and retry.

A completed codon that ran rig setup carries both SHAs:

JSONC
{
  "status": "completed",
  "rigSetupCheckpoint": "abc123...",
  "completionCheckpoint": "def456..."
}

See Checkpoints.

Track sentinel state#

SentinelState records one sentinel's model, activity counts, cost, and unload state.

TS
/**
 * State tracking for a single sentinel within a codon.
 * Mutable structure updated in-place during codon execution.
 */
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";
}

A non-terminal codon stores its sentinel collection as loaded; a terminal record that keeps sentinels stores them 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 and Sentinel configuration.

Interpret budget fields#

On a completed codon, budgetExceeded? has { currency, limit, used } and is present when the codon is force-completed at a budget limit.

State also persists the authored budget configuration carried by plan entries: executionPlan[].codon.budget ({ maxDollars?, maxTimeSeconds?, maxOutputTokens?, maxContextTokens?, onExceeded? }) and executionPlan[].loopContext.loopBudget. The resolved, effective global limits are not separately stored in state.json; they resolve from configuration at run time. Budget semantics, currencies, allocation, and the budget.summary event belong to Budgets and 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 and Events.

Validation and recovery#

Initialization first proves the checkpoint repository readable, before loading state.json. An unreadable repository throws Recovery stopped: checkpoint storage could not be read (…). Nothing has been changed; see .hankweave/logs/server.log. and never falls through to the backup/fresh ladder, which is reserved for a corrupt state file.

Load-time validation then reports the emitted errors and warning, and recovery tries the primary file, its backup, and a fresh in-memory state. The shipped validator emits:

Scroll to explore the table →
SeverityTypeMeaning
errorcorrupted_dataInvalid structure; loading throws and follows the backup path.
errormissing_runcurrentRunId is not present in runs.
warningorphaned_folderA run folder exists on disk without a state entry.

The type unions also name invalid_codon and cost_mismatch, but the shipped validator emits neither. 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 and 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:

  • Treat runs[0] as the newest run, or select a known run id, to inspect a completed or failed run.
  • Match currentRunId only to identify a live run. It is a persisted pointer that a crash leaves set, so it is not a running-server indicator, and no recipe below matches on it for a finished 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.

The commands below put those rules into practice against a concrete execution directory: latest-attempt selection, failed-codon triage for the newest run, the last successful checkpoint, and the live-run binding (useful only while a server is running).

⌁ Terminal
EXEC=./my-execution
jq '. as $state | .runs[] | select(.runId == $state.currentRunId)' "$EXEC"/.hankweave/state.json
jq '[.runs[0].codons[] | select(.codonId == "normalize")] | last' "$EXEC"/.hankweave/state.json
jq '.runs[0].codons[] | select(.status == "failed") | {codonId, failedDuring, failureReason, exitCode}' "$EXEC"/.hankweave/state.json
jq '[.runs[0].codons[] | select(.status == "completed")] | last | .completionCheckpoint' "$EXEC"/.hankweave/state.json

The first filter binds the root state before comparing runId with currentRunId; use it only while a server is running. The second takes the latest normalize attempt. The third reads runs[0] and selects failed codons in the newest 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. Token totals follow the same accessor: finalTokens for completed, partialTokens for failed, currentTokens for running and completing-sentinels, and zero for skipped and never-started records, in the TokenUsage shape { inputTokens, outputTokens, cacheCreationTokens, cacheReadTokens }; a skipped record may still store accumulated partialTokens, but the getCodonTokens rollup counts it as zero. Sentinel costs are summed separately. This is the runtime's persisted-state view, not a provider invoice: it does not add token.usage event totals (the runtime emits both incremental and final cost under token.usage, so summing double-counts) and does not depend on the optional modelUsage. Each codon.completed event carries its own attempt's cost plus the immediately preceding failed attempt's, so a last-completion sum is exact for at most one retry and under-counts after two or more.

See Client and exported types, Observe and debug, Events, and Execution directory.