Documentation
¶
Overview ¶
Package state provides manifest CRUD, locking, and session discovery.
Package state provides manifest CRUD, locking, and session discovery.
Index ¶
- Constants
- Variables
- func AppendLifecycleEvent(id string, ev StateEvent) error
- func AppendStateEvent(id string, ev StateEvent) error
- func ArtifactKindForPath(path string) string
- func ArtifactMissing(path string) bool
- func ArtifactsRegistryLockPath(root string) string
- func ArtifactsRegistryPath(root string) string
- func DisplayColorOptions() []string
- func EffectiveColor(ownColor string, ownAt time.Time, repoColor string, repoAt time.Time) string
- func EnsurePrivateStateRoot(root string) error
- func HasSessionIDPrefix(id string) bool
- func InitStartingState(id string, agentsByRole map[string]string) error
- func IsDisplayColor(color string) bool
- func IsValidSessionID(id string) bool
- func LifecycleLogLockPath(root string) string
- func LifecycleLogPath(root string) string
- func MarkSessionObserved(id string, now time.Time) (bool, error)
- func NewSessionID(timestamp int64) string
- func NewSessionIDWithSuffix(timestamp, suffix int64) string
- func NormalizeDisplayColor(color string) string
- func NowColorStamp() string
- func NowUTC() string
- func OpenCodeConfigDir(root string) string
- func ParseColorStamp(s string) time.Time
- func QuestsRegistryLockPath(root string) string
- func QuestsRegistryPath(root string) string
- func RemoveArtifact(sessionID, path string) (bool, error)
- func RemoveArtifactAt(root, sessionID, path string) (bool, error)
- func RemoveArtifactGlobal(root, path string) (bool, error)
- func RemoveQuestAt(root, id string) (bool, error)
- func SanitizeResumeID(v string) string
- func SaveSessionState(id string, ss *SessionState) error
- func SessionArtifactsPath(root, id string) string
- func SessionIDFromEnv() string
- func SessionStateDir(root, id string) string
- func SessionStateLockPath(root, id string) string
- func SessionStateLogPath(root, id string) string
- func SessionStatePath(root, id string) string
- func SortByMtime(manifests []Manifest, root string)
- func StateRoot() string
- func TrimSessionIDPrefix(id string) string
- func UpdateAndLog(id string, ev StateEvent, mutate func(*SessionState) bool) error
- func UpdateSessionState(id string, mutate func(*SessionState) bool) error
- func UpsertArtifact(sessionID string, artifact Artifact) error
- func UpsertArtifactAt(root, sessionID string, artifact Artifact) error
- func UpsertArtifactGlobal(root string, artifact Artifact) error
- type AgentManifest
- type Artifact
- func FilterArtifacts(artifacts []Artifact, scope, sessionID, projectID string) []Artifact
- func LoadArtifacts(sessionID string) ([]Artifact, error)
- func LoadArtifactsAt(root, sessionID string) ([]Artifact, error)
- func LoadArtifactsGlobal(root string) ([]Artifact, error)
- func SortedArtifacts(artifacts []Artifact) []Artifact
- type DisplayMetadata
- type Manifest
- type PaneState
- type Quest
- func FilterQuests(quests []Quest, scope, projectID, query string) []Quest
- func LoadQuests() ([]Quest, error)
- func LoadQuestsAt(root string) ([]Quest, error)
- func SetQuestDoneAt(root string, ids []string, done bool) ([]Quest, error)
- func SortedQuests(quests []Quest) []Quest
- func UpsertQuestAt(root string, quest Quest) (Quest, error)
- type RepoColor
- type RepoColorStore
- type SessionState
- type StateEvent
- type Store
- func (s *Store) AddWorker(sessionID, workerID string) error
- func (s *Store) Create(m Manifest) error
- func (s *Store) Delete(sessionID string) error
- func (s *Store) DiscoverSessions() ([]Manifest, error)
- func (s *Store) GetWorkers(sessionID string) ([]string, error)
- func (s *Store) Read(sessionID string) (Manifest, error)
- func (s *Store) RemoveWorker(sessionID, workerID string) error
- func (s *Store) Root() string
- func (s *Store) SetDisplayColor(sessionID, color string) error
- func (s *Store) Update(sessionID string, fn func(*Manifest)) error
Constants ¶
const ( ArtifactKindHTML = "html" ArtifactKindMarkdown = "markdown" ArtifactKindImage = "image" ArtifactScopeSession = "session" ArtifactScopeProject = "project" ArtifactScopeAll = "all" )
const ( QuestScopeActive = "active" QuestScopeDone = "done" QuestScopeAll = "all" )
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" )
const DefaultDisplayColor = "blue"
const DoneToIdleGrace = 10 * time.Second
DoneToIdleGrace is how long a completed agent turn stays visibly "done" before an observing tracker can fold it back to idle.
const RepoColorsFile = "repo-colors.json"
RepoColorsFile is the basename of the repo-color store under the state root.
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`.
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 ¶
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.
Functions ¶
func AppendLifecycleEvent ¶ added in v0.3.35
func AppendLifecycleEvent(id string, ev StateEvent) error
AppendLifecycleEvent appends a durable lifecycle event at the state root. Use it for events that must survive per-session cleanup, such as teardown.
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 ArtifactKindForPath ¶ added in v0.3.39
func ArtifactMissing ¶ added in v0.3.36
func ArtifactsRegistryLockPath ¶ added in v0.3.41
ArtifactsRegistryLockPath returns the root-level artifact registry lock path.
func ArtifactsRegistryPath ¶ added in v0.3.41
ArtifactsRegistryPath returns the durable root-level runtime artifact registry.
func DisplayColorOptions ¶ added in v0.3.4
func DisplayColorOptions() []string
DisplayColorOptions returns the supported named display colors.
func EffectiveColor ¶ added in v0.3.23
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 EnsurePrivateStateRoot ¶ added in v0.3.35
EnsurePrivateStateRoot creates root if needed and tightens existing roots to owner-only access. The state root holds prompts, cwd metadata, and sockets.
func HasSessionIDPrefix ¶ added in v0.2.12
HasSessionIDPrefix reports whether id starts with the session prefix, without validating the full ID shape.
func InitStartingState ¶
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 IsDisplayColor ¶ added in v0.3.35
IsDisplayColor reports whether color is one of the supported named display colors after trimming and case-folding.
func IsValidSessionID ¶ added in v0.2.12
IsValidSessionID reports whether id is a supported questmaster session ID.
func LifecycleLogLockPath ¶ added in v0.3.35
LifecycleLogLockPath returns the root-level lifecycle log lock path.
func LifecycleLogPath ¶ added in v0.3.35
LifecycleLogPath returns the root-level lifecycle event log. It survives session-state directory cleanup, unlike <session>/state.jsonl.
func MarkSessionObserved ¶ added in v0.3.35
MarkSessionObserved applies tracker-side observation to an existing state.json. It never creates state for hookless sessions.
func NewSessionID ¶ added in v0.2.12
NewSessionID formats the base ID for a new questmaster session.
func NewSessionIDWithSuffix ¶ added in v0.2.12
NewSessionIDWithSuffix formats a collision-retry ID for a new questmaster session.
func NormalizeDisplayColor ¶ added in v0.3.4
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 OpenCodeConfigDir ¶ added in v0.3.44
OpenCodeConfigDir returns Questmaster's private OpenCode config dir under root.
func ParseColorStamp ¶ added in v0.3.23
ParseColorStamp parses a color change timestamp, returning the zero time for an empty or malformed value (which then loses last-write-wins ties).
func QuestsRegistryLockPath ¶ added in v0.3.44
func QuestsRegistryPath ¶ added in v0.3.44
func RemoveArtifact ¶ added in v0.3.36
func RemoveArtifactAt ¶ added in v0.3.41
RemoveArtifactAt is RemoveArtifact with an explicit state root.
func RemoveArtifactGlobal ¶ added in v0.3.41
RemoveArtifactGlobal removes an artifact by path from the root registry.
func RemoveQuestAt ¶ added in v0.3.44
func SanitizeResumeID ¶
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 SessionArtifactsPath ¶ added in v0.3.36
SessionArtifactsPath returns the command-owned runtime artifact sidecar path for the session. Hooks own state.json; artifact commands own this file.
func SessionIDFromEnv ¶ added in v0.2.12
func SessionIDFromEnv() string
SessionIDFromEnv returns the current session ID from QUESTMASTER_SESSION.
func SessionStateDir ¶
SessionStateDir returns <root>/<id>. Caller must ensure id is valid.
func SessionStateLockPath ¶
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 ¶
SessionStateLogPath returns the state.jsonl path for the session.
func SessionStatePath ¶
SessionStatePath returns the state.json path for the session.
func SortByMtime ¶
SortByMtime sorts manifests by file modification time, newest first.
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
TrimSessionIDPrefix removes the session prefix when present.
func UpdateAndLog ¶ added in v0.3.36
func UpdateAndLog(id string, ev StateEvent, mutate func(*SessionState) bool) error
UpdateAndLog folds the JSONL event append and the state.json read-modify-write into a single critical section so the per-event hook path takes one flock instead of two. It always appends ev to state.jsonl (matching AppendStateEvent's rotate-then-append behavior), then runs mutate against the freshly-loaded SessionState and rewrites state.json only when mutate returns true (matching UpdateSessionState).
The event append uses appendRotatingJSONL directly rather than AppendStateEvent: the shared per-session lockfile is already held here, and re-locking the same file from this process would self-block.
Both halves are best-effort relative to each other in the sense that a failed state write does not undo the appended event line — but the append runs first, so the event log always records the event even if the state rewrite later fails.
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.
func UpsertArtifact ¶ added in v0.3.36
func UpsertArtifactAt ¶ added in v0.3.41
UpsertArtifactAt is UpsertArtifact with an explicit state root.
func UpsertArtifactGlobal ¶ added in v0.3.41
UpsertArtifactGlobal upserts an artifact by its path in the root registry.
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 Artifact ¶ added in v0.3.36
type Artifact struct {
Kind string `json:"kind"`
Path string `json:"path"`
Label string `json:"label,omitempty"`
SessionID string `json:"session_id"`
ProjectID string `json:"project_id"`
AddedAt string `json:"added_at"`
}
Artifact is a session-scoped runtime reference to a generated file.
func FilterArtifacts ¶ added in v0.3.41
func LoadArtifacts ¶ added in v0.3.36
LoadArtifacts reads runtime artifact references for one session.
func LoadArtifactsAt ¶ added in v0.3.36
LoadArtifactsAt is LoadArtifacts with an explicit state root.
func LoadArtifactsGlobal ¶ added in v0.3.41
LoadArtifactsGlobal reads the root-level artifact registry, lazily migrating existing per-session sidecars and legacy state.json artifacts when absent.
func SortedArtifacts ¶ added in v0.3.36
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 and change stamp so last-write-wins can rank it against repo colors.
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
DisplayColor returns the manifest's normalized display color, or an empty string when the manifest has no display color metadata.
func (Manifest) ExtraString ¶
ExtraString reads a string value from the manifest's Extra map.
func (Manifest) MarshalJSON ¶
MarshalJSON merges typed fields with Extra to preserve unknown keys.
func (*Manifest) UnmarshalJSON ¶
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"`
OpenCodeSessionID string `json:"opencode_session_id,omitempty"`
// PendingPartMsgID/PendingPartText buffer an opencode message part whose
// author role is not yet known. The text is promoted to Activity/Recent only
// once the matching assistant message.updated arrives, so a user's prompt is
// never surfaced as the worker's activity.
PendingPartMsgID string `json:"pending_part_msg_id,omitempty"`
PendingPartText string `json:"pending_part_text,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. Agent-specific carry-through fields are populated only by providers that emit structured hook metadata.
type Quest ¶ added in v0.3.44
type Quest struct {
ID string `json:"id"`
Content string `json:"content"`
ProjectID string `json:"project_id,omitempty"`
ProjectPath string `json:"project_path,omitempty"`
ProjectName string `json:"project_name,omitempty"`
Done bool `json:"done"`
CreatedAt string `json:"created_at,omitempty"`
UpdatedAt string `json:"updated_at,omitempty"`
SessionID string `json:"session_id,omitempty"`
}
func FilterQuests ¶ added in v0.3.44
func LoadQuests ¶ added in v0.3.44
func LoadQuestsAt ¶ added in v0.3.44
func SetQuestDoneAt ¶ added in v0.3.44
func SortedQuests ¶ added in v0.3.44
type RepoColor ¶ added in v0.3.23
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"`
// Artifacts are runtime-only viewer references for this session. The bytes
// remain at Path.
Artifacts []Artifact `json:"artifacts,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 OpenStore ¶
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) Create ¶
Create writes a new manifest. Returns an error if it already exists. Uses flock to prevent TOCTOU races between concurrent Create calls.
func (*Store) DiscoverSessions ¶
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 ¶
GetWorkers returns the worker IDs from a manifest.
func (*Store) RemoveWorker ¶
RemoveWorker removes a worker ID from the manifest's workers list.
func (*Store) SetDisplayColor ¶ added in v0.3.35
SetDisplayColor updates a non-worker session's display color in its manifest. Empty color clears the override so the row falls back to repo/default color. Unknown display.* keys are preserved by mutating DisplayMetadata in place.