Documentation
¶
Overview ¶
Package persist is the durable source of truth for session state (ADR-003): one meta.json per session under <root>/<id>/, written atomically so a crash mid-write is observed as old-or-new and never torn, and so a single corrupt session file never prevents scanning the rest. It carries invariant S8 (atomic-durable state) and part of ADR-004 (path-safe session ids).
Index ¶
Constants ¶
const SchemaVersion = 1
SchemaVersion is the meta.json schema this build writes. Older versions are migrated forward on read (see Load); newer versions are rejected loudly.
Variables ¶
This section is empty.
Functions ¶
func DefaultDir ¶
DefaultDir returns the default state directory per the XDG Base Directory spec: $XDG_STATE_HOME/swarm/sessions when XDG_STATE_HOME is an absolute path, otherwise $HOME/.local/state/swarm/sessions (R-1).
func FilterEnv ¶
FilterEnv returns the allowlisted subset of env in input order, each entry passed through verbatim (the whole KEY=VALUE, including values that themselves contain '='). The variable name is the text before the first '='.
func ScrubRemoteControl ¶
ScrubRemoteControl returns env without any remote-control variable, in input order. It is applied at the last gate before exec (internal/shim), so it holds regardless of what composed the environment upstream.
func ValidID ¶
ValidID reports whether id is path-safe per ADR-004: it must match idRE and must not be ".", "..", or start with "-", so it can never escape a store root, traverse a path, or be mistaken for a flag. This is the single source of truth for the path-safe id pattern; internal/protocol and internal/worktree both delegate to it instead of duplicating the regex.
Types ¶
type Meta ¶
type Meta struct {
SchemaVersion int `json:"schema_version"`
ID string `json:"id"`
AgentType string `json:"agent_type"`
Name string `json:"name"` // user-provided session label; "" falls back to AgentType at display time
// NameSetAt is when Name was last stamped, by a swarm rename or by adopting the name
// the CLI itself shows for the session (ADR-022, newest wins). Additive without a
// schema-version bump for the same rollback reason as AgentCwd below; a zero value
// on an older record means "never stamped", which any CLI-published name beats.
NameSetAt time.Time `json:"name_set_at"`
Tag string `json:"tag"` // user-assigned grouping label; empty means untagged
Cwd string `json:"cwd"`
// AgentCwd is additive and DELIBERATELY DID NOT BUMP SchemaVersion. An adversarial
// review proposed bumping it, reasoning that a rolled-back old daemon would rewrite
// meta.json from its own struct and drop the field. That is true, and bumping is the
// worse cure: Load REFUSES any meta whose schema_version exceeds the build's (see
// below), so a v2 stamp would make a rolled-back daemon fail to load EVERY session
// written by the newer one, rather than merely losing an optional field. Dropping
// AgentCwd degrades ProviderCwd to Cwd -- today's behaviour before this field existed
// -- and the value is re-stamped on the next launch. Degraded beats unloadable.
AgentCwd string `json:"agent_cwd"` // resolved agent cwd when a pre-launch hook overrode Cwd; "" otherwise. See ProviderCwd.
LaunchOptions map[string]string `json:"launch_options"`
Env []string `json:"env"`
CreatedAt time.Time `json:"created_at"`
Status status.Status `json:"status"`
// GroupEnteredAt is when Status last crossed a status.Derive display-group
// boundary. It is additive without a schema-version bump for the same rollback
// compatibility reason as AgentCwd above. Older records use LastActivity (then
// CreatedAt) as a stable best-effort fallback until their next write.
GroupEnteredAt time.Time `json:"group_entered_at"`
LastActivity time.Time `json:"last_activity"`
ShimPID int `json:"shim_pid"`
ShimStartTime int64 `json:"shim_start_time"`
ConversationID string `json:"conversation_id"`
// ShimWireVersion is the shimwire.Version compiled into the shim this session
// runs under, stamped at spawn: the daemon spawns shims from its own
// executable, so at that instant the two agree, and afterwards the shim
// OUTLIVES upgrades of the binary on disk. Activation (lifecycle R3) gates a
// wire-bump install on this field with pure disk reads -- the compat matrix
// pins old-daemon x new-shim as ProcessLost, so a new binary must never land
// under a daemon whose live shims it cannot serve. Additive without a schema
// bump (the AgentCwd reason); 0 on a pre-R3 session means "unknown", which
// gates conservatively.
ShimWireVersion int `json:"shim_wire_version,omitempty"`
// BackendPlanError is the reason this session launched with NO backend when its
// adapter declared one (daemon.BackendPlanner returned an error): the agent's PTY
// runs, but there is no app-server for the daemon to attach to. Persisted so the
// failure has a surface (`swarm ls`, the TUI, `swarm doctor`) instead of one
// daemon.log line -- the 2026-08-28 "attach failure" incident took a debugging
// session to root-cause for want of exactly this field. Additive without a
// schema-version bump for the same rollback reason as AgentCwd above; "" means
// the backend planned fine, or the session's CLI needs none (the ordinary case,
// indistinguishable by design -- absence of a backend is not a defect).
BackendPlanError string `json:"backend_plan_error,omitempty"`
ExitCode *int `json:"exit_code"`
ResumedFrom string `json:"resumed_from"`
SpawnedFrom string `json:"spawned_from"` // local id of the session that spawned this one (ADR-010 D4)
SpawnIntent string `json:"spawn_intent"` // "handoff" or "delegate"; empty when SpawnedFrom is
Supervision string `json:"supervision"` // "passive", "manual" or "none" on a handoff child (ADR-010 Amendment 3 C1); empty otherwise
}
Meta is the persisted state of a single session (S-2). Every field carries an explicit snake_case JSON tag: the on-disk key set is the durable data contract, so tags are deliberately not omitempty — the key is always present.
func (Meta) EffectiveGroupEnteredAt ¶
EffectiveGroupEnteredAt returns the durable ordering instant for a session. Records written before GroupEnteredAt existed fall back deterministically, without rewriting every meta.json merely because the daemon restarted.
func (Meta) ProviderCwd ¶
ProviderCwd is the working directory the AGENT actually ran in: AgentCwd when a pre-launch hook resolved one, and Cwd otherwise. The two differ for a worktree-isolated session (Epic 12), where Cwd is the repo the launch was requested in and the agent runs under <repo>/.swarm/worktrees/<name-slug> (or <id> when unnamed; an id suffix is added when the friendly path is already occupied).
An agent CLI files its own history under an encoding of the directory it ran in, so every provider-facing path derivation must ask for this rather than read Cwd. Cwd keeps its own meaning untouched and is what worktree teardown stays anchored to; AgentCwd supplies the exact checkout path to remove.
type Store ¶
type Store struct {
// contains filtered or unexported fields
}
Store is a session store rooted at a single state directory. The root and every session dir are 0700; committed meta.json files are 0600.
func NewStore ¶
NewStore returns a Store rooted at dir, creating dir (0700) if it is missing. A pre-existing root is hardened to 0700 as well: MkdirAll leaves an existing directory's mode untouched, so the chmod is unconditional.
func (*Store) Load ¶
Load reads and decodes <root>/<id>/meta.json. Older schema versions are migrated forward in the returned Meta (no file rewrite — G6); a future schema version, a corrupt file, or a missing session all return an error.
func (*Store) Save ¶
Save writes m atomically to <root>/<m.ID>/meta.json: marshal, write a 0600 temp file in the session dir, then rename over meta.json. A crash before the rename leaves the previous meta.json (or nothing) intact — never a torn file.
Save is the single choke point that enforces two on-disk invariants: the env is allowlist-filtered (ADR-004) and the schema version is stamped to the current build's, so a caller can persist neither an unfiltered secret nor an arbitrary schema version.
Durability model is process-crash (ADR-003); no parent-directory fsync (power-loss out of scope).
func (*Store) Scan ¶
Scan rebuilds the roster purely by directory scan (ADR-003: roster.json is a disposable index). It returns every session whose meta.json decodes cleanly and silently skips non-session entries and corrupt or future-version files, so one bad session never hides the rest (S8 isolation half).