state

package
v0.16.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jun 2, 2026 License: MIT Imports: 8 Imported by: 0

README

runtime/state/

.agent/state.json persistence for crash recovery.

What it does

store := state.NewStore()

// Read on session start (or recovery).
st, err := store.ReadExpect(worktree, "REN-1234")
switch {
case errors.Is(err, state.ErrNotFound):           // fresh worktree
case errors.Is(err, state.ErrIdentifierMismatch): // refuse cross-issue reuse
case errors.Is(err, state.ErrMalformed):          // overwrite with fresh
}

// Update under per-worktree mutex; safe under contention.
_, err = store.Update(worktree, func(st *state.State) error {
    st.CurrentStep = "spawning"
    st.AttemptCount++
    return nil
})

File layout

<worktree>/
└── .agent/
    └── state.json

Atomic write: state.json.tmp-XXXX is written + fsync'd, then renamed over state.json. A crash mid-write cannot leave a half-written file.

Schema

Field Purpose
issueId, issueIdentifier Linear identifiers; identifier is the cross-issue recovery guard.
sessionId Platform session UUID.
providerName, providerSessionId Which provider ran; native session id from agent.InitEvent.
workType, currentStep Runner-level orchestration phase.
attemptCount Increment-on-retry counter.
startedAt, lastUpdatedAt, lastHeartbeat Unix-ms timestamps (matches legacy TS Date.now()).
pid, workerId Provider subprocess pid, owning worker.

The shape mirrors the legacy TS WorktreeState (../../../agentfactory/packages/core/src/orchestrator/state-types.ts) closely enough that the two readers can co-exist during the F.0/F.5 migration window.

Concurrency

Store.Update serializes through a per-worktree sync.Mutex lazily attached on first access. Different worktrees proceed in parallel.

Tests

store_test.go covers: not-found / roundtrip / Update creation + increment, 50-way concurrent contention (final count exact), malformed recovery, identifier mismatch returns loaded state for forensics, atomic write leaves no *.tmp-* leftovers.

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

View Source
const AgentDirName = ".agent"

AgentDirName is the conventional sub-directory name inside a worktree where state.json (and friends — heartbeat.json, todos.json) live.

View Source
const StateFileName = "state.json"

StateFileName is the conventional file name for the state file.

Variables

View Source
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

func Path

func Path(worktreePath string) string

Path returns the absolute path of the state.json file for a worktree. Useful for callers that need to log or stat the file.

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 NewStore

func NewStore() *Store

NewStore returns an empty Store.

func (*Store) Read

func (s *Store) Read(worktreePath string) (*State, error)

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

func (s *Store) ReadExpect(worktreePath, expectedIdentifier string) (*State, error)

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

func (s *Store) Update(worktreePath string, fn func(*State) error) (*State, error)

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

func (s *Store) Write(worktreePath string, st *State) error

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.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL