state

package
v0.6.7 Latest Latest
Warning

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

Go to latest
Published: Aug 5, 2026 License: MIT Imports: 10 Imported by: 0

Documentation

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func AppendParticipant

func AppendParticipant(path, userID string) (added bool, err error)

AppendParticipant records userID in the append-only journal at path (one id per line). It is idempotent: if userID is already present, the file is left untouched and added is false. A missing parent directory is created.

Append uses O_APPEND so concurrent appenders (the bridge child) never race with the daemon's reads; the daemon only reads this file, never writes it.

func AppendTranscript

func AppendTranscript(path string, e TranscriptEntry) error

AppendTranscript appends one JSON-line entry. Best-effort: O_APPEND so the daemon's single writer never races a read; a missing parent is created. When the file crosses transcriptMaxBytes it is compacted to its newest entries, so scrollback stays cheap and the transcript is bounded.

func ParticipantsPath

func ParticipantsPath(dir, name string) string

ParticipantsPath returns the journal path for session name under dir (dir/participants/<name>.log). Both the supervisor (which tells the bridge where to append) and the handler (which reads it) call this so they agree.

func ReadParticipants

func ReadParticipants(path string) []string

ReadParticipants returns the de-duplicated user ids in the journal at path, in first-seen order. A missing file yields an empty slice (no error: the journal is best-effort observability).

func ReadTranscriptLast

func ReadTranscriptLast(path string) string

ReadTranscriptLast returns the timestamp of the last recorded entry, reading only the file's tail so a hot caller (the session list, polled ~1/s) never scans a long transcript. Empty only when the file is missing/empty. When the newest entry is larger than the tail window it falls back to a bounded full read (rare), so a timestamp is never lost. Kept separate from ReadTranscript, whose callers need the entries themselves.

func RemoveParticipantJournal

func RemoveParticipantJournal(path string) error

RemoveParticipantJournal deletes the journal at path. A missing file is not an error (called on session close to avoid leaking participants/*.log).

func RemoveTranscript

func RemoveTranscript(path string) error

RemoveTranscript deletes the transcript at path. A missing file is not an error (called on real session removal to avoid leaking transcripts/*.jsonl).

func TranscriptPath

func TranscriptPath(dir, name string) string

TranscriptPath returns the transcript path for session name under dir (dir/transcripts/<name>.jsonl), beside participants/<name>.log.

Types

type HomeRef

type HomeRef struct {
	ID   string `json:"id"`
	Type string `json:"type"` // "category" | "forum"
}

HomeRef points at the category or forum that holds session channels.

type Session

type Session struct {
	ID string `json:"id,omitempty"` // stable logical id, decoupled from Name and ChannelID
	// Incarnation is an opaque identity for this specific persisted session
	// object. Closing and recreating the same Name generates a different value.
	Incarnation string `json:"incarnation,omitempty"`
	Name        string `json:"name"`
	ChannelID   string `json:"channelID"`
	Type        string `json:"type"` // "text" | "forum"
	Cmd         string `json:"cmd"`
	Backend     string `json:"backend,omitempty"` // bridge backend ("" or "stream" = stream-json default; "oneshot" = per-message cmd)
	Vendor      string `json:"vendor,omitempty"`  // agent backend vendor ("claude", "codex", "cursor")
	// ModelID is the catalog identifier of the chosen model. Unlike Cmd, which is
	// an opaque invocation string, it lets a resume look up the model's ROUTE —
	// so we know whether gateway credentials need to be re-injected. Empty for
	// sessions created before the catalog existed.
	ModelID  string `json:"modelId,omitempty"`
	Worktree string `json:"worktree,omitempty"` // abs path; empty for a shared session
	Dir      string `json:"dir,omitempty"`      // bridge working dir; empty = inherit launcher cwd (pwd fallback)
	Project  string `json:"project,omitempty"`  // workspace sub-dir the session started from
	Agent    string `json:"agent,omitempty"`    // durable agent this session was provisioned from ("" = none)

	// ResumeToken is the backend's opaque resume id, folded in from each turn's
	// reply so a restart can resume the conversation with --resume. Empty =
	// start fresh.
	ResumeToken string `json:"resumeToken,omitempty"`

	// Archived marks a session closed-but-kept (session archive): its row,
	// transcript and ResumeToken are retained so /resume can revive it, but the
	// boot loop does not auto-supervise it. Absent/false = live as today.
	Archived bool `json:"archived,omitempty"`

	// Owned marks a channel this session created, and may therefore tidy away
	// when it closes. A session started in a conversation that already existed
	// binds to it instead, and that channel belongs to the people who were
	// already talking in it — closing the session must leave it alone. Absent on
	// a row written before this field existed, which is the safe direction: the
	// worst outcome is a channel left behind.
	Owned bool `json:"owned,omitempty"`

	// Learning config (P1 write side, opt-in). Extractor names a registered
	// curation extractor; empty keeps the plain Curator (no learning). Journal
	// is the call-journal path Consolidate reads (worktree-relative is fine).
	// ConsolidateEvery runs Consolidate every N turns (0 = manual only).
	Extractor        string `json:"extractor,omitempty"`
	Journal          string `json:"journal,omitempty"`
	ConsolidateEvery int    `json:"consolidateEvery,omitempty"`

	// Gateways binds the session to a set of gateway kinds (e.g. "chat",
	// "terminal"). Empty means "legacy": a pre-multi-gateway session whose
	// binding the host resolves from the gateways actually built (see IsLegacy).
	Gateways []string `json:"gateways,omitempty"`

	Participants []string `json:"participants,omitempty"` // observed authors (cache; journal is source of truth)

	// Parent names the lead session that delegated this one (result-back P3).
	// Empty = no parent. The coordinator reads it to find the delivery target
	// of this session's completion report (Report).
	Parent string `json:"parent,omitempty"`

	// Budget caps (0 = uncapped), persisted from contracts.CreateSession.
	CostCap        float64 `json:"cost_cap,omitempty"`
	TokenCap       uint64  `json:"token_cap,omitempty"`
	CohortCostCap  float64 `json:"cohort_cost_cap,omitempty"`
	CohortTokenCap uint64  `json:"cohort_token_cap,omitempty"`
	// PausedReason is non-empty when the session was halted by a budget cap:
	// "cost" | "tokens" | "cohort_cost" | "cohort_tokens". Cleared on resume.
	PausedReason string `json:"paused_reason,omitempty"`
}

Session is one bridged channel/post supervised by the daemon.

func (Session) BoundGateways

func (s Session) BoundGateways() []string

BoundGateways returns the explicit gateway kinds this session is bound to, or nil when it is legacy (no stored set). The state package stays platform-blind: it never names a concrete gateway. A legacy session's effective binding is resolved by the host against the gateways actually built (see IsLegacy).

func (Session) IsLegacy

func (s Session) IsLegacy() bool

IsLegacy reports whether this session predates explicit gateway binding: it carries a channel but no stored gateway set. The host binds such a session to the primary (non-terminal) gateways present, reproducing the original single-gateway behavior without the core naming that gateway.

type State

type State struct {
	Home            HomeRef   `json:"home"`
	Repo            string    `json:"repo,omitempty"`      // legacy single-repo root; defaults to daemon cwd
	Workspace       string    `json:"workspace,omitempty"` // abs path to the workspace root; preferred over Repo
	Source          string    `json:"source,omitempty"`    // abs path to the herrscher source checkout (for /service update)
	Sessions        []Session `json:"sessions"`
	StatusMessageID string    `json:"statusMessageID,omitempty"` // cached id of the status embed
	InstanceID      string    `json:"instanceID,omitempty"`      // per-daemon namespace for global resources; "" = legacy
	// contains filtered or unexported fields
}

State is the daemon's persisted configuration. All access is mutex-guarded.

func LoadState

func LoadState(path string) (*State, error)

LoadState reads state from path; a missing file yields an empty state.

func NewState

func NewState(path string) *State

NewState returns an empty state bound to path (not yet written).

func (*State) AddSession

func (s *State) AddSession(sess Session) error

AddSession adds a session, erroring if the name is taken, and persists.

func (*State) ApplyDefaults

func (s *State) ApplyDefaults(home *HomeRef, workspace, source string)

ApplyDefaults seeds declarative config.json values into the in-memory state for any field not already set, WITHOUT persisting. This keeps config.json the source of truth for unset fields while a live /set (which persists to state.json) always wins: persisted state > config > empty. Because it never writes, removing a value from config.json takes effect on the next restart.

func (*State) FindSession

func (s *State) FindSession(name string) (Session, bool)

FindSession returns the session with name (and whether it exists).

func (*State) QualifiedName

func (s *State) QualifiedName(name string) string

QualifiedName maps a logical session name to the name used on global resources (channel title): "<InstanceID>__<name>". In legacy mode (empty InstanceID) it returns the bare logical name, preserving pre-namespacing behavior.

func (*State) RemoveSession

func (s *State) RemoveSession(name string) error

RemoveSession drops the session named name and persists.

func (*State) Save

func (s *State) Save() error

Save atomically writes state to its path.

func (*State) SetArchived

func (s *State) SetArchived(name string, archived bool) error

SetArchived sets a session's archived flag and persists only on change. An unknown name is a no-op (best-effort, mirrors SetResumeToken).

func (*State) SetBackendTarget

func (s *State) SetBackendTarget(name, vendor, cmd, modelID string) bool

SetBackendTarget re-points a live session's backend: it rewrites Vendor and the opaque Cmd string (which carries model+effort) and clears the resume token, which is backend-specific and meaningless once the vendor/model changes. The caller restarts the supervised child so the new values take effect. Returns false when no session matches name.

func (*State) SetBudget

func (s *State) SetBudget(name string, costCap float64, tokenCap uint64, cohortCostCap float64, cohortTokenCap uint64, pausedReason string) error

SetBudget persists the budget caps and paused reason for the named session in one write. Mirrors SetResumeToken/SetPausedReason's locking + persistence. Used by session set-budget so raising a cap (which clears PausedReason) survives restart. A missing session is a no-op.

func (*State) SetHome

func (s *State) SetHome(h HomeRef) error

SetHome records the home ref and persists.

func (*State) SetInstanceID

func (s *State) SetInstanceID(id string) error

SetInstanceID records the per-daemon instance id and persists. The id is meant to be frozen after first resolution; callers enforce that invariant.

func (*State) SetPausedReason

func (s *State) SetPausedReason(name, reason string) error

SetPausedReason records why a session halted on a budget cap ("" clears it, resuming the session). Mirrors SetResumeToken's locking + persistence.

func (*State) SetResumeToken

func (s *State) SetResumeToken(name, token string) error

SetResumeToken records the backend resume token for the named session, persisting only when it changes. Turns report the same id, so this avoids rewriting state.json every turn. A missing session or an unchanged token is a no-op.

func (*State) SetSource

func (s *State) SetSource(path string) error

SetSource records the herrscher source-checkout path and persists.

func (*State) SetStatusMessageID

func (s *State) SetStatusMessageID(id string) error

SetStatusMessageID caches the status embed's message id and persists.

func (*State) SetWorkspace

func (s *State) SetWorkspace(path string) error

SetWorkspace records the workspace root and persists.

func (*State) SnapshotSessions

func (s *State) SnapshotSessions() []Session

SnapshotSessions returns a copy of the current sessions.

func (*State) SourceDir

func (s *State) SourceDir() string

SourceDir returns the configured herrscher source checkout, else "".

func (*State) WorkspaceRoot

func (s *State) WorkspaceRoot() string

WorkspaceRoot returns the configured workspace, else the legacy Repo, else "".

type TranscriptEntry

type TranscriptEntry struct {
	Ts   string  `json:"ts"`
	Role string  `json:"role"` // "user" | "assistant"
	Text string  `json:"text"`
	Cost float64 `json:"cost,omitempty"`
	Kind string  `json:"kind,omitempty"` // reserved (tool calls)
	// Per-turn usage, present on assistant entries. TokensOut is the turn's
	// output tokens; TokensIn the input; CacheRead/CacheCreate the prompt-cache
	// read/creation tokens; DurMs the turn's wall-clock duration in ms. All
	// omitempty so user turns and pre-usage transcripts stay compact.
	TokensIn    int `json:"tokens_in,omitempty"`
	TokensOut   int `json:"tokens_out,omitempty"`
	CacheRead   int `json:"cache_read,omitempty"`
	CacheCreate int `json:"cache_create,omitempty"`
	DurMs       int `json:"dur_ms,omitempty"`
}

TranscriptEntry is one recorded turn side. Kept separate from the learning call-journal: this is the human-visible conversation, replayed as scrollback.

func ReadTranscript

func ReadTranscript(path string, limit int) []TranscriptEntry

ReadTranscript returns entries in file order; when limit > 0, only the last limit. A missing file yields nil (best-effort observability, never an error). A bufio.Reader (not a Scanner) reads lines, so an oversized entry is parsed in full rather than silently dropped at a fixed token cap.

Jump to

Keyboard shortcuts

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