Documentation
¶
Overview ¶
Package state owns the per-worktree .agent/state.json file used for session persistence and crash recovery.
The legacy TS analogue lives in ../agentfactory/packages/core/src/orchestrator/state-recovery.ts. The shape ported here keeps wire compatibility with the legacy reader so a worktree initialized by either runner is recoverable by the other during the F.0/F.5 migration window.
Each worktree contains a .agent/ directory with state.json (this package) and heartbeat.json (runtime/heartbeat). The state file is written atomically (tmpfile + rename) and protected by a per-file flock so concurrent updates inside a single worktree serialize.
Cross-issue recovery is explicitly refused: if a Read finds state belonging to a different issue identifier than the caller expects, the call returns ErrIdentifierMismatch — the orchestrator must clean the worktree before reusing it.
Index ¶
Constants ¶
const AgentDirName = ".agent"
AgentDirName is the conventional sub-directory name inside a worktree where state.json (and friends — heartbeat.json, todos.json) live.
const StateFileName = "state.json"
StateFileName is the conventional file name for the state file.
Variables ¶
var ( // ErrNotFound is returned by Read when the state file does not // exist on disk. ErrNotFound = errors.New("runtime/state: state.json not found") // ErrMalformed is returned by Read when the state file exists but // cannot be parsed as a State document. Callers may handle this // by logging + treating the worktree as fresh. ErrMalformed = errors.New("runtime/state: state.json malformed") // ErrIdentifierMismatch is returned by ReadExpect when the on-disk // state belongs to a different issue identifier than the caller // expects. The runner refuses to reuse cross-issue state. ErrIdentifierMismatch = errors.New("runtime/state: state.json identifier mismatch") )
Sentinel errors. Callers may type-check these via errors.Is.
Functions ¶
Types ¶
type State ¶
type State struct {
// IssueID is the platform-side issue UUID.
IssueID string `json:"issueId"`
// IssueIdentifier is the human-readable issue id (e.g. REN-1234).
// The cross-issue recovery guard compares this to an expected
// value before allowing reuse.
IssueIdentifier string `json:"issueIdentifier"`
// SessionID is the platform-side session UUID. Populated as soon
// as the runner has claimed the work.
SessionID string `json:"sessionId,omitempty"`
// ProviderName is the agent provider that ran (or is running) this
// session.
ProviderName agent.ProviderName `json:"providerName,omitempty"`
// ProviderSessionID is the provider-native session id captured
// from agent.InitEvent. Empty until the first init event fires.
ProviderSessionID string `json:"providerSessionId,omitempty"`
// WorkType is the work-type slug (development/qa/...).
WorkType string `json:"workType,omitempty"`
// CurrentStep is a runner-level descriptor of the current
// orchestration phase (e.g. "spawning", "streaming", "backstop").
CurrentStep string `json:"currentStep,omitempty"`
// AttemptCount tracks how many times the runner has attempted this
// session — incremented on retry-after-failure flows.
AttemptCount int `json:"attemptCount"`
// StartedAt is the unix-ms timestamp of session start.
StartedAt int64 `json:"startedAt"`
// LastUpdatedAt is the unix-ms timestamp of the most recent
// Update/Write — kept fresh by every state mutation.
LastUpdatedAt int64 `json:"lastUpdatedAt"`
// LastHeartbeat is the unix-ms timestamp of the most recent
// session heartbeat the runner observed. The heartbeat package
// owns the increment; state stores the snapshot for forensics.
LastHeartbeat int64 `json:"lastHeartbeat,omitempty"`
// PID is the agent provider subprocess pid, or 0 for multiplexed
// providers (codex app-server). Populated via Spec.OnProcessSpawned.
PID int `json:"pid,omitempty"`
// WorkerID is the daemon worker that owns this session.
WorkerID string `json:"workerId,omitempty"`
}
State is the persisted per-session state.json document. It mirrors the legacy TS WorktreeState shape (state-types.ts) closely enough that a worktree initialized by either runner can be inspected by either reader. New fields go at the bottom; do not reorder.
Time fields are stored as Unix-millisecond integers to match the legacy TS Date.now() encoding.
type Store ¶
type Store struct {
// contains filtered or unexported fields
}
Store owns the .agent/state.json file inside one or more worktrees.
A single Store is safe to use concurrently across worktrees: each Update call serializes through the per-worktree mutex held inside Store.locks. Different worktrees do not share a mutex, so they proceed in parallel.
The zero value is valid.
func (*Store) Read ¶
Read parses the state.json under worktreePath. Returns ErrNotFound when the file does not exist and ErrMalformed when the bytes are not valid JSON.
func (*Store) ReadExpect ¶
ReadExpect is like Read but enforces the cross-issue recovery guard: when expectedIdentifier is non-empty and the on-disk state belongs to a different issue, ReadExpect returns ErrIdentifierMismatch (still wrapping the loaded *State so callers can log forensics).
func (*Store) Update ¶
Update reads the current state, applies fn, and writes the result — all under the per-worktree mutex so concurrent updates from the same process are serialized. fn may not return nil for *State; doing so is treated as "no change".
When the state file does not exist, fn is called with a non-nil zero-value *State so callers can populate it on first run.
Returns the post-update *State on success.
func (*Store) Write ¶
Write atomically replaces the state.json under worktreePath with the given State. Creates .agent/ when missing. The write is tmpfile + rename so a crash mid-write cannot leave a half-written file.
LastUpdatedAt is overwritten with the current monotonic-millisecond timestamp so callers do not have to set it themselves.