statedb

package
v1.9.30 Latest Latest
Warning

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

Go to latest
Published: May 22, 2026 License: MIT Imports: 14 Imported by: 0

Documentation

Index

Constants

View Source
const SchemaVersion = 9

SchemaVersion tracks the current database schema version. Bump this when adding migrations.

Variables

This section is empty.

Functions

func MarshalToolData

func MarshalToolData(
	claudeSessionID string, claudeDetectedAt time.Time,
	geminiSessionID string, geminiDetectedAt time.Time,
	geminiYoloMode *bool, geminiModel string,
	openCodeSessionID string, openCodeDetectedAt time.Time,
	codexSessionID string, codexDetectedAt time.Time,
	latestPrompt string, notes string, loadedMCPNames []string,
	toolOptionsJSON json.RawMessage,
	sandboxJSON json.RawMessage, sandboxContainer string,
	sshHost string, sshRemotePath string,
	multiRepoEnabled bool, additionalPaths []string,
	multiRepoTempDir string, multiRepoWorktrees []MultiRepoWorktreeData,
	channels []string,
	extraArgs []string,
	plugins []string,
	pluginChannelLinkDisabled bool,
	autoLinkedChannels []string,
	color string,
) json.RawMessage

func MergeToolDataExtras added in v1.7.73

func MergeToolDataExtras(oldToolData, newToolData json.RawMessage) json.RawMessage

MergeToolDataExtras preserves any keys in oldToolData that are not part of agent-deck's typed tool_data schema (the toolDataBlob fields in this package) and that are not already present in newToolData. It returns the merged JSON to write back to the instances table.

Why this exists: agent-deck's save path (SaveInstances) builds a fresh tool_data blob from typed Instance fields and INSERT OR REPLACEs the row wholesale. Any externally-written keys not modeled by toolDataBlob are silently dropped on every save cycle. The user-set `clear_on_compact` flag is the canonical example: it has no agent-deck CLI surface, so it is set by direct SQLite UPDATE; without this merge, it survives at most until the next session lifecycle event.

The function is conservative: typed-known keys are not touched (the new blob's value wins, including absence-by-omitempty), and new explicitly setting a key wins over the old value (no silent override of intended updates). Only keys that are completely unknown to the typed schema AND absent from the new blob are carried forward.

func MigrateFromJSON

func MigrateFromJSON(jsonPath string, db *StateDB) (int, int, error)

MigrateFromJSON reads a sessions.json file and inserts all data into the StateDB. Returns the number of instances and groups migrated.

func SetGlobal

func SetGlobal(db *StateDB)

SetGlobal sets the global StateDB instance.

Types

type CostEventRow added in v1.9.2

type CostEventRow struct {
	ID                  string
	SessionID           string
	Timestamp           string // RFC3339; preserve verbatim — cost_events stores TEXT
	Model               string
	InputTokens         int64
	OutputTokens        int64
	CacheReadTokens     int64
	CacheWriteTokens    int64
	CostMicrodollars    int64
	BudgetStopTriggered bool
}

CostEventRow mirrors the cost_events table for raw round-trip operations (e.g., cross-profile migration). The canonical CostEvent type lives in internal/costs, but we keep this minimal struct here so the statedb package can read/write rows without a circular import.

type GroupRow

type GroupRow struct {
	Path        string
	Name        string
	Expanded    bool
	Order       int
	DefaultPath string
	// MaxConcurrent caps simultaneous running sessions in this group (v1.9.1).
	// 0 = unlimited (legacy default for groups predating this field); 1 = serial
	// (default for newly-created groups); N>=2 = bounded parallelism.
	MaxConcurrent int
}

GroupRow represents a group row in the database.

type InstanceRow

type InstanceRow struct {
	ID                 string
	Title              string
	ProjectPath        string
	GroupPath          string
	Order              int
	Command            string
	Wrapper            string
	Tool               string
	Status             string
	TmuxSession        string
	CreatedAt          time.Time
	LastAccessed       time.Time
	ParentSessionID    string
	IsConductor        bool
	NoTransitionNotify bool
	// TmuxSocketName mirrors Instance.TmuxSocketName (v1.7.50+, issue #687).
	// Empty for pre-v1.7.50 rows — those keep targeting the default server
	// after upgrade.
	TmuxSocketName string
	// TitleLocked blocks Claude session-name sync into Title (v1.7.52+, issue #697).
	TitleLocked    bool
	WorktreePath   string
	WorktreeRepo   string
	WorktreeBranch string
	// Account is the per-session named account (v1.9.22+, issue #924). Maps to
	// `[profiles.<account>.claude].config_dir` at spawn time and becomes the
	// most-specific level in the CLAUDE_CONFIG_DIR resolution chain. Empty
	// means "fall through to conductor/group/env/profile/global/default".
	Account  string
	ToolData json.RawMessage // JSON blob for tool-specific data
}

InstanceRow represents a session row in the database.

type MultiRepoWorktreeData added in v0.26.2

type MultiRepoWorktreeData struct {
	OriginalPath string
	WorktreePath string
	RepoRoot     string
	Branch       string
}

MarshalToolData creates a tool_data JSON blob from individual fields. This is the forward path: Instance fields -> JSON blob for SQLite storage. MultiRepoWorktreeData holds multi-repo worktree info for serialization.

func UnmarshalToolData

func UnmarshalToolData(data json.RawMessage) (
	claudeSessionID string, claudeDetectedAt time.Time,
	geminiSessionID string, geminiDetectedAt time.Time,
	geminiYoloMode *bool, geminiModel string,
	openCodeSessionID string, openCodeDetectedAt time.Time,
	codexSessionID string, codexDetectedAt time.Time,
	latestPrompt string, notes string, loadedMCPNames []string,
	toolOptionsJSON json.RawMessage,
	sandboxJSON json.RawMessage, sandboxContainer string,
	sshHost string, sshRemotePath string,
	multiRepoEnabled bool, additionalPaths []string,
	multiRepoTempDir string, multiRepoWorktrees []MultiRepoWorktreeData,
	channels []string,
	extraArgs []string,
	plugins []string,
	pluginChannelLinkDisabled bool,
	autoLinkedChannels []string,
	color string,
)

UnmarshalToolData extracts individual fields from the tool_data JSON blob. This is the reverse path: JSON blob from SQLite -> individual Instance fields.

type RecentSessionRow added in v0.20.0

type RecentSessionRow struct {
	ID             string // SHA-256 dedup key (title+path+tool+group)
	Title          string
	ProjectPath    string
	GroupPath      string
	Command        string
	Wrapper        string
	Tool           string
	ToolOptions    json.RawMessage // serialized ToolOptionsWrapper
	SandboxEnabled bool
	GeminiYoloMode *bool
	DeletedAt      time.Time
}

RecentSessionRow captures the config of a deleted session for quick re-creation.

type StateDB

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

StateDB wraps a SQLite database for session/group persistence. Thread-safe for concurrent use from multiple goroutines within one process. Multiple OS processes can safely read/write via WAL mode + busy timeout.

func GetGlobal

func GetGlobal() *StateDB

GetGlobal returns the global StateDB instance (may be nil).

func Open

func Open(dbPath string) (*StateDB, error)

Open creates or opens a SQLite database at dbPath with WAL mode and busy timeout.

busy_timeout and foreign_keys are PER-CONNECTION pragmas in SQLite, so they MUST be passed via the DSN's `_pragma` parameter — setting them once via db.Exec only affects whichever pool connection happened to run the PRAGMA. Pre-fix, fresh connections in the pool defaulted to busy_timeout=0, which turned every transient lock into an immediate SQLITE_BUSY at the application level. journal_mode=WAL is persistent on the database file, so it can stay as a one-shot Exec.

func (*StateDB) AliveInstanceCount

func (s *StateDB) AliveInstanceCount() (int, error)

AliveInstanceCount returns how many TUI instances have fresh heartbeats.

func (*StateDB) CleanDeadInstances

func (s *StateDB) CleanDeadInstances(timeout time.Duration) error

CleanDeadInstances removes heartbeat entries that haven't been updated within timeout.

func (*StateDB) Close

func (s *StateDB) Close() error

Close checkpoints WAL and closes the database.

func (*StateDB) DB

func (s *StateDB) DB() *sql.DB

DB returns the underlying sql.DB for advanced use cases (e.g., testing).

func (*StateDB) DeleteCostEventsForSession added in v1.9.2

func (s *StateDB) DeleteCostEventsForSession(sessionID string) error

DeleteCostEventsForSession removes every cost_events row matching session_id.

func (*StateDB) DeleteGroup

func (s *StateDB) DeleteGroup(path string) error

DeleteGroup removes a group by path.

func (*StateDB) DeleteInstance

func (s *StateDB) DeleteInstance(id string) error

DeleteInstance removes an instance by ID.

Wrapped in withBusyRetry because parallel `agent-deck rm` invocations (e.g. xargs -P 14) all contend on the same WAL writer slot. Without retry, transient SQLITE_BUSY silently drops the DELETE while the CLI still reports success — the silent-loss half of issue #909.

func (*StateDB) DeleteInstanceRow added in v1.9.2

func (s *StateDB) DeleteInstanceRow(id string) error

DeleteInstanceRow removes a single row by ID. Cost / watcher rows are NOT touched — those are deleted explicitly by the migration so the orchestrator can sequence cleanup with the target-write phase.

func (*StateDB) DeleteWatcherEventsForSession added in v1.9.2

func (s *StateDB) DeleteWatcherEventsForSession(sessionID string) error

DeleteWatcherEventsForSession removes watcher_events rows whose session_id OR triage_session_id matches the given sessionID.

func (*StateDB) ElectPrimary added in v0.11.1

func (s *StateDB) ElectPrimary(timeout time.Duration) (bool, error)

ElectPrimary attempts to make this instance the primary. Returns true if this instance is now (or already was) the primary. Uses a transaction to atomically clear stale primaries and claim if available.

func (*StateDB) GetMeta

func (s *StateDB) GetMeta(key string) (string, error)

GetMeta gets a value from the metadata table. Returns "" if not found.

func (*StateDB) Heartbeat

func (s *StateDB) Heartbeat() error

Heartbeat updates the heartbeat timestamp for this process.

func (*StateDB) InsertCostEventRow added in v1.9.2

func (s *StateDB) InsertCostEventRow(ev *CostEventRow) error

InsertCostEventRow inserts a single cost_events row verbatim, preserving id (which is also the dedup key in costs.WriteCostEvent — using INSERT OR IGNORE makes the migration safely retriable).

func (*StateDB) InsertInstanceRow added in v1.9.2

func (s *StateDB) InsertInstanceRow(inst *InstanceRow) error

InsertInstanceRow inserts (or replaces) a single instance row. Unlike SaveInstance it does not merge tool_data extras — cross-profile migration is a verbatim transfer and the caller has already prepared the row.

func (*StateDB) InsertWatcherEventRow added in v1.9.2

func (s *StateDB) InsertWatcherEventRow(ev *WatcherEventRow) error

InsertWatcherEventRow inserts a watcher_events row with INSERT OR IGNORE against the (watcher_id, dedup_key) UNIQUE constraint — safe to retry. Note: the source row's `id` (auto-increment) is intentionally NOT preserved; the unique constraint is what dedupes across DBs.

func (*StateDB) InstanceExists added in v1.9.1

func (s *StateDB) InstanceExists(id string) (bool, error)

InstanceExists returns true iff a row with the given id is present. Used by the rm path's post-commit verify (issue #909) to detect resurrection by a concurrent SaveInstances rewrite.

func (*StateDB) IsEmpty

func (s *StateDB) IsEmpty() (bool, error)

IsEmpty returns true if the instances table has no rows.

func (*StateDB) LastModified

func (s *StateDB) LastModified() (int64, error)

LastModified returns the last_modified timestamp from metadata.

func (*StateDB) LoadCostEventsForSession added in v1.9.2

func (s *StateDB) LoadCostEventsForSession(sessionID string) ([]*CostEventRow, error)

LoadCostEventsForSession returns every cost_events row matching session_id. Timestamp is preserved verbatim as TEXT — we can't reliably round-trip through time.Time without risking timezone drift.

func (*StateDB) LoadGroup added in v1.9.2

func (s *StateDB) LoadGroup(path string) (*GroupRow, error)

LoadGroup returns the row for the given path, or (nil, nil) if absent.

func (*StateDB) LoadGroups

func (s *StateDB) LoadGroups() ([]*GroupRow, error)

LoadGroups returns all groups ordered by sort_order.

func (*StateDB) LoadInstanceByID added in v1.9.2

func (s *StateDB) LoadInstanceByID(id string) (*InstanceRow, error)

LoadInstanceByID returns the row with the given id, or (nil, nil) if it does not exist. Any other error (driver, schema, etc.) is returned as-is.

func (*StateDB) LoadInstanceChildren added in v1.9.2

func (s *StateDB) LoadInstanceChildren(parentID string) ([]*InstanceRow, error)

LoadInstanceChildren returns rows whose parent_session_id matches the given id.

func (*StateDB) LoadInstances

func (s *StateDB) LoadInstances() ([]*InstanceRow, error)

LoadInstances returns all instances ordered by sort_order.

func (*StateDB) LoadInstancesByGroup added in v1.9.2

func (s *StateDB) LoadInstancesByGroup(groupPath string) ([]*InstanceRow, error)

LoadInstancesByGroup returns rows whose group_path exactly matches the given path.

func (*StateDB) LoadRecentSessions added in v0.20.0

func (s *StateDB) LoadRecentSessions() ([]*RecentSessionRow, error)

LoadRecentSessions returns all recent sessions ordered by most recently deleted.

func (*StateDB) LoadWatcherByID added in v1.9.2

func (s *StateDB) LoadWatcherByID(id string) (*WatcherRow, error)

LoadWatcherByID returns the watcher row with the given id, or (nil, nil) if absent. Used by cross-profile migration to copy referenced watcher rows from src to dst before inserting watcher_events (the events table FK- references watchers(id) with foreign_keys=on).

func (*StateDB) LoadWatcherByName added in v1.5.1

func (s *StateDB) LoadWatcherByName(name string) (*WatcherRow, error)

LoadWatcherByName returns the watcher with the given name, or nil if not found. A missing watcher is not an error; (nil, nil) is returned.

func (*StateDB) LoadWatcherEvents added in v1.5.1

func (s *StateDB) LoadWatcherEvents(watcherID string, limit int) ([]WatcherEventRow, error)

LoadWatcherEvents returns up to limit events for the given watcher, ordered most recent first.

func (*StateDB) LoadWatcherEventsForSession added in v1.9.2

func (s *StateDB) LoadWatcherEventsForSession(sessionID string) ([]*WatcherEventRow, error)

LoadWatcherEventsForSession returns every watcher_events row whose session_id OR triage_session_id matches sessionID. The dual match preserves triage links when migrating a triage target.

func (*StateDB) LoadWatchers added in v1.5.1

func (s *StateDB) LoadWatchers() ([]*WatcherRow, error)

LoadWatchers returns all watchers ordered by name.

func (*StateDB) LookupWatcherEventSessionByDedupKey added in v1.5.1

func (s *StateDB) LookupWatcherEventSessionByDedupKey(watcherID, dedupKey string) (string, error)

LookupWatcherEventSessionByDedupKey queries the session_id for a specific event. Returns ("", nil) if no matching event exists or session_id is empty.

func (*StateDB) LookupWatcherIDByDedupKey added in v1.5.1

func (s *StateDB) LookupWatcherIDByDedupKey(dedupKey string) (string, error)

LookupWatcherIDByDedupKey returns the watcher_id for the first watcher_events row with the given dedup_key. Returns an error if no row is found. Used by the triageReaper to correlate a result.json back to its originating event (D-08).

func (*StateDB) Migrate

func (s *StateDB) Migrate() error

Migrate creates tables if they don't exist and runs any pending migrations.

func (*StateDB) ReadAllStatuses

func (s *StateDB) ReadAllStatuses() (map[string]StatusRow, error)

ReadAllStatuses returns status + acknowledged flag for every instance.

func (*StateDB) RegisterInstance

func (s *StateDB) RegisterInstance(isPrimary bool) error

RegisterInstance records this process as an active TUI instance.

func (*StateDB) ResignPrimary added in v0.11.1

func (s *StateDB) ResignPrimary() error

ResignPrimary clears the is_primary flag for this process.

func (*StateDB) SaveGroup added in v1.9.2

func (s *StateDB) SaveGroup(g *GroupRow) error

SaveGroup inserts or replaces a single group row.

func (*StateDB) SaveGroups

func (s *StateDB) SaveGroups(groups []*GroupRow) error

SaveGroups replaces all groups in a single transaction.

func (*StateDB) SaveInstance

func (s *StateDB) SaveInstance(inst *InstanceRow) error

SaveInstance inserts or replaces a single instance.

func (*StateDB) SaveInstances

func (s *StateDB) SaveInstances(insts []*InstanceRow) error

SaveInstances inserts or replaces multiple instances in a single transaction. It also removes any rows from the database that are not in the provided list, ensuring deleted sessions don't reappear on reload.

Wrapped in withBusyRetry because parallel writers (CLI + TUI + heartbeat daemons) contend on the WAL writer slot. The whole save is idempotent at the row level (INSERT OR REPLACE + DELETE WHERE NOT IN), so retrying the outer transaction on SQLITE_BUSY is safe. Part of the v1.9.1 #909 fix.

func (*StateDB) SaveRecentSession added in v0.20.0

func (s *StateDB) SaveRecentSession(row *RecentSessionRow) error

SaveRecentSession inserts or replaces a recent session entry, then prunes to 20.

The INSERT and the prune are bundled in a single transaction so a crash between them cannot leave the table over-budget (the prune always sees the just-inserted row). The whole transaction runs under withBusyRetry to absorb transient SQLITE_BUSY from concurrent writers — pre-fix, these caused user-visible "recent session lost" reports under contention.

func (*StateDB) SaveWatcher added in v1.5.1

func (s *StateDB) SaveWatcher(w *WatcherRow) error

SaveWatcher inserts or replaces a watcher row.

func (*StateDB) SaveWatcherEvent added in v1.5.1

func (s *StateDB) SaveWatcherEvent(watcherID, dedupKey, sender, subject, routedTo, sessionID string, maxEvents int) (bool, error)

SaveWatcherEvent inserts an event with dedup via INSERT OR IGNORE. Returns true if the row was inserted (new event), false if it was a duplicate. Prunes to maxEvents after successful insert.

Retries on SQLITE_BUSY: concurrent INSERTs across connections can trip the write lock even with WAL + busy_timeout if the driver surfaces BUSY before the backoff completes. Retries are cheap because the operation is idempotent (INSERT OR IGNORE).

func (*StateDB) SetAcknowledged

func (s *StateDB) SetAcknowledged(id string, ack bool) error

SetAcknowledged sets or clears the acknowledged flag for an instance.

func (*StateDB) SetMeta

func (s *StateDB) SetMeta(key, value string) error

SetMeta sets a key-value pair in the metadata table.

func (*StateDB) Touch

func (s *StateDB) Touch() error

Touch updates a metadata timestamp that other instances can poll to detect changes.

func (*StateDB) UnregisterInstance

func (s *StateDB) UnregisterInstance() error

UnregisterInstance removes this process from the heartbeat table.

func (*StateDB) UpdateWatcherEventRoutedTo added in v1.5.1

func (s *StateDB) UpdateWatcherEventRoutedTo(watcherID, dedupKey, routedTo, triageSessionID string) error

UpdateWatcherEventRoutedTo updates the routed_to and triage_session_id columns for the row matching (watcher_id, dedup_key). Returns a wrapped error if no row matches (0 rows affected), allowing the caller to distinguish "update OK" from "event not found".

Wrapped in withBusyRetry to match its sister SaveWatcherEvent — both are short idempotent writes against watcher_events called from concurrent engine + triage_reaper paths. Without retry, SQLITE_BUSY from a sister INSERT silently drops the routed_to update and the watcher event sticks in "unrouted" forever.

func (*StateDB) UpdateWatcherEventSessionID added in v1.5.1

func (s *StateDB) UpdateWatcherEventSessionID(watcherID, dedupKey, sessionID string) error

UpdateWatcherEventSessionID sets the session_id on an existing watcher event. Returns an error if no matching row exists (0 rows affected).

func (*StateDB) UpdateWatcherStatus added in v1.5.1

func (s *StateDB) UpdateWatcherStatus(watcherID string, status string) error

UpdateWatcherStatus sets the status field on a watcher row. Returns an error if no watcher with the given ID exists.

func (*StateDB) WriteClaudeSessionBinding added in v1.9.28

func (s *StateDB) WriteClaudeSessionBinding(id, sessionID string, detectedAt time.Time) error

WriteClaudeSessionBinding atomically updates claude_session_id and claude_detected_at inside the tool_data JSON column for the given instance. Used by the hook-rebind path (UpdateHookStatus → bindClaudeSessionFromHook) to persist the new session ID without a whole-row INSERT OR REPLACE — which would clobber any concurrent writes to other tool_data fields by writers holding a stale snapshot of the instance.

PERSIST-12 (see instance.go:bindClaudeSessionFromHook doc comment) originally deferred this to an external "save cycle", but none of the three UpdateHookStatus callers (TUI tick, web refresh, CLI status refresh) actually call Save after rebind. Without this targeted write, tool_data.claude_session_id stays pinned at the pre-/clear UUID indefinitely for any DB-direct consumer (claudopticon, etc.) — and the lifecycle log accumulates fresh "rebind" entries forever because concurrent processes keep reloading the stale row from disk and clobbering the in-memory mutation.

Wrapped in withBusyRetry: SQLite serializes writers through a single write lock, so under contention with WriteStatus / SaveInstance / heartbeat writers a transient SQLITE_BUSY would otherwise drop this update — matching the WriteStatus rationale above.

func (*StateDB) WriteCodexSessionBinding added in v1.9.29

func (s *StateDB) WriteCodexSessionBinding(id, sessionID string, detectedAt time.Time) error

WriteCodexSessionBinding is the Codex counterpart of WriteClaudeSessionBinding: it atomically rewrites $.codex_session_id and $.codex_detected_at inside the tool_data JSON column without touching any unrelated keys. See WriteClaudeSessionBinding for the full rationale (PERSIST-12, json_set vs. tool_data = ?, withBusyRetry). This sibling exists because the Codex rebind path in bindCodexSessionFromHook has the same in-memory-only mutation shape that the Claude fix in #1140 addressed — tracked as #1139.

func (*StateDB) WriteGeminiSessionBinding added in v1.9.29

func (s *StateDB) WriteGeminiSessionBinding(id, sessionID string, detectedAt time.Time) error

WriteGeminiSessionBinding is the Gemini counterpart of WriteClaudeSessionBinding. See that function's doc comment for the PERSIST-12 / json_set / withBusyRetry rationale; the Gemini rebind path in bindGeminiSessionFromHook had the same persistence gap (#1139).

func (*StateDB) WriteStatus

func (s *StateDB) WriteStatus(id, status, tool string) error

WriteStatus updates the status and tool for an instance.

Wrapped in withBusyRetry: the transition daemon (#755 family) calls this under contention with other writers (heartbeat, status poller, hook handler). Without retry, transient SQLITE_BUSY drops the user-visible status update and the TUI shows stale state.

type StatusRow

type StatusRow struct {
	Status       string
	Tool         string
	Acknowledged bool
}

StatusRow holds status + acknowledgment for a session.

type WatcherEventRow added in v1.5.1

type WatcherEventRow struct {
	ID              int64
	WatcherID       string
	DedupKey        string
	Sender          string
	Subject         string
	RoutedTo        string
	SessionID       string
	TriageSessionID string
	CreatedAt       time.Time
}

WatcherEventRow represents a single event row from the watcher_events table.

type WatcherRow added in v1.5.1

type WatcherRow struct {
	ID         string
	Name       string
	Type       string
	ConfigPath string
	Status     string
	Conductor  string
	CreatedAt  time.Time
	UpdatedAt  time.Time
}

WatcherRow represents a watcher row in the database.

Jump to

Keyboard shortcuts

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