Documentation
¶
Overview ¶
Package store owns Muster's SQLite database: opening it in WAL mode, applying migrations, and the hand-written SQL for the kv and event tables. Nothing here knows about Claude Code's wire formats — callers pass in neutral values.
Index ¶
- func Migrate(ctx context.Context, db *sql.DB) error
- type Event
- type EventSummary
- type GroupLayout
- type GroupRow
- type InsertSessionParams
- type Repo
- type SessionRow
- type Store
- func (s *Store) BumpIDWatermark(ctx context.Context, minID int64) error
- func (s *Store) Close() error
- func (s *Store) DeleteGroup(ctx context.Context, id int64, layout GroupLayout) error
- func (s *Store) DeleteSession(ctx context.Context, id int64) error
- func (s *Store) EventSummary(ctx context.Context, sessionID int64) (EventSummary, error)
- func (s *Store) GetRepo(ctx context.Context, id int64) (Repo, error)
- func (s *Store) GetSession(ctx context.Context, id int64) (SessionRow, error)
- func (s *Store) InsertEvent(ctx context.Context, ev Event) error
- func (s *Store) InsertGroup(ctx context.Context, name string, pos int64, layout GroupLayout) (GroupRow, error)
- func (s *Store) InsertSession(ctx context.Context, p InsertSessionParams) (SessionRow, error)
- func (s *Store) InsertUsageModelSamples(ctx context.Context, rows []UsageModelSampleRow) error
- func (s *Store) InsertUsageSample(ctx context.Context, r UsageSampleRow) error
- func (s *Store) KVGet(ctx context.Context, key string) (string, bool, error)
- func (s *Store) KVSet(ctx context.Context, key, value string) error
- func (s *Store) ListGroups(ctx context.Context) ([]GroupRow, error)
- func (s *Store) ListRepos(ctx context.Context) ([]Repo, error)
- func (s *Store) ListSessions(ctx context.Context) ([]SessionRow, error)
- func (s *Store) ReadUngroupedLayout(ctx context.Context) (layout UngroupedLayout, ok bool, err error)
- func (s *Store) SaveGroups(ctx context.Context, layout GroupLayout) error
- func (s *Store) TouchRepo(ctx context.Context, p TouchRepoParams) (Repo, bool, error)
- func (s *Store) UpdateSession(ctx context.Context, row SessionRow) error
- func (s *Store) UpdateSnapshot(ctx context.Context, id int64, text string, at time.Time) error
- func (s *Store) UpsertRepo(ctx context.Context, p UpsertRepoParams) (Repo, bool, error)
- type TouchRepoParams
- type UngroupedLayout
- type UpsertRepoParams
- type UsageModelSampleRow
- type UsageSampleRow
Constants ¶
This section is empty.
Variables ¶
This section is empty.
Functions ¶
Types ¶
type Event ¶
type Event struct {
ClaudeSessionID string
Type string
PromptID *string
ToolUseID *string
MusterSession *int64
TmuxPane *string
Payload []byte // verbatim inner payload JSON
// SessionID is the Muster session (session.id) this event routed to, resolved by
// the caller (internal/server/ingest.go, via internal/session's binding map)
// before persistence. Nil means unrouted: an unknown Claude session id, or a
// stale/absent envelope — never guessed at by cwd.
SessionID *int64
}
Event is a fully-parsed, storage-ready ingest event. Callers (internal/server) are responsible for translating whatever Claude-Code-specific parsing produced into this neutral shape — this package never sees Claude Code's own hook/status field names.
type EventSummary ¶
type EventSummary struct {
FirstSeq *int64
LastSeq *int64
Count int
LastReceivedAt *time.Time // nil when Count == 0
RecentTypes []string // last 10 event.type values, oldest-first
}
EventSummary is the per-Muster-session event-routing summary behind the issue-capture snapshot's allowlisted session.events fields (kb:adr/issue-payload-allowlist-never-dump) — bounds, a count and the last 10 event.type values, never a payload.
type GroupLayout ¶ added in v0.20.0
type GroupLayout struct {
Groups []GroupRow
Ungrouped UngroupedLayout
}
GroupLayout is every group's mutable fields plus the Ungrouped section, written wholesale: a group list is never more than a dozen long, so each change rewrites it in one transaction rather than diffing rows.
type GroupRow ¶ added in v0.20.0
GroupRow is the persisted shape of a rail group (kb:ref/data-model), the storage twin of internal/session.Group.
type InsertSessionParams ¶
type InsertSessionParams struct {
RepoID int64
Directory string
Branch *string
IsWorktree bool
Title *string
PermissionMode string
Model *string
FirstLaunchHere bool
// PendingResumeClaudeSessionID is the Claude session id a resume-from-list spawn was
// launched to resume; nil for an ordinary launch.
PendingResumeClaudeSessionID *string
// RailPos is the manual rail position for the new session
// (kb:adr/rail-order-daemon-owned-per-session-fields): the caller
// (internal/session.Manager, under its lock) hands in its own monotonic nextRailPos
// counter so the newest session lands at the bottom of the unpinned block. Pinned
// always starts false.
RailPos int64
// GroupID is the rail group the new session joins; nil = Ungrouped. The group row must
// already exist (the session.group_id foreign key).
GroupID *int64
// MinID floors the allocated id above this value (0 = no floor) — the launcher
// passes its MaxSessionID probe of the tmux socket here, so a new row never lands on
// an id an orphaned "muster-<N>" tmux session already owns
// (kb:adr/lifecycle-session-ids-monotonic-never-reused).
MinID int64
EffortRequested *string
EffortSeed *string
}
InsertSessionParams seeds a new session row: state "started", the permission-mode latch seeded with source "seed", alive, created now. TmuxTarget is a placeholder ("") until the caller records the real tmux window via UpdateSession — the row must exist (and get an id) before the tmux spawn that needs that id in the pane environment.
type Repo ¶
type Repo struct {
ID int64
Path string
Name string
IsGit bool
Pinned bool
LastLaunchedAt time.Time
LaunchCount int
LastModel *string
LastPermissionMode *string
LastEffort *string
CreatedAt time.Time
}
Repo is one row of the MRU directory picker (kb:anchor/repos.list), also the home of the per-directory launch defaults (kb:ref/data-model).
type SessionRow ¶
type SessionRow struct {
ID int64
TmuxTarget string
TmuxPane *string
ClaudeSessionID *string
PendingResumeClaudeSessionID *string
RepoID int64
Directory string
Branch *string
IsWorktree bool
Title *string
State string
StateSince time.Time
PermissionMode string
PermissionModeSource string
Model *string
ModelDisplayName *string // NULL = derive the display name from Model (the raw id)
Compactions int
AttentionReason *string
AttentionSince *time.Time
FailureError *string
FailureMessage *string
LastActivity *string
Alive bool
EndedAt *time.Time
FirstLaunchHere bool
CreatedAt time.Time
// Context gauge (kb:adr/usage-context-gauge-shows-tokens-and-compactions): always
// all-nil or all-non-nil. NULL = unknown — a session that hasn't yet received a
// post-first-response status post.
ContextUsedPct *float64
ContextTotalInputTokens *int64
ContextWindowSize *int64
CacheExpiresAt *time.Time
CacheTTLSeconds *int64
CacheRequests *int64
CacheReportedExpiresAt *time.Time
// LastSnapshot/LastSnapshotAt (kb:anchor/sessions.pane): the last capture-pane text
// and when it was captured — always both-nil or both-non-nil. Display source only,
// never read by the state machine; never logged (may hold prompt text).
LastSnapshot *string
LastSnapshotAt *time.Time
// Pinned/RailPos (kb:adr/rail-order-daemon-owned-per-session-fields): user-owned
// rail order. Display-only — never read by the state machine or the status path.
Pinned bool
RailPos int64
// TitleOverride (kb:adr/rename-muster-owned-title-override-wins): the user's rename
// via PUT .../title. Display-only, nullable, never touched by the status path —
// wins over Title in the wire "title" (internal/session.Session.DisplayTitle()).
TitleOverride *string
// TranscriptPath/PlanPath/PlanExists (docs/features/reader/spec.md): the latest
// transcript path a routed hook named, and the plan derived from it. Display-only,
// never read by the state machine. TranscriptPath is NULL until a hook sets it.
// PlanPath is NULL until a transcript has named a plan; once set, a planless scan
// keeps it rather than nulling it back out (kb:adr/reader-plan-sticky-once-named).
// PlanExists defaults to 0.
TranscriptPath *string
PlanPath *string
PlanExists bool
// Unread (kb:adr/rail-unread-inferred-from-live-terminal-client) is true iff the
// session's turn closed with no terminal client attached, since cleared by a
// non-idle transition or an attach. LastPrompt
// (kb:adr/rail-activity-line-turn-aware-default-with-pref) is the user's most recent
// prompt, truncated to 200 chars, NULL until a first prompt or after /clear. Both
// display-only, never read by the state machine.
Unread bool
LastPrompt *string
// BackgroundTasks (kb:adr/lifecycle-background-tasks-count-not-state) is the running
// background work the latest Stop reported; AttentionAgent is the subagent id that
// raised the current wait, NULL for the main agent or no wait. Neither is read by
// anything but the state machine's own persistence round trip and the wire.
BackgroundTasks int
AttentionAgent *string
// ClaudeDir (kb:adr/lifecycle-card-shows-launch-directory-marks-claude-elsewhere) is the
// working directory Claude last reported; NULL = nothing reported since the last launch
// or resume. Display-only, never read by the state machine.
ClaudeDir *string
// GroupID (kb:spec/rail) is the rail group the session belongs to, nil = the Ungrouped
// section. Display-only, never read by the state machine or the status path.
GroupID *int64
EffortKnown bool
EffortLevel *string
EffortRequested *string
EffortWarningRequested *string
EffortWarningActual *string
EffortWarningModel *string
}
SessionRow is the persisted shape of a session (kb:ref/data-model). It is the storage-level twin of internal/session.Session; internal/server converts between the two so this package stays free of the kb:anchor/state state machine's own vocabulary.
type Store ¶
type Store struct {
// contains filtered or unexported fields
}
Store owns the daemon's single SQLite connection.
func Open ¶
Open opens (creating if absent) the SQLite database at path, enables WAL mode, and applies any pending migrations. A single connection is used deliberately: it serialises every writer, so SQLite's own one-writer-at-a-time rule never surfaces as SQLITE_BUSY. log is scanSession's corrupt-row warnings sink (see decodeRowTime/decodeRowTimePtr) — wired at construction, the same way session.NewManager takes Config.Logger and every internal/server feature constructor takes a trailing log zerolog.Logger. The zero value (zerolog.Logger{}) is a working no-op, so a caller with nothing to wire (most tests) can pass one and a corrupt row just logs nowhere.
func (*Store) BumpIDWatermark ¶ added in v0.13.2
BumpIDWatermark raises the persisted session id watermark to at least minID, leaving it untouched if it's already higher: an unknown "muster-<N>" or "muster-<N>-shell" tmux session found on the socket during reconcile must still block id N from ever being allocated to a new row, even though no row names it (kb:adr/lifecycle-session-ids-monotonic-never-reused).
The read and the conditional write run in one transaction: Open's single connection (SetMaxOpenConns(1)) means a held *sql.Tx has exclusive use of it until Commit/Rollback, so this can never interleave with InsertSession's own watermark-reading transaction — a bump can never lower a watermark InsertSession has concurrently raised, whichever one actually runs first.
func (*Store) DeleteGroup ¶ added in v0.20.0
DeleteGroup removes group id and, in the same transaction, rewrites the layout that remains (layout.Groups excludes id). The session.group_id foreign key's ON DELETE SET NULL is only a guard: the caller moves members out before it deletes.
func (*Store) DeleteSession ¶
DeleteSession removes a session row — the launch-failure rollback path: on a spawn failure the row is deleted rather than left behind.
func (*Store) EventSummary ¶
EventSummary reads the routing summary for sessionID (event.session_id — the Muster session id, not claude_session_id). A session with no routed events yet returns a zero-value-ish EventSummary: Count 0, both seq bounds and LastReceivedAt nil, RecentTypes nil.
func (*Store) GetRepo ¶
GetRepo looks up a repo row by id. No production code calls this today — production always already has the row it needs from ListRepos or UpsertRepo's own return value. It exists for test setup that wants one row back after UpsertRepo without listing every repo.
func (*Store) GetSession ¶
func (*Store) InsertEvent ¶
InsertEvent persists ev, assigning it the next seq for its ClaudeSessionID as part of the same insert (MAX(seq)+1 scoped to that session). Safe under the single ingest worker; it is not a general-purpose concurrent seq allocator.
func (*Store) InsertGroup ¶ added in v0.20.0
func (s *Store) InsertGroup(ctx context.Context, name string, pos int64, layout GroupLayout) (GroupRow, error)
InsertGroup inserts a new group at pos and, in the same transaction, rewrites the layout the insert displaced (every other group's pos and the Ungrouped section), so a crash can never leave two sections on one place. layout.Groups names the other groups, not the new one. The id is SQLite's rowid and may be reused after a delete.
func (*Store) InsertSession ¶
func (s *Store) InsertSession(ctx context.Context, p InsertSessionParams) (SessionRow, error)
InsertSession allocates the new row's id as max(COALESCE(MAX(id) from session, 0), the persisted watermark, p.MinID) + 1, inserted with that id explicit, and persists the new watermark — all in one transaction (kb:adr/lifecycle-session-ids-monotonic-never-reused). This is what makes an id un-reissuable: SQLite's ROWID (no AUTOINCREMENT on this table) would otherwise reuse max(rowid)+1 the moment the highest row is deleted (launch rollback, Remove, reconcile's sweep), and that id doubles as both the tmux session name and the ingest bearer credential MUSTER_SESSION=<id>.
func (*Store) InsertUsageModelSamples ¶
func (s *Store) InsertUsageModelSamples(ctx context.Context, rows []UsageModelSampleRow) error
InsertUsageModelSamples persists rows in one transaction sharing a single receipt timestamp — one row per window, all stamped `at` the same instant. An empty slice is a no-op, not an error — Record only calls this when the list actually changed and non-empty input is never guaranteed by callers in general.
func (*Store) InsertUsageSample ¶
func (s *Store) InsertUsageSample(ctx context.Context, r UsageSampleRow) error
InsertUsageSample persists one usage_sample row, stamping its receipt time itself (RFC3339Nano UTC) rather than trusting a caller-supplied timestamp.
func (*Store) KVGet ¶
KVGet reads a key from the kv table. The bool return reports whether the key existed.
func (*Store) ListGroups ¶ added in v0.20.0
ListGroups returns every group row (order unspecified — the caller sorts by pos), for daemon startup reload.
func (*Store) ListRepos ¶
ListRepos returns every repo row ordered `pinned DESC, last_launched_at DESC` (kb:anchor/repos.list), for the launch modal's MRU list.
func (*Store) ListSessions ¶
func (s *Store) ListSessions(ctx context.Context) ([]SessionRow, error)
ListSessions returns every session row (order unspecified — the client sorts, kb:anchor/ws.snapshot), for daemon startup reload.
func (*Store) ReadUngroupedLayout ¶ added in v0.20.0
func (s *Store) ReadUngroupedLayout(ctx context.Context) (layout UngroupedLayout, ok bool, err error)
ReadUngroupedLayout returns the persisted Ungrouped layout; ok is false when none was ever written, which the caller reads as last place, expanded. An unparseable value is treated as absent: a corrupt kv row must never stop the daemon loading its groups.
func (*Store) SaveGroups ¶ added in v0.20.0
func (s *Store) SaveGroups(ctx context.Context, layout GroupLayout) error
SaveGroups rewrites every group in layout and the Ungrouped section in one transaction: the one write behind rename, collapse, reorder and collapse-all.
func (*Store) TouchRepo ¶ added in v0.19.0
TouchRepo is UpsertRepo's sibling for a resume-from-list launch: it advances launch_count and last_launched_at exactly like UpsertRepo, but never writes last_model or last_permission_mode — a resume from the list carries no model or Start-in choice of its own, and must not clobber whatever a real launch last remembered there. A never- before-seen directory's row is created with both columns null (the Schema Changes note's "a query, not a migration"). Same single-statement-atomic reasoning as UpsertRepo: "was this call's branch the INSERT" is read off the returned launch_count, not a follow-up existence check.
func (*Store) UpdateSession ¶
func (s *Store) UpdateSession(ctx context.Context, row SessionRow) error
UpdateSession writes back the full row (whole-object, matching the whole-object sessionUpsert design — kb:anchor/ws.session) after any mutation.
func (*Store) UpdateSnapshot ¶
UpdateSnapshot persists only the last captured pane screen for id (kb:anchor/sessions.pane), separate from UpdateSession's whole-row write since it happens on every liveness tick for every alive session — called only when the captured text actually changed (same pattern as usage_sample). Never logged (may hold prompt text).
func (*Store) UpsertRepo ¶
UpsertRepo creates the repo row for p.Path if absent (launch_count starts at 1) or updates the launch defaults and increments launch_count if present. The bool return is true iff the row was newly created — the source of the Session's firstLaunchHere field (kb:anchor/ws.session).
A single `INSERT ... ON CONFLICT(path) DO UPDATE ... RETURNING` statement, not a SELECT followed by an INSERT/UPDATE: the prior two-step check-then-insert let two concurrent launches into the same never-before-seen directory both see "not found" and both attempt an INSERT, so the loser hit `UNIQUE constraint failed: repo.path` and sessionLauncher.Launch surfaced a plain 500 (daemon-tests, TestLauncher_ConcurrentLaunchesForTheSameDirectoryProduceTwoDistinctRows). The store already serializes all writes onto one connection (SetMaxOpenConns(1), this package's CLAUDE.md), so a single statement is atomic with respect to every other caller; two separate round trips through the connection pool were not. "Was this call's branch the INSERT" is read off the returned launch_count rather than a follow-up existence check (which would reopen the same race): every pre-existing row already has launch_count >= 1 from its own first insert, so the UPDATE branch's `+ 1` can never produce 1 — a returned launch_count of 1 is only reachable via the INSERT branch.
type TouchRepoParams ¶ added in v0.19.0
TouchRepoParams are the values a resume-from-list launch records on the repo row — MRU and launch count only.
type UngroupedLayout ¶ added in v0.20.0
UngroupedLayout is the Ungrouped section's place among every section and its collapsed flag.
type UpsertRepoParams ¶
type UpsertRepoParams struct {
Path string
Name string
IsGit bool
Model string
PermissionMode string
Effort *string
}
UpsertRepoParams are the values a launch records on the repo row.
type UsageModelSampleRow ¶
UsageModelSampleRow is one persisted usage_model_sample row (kb:adr/usage-model-window-polled-from-oauth-api, kb:ref/data-model) — internal/usage's storage-level twin, keeping this package free of internal/usage's own domain vocabulary. At is not part of this shape: every row in one InsertUsageModelSamples batch shares a single receipt timestamp, stamped by the store itself (mirrors InsertUsageSample), not trusted from a caller.
type UsageSampleRow ¶
type UsageSampleRow struct {
ModelID string
ModelDisplayName string
FiveHourPct float64
FiveHourResetsAt time.Time
SevenDayPct float64
SevenDayResetsAt time.Time
Source string
}
UsageSampleRow is one persisted usage_sample row (kb:ref/data-model) — internal/usage's storage-level twin, keeping this package free of internal/usage's own domain vocabulary. A row is only ever inserted for a complete sample; a partial sample is never persisted, so every field here is required.