When a run stops partway through a plan and a later run resumes it, the state file holds two partial histories rather than one complete one. The execution thread is how Hankweave reconciles them: a newest-first walk across the continuation chain that answers the questions the runtime actually asks, such as which codon runs next, whether the thread has failed, and which session a continue-previous codon should resume.
This page is a source-level reference for that reconstruction. It defines the thread's data types, walks through the analysis algorithm and its continuation edge cases, and ends with how to exercise the logic using replay. It assumes you know what a run and a codon are in passing; where a concept has its own home, the page links out rather than re-teaching it.
How runs form an execution thread#
An execution thread is the newest-first history Hankweave reconstructs across a chain of runs: the current run's codons plus retained codons from continuation ancestors. A run is a recorded pass through a codon plan, and a codon is an individual agent task. A hank is the configured work description whose codons form that plan. A checkpoint – a recorded git state associated with a codon – carries checkpoint metadata, and a rig – a codon's setup step – can produce a rig-setup checkpoint.
To trace this reconstruction in 0.10.0, start with server/execution-thread.ts, server/state-manager.ts, and server/types/state-types.ts. Source references use repository-relative paths such as server/execution-thread.ts, not package import specifiers.
The entry points are CheckpointInfo, ThreadCodon, ExecutionThread and its failed getter, analyzeExecutionThread, getNextCodonId, findContinuationSessionId, and StateManager.getExecutionThread. The sections below follow that order: first the data a thread is made of, then the failure rule, then the walk that builds it, then the lookups and the consumer that tie it into the runtime. Declarations and signatures are copied from the tagged source, with behavior notes for reading or extending the implementation.
The package exports only ., ./schemas, and ./types. These definitions are therefore source-reference material, not consumer imports. See client and exported types for the public export contract.
Which fields identify a thread codon#
CheckpointInfo#
CheckpointInfo records the kind and git metadata of a checkpoint associated with a codon. Its type is one of rig-setup, completed, error, or skipped; the record also contains a SHA, message, ISO 8601 timestamp, and branch.
export interface CheckpointInfo {
type: "rig-setup" | "completed" | "error" | "skipped";
sha: string;
message: string;
timestamp: string; // ISO 8601 timestamp
branch: string;
}
/**
* A codon with its complete context
*/
export interface ThreadCodon {
// The codon data
codon: CodonExecution;
// Run context
runId: RunId;
runStatus: "running" | "completed" | "failed" | "crashed";
runStartTime: string;
runEndTime: string | null; // null for running runs, string for completed runs
gitBranch: string;
// Position in execution history
globalIndex: number; // 0 = latest codon across all runs
runIndex: number; // Which run this came from (0 = latest run)
codonIndexInRun: number; // Position within that run
// All checkpoints for this codon with validation
validatedCheckpoints: CheckpointInfo[];
// Derived information
continuationSessionId: SessionId | null; // Session ID this codon continued from, null if none
}
/**
The validatedCheckpoints list on each thread codon is filtered against git, not trusted from state alone. Validation keeps only checkpoint SHAs present in the supplied git checkpoint-data map. A state-file checkpoint missing from git is dropped, and omitting checkpoint data produces an empty validatedCheckpoints list. Checkpoints explains the checkpoint lifecycle; the state-file reference owns persisted run data.
ThreadCodon#
ThreadCodon carries one codon together with its run context, position in the reconstructed history, validated checkpoints, and continuationSessionId. The thread is newest-first: globalIndex 0 is the most recently executed codon across all runs, runIndex 0 is the latest run, and larger run indexes move up the continuation chain. The walk retains each codon's source codonIndexInRun while visiting a run from its last index toward zero, so a codon's position in the thread and its position in its original run are both recoverable.
When present, loop membership is carried by the codon value's loopContext: loopId, zero-based iteration, and codonIndexInLoop. continuationSessionId comes from previousSessionId when that field is present; otherwise it is null.
How the thread reports failure#
ExecutionThread#
ExecutionThread stores the reconstructed codons, total run count, running-codon flag, and the already-selected nextCodonId; all constructor fields have defaults. failed is a derived getter rather than stored state, and its rule is narrower than "some run failed".
export class ExecutionThread {
constructor(
public codons: ThreadCodon[] = [],
public totalRuns: number = 0,
public hasRunningCodon: boolean = false,
public nextCodonId: CodonId | null = null,
) {}
/**
* Check if the execution thread is in a failed state.
*
* The thread is considered failed only if:
* 1. The most recent codon explicitly has status "failed"
* 2. OR the run crashed/failed while a codon was still running (non-terminal state)
*
* Historical runs being marked as "failed" (e.g., due to server shutdown after
* successful codon completion) should NOT cause the thread to be considered failed.
*/
get failed(): boolean {
// If there are no codons, the thread is not failed
if (this.codons.length === 0) {
return false;
}
// Check only the most recent codon (index 0, since thread is in reverse order)
const mostRecent = this.codons[0];
// Thread is failed if the most recent codon explicitly failed
if (mostRecent.codon.status === "failed") {
return true;
}
// Thread is failed if the run crashed/failed while a codon was still running
// (codon wasn't terminal when run ended)
if (
(mostRecent.runStatus === "failed" || mostRecent.runStatus === "crashed") &&
!isTerminalCodonStatus(mostRecent.codon.status)
) {
return true;
}
return false;
}
}
// -------------
// Main Analysis Function - Simplified Algorithm
// -------------
/**
* Analyze execution history to build a unified thread with all metadata.
*
* @param state - The complete Hankweave state (including executionPlan)
* @param checkpointData - Map of SHA to git checkpoint data (optional)
* @param targetRunId - Specific run to analyze (defaults to latest)
* @param logger - Optional logger for debugging
* @returns Complete execution thread with all metadata preserved
The getter is true when the most recent codon explicitly has status failed, or when the latest run is failed or crashed while that codon is non-terminal. The statuses completed, failed, and skipped are terminal; preparing, starting, initializing, running, and completing-sentinels are not, so hasRunningCodon means "not yet terminal" rather than only running. A failed status in a superseded historical run does not, by itself, fail the thread. Resume, rollback, and retry uses this state during recovery.
How the cross-run walk chooses history#
analyzeExecutionThread#
analyzeExecutionThread is the function that builds everything above. It starts with the run named by targetRunId, or with state.runs[0] when no target is supplied. Runs are stored latest-first. With no runs it returns an empty thread. It visits the selected run's codons backward from the last index, then follows each continuation's startingConditions.source.runId to its parent. A visited-run set prevents a cycle from extending the walk.
export async function analyzeExecutionThread(
state: HankweaveState,
checkpointData?: Map<string, { message: string; timestamp: string; branch: string }>,
targetRunId?: RunId,
logger?: Logger,
): Promise<ExecutionThread> {
// Get execution plan from state
const executionPlan = state.executionPlan;
// Find starting run
const startRun = targetRunId ? state.runs.find((r) => r.runId === targetRunId) : state.runs[0]; // Latest run is first
if (!startRun) {
logger?.log("No runs found for execution thread analysis", "debug");
return new ExecutionThread([], 0, false, null);
}
// Initialize thread building
const codons: ThreadCodon[] = [];
const visited = new Set<RunId>();
let currentRun: Run | null = startRun;
let untilCodon = startRun.codons.length - 1; // Start by including all codons
let runIndex = 0;
let globalIndex = 0;
let hasRunningCodon = false;
// Process runs following the continuation chain
while (currentRun && !visited.has(currentRun.runId)) {
visited.add(currentRun.runId);
logger?.log(
`Processing run ${currentRun.runId} (status: ${currentRun.status}, ` +
`codons: ${currentRun.codons.length}, including up to index ${untilCodon})`,
"debug",
);
// Process codons in this run (backwards, from untilCodon to 0)
for (let i = untilCodon; i >= 0; i--) {
const codon = currentRun.codons[i];
// Check if this is a running codon
if (!isTerminalCodonStatus(codon.status)) {
hasRunningCodon = true;
}
// Build checkpoint information with git metadata
const validatedCheckpoints = buildCheckpointInfo(codon, checkpointData);
// Extract continuation session ID if present
const continuationSessionId: SessionId | null =
"previousSessionId" in codon && codon.previousSessionId ? codon.previousSessionId : null;
// Build the thread codon entry with all metadata
const threadCodon: ThreadCodon = {
codon,
runId: currentRun.runId,
runStatus: currentRun.status,
runStartTime: currentRun.startTime,
runEndTime: currentRun.endTime || null,
gitBranch: currentRun.gitBranch,
globalIndex,
runIndex,
codonIndexInRun: i,
validatedCheckpoints,
continuationSessionId,
};
codons.push(threadCodon);
globalIndex++;
}
// Check if this run is a continuation and move to parent
if (currentRun.startingConditions.type === "continuation") {
const source = currentRun.startingConditions.source;
const parentRunId: RunId = source.runId;
const afterCodon = source.afterCodon;
const checkpointSha = source.checkpointSha;
// Find parent run
const parentRun = state.runs.find((r: Run) => r.runId === parentRunId);
if (!parentRun) {
logger?.log(`Parent run ${parentRunId} not found, ending chain`, "info");
break;
}
// Calculate untilCodon for the parent run
if (!afterCodon) {
// Continuation from beginning - exclude all codons from parent
untilCodon = -1;
} else {
// Find the codon in parent run
const afterCodonIndex = parentRun.codons.findIndex(
(p: CodonExecution) => p.codonId === afterCodon,
);
if (afterCodonIndex === -1) {
logger?.log(
`Codon ${afterCodon} not found in parent run ${parentRunId}, including all codons`,
"info",
);
untilCodon = parentRun.codons.length - 1;
} else {
const afterCodonData = parentRun.codons[afterCodonIndex];
// Check if it's a rig-setup continuation
if (
"rigSetupCheckpoint" in afterCodonData &&
afterCodonData.rigSetupCheckpoint === checkpointSha
) {
// Rig setup continuation - exclude the codon that will be re-run
untilCodon = afterCodonIndex - 1;
logger?.log(
`Rig setup continuation for ${afterCodon}, excluding it from parent`,
"debug",
);
} else {
// Normal continuation - include up to and including afterCodon
untilCodon = afterCodonIndex;
logger?.log(
`Normal continuation after ${afterCodon}, including codons up to index ${afterCodonIndex}`,
"debug",
);
}
}
}
// Move to parent run
currentRun = parentRun;
runIndex++;
} else {
// Fresh start - we're done
break;
The continuation branch at the end of the loop is where the walk decides how much of a parent run to keep. For a normal continuation, the parent contributes codons through and including afterCodon. If the parent codon's rigSetupCheckpoint equals source.checkpointSha, the rig-setup continuation stops before that codon because it will be run again. afterCodon: null contributes no parent codons. A missing parent ends the chain with a log message; if afterCodon is absent from the parent, the walk logs the condition and includes all of that parent's codons.
Read the diagram as text
+-------------------------+
| startingConditions.type |
+-----+-------------+-----+
fresh | | continuation
v v
+--------------+ +------------------------------+
| end the walk | | inspect source.afterCodon |
+--------------+ +----+-----------+-----------+-+
| | |
afterCodon is null | matches a | | normal
| rigSetupCheckpoint | continuation
v v v
+--------------------+ +--------------------+ +--------------------+
| untilCodon = -1 | | untilCodon = | | untilCodon = |
| (exclude all | | afterCodonIndex - 1| | afterCodonIndex |
| parent codons) | | (exclude afterCodon| | (include through |
+---------+----------+ +---------+----------+ | afterCodon) |
| | +---------+----------+
+----------------------+----------------------+
v
+------------------------+
| move to the parent run |
+------------------------+
After the walk, a separate block selects nextCodonId, the codon the runtime would execute if it continued from here.
let nextCodonId: CodonId | null = null;
// Don't suggest next codon if the current run failed
if (startRun.status === "failed") {
// TODO
nextCodonId = null;
} else if (!hasRunningCodon && codons.length > 0) {
const latestCodon = codons[0];
// Check if we're continuing from a rig-setup checkpoint
// This happens when the latest run is a continuation that will re-run a codon
if (startRun.startingConditions.type === "continuation") {
const { afterCodon, checkpointSha } = startRun.startingConditions.source;
// Check if this continuation is from a rig-setup checkpoint
if (afterCodon && codons.length === 0) {
// TODO
// No codons executed yet in continuation run
// Check if the continuation is from rig-setup
const sourceRun = state.runs.find(
(r) =>
r.runId ===
(
startRun.startingConditions as {
type: "continuation";
source: { runId: RunId };
}
).source.runId,
);
if (sourceRun) {
const sourceCodon = sourceRun.codons.find((p) => p.codonId === afterCodon);
if (
sourceCodon &&
"rigSetupCheckpoint" in sourceCodon &&
sourceCodon.rigSetupCheckpoint === checkpointSha
) {
// Rig-setup continuation - next codon is the same codon
nextCodonId = afterCodon;
}
}
}
}
// If not rig-setup continuation, find next codon in execution plan
if (!nextCodonId) {
const codonIndex = executionPlan.findIndex((e) => e.codonId === latestCodon.codon.codonId);
if (codonIndex >= 0 && codonIndex < executionPlan.length - 1) {
nextCodonId = executionPlan[codonIndex + 1].codonId;
}
}
} else if (!hasRunningCodon && codons.length === 0) {
// No codons executed yet
if (startRun.startingConditions.type === "continuation") {
const { afterCodon, checkpointSha } = startRun.startingConditions.source;
if (!afterCodon) {
// Continuation from beginning
nextCodonId = executionPlan[0]?.codonId ?? null;
} else {
// Check if it's a rig-setup continuation
const sourceRun = state.runs.find(
(r) =>
r.runId ===
(
startRun.startingConditions as {
type: "continuation";
source: { runId: RunId };
}
).source.runId,
);
if (sourceRun) {
const sourceCodon = sourceRun.codons.find((p) => p.codonId === afterCodon);
if (
sourceCodon &&
"rigSetupCheckpoint" in sourceCodon &&
sourceCodon.rigSetupCheckpoint === checkpointSha
) {
// Rig-setup continuation - re-run the same codon
nextCodonId = afterCodon;
} else {
// Normal continuation - run next codon after afterCodon
const codonIndex = executionPlan.findIndex((e) => e.codonId === afterCodon);
if (codonIndex >= 0 && codonIndex < executionPlan.length - 1) {
nextCodonId = executionPlan[codonIndex + 1].codonId;
}
}
}
}
} else {
// Fresh run - start with first codon
nextCodonId = executionPlan[0]?.codonId ?? null;
}
}
The decision tree has three outcomes worth remembering. nextCodonId is null when the selected start run is failed. A rig-setup continuation selects the same codon again; otherwise the walk selects the execution-plan successor of the latest thread codon. An empty fresh run starts at executionPlan[0]. When all codons are complete, nextCodonId is null; a re-invoked server logs all codons completed and exits 0 without naming the execution.
The walk reads each run's continuation information from the StartingConditions union, which distinguishes a fresh run from a continuation. A fresh value may contain initialCheckpointSha; a continuation names source.runId, afterCodon, and checkpointSha, and may record reason as retry, rollback, or continue.
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";
};
The capture below shows what this looks like in practice: a normalized excerpt of state.json after a kill-and-resume, not a literal HankweaveState serialization. Its latestRun key is a capture-added convenience key that duplicates runs[0], not a state field; the runs[] entries retain the continuation-chain evidence. The continuation run appears first and contains write-second; its parent contains completed write-line and failed write-second. The captured server log beneath it shows the thread the analyzer builds from that state: first one codon across two runs with write-second as next, then two codons with nothing left to run.
{
"currentRunId": null,
"latestRun": {
"runId": "<id>",
"status": "completed",
"startingConditions": {
"type": "continuation",
"source": {
"runId": "<id>",
"afterCodon": "write-line",
"checkpointSha": "<sha>"
},
"reason": "rollback"
},
"codons": [
{
"codonId": "write-second",
"status": "completed",
"exitCode": 0
}
]
},
"runs": [
{
"runId": "<id>",
"status": "completed",
"startingConditions": {
"type": "continuation",
"source": {
"runId": "<id>",
"afterCodon": "write-line",
"checkpointSha": "<sha>"
},
"reason": "rollback"
},
"codons": [
{
"codonId": "write-second",
"status": "completed",
"exitCode": 0
}
]
},
{
"runId": "<id>",
"status": "completed",
"startingConditions": {
"type": "fresh"
},
"codons": [
{
"codonId": "write-line",
"status": "completed",
"exitCode": 0
},
{
"codonId": "write-second",
"status": "failed",
"exitCode": -1
}
]
}
]
}
[<ts>] [DEBUG] Built execution thread: 1 codons across 2 runs, next codon: write-second
[<ts>] [DEBUG] Built execution thread: 1 codons across 2 runs, next codon: write-second
…
[<ts>] [DEBUG] Built execution thread: 2 codons across 2 runs, next codon: none
[<ts>] [DEBUG] Built execution thread: 2 codons across 2 runs, next codon: none
[<ts>] [DEBUG] Built execution thread: 2 codons across 2 runs, next codon: none
See checkpoints for checkpoint records and resume, rollback, and retry for continuation procedures.
How a continuation finds its session#
Two small lookups sit on top of the finished thread. One reads the plan position the analyzer already chose; the other finds the agent session a continue-previous codon should resume.
getNextCodonId#
getNextCodonId(thread) returns the nextCodonId computed during analysis; it does not recompute the plan position.
export function getNextCodonId(thread: ExecutionThread): CodonId | null {
// Simply return what was already calculated at the thread level
return thread.nextCodonId || null;
}
/**
* Find session ID for continuing a specific codon
*/
export function findContinuationSessionId(
thread: ExecutionThread,
codonId: CodonId,
state: HankweaveState,
): SessionId | null {
const executionPlan = state.executionPlan || [];
const entry = executionPlan.find((e) => e.codonId === codonId);
// Only continue-previous codons need a session
if (!entry || entry.codon.continuationMode !== "continue-previous") {
return null;
}
// Find the codon before this one in the execution plan
const entryIndex = executionPlan.findIndex((e) => e.codonId === codonId);
if (entryIndex <= 0) return null;
const previousCodonId = executionPlan[entryIndex - 1].codonId;
// Find the most recent execution of the previous codon
for (const threadCodon of thread.codons) {
if (threadCodon.codon.codonId !== previousCodonId) continue;
const codon = threadCodon.codon;
// Must have a session ID
if (!("claudeSessionId" in codon) || !codon.claudeSessionId) continue;
// Check if it's valid for continuation
if (codon.status === "completed") {
return codon.claudeSessionId;
}
if (
codon.status === "skipped" &&
"assistantMessageCount" in codon &&
codon.assistantMessageCount &&
codon.assistantMessageCount > 0
) {
return codon.claudeSessionId;
}
}
return null;
}
findContinuationSessionId#
findContinuationSessionId(thread, codonId, state) returns null unless the plan entry requests continue-previous. It then scans the thread newest-first for the previous plan codon and returns its session only when that codon is completed, or is skipped with assistantMessageCount > 0.
The state fields are literally claudeSessionId, claudeLogPath, and claudePid for the Claude SDK and Pi SDK harnesses at 0.10.0. Pi session ids are translated into the Claude-shaped session_id message field before storage; the claude* prefix is a naming remnant, not evidence of shims. The reconstruction's previousSessionId and this lookup's claudeSessionId are separate fields: the former populates continuationSessionId, while the latter is returned for a valid previous codon.
For the execution directory used by a continuation, the checkpoint store is a shadow git repository at <exec>/.hankweave/checkpoints/.hankweavecheckpoints; here <exec> denotes that execution directory, with the git directory redirected there and the working tree at agentRoot/. A session is the per-codon agent conversation identified by claudeSessionId/SessionId. The state-file reference owns persisted fields; client and exported types covers the public consumer surface.
Where the thread is consumed#
StateManager.getExecutionThread#
StateManager.getExecutionThread is the runtime-facing wrapper around the analyzer. It accepts an optional target run and defaults includeCheckpointValidation to true. It builds the git checkpoint-data map only when validation is requested and CheckpointGit is initialized. If state.executionPlan is empty during startup, it synthesizes an effective plan from codonConfigs before calling the analyzer.
* @param targetRunId - Optional run ID to start from (defaults to latest)
* @param includeCheckpointValidation - Whether to validate checkpoints against git
* @returns Complete execution thread with all metadata
*
* NOTE: The codonConfigs fallback exists for initialization timing issues where the plan
* hasn't been built yet (e.g., during HankweaveRuntime.start() before startNewRun()).
*/
async getExecutionThread(
targetRunId?: RunId,
includeCheckpointValidation = true,
): Promise<ExecutionThread> {
// Get checkpoint data if requested and available
const checkpointData =
includeCheckpointValidation && this.checkpointGit?.isInitialized()
? await this.getCheckpointDataMap()
: undefined;
let effectivePlan: ExecutionCodonEntry[];
if (this.state.executionPlan.length > 0) {
effectivePlan = this.state.executionPlan;
} else {
// Fallback: Convert codonConfigs to ExecutionCodonEntry format
// This treats each config as a single execution entry with no loop context
effectivePlan = (this.codonConfigs || []).map((config) => ({
codon: config.type === "loop" ? config.codons[0] : config,
codonId: CodonIdConstructor(config.id),
loopContext: undefined,
}));
}
// If using a custom plan different from stored state, create temporary state
const stateToAnalyze: ST.HankweaveState =
effectivePlan !== this.state.executionPlan
? { ...this.state, executionPlan: effectivePlan }
: this.state;
return analyzeExecutionThread(stateToAnalyze, checkpointData, targetRunId, this.logger);
The runtime consumes the thread to resolve a continue-previous session before a codon starts, search for rollback targets across runs, advance autoStartNextCodon, and perform the startup failure check. When thread.failed is true at startup, the runtime logs Execution thread failed, rolling back... and calls rollbackToLastSuccess. If that startup rollback cannot find its target in the archive manifest, the resulting Target checkpoint … not found in manifest failure belongs to resume, rollback, and retry, which documents the failure mode.
How to exercise the thread with replay#
Reading the source is one way to understand the walk; watching it run against a recorded execution is another. Clone the SouthBridgeAI/hankweave-runtime repository at tag v0.10.0; the implementation is server/execution-thread.ts. A multi-run thread is produced when a hank is killed or fails mid-plan and resumes with -e/--execution <dir>. Resume-by-state uses the execution-directory option; there is no --resume flag.
--replay supplies a provider-free run#
--replay <dir> re-runs a recorded execution without provider contact. Since 0.5.6, replay skips model self-tests and requires no API keys, making it a provider-free way to exercise thread and resume logic. The flag is parsed but absent from --help at 0.10.0; the CLI reference owns its flag contract.
Replay always starts a fresh run and ignores existing state, logging [REPLAY] Starting fresh run (replay mode ignores existing state). It therefore does not reconstruct an existing multi-run state; resume-by-state with -e/--execution <dir> is the path described above for that. The recorded state and logs under <dir> remain replay's input.
Replay reads recorded files#
Replay loads <dir>/.hankweave/state.json. For each codon, it resolves the log through that codon's recorded claudeLogPath, normalizing path separators for portability. Missing logs cause [ReplayLoader] N codon log file(s) not found. Lookup first uses the runtime codon id and falls back to the base codon id for older loop-suffixed ids; a missing log never falls back to real execution.
When explicit paths are absent, replay discovers the hank configuration and data path from <dir>/.hankweave/execution-meta.json, using hankPath and readOnlySourceDataPath. That metadata has schema version 1.1.0, changed in 0.5.0, and includes hankweaveVersion plus environment.{invocationMethod,platform,arch,osRelease,runtime}. Its own version: "1.1.0" is separate from hankweaveVersion, so the metadata and Hankweave 0.10.0 are independently versioned. Execution directory owns the complete metadata contract.
The CLI reference owns flag syntax; resume, rollback, and retry connects replay with recovery.