persist

package
v0.13.11 Latest Latest
Warning

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

Go to latest
Published: Aug 30, 2026 License: MIT Imports: 8 Imported by: 0

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

View Source
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

func DefaultDir() (string, error)

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

func FilterEnv(env []string) []string

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

func ScrubRemoteControl(env []string) []string

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

func ValidID(id string) bool

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

func (m Meta) EffectiveGroupEnteredAt() time.Time

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

func (m Meta) ProviderCwd() string

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

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

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) Delete

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

Delete removes a session's whole directory (R-3 retention delete).

func (*Store) Load

func (s *Store) Load(id string) (Meta, error)

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

func (s *Store) Save(m Meta) error

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

func (s *Store) Scan() ([]Meta, error)

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).

Jump to

Keyboard shortcuts

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