pub struct Node {Show 25 fields
pub schema_version: u32,
pub node_id: NodeId,
pub run_id: RunId,
pub parent_node_id: Option<NodeId>,
pub kind: Kind,
pub status: Status,
pub task: Option<String>,
pub worktree_path: Option<String>,
pub branch: Option<String>,
pub base_sha: Option<String>,
pub tmux_window: Option<String>,
pub tmux_identity: Option<TmuxIdentity>,
pub agent_pid: Option<i32>,
pub agent_pid_start_time: Option<DateTime<Utc>>,
pub supervisor_pid: Option<i32>,
pub children: Vec<ChildRef>,
pub started_at: Option<DateTime<Utc>>,
pub updated_at: DateTime<Utc>,
pub last_report: Option<Value>,
pub last_processed_report_seq_by_child: Map<String, Value>,
pub retry_attempts: u32,
pub worker_exit: Option<WorkerExit>,
pub pending_merge: Option<Box<MergeTxn>>,
pub first_death_at: Option<DateTime<Utc>>,
pub awaiting_input: Option<Box<AwaitingInput>>,
}Expand description
nodes/<node-id>.json (design.md §1.3).
Fields§
§schema_version: u32State-schema version this file was written with.
node_id: NodeIdUnique node identifier within its run (e.g. n-0001). Validated on
read; this is the projection’s filename key, so it can never name a
path outside nodes/.
run_id: RunIdRun this node belongs to. Validated on read.
parent_node_id: Option<NodeId>Parent node within the same run, if this is a sub-node.
kind: KindKind of work this node performs.
status: StatusCurrent node status.
task: Option<String>Task description / prompt driving the node, if recorded.
worktree_path: Option<String>Filesystem path of the node’s git worktree, if created.
branch: Option<String>Git branch the node works on, if any.
base_sha: Option<String>The commit SHA the node’s branch/worktree was forked from at spawn
(the branch tip the moment create.sh materialized the worktree). It
is the fixed reference point that lets the supervisor tell “this branch
produced work that merged into source” from “this branch never diverged
from its fork point”: a branch still at base_sha is trivially an
ancestor of its source branch but has merged nothing, so it must NOT be
reconciled to success or torn down (that would drop a live agent’s
uncommitted work). Only a branch whose tip has moved past base_sha
and is now an ancestor of the run’s source_branch is a confirmed
merge (issues false-failed-after-merge /
supervisor-stuck-pending-after-self-merge). #[serde(default)] keeps a
node written before this field existed readable (None → the
git-reconcile fallback simply does not fire for it).
tmux_window: Option<String>tmux window hosting the node’s agent, if interactive. This is the
human-readable window name — not unique across sessions and blind to
non-default sockets. Kept for display and as the legacy liveness key;
prefer Node::tmux_identity when present.
tmux_identity: Option<TmuxIdentity>Fully-qualified tmux identity (session:window_id + socket path)
captured at spawn time. None for nodes registered before create.sh
emitted the qualified fields — those fall back to bare-name matching on
Node::tmux_window. New spawns always populate this when create.sh
returns it.
agent_pid: Option<i32>PID of the running agent process, if live.
agent_pid_start_time: Option<DateTime<Utc>>Start time of the agent process, used to detect PID reuse.
supervisor_pid: Option<i32>PID of the supervisor watching this node, if live.
children: Vec<ChildRef>Children this node has spawned.
started_at: Option<DateTime<Utc>>When the node started executing, if it has.
updated_at: DateTime<Utc>When the node file was last modified.
last_report: Option<Value>The node.report payload that drove this node to its terminal status.
Set only by the report that actually transitions the node (Done /
Failed / Cancelled). Once the node is terminal it is frozen: a late
report against an already-settled node is dropped without overwriting
this field (see reducer::apply_node_report). So for a node cancelled
by run cancel, this holds the synthesized cancel report, not a
later-arriving agent report — that payload remains only in
events.jsonl.
last_processed_report_seq_by_child: Map<String, Value>Highest report seq consumed per child run id, for idempotent
report processing across supervisor restarts.
retry_attempts: u32Number of times the supervisor has auto-retried this node after an
empty-handed agent-died (issue autoretry-agent-died-worker). The
DURABLE, restart-safe bound on the bounded-retry loop: each node.retry
event increments it, and the watchdog terminalizes the run failed once
it reaches RETRY_MAX_ATTEMPTS. #[serde(default)] keeps a node written
before this field existed readable (0 — never retried).
worker_exit: Option<WorkerExit>The told exit status of the node’s worker process, recorded durably by
the run-worker launcher shim (crates/octl-cli/src/run_worker.rs) when
it wait()s on the agent it wrapped. This is a fact, not an inference:
the supervisor consumes it via the typed outcome table instead of guessing
completion from pid/pane/activity proxies (design.md §2.1, issue
thin-exit-status-launcher). A non-zero code or a terminating signal is a
failed worker; code == 0 with no explicit-merge transition is the
finished-but-unmerged case that must stay non-terminal (attention-
required), NOT be auto-failed. None until the shim records an exit — or
forever, for a worker never launched through the shim (the crash backstop
still covers that path). #[serde(default)] keeps a node written before
this field existed readable.
pending_merge: Option<Box<MergeTxn>>The in-flight run merge transaction for this node, if one has been
STARTED but not yet completed. run merge records a merge.started
event (setting this field) BEFORE it mutates git, because the merge spans
two durability domains — git refs and the event log — and is not atomic
across them (design.md §2.1b / A2, issue merge-transaction-recovery). A
crash after the git merge but before the terminal explicit-merge
node.report would otherwise strand the work merged in source with no
merge event → a false failed.
This field is the durable op-log record that lets recovery finish or
reject that ONE known transaction deterministically, by OID — never a
general branch-content heuristic. It is set by crate::MergeTxn-carrying
merge.started, and cleared when the transaction resolves: a terminal
node.report (the merge completed) or a merge.aborted (recovery found
the git mutation never landed). #[serde(default)] keeps a node written
before this field existed readable (None — no in-flight merge).
Boxed so the (rare) in-flight transaction does not inflate every Node /
ProjectionOp by the full MergeTxn footprint.
first_death_at: Option<DateTime<Utc>>The durable, monotonic timestamp of the FIRST tick on which the supervisor
observed this node’s worker process confirmed-dead with no told
worker.exited and no merge — the anchor for the residual crash
backstop’s fixed post-death grace (design.md §2.1a, issue
typed-supervisor-outcomes).
The backstop is the ONLY place pid liveness still governs an outcome
(pid liveness is a pure crash backstop now, never a primary signal). When
the launcher shim’s exit fact is lost — a hard kill of the shim, host
death — the supervisor never sees a worker.exited event, so it falls
back to “process confirmed gone → failed”. The grace exists only to let
an in-flight worker.exited / merge append land before that fires: on the
first confirmed death the supervisor records this timestamp (via a
node.death_observed event) and DEFERS; it terminalizes failed only on a
later tick once a fixed short window has elapsed AND an exclusive-lock
re-read confirms no exit/merge landed in the race window.
Persisted (not in-memory) so the grace survives a supervisor restart in the
window — a restart re-reads it rather than restarting the clock. First-write
-wins in the reducer, so the anchor is monotonic. None until the first
confirmed-death observation, or forever for a worker that exits cleanly
(the shim records worker.exited and the backstop never engages).
#[serde(default)] keeps a node written before this field existed readable.
awaiting_input: Option<Box<AwaitingInput>>An open, agent-authored request for a human decision. The worker records
this through node.awaiting_input instead of blocking on interactive
stdin. It remains non-terminal and is cleared by node.input_resolved, a
terminal node.report, or node.retry.
opened_at is stamped from the event envelope and is therefore a durable,
monotonic grace-window anchor that survives supervisor restarts. The
original discussion objects are retained verbatim so read surfaces and
notification hooks can carry the question, options, and recommended
default without inventing a second advisory schema.