daemon

package
v0.67.9 Latest Latest
Warning

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

Go to latest
Published: Jul 14, 2026 License: MIT Imports: 61 Imported by: 0

Documentation

Index

Constants

View Source
const (
	DriverPTY      = "pty"
	DriverHeadless = "headless"
)

Driver-kind identifiers persisted on SessionState.DriverKind (issue #1075).

View Source
const (
	GCOrphanWorktree = "worktree"
	GCOrphanScratch  = "scratch"
)

GCOrphanType classifies where an orphan directory lives.

View Source
const (
	StopReasonCrash    = "crash"
	StopReasonIdle     = "idle"
	StopReasonUser     = "user"
	StopReasonShutdown = "shutdown"
	StopReasonWatchdog = "watchdog"
	// StopReasonDelete labels a daemon-initiated kill that is part of a
	// delete/purge teardown. It is used only for the "stopping session" audit
	// line (issue #1104); a deleted session is removed from state, so its exit
	// is ignored and never reaches the "session exited" log path.
	StopReasonDelete = "delete"
)

StopReason constants

View Source
const CurrentStateVersion = 18
View Source
const OrchestratorSessionName = "orchestrator"
View Source
const SystemKindOrchestrator = "orchestrator"

Variables

View Source
var ErrDaemonRunning = errors.New("daemon already running")

Functions

func AcquirePIDFile

func AcquirePIDFile(path string) error

func ExecUpgrade

func ExecUpgrade(manifestPath, configFile, clientExecPath string) error

func HandleConnection

func HandleConnection(ctx context.Context, conn net.Conn, origin ConnOrigin, sm *SessionManager, log *slog.Logger)

HandleConnection processes the frame protocol for a single client connection. origin describes where the connection came from (local Unix socket vs remote tailnet listener) and carries the tailnet identity for remote connections; it is threaded to resolveAuth so authorization is origin-aware. The zero ConnOrigin{} is a local connection, preserving the existing trust model.

func IsGraithDaemon added in v0.16.5

func IsGraithDaemon(pid int) bool

func IsSystemSession added in v0.42.0

func IsSystemSession(s *SessionState) bool

func ListStateBackups added in v0.67.4

func ListStateBackups(statePath string) []string

ListStateBackups returns the sorted paths of every pre-migration backup sitting next to statePath (files named "<base>.v<N>.bak"). It reads the directory rather than globbing so glob metacharacters in statePath can't break the match. Returns nil if the directory can't be read.

func Listen

func Listen(sockPath string) (net.Listener, error)

func ReleasePIDFile

func ReleasePIDFile(path string)

func Run

func Run(cfg *config.Config, paths config.Paths, configFile, adoptFrom string) error

Run starts the daemon: acquires PID file, listens on the Unix socket, serves connections, and blocks until SIGTERM/SIGINT or an upgrade signal.

func SaveState

func SaveState(path string, state *State) error

func StateBackupPath added in v0.67.4

func StateBackupPath(statePath string, version int) string

StateBackupPath returns the pre-migration backup path for a state file at a given on-disk version. Backups sit next to the state file as "<state>.v<version>.bak" so a human recovering a downgrade can see which schema version a backup holds without opening it.

func StopDaemon

func StopDaemon(pidFile string) error

func ValidateScenarioName added in v0.52.0

func ValidateScenarioName(name string) error

func ValidateSessionName added in v0.29.2

func ValidateSessionName(name string) error

ValidateSessionName checks that a session name is safe for use in git branch names, shell commands, osascript, template expansion, and environment variables.

func WriteManifest

func WriteManifest(dir string, m *UpgradeManifest) (string, error)

Types

type CIStatus added in v0.59.0

type CIStatus struct {
	State         string // passing | failing | pending | "" (unknown)
	FailingChecks []string
}

CIStatus is the runtime-only aggregate CI status for a session's linked PR.

type ConnOrigin added in v0.66.3

type ConnOrigin struct {
	// Remote is false for the local Unix socket, true for a tailnet listener.
	Remote bool
	// Identity is the WhoIs result for remote connections; nil for local.
	Identity *TailnetIdentity
}

ConnOrigin describes where a connection came from. It is threaded into HandleConnection so authorization can distinguish the local Unix socket (the 0700 trust boundary) from a remote tailnet connection, and recover the tailnet identity for the latter.

The zero value (Remote: false, Identity: nil) is a local Unix-socket connection — the existing trust model — so existing callers that pass ConnOrigin{} keep today's behaviour.

type CreateOpts added in v0.67.0

type CreateOpts struct {
	// ID, when non-empty, is the session ID to use instead of generating a
	// fresh one. It must match the generated ID format (8 lowercase hex chars)
	// and not collide with an existing session — Create validates both and
	// fails closed otherwise. Callers that must know the ID before Create
	// returns (e.g. scenario reservation, where a placeholder ID would
	// otherwise differ from the final session ID) supply it here. When empty,
	// Create generates the ID as before.
	ID                  string
	Name                string
	AgentName           string
	RepoPath            string
	BaseBranch          string
	Prompt              string
	Model               string
	ParentID            string
	NoRepo              bool
	Mirror              string
	AgentHooks          bool
	InPlace             bool
	AllowConcurrent     bool
	SkipModelValidation bool
	Yolo                bool
	// Headless requests a headless stream-json session instead of an
	// interactive PTY (issue #1075). Honoured only when the agent is
	// headless_capable and [headless] experimental is enabled; otherwise Create
	// fails closed rather than silently downgrading. See resolveDriverKind.
	Headless bool
	Rows     uint16
	Cols     uint16
	EnvExtra []map[string]string
	// TriggerID / TriggerReactor tag a session spawned by a trigger, applied in
	// the same durable reservation as creation so reactor ownership survives a
	// crash between Create and a separate tag-and-save.
	TriggerID      string
	TriggerReactor bool
	// AutoCleanup marks a trigger-spawned session for soft-deletion when it
	// stops (config.CleanupAlways / config.CleanupOnSuccess; empty disables).
	AutoCleanup string
	// IdleTimeoutSecs overrides the agent-default idle-stop window for this
	// session (seconds; 0 = agent default).
	IdleTimeoutSecs int
}

CreateOpts holds the parameters for SessionManager.Create. Using a struct keeps call sites self-documenting and lets new options default to their zero value without breaking existing callers.

type CreationConfig added in v0.27.0

type CreationConfig struct {
	Agent         config.Agent         `json:"agent"`
	SandboxConfig config.SandboxConfig `json:"sandbox_config"`
}

CreationConfig captures the agent and sandbox configuration at session creation time so the overlay can detect when the live config has diverged.

type GCOrphan added in v0.66.16

type GCOrphan struct {
	Type          string `json:"type"`
	Path          string `json:"path"`
	ID            string `json:"id"`
	IsGitWorktree bool   `json:"is_git_worktree,omitempty"`
	HasDirtyFiles bool   `json:"has_dirty_files,omitempty"`
	Removed       bool   `json:"removed,omitempty"`
	Skipped       bool   `json:"skipped,omitempty"`
	Reason        string `json:"reason,omitempty"`
	// contains filtered or unexported fields
}

GCOrphan describes a directory under the data dir that has no matching session. When RunGC runs with force=true, Removed/Skipped/Reason record the outcome of the cleanup attempt.

type IncludedRepoState added in v0.19.0

type IncludedRepoState struct {
	RepoPath     string `json:"repo_path"`
	RepoName     string `json:"repo_name"`
	WorktreePath string `json:"worktree_path"`
	Branch       string `json:"branch"`
	BaseBranch   string `json:"base_branch"`
	// contains filtered or unexported fields
}

type JailedComment added in v0.67.6

type JailedComment struct {
	ID            string `json:"id"`
	CommentID     int64  `json:"comment_id"`
	Surface       string `json:"surface"` // "inline review" | "conversation"
	PRNumber      int    `json:"pr_number"`
	RepoSlug      string `json:"repo_slug,omitempty"`
	Branch        string `json:"branch,omitempty"`
	Author        string `json:"author"`
	Association   string `json:"association,omitempty"`
	IsBot         bool   `json:"is_bot,omitempty"`
	Path          string `json:"path,omitempty"`
	Line          int    `json:"line,omitempty"`
	Body          string `json:"body"`
	TargetSession string `json:"target_session"`
	TargetName    string `json:"target_name,omitempty"`
	JailedAt      string `json:"jailed_at"`
	ReleasedAt    string `json:"released_at,omitempty"`
}

JailedComment is a PR comment that pr_watch blocked as untrusted and quarantined instead of discarding (issue #1082). It carries enough metadata for the human/orchestrator to inspect the comment and decide whether to release it (deliver it to the target session) or leave it jailed.

func (JailedComment) Released added in v0.67.6

func (j JailedComment) Released() bool

Released reports whether this jailed comment has already been released.

type MCPManager added in v0.23.0

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

MCPManager manages MCP server processes. Each proxy connection gets its own dedicated MCP server process, started lazily on connect.

func NewMCPManager added in v0.23.0

func NewMCPManager(cfg *config.Config, extraServers []config.MCPServerConfig, logDir string, log *slog.Logger) *MCPManager

NewMCPManager creates an MCPManager. extraServers are auto-injected servers (like the graith MCP server) that aren't in the config file but must be available for proxy connections.

func (*MCPManager) Connect added in v0.23.0

func (m *MCPManager) Connect(serverName, proxyID string, vars config.TemplateVars) (*MCPProcess, error)

Connect starts a new MCP server process for the given proxy and returns the process's stdin (for writing JSON-RPC) and stdout reader (for reading JSON-RPC). The caller owns the I/O loop. vars supplies the per-session template values ({session_id}, {session_name}, {worktree_path}) expanded in the server's args and env, giving each session an isolated process — e.g. a per-session Chrome profile dir for chrome-devtools-mcp.

func (*MCPManager) Disconnect added in v0.23.0

func (m *MCPManager) Disconnect(proxyID string, proc *MCPProcess)

Disconnect kills the MCP server process for the given proxy. It is identity-checked against proc: it only removes and kills the process still registered under proxyID if it is the *same* process the caller owns. This prevents an ABA race where Restart/Reload kills a process, the proxy reconnects and Connect installs a replacement under the same deterministic proxyID, and this (stale) deferred cleanup would otherwise kill the fresh replacement.

func (*MCPManager) HasServer added in v0.23.0

func (m *MCPManager) HasServer(name string) bool

HasServer returns true if the named MCP server is configured.

func (*MCPManager) List added in v0.67.3

func (m *MCPManager) List() []protocol.MCPServerStatus

List returns the status of every configured MCP server, including any live proxy processes. Auto-injected servers (e.g. graith) are flagged.

func (*MCPManager) LogFiles added in v0.67.3

func (m *MCPManager) LogFiles(name string, lines int) ([]protocol.MCPLogFile, error)

LogFiles returns the captured stderr for the named server. It reads every per-proxy log file for that server (both live and historical), returning the last `lines` lines of each. It errors if the server is not configured.

func (*MCPManager) Reload added in v0.23.0

func (m *MCPManager) Reload(cfg *config.Config)

Reload updates the server config. Running processes for removed or changed servers are killed so proxies reconnect with the new config.

func (*MCPManager) Restart added in v0.67.3

func (m *MCPManager) Restart(name string) (int, error)

Restart stops all running processes for the named server. Proxies detect the broken connection and reconnect, at which point the daemon starts fresh processes with the current config. It returns the number of processes stopped, or an error if the server is not configured.

func (*MCPManager) Shutdown added in v0.23.0

func (m *MCPManager) Shutdown()

Shutdown kills all running MCP server processes.

type MCPProcess added in v0.23.0

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

MCPProcess represents a running MCP server process for a single proxy connection.

type Message

type Message struct {
	ID         string `json:"id"`
	Seq        int64  `json:"seq"`
	Stream     string `json:"stream"`
	SenderID   string `json:"sender_id"`
	SenderName string `json:"sender_name,omitempty"`
	Body       string `json:"body"`
	ThreadID   string `json:"thread_id,omitempty"`
	ReplyTo    string `json:"reply_to,omitempty"`
	CreatedAt  string `json:"created_at"`
	// System marks a daemon-authored automated notification (PR/CI notices,
	// etc.) as distinct from an LLM/session/human message. It is derived from
	// the sender ID rather than stored, so it is set when messages are read or
	// published, not persisted as a column. See issue #887.
	System bool `json:"system,omitempty"`
}

type MigrationInfo added in v0.58.0

type MigrationInfo struct {
	Agent          string    `json:"agent"`
	Model          string    `json:"model,omitempty"`
	AgentSessionID string    `json:"agent_session_id,omitempty"`
	RenderedPath   string    `json:"rendered_path,omitempty"`
	At             time.Time `json:"at"`
}

MigrationInfo records the agent a session was migrated from, so a failed migration can be reverted and the user can migrate back later.

type MsgStore

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

func NewMsgStore

func NewMsgStore(dbPath string) (*MsgStore, error)

func (*MsgStore) Ack

func (s *MsgStore) Ack(stream, subscriber string, upToSeq int64) error

func (*MsgStore) AckLatest

func (s *MsgStore) AckLatest(stream, subscriber string) error

func (*MsgStore) AckMessages added in v0.16.5

func (s *MsgStore) AckMessages(stream, subscriber string, seqs []int64) error

func (*MsgStore) Cleanup added in v0.3.0

func (s *MsgStore) Cleanup(maxAge time.Duration, maxPerStream int) (int64, error)

func (*MsgStore) Close

func (s *MsgStore) Close() error

func (*MsgStore) Conversation added in v0.59.0

func (s *MsgStore) Conversation(self string, limit int) ([]Message, error)

Conversation returns every direct message involving `self`, both directions: messages delivered to self's inbox (stream = "inbox:"+self) and messages self sent to any peer's inbox (sender_id = self AND an inbox: stream other than self's own). Topic messages are excluded — a "conversation" is direct messages only. Results are ordered by created_at, with id as a deterministic tie-breaker (seq is per-stream, so it is not a usable cross-stream order key).

When limit > 0, the most recent `limit` messages are returned (still in ascending order). The query reads inbox streams the caller may not own; the daemon authorises the target via checkTarget before calling this, and the sender_id filter ensures the outbound branch only returns messages the target session actually authored.

func (*MsgStore) GetJailed added in v0.67.6

func (s *MsgStore) GetJailed(id string) (JailedComment, bool, error)

GetJailed returns a single quarantined comment by jail ID.

func (*MsgStore) Jail added in v0.67.6

func (s *MsgStore) Jail(j JailedComment) (id string, created bool, err error)

Jail quarantines a blocked PR comment. It assigns and returns the generated jail ID and is idempotent on (comment_id, surface, target_session): a repeat of the same comment leaves the existing row untouched and returns its existing ID with created=false.

func (*MsgStore) ListJailed added in v0.67.6

func (s *MsgStore) ListJailed(includeReleased bool) ([]JailedComment, error)

ListJailed returns quarantined comments, newest first, capped at 2000 rows so a long-running daemon with retention disabled can't be made to serialize an unbounded result set (mirrors the msg_conversation clamp; newest-first, so the cap drops the oldest). When includeReleased is false, already-released entries are excluded.

func (*MsgStore) ListStreams

func (s *MsgStore) ListStreams(subscriber string, includeSystem bool) ([]StreamInfo, error)

func (*MsgStore) MarkReleased added in v0.67.6

func (s *MsgStore) MarkReleased(id string) (JailedComment, bool, error)

MarkReleased stamps a jailed comment as released. It returns ok=false if the entry does not exist or was already released (so a double-release can't re-deliver the same comment). The returned entry is the pre-release snapshot, used by the caller to build the delivery.

func (*MsgStore) Publish

func (s *MsgStore) Publish(opts PublishOpts) (Message, error)

func (*MsgStore) Read

func (s *MsgStore) Read(stream, subscriber string, onlyUnread bool, threadID string) ([]Message, error)

func (*MsgStore) Subscribe

func (s *MsgStore) Subscribe(stream string) (chan Message, func())

func (*MsgStore) TotalUnread added in v0.3.0

func (s *MsgStore) TotalUnread(subscriber string) int

func (*MsgStore) Unrelease added in v0.67.6

func (s *MsgStore) Unrelease(id string) (bool, error)

Unrelease reverts a release stamp back to unreleased. Used to un-claim a release whose delivery failed, so it stays retryable rather than stuck released-but-undelivered. Returns ok=true if a row was actually reverted.

func (*MsgStore) UnreleasedJailed added in v0.67.6

func (s *MsgStore) UnreleasedJailed() ([]JailedComment, error)

UnreleasedJailed returns all not-yet-released quarantined comments. Used by auto-release to re-evaluate trust against a freshly-reloaded config.

type PRStatus added in v0.59.0

type PRStatus struct {
	Number         int
	State          string // open | draft | merged | closed
	URL            string
	ReviewDecision string
	HeadRefOid     string // head commit SHA — keys the per-SHA notify cap
	Mergeable      string // MERGEABLE | CONFLICTING | UNKNOWN
}

PRStatus is the runtime-only linked-PR state for a session, derived by the PR-watch loop. A zero Number means "no PR resolved". The PR-watch loop always assigns a freshly-built value (never mutates in place), so reads from cloned SessionState off-lock are race-free.

type PairedDevice added in v0.66.3

type PairedDevice struct {
	ID          string `json:"id"`
	Label       string `json:"label"`
	PubKey      string `json:"pub_key"`
	TailnetUser string `json:"tailnet_user"`
	TailnetNode string `json:"tailnet_node"`
	TokenHash   string `json:"token_hash"`
	// ReadOnly marks a device paired while require_pairing=false (the unsafe,
	// WhoIs-only mode): it maps to roleRemoteGuest and gets a read-only subset.
	ReadOnly   bool      `json:"read_only,omitempty"`
	CreatedAt  time.Time `json:"created_at"`
	LastSeenAt time.Time `json:"last_seen_at,omitempty"`
}

PairedDevice is a remote client device authorized via pairing (design §B.2). It is bound to the tailnet identity observed at pairing time; TokenHash is an HMAC of the client token, never the token itself.

type PublishOpts added in v0.67.2

type PublishOpts struct {
	// Stream is the target stream (topic, inbox:<id>, or system stream).
	Stream string
	// SenderID is the sender's session ID (or a system sender ID).
	SenderID string
	// SenderName is the sender's human-readable name.
	SenderName string
	// Body is the message body.
	Body string
	// ThreadID groups the message into a conversation thread (optional).
	ThreadID string
	// ReplyTo is the stream this message replies to (optional).
	ReplyTo string
}

PublishOpts describes a message to publish to a stream. It replaces the six consecutive positional string parameters of Publish — same-typed positional strings are the highest-risk transposition case, and the trailing ThreadID / ReplyTo are frequently empty, so a struct with named fields is both safer and less noisy at the call site.

type RemoteListener added in v0.66.3

type RemoteListener interface {
	// Listen begins listening and returns the raw net.Listener (TLS is layered
	// on by the caller).
	Listen() (net.Listener, error)
	// WhoIs resolves the tailnet identity behind remoteAddr.
	WhoIs(ctx context.Context, remoteAddr string) (*TailnetIdentity, error)
	// Close releases the listener's resources.
	Close() error
}

RemoteListener is a tailnet-facing listener plus a way to resolve the tailnet identity behind an accepted connection (Gate 1). Two implementations back the two config modes: tsnet (embedded node) and interface (host tailscaled).

type ScenarioSession added in v0.52.0

type ScenarioSession struct {
	Name     string `json:"name"`
	Role     string `json:"role"`
	Task     string `json:"task"`
	TaskDone bool   `json:"task_done,omitempty"`
	Repo     string `json:"repo"`
	Agent    string `json:"agent"`
	Model    string `json:"model,omitempty"`
	Shared   bool   `json:"shared,omitempty"`
}

type ScenarioState added in v0.52.0

type ScenarioState struct {
	ID             string            `json:"id"`
	Name           string            `json:"name"`
	OrchestratorID string            `json:"orchestrator_id"`
	Goal           string            `json:"goal"`
	SessionIDs     []string          `json:"session_ids"`
	Sessions       []ScenarioSession `json:"sessions"`
	CreatedAt      time.Time         `json:"created_at"`
	SourceFileHash string            `json:"source_file_hash,omitempty"`
}

type Server

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

func NewServer

func NewServer(l net.Listener, handler func(ctx context.Context, conn net.Conn), log *slog.Logger) *Server

func (*Server) Serve

func (s *Server) Serve(ctx context.Context) error

func (*Server) Shutdown

func (s *Server) Shutdown()

type SessionDriver added in v0.67.8

type SessionDriver interface {
	// Lifecycle / identity.
	ProcessPID() int
	Pgid() int
	Fd() uintptr
	Done() <-chan struct{}
	Exited() bool
	ExitCode() int
	ExitSignal() syscall.Signal
	PeakRSSBytes() int64
	LastOutputAt() time.Time
	RecentlyAdopted(grace time.Duration) bool
	BytesRead() int64
	WasAdopted() bool
	CreatedAt() time.Time
	Kill() error
	ForceKill() error
	Close()

	// Input the daemon issues on the session's behalf.
	WriteInput(data []byte) error
	WriteInputAndSubmit(data []byte) error
	Interrupt(count int, delay time.Duration) error
	NotifyUserInput()
	WaitForUserIdle(idleTimeout, maxWait time.Duration) bool

	// PTY-shaped controls (no-ops or best-effort for non-PTY drivers).
	Resize(rows, cols uint16) error
	Poke()

	// Output surfaces: attach fan-out, scrollback, preview/snapshot.
	Attach(w io.Writer)
	Detach()
	DetachWriter(w io.Writer)
	ScreenPreview() string
	ScreenSnapshot() grpty.ScreenCapture
	ScrollbackFile() *grpty.Scrollback
}

SessionDriver is the transport-agnostic surface the daemon uses to drive a running agent process. Today the only implementation is *grpty.Session (an interactive PTY); the headless stream-json driver (issue #1075) will be a second implementation. The daemon holds sessions as SessionDriver values (sessions map, GetPTY) so a second driver can slot in without the lifecycle code caring which transport backs a session.

This is deliberately the *full* current call surface of *grpty.Session, so introducing the interface is a pure, no-behaviour-change refactor. Some members (Fd, Resize, Poke, ScreenSnapshot, NotifyUserInput, WaitForUserIdle) are PTY-shaped; when the headless driver lands, the design splits these into an optional interactiveDriver so headless doesn't have to no-op them (see docs/design/2026-07-13-headless-stream-json-design.md). Keeping one interface here means zero call-site churn in this first slice.

type SessionManager

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

SessionManager orchestrates PTY sessions, state persistence, and git worktrees.

func NewSessionManager

func NewSessionManager(cfg *config.Config, paths config.Paths, log *slog.Logger) *SessionManager

NewSessionManager creates a SessionManager with the given config and paths.

func (*SessionManager) AddApprovalSubscriber added in v0.66.3

func (sm *SessionManager) AddApprovalSubscriber(conn net.Conn, sendControl func(string, any))

AddApprovalSubscriber registers a connection to receive approval notifications without attaching to a session (design §C.6), so a remote app browsing the fleet gets approval prompts without kicking a desktop attach.

func (*SessionManager) AddPendingPairing added in v0.66.3

func (sm *SessionManager) AddPendingPairing(label, pubKey string, id TailnetIdentity, now time.Time) (string, chan pairApproval, error)

AddPendingPairing records a device pairing request, subject to a per-daemon rate limit and a cap on outstanding requests. now is passed for deterministic testing. It does not persist state — pending pairings are in-memory only. The returned channel is the waiter for this request: it is registered under the same lock that creates the pending entry, so an approval can never race ahead of the waiter and drop the delivery. The caller reads it (with its own timeout / disconnect handling) and must unregisterPairWaiter when done.

func (*SessionManager) AddToScenario added in v0.52.0

func (sm *SessionManager) AddToScenario(name string, input protocol.ScenarioSessionInput, rows, cols uint16) (*SessionState, error)

func (*SessionManager) AdoptSessions

func (sm *SessionManager) AdoptSessions(manifest *UpgradeManifest) error

func (*SessionManager) ApprovePairing added in v0.66.3

func (sm *SessionManager) ApprovePairing(requestID string, readOnly bool, now time.Time) (deviceID, clientToken string, err error)

ApprovePairing approves a pending pairing, minting and persisting a new paired device. readOnly marks a device paired while require_pairing=false (maps to roleRemoteGuest). It returns the device ID and the one-time client token (only returned here; only its HMAC is stored). now is passed for testability.

func (*SessionManager) ClearAttachedClient

func (sm *SessionManager) ClearAttachedClient(sessionID string, conn net.Conn)

func (*SessionManager) ClearSummary added in v0.32.0

func (sm *SessionManager) ClearSummary(sessionID string) error

func (*SessionManager) Config added in v0.47.0

func (sm *SessionManager) Config() *config.Config

Config returns a snapshot of the current config pointer, safe for use outside the lock. The returned *Config must not be modified.

func (*SessionManager) Create

func (sm *SessionManager) Create(opts CreateOpts) (SessionState, error)

Create starts a new agent session, either in a git worktree, in-place in an existing repo, or as a standalone scratch session (when noRepo is true).

The method uses three-phase locking to avoid holding the daemon mutex during potentially blocking git/network operations (fetch, GitHub API calls, PTY spawn):

  1. Lock: validate, reserve session as StatusCreating, unlock
  2. Git setup and PTY spawn (no lock held)
  3. Lock: commit to StatusRunning, unlock

func (*SessionManager) Delete

func (sm *SessionManager) Delete(id string) error

Delete stops a session, removes its worktree/branch, and deletes state. Git teardown is attempted before removing the session from state; if teardown fails the session is kept for retry and the error is returned.

func (*SessionManager) DeleteScenario added in v0.52.0

func (sm *SessionManager) DeleteScenario(name string) ([]string, error)

func (*SessionManager) DeleteWithChildren added in v0.20.0

func (sm *SessionManager) DeleteWithChildren(id string, excludeRoot bool) ([]string, error)

DeleteWithChildren deletes a session and all its transitive descendants. Git teardown is attempted before removing each session from state; sessions whose teardown fails are kept for retry. Returns the list of deleted session IDs and an error if any teardowns failed.

func (*SessionManager) DeviceForToken added in v0.66.3

func (sm *SessionManager) DeviceForToken(token string) *PairedDevice

DeviceForToken resolves a client token to its paired device, or nil. Must be called under at least RLock.

func (*SessionManager) Diagnostics added in v0.28.0

func (sm *SessionManager) Diagnostics() protocol.DiagnosticsMsg

Diagnostics collects runtime health data for gr doctor.

func (*SessionManager) EnsureHumanToken added in v0.67.1

func (sm *SessionManager) EnsureHumanToken() error

EnsureHumanToken loads or creates the local human credential. It is exported for tests and embedders that construct a SessionManager without going through Run (which calls loadOrCreateHumanToken during startup).

func (*SessionManager) FindOrphans added in v0.66.16

func (sm *SessionManager) FindOrphans(now time.Time) []GCOrphan

FindOrphans scans the data dir for worktree and scratch directories with no matching session that are older than gcOrphanMinAge. It reads state under the lock to build the known-session set, then walks the filesystem lock-free.

func (*SessionManager) Fork added in v0.10.0

func (sm *SessionManager) Fork(name, sourceSessionID string, rows, cols uint16) (SessionState, error)

Fork creates a new session that branches from an existing session's git state and uses the agent's fork_args to carry over the conversation history.

Uses three-phase locking like Create to avoid holding the mutex during git fetch and PTY spawn.

func (*SessionManager) Get

func (sm *SessionManager) Get(id string) (SessionState, bool)

Get returns a copy of a session state by ID.

func (*SessionManager) GetPTY

func (sm *SessionManager) GetPTY(id string) (SessionDriver, bool)

GetPTY returns the live session driver by ID. Named for its historic PTY-only past; today it may return any SessionDriver implementation.

func (*SessionManager) HandleHookReport added in v0.11.0

func (sm *SessionManager) HandleHookReport(sr protocol.StatusReportMsg)

HandleHookReport processes a status report from an agent hook, updating the in-memory hookReports map and the session's AgentStatus. This is the authoritative source of agent status when hooks are active.

func (*SessionManager) HasAttachedClient added in v0.52.0

func (sm *SessionManager) HasAttachedClient(sessionID string) bool

func (*SessionManager) InterruptSession added in v0.66.2

func (sm *SessionManager) InterruptSession(sessionID string) error

InterruptSession delivers an interrupt (Ctrl-C) to a session's live PTY using the agent's configured interrupt count and delay. Different agent CLIs need different sequences — e.g. Claude's TUI ignores a single Ctrl-C and needs two rapid presses — so delivery is agent-aware rather than a single 0x03 (issue #620). Returns an error if the session has no live PTY.

func (*SessionManager) IsAttachedClient added in v0.16.5

func (sm *SessionManager) IsAttachedClient(sessionID string, conn net.Conn) bool

func (*SessionManager) KickAttachedClient

func (sm *SessionManager) KickAttachedClient(sessionID string)

func (*SessionManager) List

func (sm *SessionManager) List() []SessionState

List returns copies of all known session states.

func (*SessionManager) ListPairings added in v0.66.3

func (sm *SessionManager) ListPairings() ([]pendingPairing, []PairedDevice)

ListPairings returns snapshots of the pending pairing requests and the persisted paired devices. Must be called without holding sm.mu.

func (*SessionManager) ListScenarios added in v0.52.0

func (sm *SessionManager) ListScenarios() []protocol.ScenarioRecord

func (*SessionManager) LoadState

func (sm *SessionManager) LoadState() error

LoadState reads persisted state from disk and reconciles dead processes.

func (*SessionManager) Migrate added in v0.58.0

func (sm *SessionManager) Migrate(id, targetAgent, targetModel string, rows, cols uint16) (SessionState, error)

Migrate swaps the agent on an existing session in place: it renders the current agent's conversation to a neutral Markdown file, stops the current agent, changes the session's agent type, and restarts it in the *same* worktree seeded with that file. The session keeps its id, name, worktree and branch — only the agent type changes — so all code state (commits and uncommitted edits) carries over with no branching.

Ordering is chosen so a doomed migration aborts before the running agent is touched (read+render happen first and fail fast), and so the context file exists before the new agent starts. If the target agent fails to start, the original agent is restored.

See docs/design/2026-06-24-cross-agent-conversation-migration-design.md.

func (*SessionManager) PendingApprovals added in v0.13.0

func (sm *SessionManager) PendingApprovals() []protocol.ApprovalInfo

PendingApprovals returns a snapshot of all current pending approvals.

func (*SessionManager) PrepareUpgrade

func (sm *SessionManager) PrepareUpgrade(listenerFd uintptr, configFile string) (*UpgradeManifest, error)

func (*SessionManager) RegisterDeviceConn added in v0.66.3

func (sm *SessionManager) RegisterDeviceConn(deviceID string, conn net.Conn)

RegisterDeviceConn records a live connection for a device so revocation can force-close it. Called once per connection after successful authentication.

func (*SessionManager) ReleaseJailed added in v0.67.6

func (sm *SessionManager) ReleaseJailed(id string) (JailedComment, error)

ReleaseJailed releases a single jailed comment by ID: marks it released and delivers its content to the target session's inbox (auto-resuming a stopped agent). It returns the released entry. Authorization (human/orchestrator only) is enforced by the caller — this is the mechanism, not the gate.

func (*SessionManager) ReleaseJailedByAuthor added in v0.67.6

func (sm *SessionManager) ReleaseJailedByAuthor(login string) ([]JailedComment, error)

ReleaseJailedByAuthor releases every not-yet-released jailed comment whose author login matches (case-insensitive). Used by `gr msg jail release --all --author <login>` after a newly-trusted author is allowlisted. It is best-effort per comment: a failure on one (claim or delivery) is logged and that comment is left jailed (un-claimed) for retry, but the loop continues, so the returned slice reflects exactly what was delivered — a mid-batch error never hides the comments that did go out. It returns an error only when the initial listing fails (nothing could be attempted).

func (*SessionManager) ReloadConfig added in v0.3.0

func (sm *SessionManager) ReloadConfig() error

ReloadConfig loads the config from disk and swaps it in, logging what changed.

func (*SessionManager) RemoveApprovalSubscriber added in v0.66.3

func (sm *SessionManager) RemoveApprovalSubscriber(conn net.Conn)

RemoveApprovalSubscriber deregisters an approval subscriber (on disconnect). It is a no-op if the connection was not subscribed.

func (*SessionManager) Rename

func (sm *SessionManager) Rename(id, newName string) error

func (*SessionManager) RespondToApproval added in v0.13.0

func (sm *SessionManager) RespondToApproval(requestID, decision, reason string) error

RespondToApproval delivers a user decision to a waiting approval request.

func (*SessionManager) Restart added in v0.26.0

func (sm *SessionManager) Restart(id string, rows, cols uint16) (SessionState, error)

Restart stops a running session (or no-ops if already stopped) and resumes it, picking up the current agent and sandbox configuration. A plain user restart attributes the teardown to StopReasonUser; internal callers use restartWithReason to preserve the true subsystem (e.g. the startup watchdog).

func (*SessionManager) RestartWithChildren added in v0.53.0

func (sm *SessionManager) RestartWithChildren(rootID string, excludeRoot bool, rows, cols uint16) ([]string, error)

func (*SessionManager) Restore added in v0.66.16

func (sm *SessionManager) Restore(id string) (SessionState, error)

Restore un-deletes a soft-deleted session, clearing its deletion marker and leaving it in the stopped state so it can be resumed. Returns an error if the session does not exist, is not soft-deleted, or its recovery window has already elapsed (in which case it is scheduled for purge and must not be resurrected past its advertised deadline).

func (*SessionManager) RestoreWithChildren added in v0.66.16

func (sm *SessionManager) RestoreWithChildren(rootID string) ([]SessionState, error)

RestoreWithChildren restores a soft-deleted session and every soft-deleted descendant, bringing a subtree hidden by a `--children` delete back at once. Non-deleted or expired descendants are skipped. Returns the restored IDs.

func (*SessionManager) Resume

func (sm *SessionManager) Resume(id string, rows, cols uint16) (SessionState, error)

Resume restarts a stopped session using the agent's resume_args.

Uses two-phase locking: the GitHub username discovery happens before the lock, and the PTY spawn happens after releasing the lock to avoid blocking the daemon.

func (*SessionManager) ResumeScenario added in v0.52.0

func (sm *SessionManager) ResumeScenario(name string, rows, cols uint16) ([]string, error)

func (*SessionManager) RevokeDevice added in v0.66.3

func (sm *SessionManager) RevokeDevice(deviceID string) (int, error)

RevokeDevice removes a paired device and force-closes all of its live connections, so a revoked (e.g. lost/stolen) device loses control immediately rather than at next-connection (design §B.5). It returns the number of connections closed.

func (*SessionManager) RunDetectionLoop

func (sm *SessionManager) RunDetectionLoop(ctx context.Context)

RunDetectionLoop periodically scans PTY scrollback to detect low-risk agent status (active/ready) for all running sessions. Approval status comes from hooks or the daemon approval queue, not PTY text.

func (*SessionManager) RunFileWatchLoop added in v0.67.0

func (sm *SessionManager) RunFileWatchLoop(ctx context.Context)

RunFileWatchLoop is the daemon-owned file-watch (#593) trigger source. It reconciles bindings (watch trigger × matching live session) against live fsnotify watchers each tick, and feeds debounced, filtered events into the shared trigger action executor.

func (*SessionManager) RunGC added in v0.66.16

func (sm *SessionManager) RunGC(force bool, now time.Time) []GCOrphan

RunGC finds orphaned directories and, when force is true, removes those that are safe to delete. A worktree with uncommitted changes is never removed (its unreachable work is preserved for manual recovery) and is reported as skipped. When force is false the returned orphans are a dry-run listing with no filesystem changes.

func (*SessionManager) RunGitPullLoop added in v0.42.0

func (sm *SessionManager) RunGitPullLoop(ctx context.Context)

func (*SessionManager) RunMessageCleanupLoop added in v0.3.0

func (sm *SessionManager) RunMessageCleanupLoop(ctx context.Context)

func (*SessionManager) RunPRWatchLoop added in v0.59.0

func (sm *SessionManager) RunPRWatchLoop(ctx context.Context)

RunPRWatchLoop is the daemon-owned PR/CI watcher. Modeled on RunGitPullLoop: config-gated, tolerant of errors, off the request path.

func (*SessionManager) RunPurgeLoop added in v0.66.16

func (sm *SessionManager) RunPurgeLoop(ctx context.Context)

RunPurgeLoop periodically hard-deletes soft-deleted sessions whose retention window has elapsed. Modeled on RunGitPullLoop: one sweep shortly after startup (to catch windows that elapsed while the daemon was down), then a coarse ticker. Stops cleanly on context cancel.

func (*SessionManager) RunStartupWatchdogLoop added in v0.67.7

func (sm *SessionManager) RunStartupWatchdogLoop(ctx context.Context)

RunStartupWatchdogLoop periodically scans for sessions that are stuck in startup — running, but never having produced output, sitting at agent_status "unknown" past the configured startup_timeout — and restarts them fresh (#1092). The timeout is re-read each tick so config reloads take effect.

func (*SessionManager) RunTokenLoop added in v0.67.6

func (sm *SessionManager) RunTokenLoop(ctx context.Context)

RunTokenLoop periodically re-derives per-session token usage from each supported session's on-disk transcript, writing runtime-only TokenStats onto SessionState (never persisted, repopulated within a tick after restart).

func (*SessionManager) RunTriggerLoop added in v0.67.0

func (sm *SessionManager) RunTriggerLoop(ctx context.Context)

RunTriggerLoop is the daemon-owned schedule (#592) trigger loop. Modeled on RunPRWatchLoop: config-gated, off the request path, independent mutex.

func (*SessionManager) ScenarioStatus added in v0.52.0

func (sm *SessionManager) ScenarioStatus(name string) (*protocol.ScenarioRecord, error)

func (*SessionManager) ScenarioTaskDone added in v0.52.0

func (sm *SessionManager) ScenarioTaskDone(scenarioName, sessionID string) error

func (*SessionManager) SendPushNotification added in v0.67.2

func (sm *SessionManager) SendPushNotification(n pushNotification) (bool, string)

SendPushNotification applies gating (enabled, coalescing, quiet hours, rate limit) then dispatches to the configured backend. It returns whether the notification was delivered and a human-readable reason when suppressed. A backend dispatch error is logged and reported (delivered=false, reason=err) but is not returned as a Go error — callers treat suppression and failure the same way (the notification did not reach the user).

func (*SessionManager) SessionForToken added in v0.52.0

func (sm *SessionManager) SessionForToken(token string) string

SessionForToken returns the session ID that owns the given token, or empty string if the token is not recognized. Must be called under at least RLock.

func (*SessionManager) SetAttachedClient

func (sm *SessionManager) SetAttachedClient(sessionID string, conn net.Conn, kick func(), sendCtrl func(string, any))

func (*SessionManager) SetMCPManager added in v0.23.0

func (sm *SessionManager) SetMCPManager(mm *MCPManager)

func (*SessionManager) SetMsgStore

func (sm *SessionManager) SetMsgStore(ms *MsgStore)

func (*SessionManager) SetSummary added in v0.32.0

func (sm *SessionManager) SetSummary(sessionID, text string, ttlSeconds int) error

func (*SessionManager) SoftDelete added in v0.66.16

func (sm *SessionManager) SoftDelete(id string) (SessionState, error)

SoftDelete marks a session as deleted without removing its worktree or state. The agent process is stopped and the session moves to the stopped state, but everything is preserved so `gr restore` can recover it within the configured retention window. The daemon's purge loop hard-deletes it once the window elapses. System and starred sessions are protected, matching Delete. Returns a snapshot of the soft-deleted session so the caller can report the expiry.

func (*SessionManager) SoftDeleteWithChildren added in v0.66.16

func (sm *SessionManager) SoftDeleteWithChildren(rootID string, excludeRoot bool) ([]string, error)

SoftDeleteWithChildren soft-deletes a session and all of its transitive descendants. If excludeRoot is true, the root session itself is left alone. Sessions that are already soft-deleted, starred, system, or mid-creation are skipped. A lightweight sweep re-marks descendants that appear mid-operation (a child agent spawning a new session) so the subtree stays coherent — it only re-marks, never tears down, since deferring teardown is the whole point. Returns the list of session IDs that were soft-deleted.

func (*SessionManager) Star added in v0.31.0

func (sm *SessionManager) Star(id string) error

Star marks a session as starred.

func (*SessionManager) StartScenario added in v0.52.0

func (sm *SessionManager) StartScenario(msg protocol.ScenarioStartMsg, rows, cols uint16) (*ScenarioState, error)

func (*SessionManager) Stop

func (sm *SessionManager) Stop(id string) error

Stop sends SIGTERM to a session's process without removing the session or worktree.

func (*SessionManager) StopAll

func (sm *SessionManager) StopAll(ctx context.Context)

StopAll gracefully terminates all running sessions concurrently. Each session gets up to 5 seconds to exit after SIGTERM before being force-killed. Sessions are waited on in parallel so the total wait time is bounded by the slowest session, not the sum.

func (*SessionManager) StopScenario added in v0.52.0

func (sm *SessionManager) StopScenario(name string) ([]string, error)

func (*SessionManager) StopWithChildren added in v0.30.0

func (sm *SessionManager) StopWithChildren(rootID string, excludeRoot bool) ([]string, error)

StopWithChildren stops all descendants of rootID. If excludeRoot is true, the root session itself is not stopped. Already-stopped sessions are skipped. Returns the list of session IDs that were actually stopped.

func (*SessionManager) SubmitApproval added in v0.13.0

SubmitApproval registers a pending approval and blocks until a decision is made, the context is cancelled (hook disconnect), or the configured timeout elapses. An automated backend (if configured) is consulted first; a backend that defers, errors, or is absent falls through to the human-queue path.

func (*SessionManager) TriggerList added in v0.67.0

func (sm *SessionManager) TriggerList() []protocol.TriggerRecord

TriggerList returns records for all configured triggers.

func (*SessionManager) TriggerPause added in v0.67.0

func (sm *SessionManager) TriggerPause(name string, pause bool) error

TriggerPause pauses/resumes a trigger. A config-disabled trigger cannot be resumed.

func (*SessionManager) TriggerRunNow added in v0.67.0

func (sm *SessionManager) TriggerRunNow(ctx context.Context, name string) error

TriggerRunNow fires a trigger out-of-band (cause "manual"), respecting the overlap guard but not shifting the schedule cursor.

func (*SessionManager) TriggerStatus added in v0.67.0

func (sm *SessionManager) TriggerStatus(name string) (protocol.TriggerRecord, error)

TriggerStatus returns one trigger's record, or an error if unknown.

func (*SessionManager) UnregisterDeviceConn added in v0.66.3

func (sm *SessionManager) UnregisterDeviceConn(deviceID string, conn net.Conn)

UnregisterDeviceConn removes a connection from a device's live set (on connection close).

func (*SessionManager) Unstar added in v0.31.0

func (sm *SessionManager) Unstar(id string) error

func (*SessionManager) Update added in v0.52.0

func (sm *SessionManager) Update(id string, name *string, parentID *string) error

type SessionState

type SessionState struct {
	ID             string `json:"id"`
	ParentID       string `json:"parent_id,omitempty"`
	Name           string `json:"name"`
	RepoPath       string `json:"repo_path"`
	RepoName       string `json:"repo_name"`
	WorktreePath   string `json:"worktree_path"`
	Branch         string `json:"branch"`
	BaseBranch     string `json:"base_branch"`
	Agent          string `json:"agent"`
	AgentSessionID string `json:"agent_session_id,omitempty"`
	// DriverKind is the session's transport: DriverPTY (interactive PTY) or
	// DriverHeadless (headless stream-json, issue #1075). Resolved once at
	// creation and never re-derived from config. Empty is treated as DriverPTY.
	DriverKind      string        `json:"driver_kind,omitempty"`
	Model           string        `json:"model,omitempty"`
	Status          SessionStatus `json:"status"`
	AgentStatus     string        `json:"agent_status,omitempty"`
	StatusChangedAt time.Time     `json:"status_changed_at"`
	IdleSince       *time.Time    `json:"-"`
	GitDirty        bool          `json:"-"`
	GitUnpushed     int           `json:"-"`
	PullRequest     PRStatus      `json:"-"`
	CI              CIStatus      `json:"-"`
	// Tokens is the runtime-derived token usage for the session's current agent,
	// re-derived from the on-disk transcript by RunTokenLoop. Like PR/CI it is
	// NOT persisted (repopulates within one tick after a restart) and is always
	// replaced with a freshly-built pointer under the lock, never mutated in
	// place, so an off-lock cloneSessionState is race-free. Nil = never observed.
	Tokens       *TokenStats `json:"-"`
	HookToolName string      `json:"-"`
	// ContextPressure / ContextPressureAt track Claude's compaction signal:
	// PreCompact sets the flag + timestamp, PostCompact clears the flag. Runtime
	// only (json:"-"), so a daemon restart forgets them and the next Pre/PostCompact
	// re-establishes the picture. Cleared on SessionStart and on resume/restart.
	ContextPressure   bool      `json:"-"`
	ContextPressureAt time.Time `json:"-"`
	// SubAgents maps a Claude sub-agent's agent_id -> agent_type; len() is the
	// live count. A map (not a counter) so a duplicate or missing SubagentStop
	// can't underflow or strand a count — a stop is an idempotent delete. Runtime
	// only, cleared on SessionStart and resume/restart. Never mutated in place:
	// each SubagentStart/Stop replaces it with a fresh map so an off-lock
	// cloneSessionState is race-free (mirrors the Tokens discipline above).
	SubAgents map[string]string `json:"-"`
	// SessionEndReason is Claude's raw SessionEnd reason (clear/resume/logout/
	// prompt_input_exit/other). Runtime-only (never persisted): a daemon restart
	// forgets it and hooks re-establish the picture. It is distinct from
	// StopReason — the process-exit path maps only process-ending reasons onto a
	// StopReason (see mapSessionEndReason). Cleared on SessionStart and on
	// resume/restart so a stale reason can't outlive its turn.
	SessionEndReason string `json:"-"`
	// SessionEndReasonGen binds SessionEndReason to the process generation
	// (PIDStartTime) that produced it, so the exit path consumes it only for the
	// same process — a reason recorded against an older generation is ignored.
	SessionEndReasonGen int64 `json:"-"`
	// LastMessage is the truncated last_assistant_message from the most recent
	// Stop event. Runtime-only; kept off the guest-visible SessionInfo unredacted.
	LastMessage    string                `json:"-"`
	ExitCode       *int                  `json:"exit_code,omitempty"`
	ExitSignal     string                `json:"exit_signal,omitempty"`
	PID            int                   `json:"pid,omitempty"`
	PIDStartTime   int64                 `json:"pid_start_time,omitempty"`
	Sandboxed      bool                  `json:"sandboxed,omitempty"`
	SandboxConfig  *config.SandboxConfig `json:"sandbox_config,omitempty"`
	Mirror         bool                  `json:"mirror,omitempty"`
	MirrorSourceID string                `json:"mirror_source_id,omitempty"`
	// LegacyMirror / LegacyMirrorSourceID hold the pre-v15 persisted keys for
	// Mirror / MirrorSourceID (issue #1021 renamed --share-worktree to
	// --mirror). They exist only so migrateV14ToV15 can copy old state forward;
	// they are cleared after migration and never written under the new binary.
	LegacyMirror         bool                `json:"shared_worktree,omitempty"`           // deprecated: migrated to Mirror in v15
	LegacyMirrorSourceID string              `json:"shared_worktree_source_id,omitempty"` // deprecated: migrated to MirrorSourceID in v15
	InPlace              bool                `json:"in_place,omitempty"`
	Includes             []IncludedRepoState `json:"includes,omitempty"`
	AgentHooks           bool                `json:"agent_hooks,omitempty"`
	ApprovalsEnabled     bool                `json:"approvals_enabled,omitempty"` // deprecated: migrated to AgentHooks in v4
	// Yolo opts this session into auto-approve ("yolo") mode: the PreToolUse
	// approval hook is installed and every request is auto-allowed via the
	// "auto" approvals backend, regardless of the global [approvals] backend.
	Yolo         bool   `json:"yolo,omitempty"`
	Starred      bool   `json:"starred,omitempty"`
	SystemKind   string `json:"system_kind,omitempty"`
	StopReason   string `json:"stop_reason,omitempty"`
	BackoffLevel int    `json:"backoff_level,omitempty"`
	FreshStart   bool   `json:"fresh_start,omitempty"`
	// StuckRestarts counts consecutive startup-watchdog restarts for this session
	// (#1092). It caps restart storms for a permanently-broken session and is
	// reset to 0 once the session produces output. Runtime-recovery state, but
	// persisted so the cap survives a daemon restart mid-storm.
	StuckRestarts  int             `json:"stuck_restarts,omitempty"`
	LastStartedAt  time.Time       `json:"last_started_at,omitempty"`
	SummaryText    string          `json:"summary_text,omitempty"`
	SummarySetAt   *time.Time      `json:"summary_set_at,omitempty"`
	SummaryTTL     int             `json:"summary_ttl,omitempty"`
	LastOutputAt   *time.Time      `json:"last_output_at,omitempty"`
	CreatedAt      time.Time       `json:"created_at"`
	LastAttachedAt *time.Time      `json:"last_attached_at,omitempty"`
	CreationCfg    *CreationConfig `json:"creation_config,omitempty"`
	Token          string          `json:"token,omitempty"`
	ScenarioID     string          `json:"scenario_id,omitempty"`
	ScenarioName   string          `json:"scenario_name,omitempty"`
	ScenarioRole   string          `json:"scenario_role,omitempty"`
	ScenarioGoal   string          `json:"scenario_goal,omitempty"`
	// TriggerID / TriggerReactor mark a session spawned by a trigger's
	// ensure/session action, so the trigger can find and reuse it idempotently
	// (mirroring the ScenarioID markers).
	TriggerID      string `json:"trigger_id,omitempty"`
	TriggerReactor bool   `json:"trigger_reactor,omitempty"`
	// AutoCleanup, when non-empty, soft-deletes this trigger-spawned session
	// when it stops: config.CleanupAlways on any stop, config.CleanupOnSuccess
	// only on a clean (exit 0) stop. Empty disables it. Set only on
	// trigger-spawned sessions, so cleanup never touches a manually created one.
	AutoCleanup string `json:"auto_cleanup,omitempty"`
	// IdleTimeoutSecs overrides the agent-default idle-stop window for this
	// session (seconds; 0 = use the agent default). Set on trigger-spawned
	// sessions so an auto_cleanup briefing reaps itself promptly.
	IdleTimeoutSecs int            `json:"idle_timeout_secs,omitempty"`
	MigratedFrom    *MigrationInfo `json:"migrated_from,omitempty"`
	// DeletedAt marks a session as soft-deleted. When set, the session is
	// hidden from the default `gr list` and overlay, its worktree and state are
	// preserved until ExpiresAt, and the daemon purges it (hard delete) once the
	// window elapses. `gr restore` clears this field.
	DeletedAt *time.Time `json:"deleted_at,omitempty"`
	// ExpiresAt is the purge deadline, frozen to DeletedAt + retention at delete
	// time. It is NOT recomputed from current config on each sweep, so a config
	// change only affects future deletes and the "Recoverable until <time>" the
	// user was promised never shifts under them. `gr restore` clears this too.
	ExpiresAt *time.Time `json:"expires_at,omitempty"`
}

func (*SessionState) IsSoftDeleted added in v0.66.16

func (s *SessionState) IsSoftDeleted() bool

IsSoftDeleted reports whether the session has been soft-deleted and is awaiting restore or purge.

type SessionStatus

type SessionStatus string
const (
	StatusRunning  SessionStatus = "running"
	StatusStopped  SessionStatus = "stopped"
	StatusErrored  SessionStatus = "errored"
	StatusDeleting SessionStatus = "deleting"
	StatusCreating SessionStatus = "creating"
)

type State

type State struct {
	Version   int                       `json:"version"`
	Sessions  map[string]*SessionState  `json:"sessions"`
	Scenarios map[string]*ScenarioState `json:"scenarios,omitempty"`
	// PairedDevices holds remote client devices authorized via pairing for the
	// optional network control surface (design §B.2), keyed by device ID.
	PairedDevices map[string]*PairedDevice `json:"paired_devices,omitempty"`
	// PairingHMACKey is the key used to HMAC client tokens at rest. Generated
	// lazily on first pairing via EnsurePairingHMACKey; never the token itself.
	PairingHMACKey string `json:"pairing_hmac_key,omitempty"`
	// TriggerRuntime holds per-trigger-definition runtime facts that must survive
	// a daemon restart (at-most-once fire anchor, pause state, history). Keyed by
	// the namespaced trigger name. The trigger *definition* lives in config and is
	// not persisted here. Per-binding watch state is in-memory (rebuilt from live
	// sessions) and is not persisted.
	TriggerRuntime map[string]*TriggerRuntimeState `json:"trigger_runtime,omitempty"`
	// PRWatchPromptedAuthors is the set of untrusted PR-comment author logins the
	// PR-watch loop has already surfaced to the orchestrator (the trust prompt),
	// so a given author is surfaced at most once ever. Keyed by lower-cased login,
	// global (matching the global comment allowlist). Persisted so the once-only
	// guarantee survives a daemon restart. Bounded (see maxPRWatchPromptedAuthors)
	// so it can't grow without limit on a busy public repo.
	PRWatchPromptedAuthors map[string]bool `json:"pr_watch_prompted_authors,omitempty"`
}

func LoadState

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

func NewState

func NewState() *State

func (*State) EnsurePairingHMACKey added in v0.66.3

func (s *State) EnsurePairingHMACKey() (string, error)

EnsurePairingHMACKey returns the key used to HMAC client tokens at rest, generating and storing it on first use. The caller must hold the state write lock and persist the state afterward.

func (*State) Reconcile

func (s *State) Reconcile()

type StateVersionError added in v0.66.16

type StateVersionError struct {
	FileVersion   int
	BinaryVersion int
}

StateVersionError is returned by LoadState when the on-disk state file is newer than this binary understands. The daemon treats this as fatal (refuses to start) rather than starting with empty state, which would orphan running agents and operate against the wrong picture — see Run.

func (*StateVersionError) Error added in v0.66.16

func (e *StateVersionError) Error() string

type StreamInfo

type StreamInfo struct {
	Name     string `json:"name"`
	Total    int64  `json:"total"`
	Unread   int64  `json:"unread"`
	LatestAt string `json:"latest_at,omitempty"`
}

type TailnetIdentity added in v0.66.3

type TailnetIdentity struct {
	User string
	Node string
	Tags []string
}

TailnetIdentity is the resolved Tailscale identity behind a remote connection, obtained via the tailnet LocalClient's WhoIs at accept time. User and Node are stable Tailscale identifiers (not display names). For a tagged node WhoIs returns no user, so User is empty and Tags carries the node's tags.

type TokenStats added in v0.67.6

type TokenStats struct {
	Input         int64
	Output        int64
	CacheCreation int64
	CacheRead     int64
	Unclassified  int64
	Total         int64
	// Degraded reports that the count was parsed but with conflicts or
	// un-deduplicatable records, so it is approximate.
	Degraded bool
	// CountedAt is the time of the last successful observation, for staleness.
	CountedAt time.Time
}

TokenStats is the runtime-only token usage for a session's current agent, aggregated from its on-disk transcript. The four categories plus Unclassified are mutually exclusive, so Total is a real total. Built fresh each poll and assigned whole (never mutated in place) so off-lock clones are race-free.

type TriggerRun added in v0.67.0

type TriggerRun struct {
	ScheduledAt     time.Time `json:"scheduled_at"`
	SourceSessionID string    `json:"source_session_id,omitempty"`
	Cause           string    `json:"cause"`  // schedule | catch_up | manual | file
	Result          string    `json:"result"` // per action type
}

TriggerRun is one entry in a trigger's bounded run history.

type TriggerRuntimeState added in v0.67.0

type TriggerRuntimeState struct {
	Name                string       `json:"name"`
	Fingerprint         string       `json:"fingerprint"`
	Paused              bool         `json:"paused,omitempty"`
	ActivatedAt         *time.Time   `json:"activated_at,omitempty"`
	LastScheduledFireAt *time.Time   `json:"last_scheduled_fire_at,omitempty"`
	NextScheduledFireAt *time.Time   `json:"next_scheduled_fire_at,omitempty"`
	LastError           string       `json:"last_error,omitempty"`
	RunCount            int          `json:"run_count,omitempty"`
	History             []TriggerRun `json:"history,omitempty"`
}

TriggerRuntimeState is the persisted, per-definition runtime state for a trigger. See docs/design/2026-07-11-triggers-design.md §State model.

type UpgradeManifest

type UpgradeManifest struct {
	ListenerFd int              `json:"listener_fd"`
	ConfigFile string           `json:"config_file"`
	Profile    string           `json:"profile,omitempty"`
	Sessions   []UpgradeSession `json:"sessions"`
}

func ReadManifest

func ReadManifest(path string) (*UpgradeManifest, error)

type UpgradeSession

type UpgradeSession struct {
	ID  string `json:"id"`
	Fd  int    `json:"fd"`
	PID int    `json:"pid"`
}

Jump to

Keyboard shortcuts

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