state

package
v0.3.33 Latest Latest
Warning

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

Go to latest
Published: Jun 18, 2026 License: MIT Imports: 14 Imported by: 0

Documentation

Overview

Package state provides manifest CRUD, locking, and session discovery.

Package state provides manifest CRUD, locking, and session discovery.

Index

Constants

View Source
const (
	// QuestLoopPhaseWaiting means the loop is watching for the next done-edge.
	QuestLoopPhaseWaiting = "waiting"
	// QuestLoopPhaseChecking means the loop is running the quest's auto gates.
	QuestLoopPhaseChecking = "checking"
	// QuestLoopPhasePaused means the agent is blocked on human input and the
	// loop is holding rather than injecting.
	QuestLoopPhasePaused = "paused"
)

Quest-loop phases, written into the advisory marker at each transition so renderers can show what the armed loop is doing between iterations.

View Source
const (
	// SessionIDPrefix is used for all questmaster session IDs.
	SessionIDPrefix = "qm-"

	// SessionEnv is the environment variable for the current session ID.
	SessionEnv = "QUESTMASTER_SESSION"

	// StateRootEnv is the environment variable for the state root.
	StateRootEnv = "QUESTMASTER_STATE_ROOT"
)
View Source
const DefaultDisplayColor = "blue"
View Source
const RepoColorsFile = "repo-colors.json"

RepoColorsFile is the basename of the repo-color store under the state root.

View Source
const SchemaVersion = 1

SchemaVersion is the current PaneState/SessionState schema version. Hook payloads that load a SessionState with a different Version overwrite it; readers (the tracker) treat foreign-version state as `unknown`.

View Source
const StateJSONLMaxSize = 1 << 20 // 1 MiB

StateJSONLMaxSize is the rotation threshold for state.jsonl. When the file exceeds this size, the writer rotates it to state.jsonl.1 and starts a fresh log. One historical file is kept.

Variables

View Source
var (
	// ErrManifestExists is returned when Create finds an existing manifest.
	ErrManifestExists = errors.New("manifest already exists")
	// ErrManifestNotFound is returned when Delete targets a missing manifest.
	ErrManifestNotFound = errors.New("manifest not found")
)

Sentinel errors for manifest operations.

View Source
var ErrQuestLoopArmed = errors.New("quest loop already armed")

ErrQuestLoopArmed means a session already carries an advisory loop marker.

Functions

func AppendStateEvent

func AppendStateEvent(id string, ev StateEvent) error

AppendStateEvent appends to state.jsonl and rotates the file when it crosses StateJSONLMaxSize. The state root must already exist (the session-state directory is created lazily). Rotation drops the previous .1 file, so only one rolled file is retained on disk.

func ArmQuestLoop added in v0.3.13

func ArmQuestLoop(sessionID string, since time.Time, force bool) error

ArmQuestLoop sets the advisory marker for an armed foreground loop. With force=true, an existing stale marker is replaced. A fresh marker starts in the waiting phase (the loop arms, then watches for a done-edge).

func ClearQuest added in v0.3.12

func ClearQuest(sessionID string) error

ClearQuest detaches a session from its quest, returning the quest to the board. A no-op when the session is already unattached.

func ClearQuestLoop added in v0.3.13

func ClearQuestLoop(sessionID string) error

ClearQuestLoop removes the advisory loop marker. It is safe to call even when no loop is currently marked.

func DisplayColorANSIIndex added in v0.3.27

func DisplayColorANSIIndex(color string) string

DisplayColorANSIIndex returns the ANSI palette index for a supported display color name. Unknown names use the default display color.

func DisplayColorOptions added in v0.3.4

func DisplayColorOptions() []string

DisplayColorOptions returns the supported named display colors.

func EffectiveColor added in v0.3.23

func EffectiveColor(ownColor string, ownAt time.Time, repoColor string, repoAt time.Time) string

EffectiveColor resolves the last-write-wins color for a session: between its own color (changed at ownAt) and its repo color (changed at repoAt), whichever changed most recently wins. An empty color means unset; "" is returned when neither is set so the caller applies the default. The repo wins only when strictly newer, so a same-instant tie keeps the session's own color.

func HasSessionIDPrefix added in v0.2.12

func HasSessionIDPrefix(id string) bool

HasSessionIDPrefix reports whether id starts with the session prefix, without validating the full ID shape.

func InitStartingState

func InitStartingState(id string, agentsByRole map[string]string) error

InitStartingState seeds state.json with State="starting" / Activity="started" for each provided pane (roleName → agentName, e.g. {"primary": "codex"}). Skips any pane that already has a non-empty State so the agent's first hook (which fires moments later) is never overwritten. This eliminates the brief "unknown" the tracker would otherwise render between spawn and the SessionStart hook.

func IsQuestAttached added in v0.3.12

func IsQuestAttached(questID string) (bool, error)

IsQuestAttached reports whether any session is on the quest.

func IsValidSessionID added in v0.2.12

func IsValidSessionID(id string) bool

IsValidSessionID reports whether id is a supported questmaster session ID.

func NewSessionID added in v0.2.12

func NewSessionID(timestamp int64) string

NewSessionID formats the base ID for a new questmaster session.

func NewSessionIDWithSuffix added in v0.2.12

func NewSessionIDWithSuffix(timestamp, suffix int64) string

NewSessionIDWithSuffix formats a collision-retry ID for a new questmaster session.

func NormalizeDisplayColor added in v0.3.4

func NormalizeDisplayColor(color string) string

NormalizeDisplayColor returns a supported color name or the default.

func NowColorStamp added in v0.3.23

func NowColorStamp() string

NowColorStamp returns the current time as an RFC3339Nano UTC string. Both a session color and a repo color stamp their change time with it; the nanosecond precision keeps two near-simultaneous recolors from tying.

func NowUTC

func NowUTC() string

NowUTC returns the current time in the format used by bash manifest helpers.

func ParseColorStamp added in v0.3.23

func ParseColorStamp(s string) time.Time

ParseColorStamp parses a color change timestamp, returning the zero time for an empty or malformed value (which then loses last-write-wins ties).

func QuestIDForSession added in v0.3.12

func QuestIDForSession(sessionID string) (string, error)

QuestIDForSession returns the session's quest_id, or "" when the session is free or has no state.

func SanitizeResumeID

func SanitizeResumeID(v string) string

SanitizeResumeID returns v unchanged when it's a safe identifier, or "" when it contains characters that could escape filesystem / glob contexts (path traversal, wildcards, NUL, etc.). Exported so downstream consumers share the single source of truth rather than carrying their own copy of the regex.

func SaveSessionState

func SaveSessionState(id string, ss *SessionState) error

SaveSessionState writes the state.json for a session atomically. The write is serialized via flock on a sibling lockfile.

func SessionIDFromEnv added in v0.2.12

func SessionIDFromEnv() string

SessionIDFromEnv returns the current session ID from QUESTMASTER_SESSION.

func SessionStateDir

func SessionStateDir(root, id string) string

SessionStateDir returns <root>/<id>. Caller must ensure id is valid.

func SessionStateLockPath

func SessionStateLockPath(root, id string) string

SessionStateLockPath returns the flock path. We lock a sibling file rather than state.json itself so the atomic rename of state.json never races a still-held lock fd.

func SessionStateLogPath

func SessionStateLogPath(root, id string) string

SessionStateLogPath returns the state.jsonl path for the session.

func SessionStatePath

func SessionStatePath(root, id string) string

SessionStatePath returns the state.json path for the session.

func SessionsForQuest added in v0.3.12

func SessionsForQuest(questID string) ([]string, error)

SessionsForQuest scans every session's state.json and returns the ids stamped with questID, sorted. These are the directly attached sessions; a quest with none reads unattached.

func SetQuestLoopPhase added in v0.3.21

func SetQuestLoopPhase(sessionID, phase string) error

SetQuestLoopPhase records a phase transition on an armed marker. Advisory only; a missing marker is a no-op.

func SortByMtime

func SortByMtime(manifests []Manifest, root string)

SortByMtime sorts manifests by file modification time, newest first.

func StampQuest added in v0.3.12

func StampQuest(sessionID, questID string) error

StampQuest sets the session's quest_id. The read-modify-write runs inside the per-session flock (UpdateSessionState), so it never clobbers a concurrent hook write. A session with no state.json yet gets a minimal one — attaching is an explicit act.

func StateRoot

func StateRoot() string

StateRoot resolves the directory that holds per-session state. Honors $QUESTMASTER_STATE_ROOT, then falls back to ~/.questmaster-state. Returns the empty string when neither an override nor $HOME is set (caller treats that as a no-op condition).

func TrimSessionIDPrefix added in v0.2.12

func TrimSessionIDPrefix(id string) string

TrimSessionIDPrefix removes the session prefix when present.

func UpdateQuestLoop added in v0.3.13

func UpdateQuestLoop(sessionID string, iterations int, verdict, phase string) error

UpdateQuestLoop records the latest visible loop progress if a marker is present. It is advisory only; a missing marker is a no-op.

func UpdateSessionState

func UpdateSessionState(id string, mutate func(*SessionState) bool) error

UpdateSessionState performs a locked read-modify-write. mutate runs against the freshly-loaded state inside the critical section. Returning false from mutate aborts the write (used when the freshly-read state no longer satisfies the precondition the caller checked optimistically).

This is the only safe way to apply tracker-side mutations like done → idle: a naive Load → mutate → Save races against hooks because the load happens outside the lock.

Types

type AgentManifest

type AgentManifest struct {
	Name     string `json:"name"`
	Role     string `json:"role"`
	CLI      string `json:"cli"`
	ResumeID string `json:"resume_id,omitempty"`
	Window   int    `json:"window"`
}

AgentManifest stores per-agent runtime state in the manifest.

type DisplayMetadata added in v0.3.4

type DisplayMetadata struct {
	Color string `json:"color,omitempty"`
	// ColorChangedAt is when Color was last set (RFC3339Nano, UTC). It drives
	// last-write-wins resolution against a repo color; empty means "unknown",
	// which loses to any repo color carrying a real timestamp.
	ColorChangedAt string                     `json:"color_changed_at,omitempty"`
	Extra          map[string]json.RawMessage `json:"-"`
}

DisplayMetadata stores user-facing presentation hints for a session. Extra preserves unknown nested display keys written by newer versions.

func NewDisplayMetadata added in v0.3.4

func NewDisplayMetadata(color string) *DisplayMetadata

NewDisplayMetadata creates display metadata with a valid color.

func (DisplayMetadata) IsZero added in v0.3.4

func (d DisplayMetadata) IsZero() bool

func (DisplayMetadata) MarshalJSON added in v0.3.4

func (d DisplayMetadata) MarshalJSON() ([]byte, error)

MarshalJSON merges typed display fields with Extra to preserve unknown keys.

func (*DisplayMetadata) UnmarshalJSON added in v0.3.4

func (d *DisplayMetadata) UnmarshalJSON(data []byte) error

UnmarshalJSON preserves unknown nested display fields in Extra.

type Manifest

type Manifest struct {
	SessionID   string           `json:"session_id"`
	CreatedAt   string           `json:"created_at,omitempty"`
	UpdatedAt   string           `json:"updated_at,omitempty"`
	Title       string           `json:"title,omitempty"`
	Cwd         string           `json:"cwd,omitempty"`
	WindowName  string           `json:"window_name,omitempty"`
	Agents      []AgentManifest  `json:"agents,omitempty"`
	AgentPath   string           `json:"agent_path,omitempty"`
	SessionType string           `json:"session_type,omitempty"`
	Workers     []string         `json:"workers,omitempty"`
	Display     *DisplayMetadata `json:"display,omitempty"`

	// Extra preserves unknown fields written by bash helpers
	// (e.g. parent_session, initial_prompt).
	Extra map[string]json.RawMessage `json:"-"`
}

Manifest represents a questmaster session's persisted state. Extra holds unknown fields to preserve fields this version does not interpret.

func (Manifest) DisplayColor added in v0.3.4

func (m Manifest) DisplayColor() string

DisplayColor returns the manifest's normalized display color, or an empty string when the manifest has no display color metadata.

func (Manifest) ExtraString

func (m Manifest) ExtraString(key string) string

ExtraString reads a string value from the manifest's Extra map.

func (Manifest) MarshalJSON

func (m Manifest) MarshalJSON() ([]byte, error)

MarshalJSON merges typed fields with Extra to preserve unknown keys.

func (*Manifest) SetExtra

func (m *Manifest) SetExtra(key, value string)

SetExtra sets a string value in the manifest's Extra map.

func (*Manifest) UnmarshalJSON

func (m *Manifest) UnmarshalJSON(data []byte) error

UnmarshalJSON preserves unknown fields in Extra. It decodes the typed fields in one pass (encoding/json's optimized struct path) and captures unknown keys in a second shallow pass into RawMessage — far cheaper than the previous token-stream loop, which re-entered the reflection decoder per field and boxed every delimiter through Token().

type PaneState

type PaneState struct {
	Role         string    `json:"role"`
	Agent        string    `json:"agent"`
	State        string    `json:"state"`
	Activity     string    `json:"activity"`
	Tool         string    `json:"tool,omitempty"`
	Seq          int64     `json:"seq"`
	LastEvent    time.Time `json:"last_event"`
	LastKind     string    `json:"last_kind"`
	WorkingSince time.Time `json:"working_since,omitempty"`

	Recent      []string `json:"recent,omitempty"`
	SessionFile string   `json:"session_file,omitempty"`
	PiSessionID string   `json:"pi_session_id,omitempty"`
}

PaneState is the renderer-visible state for one role within a session. WorkingSince timestamps the moment State transitioned to "working" and is preserved across PreToolUse/PostToolUse cycles within the same turn, so the tracker can render an ever-growing "working 2m14s" suffix. Pi-specific carry-through fields are populated only when Agent == "pi".

type QuestLoopState added in v0.3.13

type QuestLoopState struct {
	Since       time.Time `json:"since"`
	Iterations  int       `json:"iterations"`
	LastVerdict string    `json:"last_verdict,omitempty"`
	Phase       string    `json:"phase,omitempty"`
}

QuestLoopState is the renderer-visible marker for an armed quest loop. Phase is the loop's current step (waiting | checking | paused), written at each transition so the board/tracker can show what the armed loop is doing between iterations. Like the rest of the marker it is advisory only.

type RepoColor added in v0.3.23

type RepoColor struct {
	Color     string `json:"color"`
	UpdatedAt string `json:"updated_at"`
}

RepoColor is a repository's persisted display color and when it last changed. UpdatedAt (RFC3339Nano, UTC) drives last-write-wins resolution against a session's own color.

type RepoColorStore added in v0.3.23

type RepoColorStore struct {
	// contains filtered or unexported fields
}

RepoColorStore persists per-repository colors keyed by repo identity (the resolved common git dir) in a single JSON file under the state root, so a repo color survives a tracker restart. It mirrors the manifest store's atomic-write + flock discipline so concurrent tracker panes cannot clobber each other.

func NewRepoColorStore added in v0.3.23

func NewRepoColorStore(root string) *RepoColorStore

NewRepoColorStore returns a store backed by <root>/repo-colors.json.

func (*RepoColorStore) Get added in v0.3.23

func (s *RepoColorStore) Get(identity string) (RepoColor, bool, error)

Get returns a repo's color and whether one is set.

func (*RepoColorStore) Load added in v0.3.23

func (s *RepoColorStore) Load() (map[string]RepoColor, error)

Load reads every repo color. A missing file is not an error — it returns an empty map so the tracker degrades to "no repo colors set".

func (*RepoColorStore) Set added in v0.3.23

func (s *RepoColorStore) Set(identity, color string) error

Set records a repo's color, stamping the change time. An empty color clears the override so sessions fall back to their own explicit color or the default. An empty identity is a no-op (a path outside any repo cannot carry a repo color).

type SessionState

type SessionState struct {
	SessionID string               `json:"session_id"`
	Version   int                  `json:"version"`
	Panes     map[string]PaneState `json:"panes"`
	SeenAt    time.Time            `json:"seen_at"`

	// QuestID links a session to an active quest. It is stamped on explicit
	// attach/spawn, including workers, and cleared on detach. "Sessions on a
	// quest" is derived by scanning this field, never stored on the quest.
	// Preserved across hook writes because hooks read-modify-write the whole
	// SessionState (UpdateSessionState).
	QuestID string `json:"quest_id,omitempty"`

	// QuestLoop is an advisory marker written while `qm quest loop` is armed
	// for this session. The foreground process is authoritative; this marker
	// only drives visibility and double-arm refusal.
	QuestLoop *QuestLoopState `json:"quest_loop,omitempty"`
}

SessionState is the authoritative per-session state snapshot written by hooks and read by the tracker. One file per session at <state_root>/<session_id>/state.json.

func LoadSessionState

func LoadSessionState(id string) (*SessionState, error)

LoadSessionState reads the state.json for a session. Returns (nil, nil) if the file does not exist (the renderer should treat this as "unknown"). The state root is resolved from $QUESTMASTER_STATE_ROOT or $HOME.

func LoadSessionStateAt added in v0.3.30

func LoadSessionStateAt(root, id string) (*SessionState, error)

LoadSessionStateAt is LoadSessionState with the state root supplied by the caller. A refresh loop that already knows the root (e.g. the tracker's fetcher, iterating every session each tick) uses this to skip the per-call StateRoot() — two os.Getenv plus a join — on every session.

type StateEvent

type StateEvent struct {
	Ts       time.Time              `json:"ts"`
	Agent    string                 `json:"agent"`
	Role     string                 `json:"role,omitempty"`
	Action   string                 `json:"action"`
	State    string                 `json:"state,omitempty"`
	Activity string                 `json:"activity,omitempty"`
	Tool     string                 `json:"tool,omitempty"`
	Kind     string                 `json:"kind,omitempty"`
	Fields   map[string]interface{} `json:"fields,omitempty"`
}

StateEvent is one line of state.jsonl. Free-form fields beyond the fixed columns belong in Fields so consumers parsing the log don't have to keep up with a moving schema.

type Store

type Store struct {
	// contains filtered or unexported fields
}

Store manages manifest files on disk with flock-based locking.

func NewStore

func NewStore(root string) (*Store, error)

NewStore creates a Store rooted at the given directory, creating it if needed.

func OpenStore

func OpenStore(root string) *Store

OpenStore opens a Store for read-only access without creating the directory. DiscoverSessions and other reads gracefully return empty results if the directory does not exist. Use NewStore for mutating operations that need the directory.

func (*Store) AddWorker

func (s *Store) AddWorker(sessionID, workerID string) error

AddWorker adds a worker ID to the manifest's workers list (deduplicated).

func (*Store) Create

func (s *Store) Create(m Manifest) error

Create writes a new manifest. Returns an error if it already exists. Uses flock to prevent TOCTOU races between concurrent Create calls.

func (*Store) Delete

func (s *Store) Delete(sessionID string) error

Delete removes a manifest file and its lock file.

func (*Store) DiscoverSessions

func (s *Store) DiscoverSessions() ([]Manifest, error)

DiscoverSessions returns all supported questmaster session manifests found in the state root. Non-session files, lock files, and corrupt manifests are silently skipped.

func (*Store) GetWorkers

func (s *Store) GetWorkers(sessionID string) ([]string, error)

GetWorkers returns the worker IDs from a manifest.

func (*Store) Read

func (s *Store) Read(sessionID string) (Manifest, error)

Read loads a manifest by session ID.

func (*Store) RemoveWorker

func (s *Store) RemoveWorker(sessionID, workerID string) error

RemoveWorker removes a worker ID from the manifest's workers list.

func (*Store) Root

func (s *Store) Root() string

Root returns the state directory path.

func (*Store) Update

func (s *Store) Update(sessionID string, fn func(*Manifest)) error

Update applies a mutation function to an existing manifest under lock.

Jump to

Keyboard shortcuts

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