Documentation
¶
Overview ¶
Package sessions owns the daemon's tab-level session registry: the ordered list of live sessions, per-tab metadata (name, agent kind, cwd), the active pointer, and the maxSessions guard. It wraps a *pty.Manager — the PTY process + ring buffer live in package pty; this package layers the REST/UX-facing metadata on top.
Layering:
sessions.Manager (tab metadata, ordering, active, maxSessions)
└── pty.Manager (PTY process + ring, ID→Session map)
└── pty.Session (one PTY: readLoop, ring, fan-out, C1 stubber)
The WS handler (/ws/{id}) takes the underlying *pty.Manager directly — streaming bytes doesn't need tab metadata. The REST handlers (/api/sessions/*) take *sessions.Manager for create/list/select/destroy.
In-memory create + list + active pointer + maxSessions + RMOTE_SESSION_ID env injection. Step 1b adds state.json persistence + restore; Step 1c adds per-session routes (select/leave/rename/destroy/swap/exec/scrollback).
Index ¶
- Constants
- Variables
- func ResumeLaunchLine(agent AgentKind, agentSessionID, cwd string) (string, bool)
- func ShellQuote(s string) string
- func ValidAgentSessionID(id string) bool
- type AgentKind
- type CreateRequest
- type Manager
- func (m *Manager) ActiveID() string
- func (m *Manager) ClaimProfileBoot(id string) bool
- func (m *Manager) ClearAgentOnExit(rmoteID, agent string)
- func (m *Manager) ClearAgentSessionID(rmoteID, agent string)
- func (m *Manager) Close()
- func (m *Manager) ConsumeSummonReply(id, summonerID string) bool
- func (m *Manager) Create(req CreateRequest) (*Meta, error)
- func (m *Manager) Destroy(id string) error
- func (m *Manager) FindByProfile(profile string) *Meta
- func (m *Manager) FindOrCreateByProfile(req CreateRequest) (*Meta, bool, error)
- func (m *Manager) ForegroundAgent(id string) AgentKind
- func (m *Manager) GetMeta(id string) (Meta, bool)
- func (m *Manager) GetOrAssignShortName(id, abbrev string) string
- func (m *Manager) Input(id string, p []byte) error
- func (m *Manager) LastReply(id string) time.Time
- func (m *Manager) List() []*Meta
- func (m *Manager) LoadFromState() (int, error)
- func (m *Manager) MarkReply(id, targetID string)
- func (m *Manager) PTYManager() *pty.Manager
- func (m *Manager) RawInput(id string, p []byte) error
- func (m *Manager) ReleaseProfileBoot(id string)
- func (m *Manager) Rename(id, name string) error
- func (m *Manager) SaveStateNow()
- func (m *Manager) Select(id string) error
- func (m *Manager) SetAgentSessionID(rmoteID, agent, agentSessionID string)
- func (m *Manager) SetCwd(rmoteID, cwd string)
- func (m *Manager) SetForegroundAgent(rmoteID, agent string)
- func (m *Manager) SetModelTitle(rmoteID, title string)
- func (m *Manager) SetOnChange(fn func())
- func (m *Manager) SetOnCmdDone(fn func(sessionID string, exitCode *int))
- func (m *Manager) SetOnForegroundAgent(fn func(sessionID, agent string))
- func (m *Manager) SetOnRemove(fn func(sessionID string))
- func (m *Manager) SetSessionTitle(rmoteID, title string)
- func (m *Manager) SetStatsModel(rmoteID, model string)
- func (m *Manager) SetSummary(rmoteID, summary string)
- func (m *Manager) SetSummoner(id, summonerID string)
- func (m *Manager) SetWorktreeResolver(resolve func(id, savedPath string) (cwd, health string))
- func (m *Manager) String() string
- func (m *Manager) SummonTarget(id string) (string, bool)
- func (m *Manager) UpdateStats(rmoteID string, fn func(*SessionStats))
- func (m *Manager) UpdateTitle(rmoteID string, fn func(*TitleState))
- func (m *Manager) ValidateSessionToken(id, token string) bool
- type Meta
- type SessionStats
- type TitleState
Constants ¶
const AgentExitSentinelShell = " ; printf '\\e]rmote-agent-exit\\a'"
AgentExitSentinelShell is the shell snippet appended to every agent launch line. Emits an OSC sequence (`ESC ] rmote-agent-exit BEL`) that iOS's client-side detector recognizes to clear activeAgent when the agent exits and returns to the shell. Codex/grok have no SessionEnd hook; claude's is unreliable — the sentinel is the universal exit signal. Matches Mac's NativePtySession.agentExitSentinelShell.
const MaxSessions = 30
MaxSessions caps the number of live sessions a daemon will host. 30 matches the previous implementation (ServerConstants.maxSessions). Each session holds a 2 MB ring + a PTY FD + subscriber chans, so the bound is as much about memory/fd hygiene as UX. A create that would exceed this returns ErrTooManySessions.
Variables ¶
var ErrNoSuchSession = errors.New("sessions: no such session")
ErrNoSuchSession is returned by Select/Destroy/ListMeta lookups when the ID is not a live session. REST handlers map this to HTTP 404.
var ErrTooManySessions = errors.New("sessions: max session count reached")
ErrTooManySessions is returned by Create when the live session count is at MaxSessions. The REST handler maps this to HTTP 503.
Functions ¶
func ResumeLaunchLine ¶
ResumeLaunchLine builds the shell line that relaunches agent against its own saved session id, appending the exit sentinel. Returns ok=false when the agent is not resumable (KindShell or unknown) or the id fails validation, so callers can skip silently. cwd is only used by grok (which takes --cwd); claude and codex launch in the PTY's cwd and ignore it.
The returned line is written into a PTY, not exec'd, which is why the id is validated here rather than trusted — state.json is user-writable and an injection-shaped id would run arbitrary shell.
func ShellQuote ¶
ShellQuote wraps a path in single quotes for safe interpolation into a shell command line. Handles paths with spaces/special chars. Matches the Mac's NativePtySession.shellQuote. A path containing a single quote itself is split into a concatenated pair of single-quoted strings (the standard shell idiom: 'foo' + \' + 'bar' for foo'bar).
func ValidAgentSessionID ¶
ValidAgentSessionID reports whether id is safe to interpolate into an agent launch line. Alphanumerics, dash and underscore only — no shell metacharacters. The id comes from a user-writable state.json (restore) or an HTTP body (resume route), and is written into a PTY, so validation is mandatory at every call site.
Types ¶
type AgentKind ¶
type AgentKind string
AgentKind classifies a session by what runs inside it. "shell" is the default (user's $SHELL); the others are set when iOS creates a tab intending to run that agent (the daemon doesn't enforce it — the user can type any command — but the kind drives UI: icon, skills bar, detection rules). Matches the previous implementation's agent string set.
type CreateRequest ¶
type CreateRequest struct {
ID string
Name string
NameIsCustom bool
Agent AgentKind
Command string
Args []string
Cwd string
Rows uint16
Cols uint16
Profile string // agent-profile name tag (summon dedup)
ProfileAgent AgentKind
ProfileModel string
ProfileEffort string
WorktreeID string
BindingHealth string
}
CreateRequest is the spawn spec handed to Manager.Create. Name is optional (auto-derived from the shell basename if empty). Agent defaults to KindShell. Cwd must be absolute (the REST handler validates this before calling). Rows/Cols seed the PTY winsize; zero → 24×80 default in pty.Start.
ID is optional: when non-empty, the session is created with that exact ID (used by state.json restore so iOS reconnect finds the same tab IDs). When empty, a fresh UUIDv4 is minted.
type Manager ¶
type Manager struct {
// contains filtered or unexported fields
}
Manager owns the ordered live-session list + active pointer. It wraps a *pty.Manager for the actual PTY lifecycle. Goroutine-safe.
func NewManager ¶
NewManager builds a sessions.Manager wrapping the given pty.Manager. The caller (server.New) constructs one pty.Manager and shares it between the WS handler (streaming) and this manager (REST metadata).
func (*Manager) ClaimProfileBoot ¶
ClaimProfileBoot atomically reserves the right to boot a shell-hosted profile session. It suppresses duplicate launch lines while the first boot is still waiting for foreground-agent detection. A stale claim expires so a failed launch can be retried by a later summon.
func (*Manager) ClearAgentOnExit ¶
ClearAgentOnExit is the authoritative, agent-agnostic "the agent quit" reset for a tab. Called from the SessionEnd hook branch (claude/codex/grok/ antigravity/pi all reach it) it clears the per-agent session-id binding, reverts the tab's agent label back to shell, drops the stale live summary, clears the conversation topic title (ModelTitle + naming lifecycle) so the tab's displayed name reverts to the shell-derived name (folder), and reverts the transient OSC title the agent pinned on launch.
The title is tied to the LIVE agent conversation: quitting drops it (the tab shows its shell name again), and resuming re-derives it from the transcript via the SessionStart → SetSessionTitle path. TitleSessionID is intentionally LEFT intact (not cleared here) so a later SetAgentSessionID can still tell a resume (same id) from a genuinely new conversation (different id) — only the displayed title + lifecycle reset, not the detection key.
The label revert + title clear fire only when the tab currently shows THIS agent, so a stale SessionEnd (e.g. an old pi session_end arriving after the user already launched claude in the same tab) cannot clobber the newer agent's label. The session-id binding for the named agent is cleared regardless. Generic over the agent id — no per-agent hardcoding.
func (*Manager) ClearAgentSessionID ¶
ClearAgentSessionID clears the agent's session id, called on SessionEnd so the skills bar disappears once the agent quits. The PTY tab itself stays alive (the user is dropped back into their shell).
func (*Manager) Close ¶
func (m *Manager) Close()
Close permanently stops debounced persistence and flushes one final state snapshot. Unlike SaveStateNow, it rejects schedulers racing with shutdown.
func (*Manager) ConsumeSummonReply ¶
ConsumeSummonReply atomically claims the active summon reply binding. The Stop-hook auto-reply calls this immediately before delivery so an explicit peer reply can consume the same binding first.
func (*Manager) Create ¶
func (m *Manager) Create(req CreateRequest) (*Meta, error)
Create spawns a new session and registers its metadata. Enforces MaxSessions. Injects RMOTE_SESSION_ID=<id> into the child env so hook events can be attributed back to this tab (the hook ingest to fan out per-session state). Returns the new session's Meta.
func (*Manager) Destroy ¶
Destroy kills and removes a session by ID. Idempotent — a missing ID returns ErrNoSuchSession (so the REST handler can 404), but the underlying pty.Manager.Delete is a no-op on missing IDs. Reclaims the slot in the order list; if the destroyed session was active, the first remaining session becomes active (or none if the list is now empty).
func (*Manager) FindByProfile ¶
FindByProfile returns the first live session tagged with the given profile name, or nil if none exists. Returns a copy to avoid encapsulation leaks.
func (*Manager) FindOrCreateByProfile ¶
func (m *Manager) FindOrCreateByProfile(req CreateRequest) (*Meta, bool, error)
FindOrCreateByProfile atomically finds an existing profile-tagged session or creates a new one. The profileCreateMu serializes concurrent calls for the same profile, preventing duplicate sessions from racing past each other. Returns (meta, created=true) for a new session, (meta, created=false) for an existing one.
func (*Manager) ForegroundAgent ¶
ForegroundAgent returns the currently detected foreground agent kind for a session, or KindShell if no agent is running (shell prompt visible). Used by the peer-messaging endpoint to gate delivery: direct PTY injection only when an agent is foreground, never into a bare shell (red-team C1).
func (*Manager) GetMeta ¶
GetMeta returns the metadata for a single session by ID. Returns ok=false if the session isn't live. Used by git routes to resolve a session's cwd (the working directory git operates in).
func (*Manager) GetOrAssignShortName ¶
GetOrAssignShortName returns the peer-messaging alias for a session.
Profile-summoned sessions (meta.Profile set) use a stable, meaningful alias derived from the profile name (#ccworker, #cxsol) — the tab is "the ccworker tab" and peers address it by profile name, regardless of whether its agent is currently foreground or reverted to shell. Ad-hoc sessions use the agent abbreviation + counter (#cc1, #cx2); those update when the agent type changes (shell → codex → #cx1) and clear when the agent exits to shell.
func (*Manager) Input ¶
Input writes raw bytes to a session's PTY master, delivering them to the child process. Used by the resume routes (write `claude --resume <id>` to a shell tab) and the future exec route (POST /api/sessions/{id}/exec). Returns ErrNoSuchSession for an unknown ID, or pty.ErrSessionClosed if the child has exited. Input writes user input to the session's PTY through the input hook (if set). Used by REST API endpoints that forward user input (e.g., iOS chat submission). The hook intercepts peer-messaging patterns (#name message).
func (*Manager) List ¶
List returns the ordered slice of live-session metadata. The active session has Active=true. The returned slice is a snapshot; callers may mutate freely. Sessions that exited naturally (reaper removed them from pty.Manager) are filtered here so iOS doesn't render dead tabs.
The Title field is pulled fresh from the live pty.Session (the OSC 0/2 scanner updates it in real-time from shell output). A user-set custom name (Meta.Name) takes precedence over the captured title — the title is only used when the user hasn't named the tab explicitly.
func (*Manager) LoadFromState ¶
LoadFromState reads state.json + re-spawns each saved session as a fresh PTY with the saved ID + cwd + name. Returns the count restored. The caller (server.New) skips auto-create-default-session when this returns ≥1.
Saved agent IDs (claude/codex/grok) are restored into Meta so iOS's isActiveClaudeSession fallback works on the first reconnect. The live PTY starts as a plain shell, and for any tab whose saved agent has a matching per-agent session id, the agent is re-launched inside that shell PTY by writing ResumeLaunchLine(id) + "\n" — asynchronously, off the startup critical path, so a slow CLI boot never blocks the listener. Tabs whose agent session id is empty (the user quit the agent before restart, or the tab was always a shell) come back as plain shells. Set RMOTE_AGENT_RESTORE_AGENTS=0 to disable agent relaunch entirely.
func (*Manager) MarkReply ¶
MarkReply stamps LastReplyAt and consumes the active summon binding when the message is addressed to that summoner. Messages to other peers still satisfy `wait --until replied` but do not suppress the expected summon result.
func (*Manager) PTYManager ¶
PTYManager returns the underlying pty.Manager. The WS handler uses this to look up sessions for /ws/{id} streaming without going through the metadata layer.
func (*Manager) RawInput ¶
RawInput writes directly to the PTY WITHOUT the input hook. Used by the daemon for programmatic injection (peer messages, seeding, resume launch, uploads) so injected text starting with '#' is not re-intercepted.
func (*Manager) ReleaseProfileBoot ¶
ReleaseProfileBoot clears the in-flight boot marker after foreground detection or an immediate PTY write failure.
func (*Manager) Rename ¶
Rename sets a custom display name on a session. Persists to state.json so the name survives daemon restart. Empty name is allowed (clears the custom name; callers may then re-derive from AutoTitle). Returns ErrNoSuchSession for an unknown ID.
func (*Manager) SaveStateNow ¶
func (m *Manager) SaveStateNow()
SaveStateNow cancels any pending debounced save and writes state.json synchronously. Used by the daemon's SIGINT/SIGTERM handler so brm never drops the latest batch of session mutations to the 200 ms debounce window.
Lock order: take stateMu only to cancel the timer, release it BEFORE calling writeState. writeState takes m.mu (the meta lock) internally and never stateMu — nesting stateMu inside m.mu would invert the documented order at manager.go:352 ("stateMu never nests inside m.mu"). Safe to call concurrently with saveStateDebounced: a just-fired timer will already have released stateMu by the time we acquire it (Stop returns false, we proceed); a saveStateDebounced call arriving during writeState schedules a fresh timer that never fires because the process is exiting.
func (*Manager) Select ¶
Select marks id as the active session. Returns ErrNoSuchSession if id is not a live session. The active pointer drives which tab iOS lands on after a session list refresh.
func (*Manager) SetAgentSessionID ¶
SetAgentSessionID records the agent's own session id on the tab's meta, keyed generically by agent id in AgentSessionIDs (the single source of truth). Called when a SessionStart hook event arrives — the agent has just launched and reported its session id. Exposed via /api/sessions (derived from the map) so iOS's isActiveClaudeSession/isActiveTabCodex/isActiveTabGrok fallbacks work after force-quit-reopen, when the per-tab WS 0x08 frame hasn't arrived.
func (*Manager) SetCwd ¶
SetCwd updates a tab's stored cwd to live, persisting it so daemon-restart auto-resume lands the agent in its actual project directory. Called by the server-layer foreground-agent callback (Server.handleFgAgentChange) right after resolveLiveCwd reads the PTY's live process cwd — claude/codex session ids are scoped to a project path (~/.claude/projects/<encoded-cwd>/…), so a stale creation-time cwd in state.json makes claude --resume <id> look under the wrong encoded path and fail to find the session.
Matches SetForegroundAgent's mutate-under-m.mu → saveStateDebounced + notifyChange shape. No-op when the tab is unknown, live is empty, or live matches the stored value. The debounce coalesces the concurrent SetForegroundAgent + SetCwd writes from the same fg-detection tick into a single disk write, so the cwd refresh adds no extra I/O on the hot path.
func (*Manager) SetForegroundAgent ¶
process (codex/grok/claude found among the shell's descendants), or reverts it to shell when `agent` is "" (the agent exited). Authoritative — overrides any prior label so switching agents in one tab is reflected, and so the tab reverts to its default chip/skills bar when the agent quits. Persists + notifies so iOS updates on the next session-list refresh.
func (*Manager) SetModelTitle ¶
SetModelTitle stores a model-derived session title (Line 1) WITHOUT touching Name/NameIsCustom. Read precedence (List + session row) is custom > model > OSC > first-msg, so a user /name always wins and OSC never clobbers a model title (advisory (a): the old code overwrote Meta.Name from AutoTitle). Blank is ignored (keep last good title). Persists to state.json like SetSummary. The server drives the 0x04 push separately.
func (*Manager) SetOnChange ¶
func (m *Manager) SetOnChange(fn func())
SetOnChange registers a best-effort notification for mutations visible in the live session list. The callback runs after the manager lock is released.
func (*Manager) SetOnCmdDone ¶
SetOnCmdDone wires the cmdDone callback. The callback receives the session ID + the shell-reported exit code (nil if the marker carried none). Called from each session's readLoop goroutine — the callback MUST be goroutine- safe (the server's implementation publishes to hooks.Bus, which is).
func (*Manager) SetOnForegroundAgent ¶
SetOnForegroundAgent wires the foreground-agent-change callback. Fires with the detected agent kind ("codex"/"grok"/"claude") on launch or "" on exit. Called from the pty readLoop goroutine — the callback MUST be goroutine-safe (the server's implementation pushes a WS frame, which is).
func (*Manager) SetOnRemove ¶
SetOnRemove registers a callback for proven session removal, including natural child exit and explicit destroy. It runs after manager locks release.
func (*Manager) SetSessionTitle ¶
SetSessionTitle sets the tab's topic label from the agent's OWN session name (forwarded on SessionStart / session_info_changed) and locks the on-device naming lifecycle so the model doesn't re-derive/overwrite it on the next turn. Unlike SetModelTitle (which ignores blank to keep the last good value), a blank title clears the label and unlocks re-derivation — used when the agent reports its session is unnamed. Generic over the agent id; any agent that forwards a `title` in its hook payload reaches this. A user /name (NameIsCustom) always wins the displayed slot regardless.
func (*Manager) SetStatsModel ¶
SetStatsModel records the active model for a session (from the hook stream's agent identity or a profile model tag). Convenience over UpdateStats.
func (*Manager) SetSummary ¶
SetSummary stores the Foundation Model's "what it's doing now" one-liner for a tab and persists it to state.json so a daemon restart keeps the panel populated until the next hook refreshes it. Matches the SetCwd mutate-under-m.mu → saveStateDebounced shape. A blank summary is ignored: the summarizer emits blank only when the model is unavailable or input is empty, and overwriting a good summary with blank would blank the panel for no reason. The server caller drives the 0x04 client push separately (markLiveSessionsChanged), so this only stores + persists. No-op for an unknown tab or an unchanged value.
func (*Manager) SetSummoner ¶
SetSummoner marks id as summoned by summonerID and stamps the auto-reply window start. The Stop-hook auto-reply fires while the binding is active.
func (*Manager) SetWorktreeResolver ¶
func (*Manager) SummonTarget ¶
SummonTarget returns the summoner's session ID when id has an active summon binding (non-empty and within summonReplyWindow). A lapsed binding is cleared lazily here so the tab stops replying after the window.
func (*Manager) UpdateStats ¶
func (m *Manager) UpdateStats(rmoteID string, fn func(*SessionStats))
UpdateStats mutates a session's Stats under the meta lock and persists on change. The fn receives a pointer to the current Stats (zero-value if none) and may set any field. Returning leaves persistence to saveStateDebounced. No-op for an unknown session.
func (*Manager) UpdateTitle ¶
func (m *Manager) UpdateTitle(rmoteID string, fn func(*TitleState))
UpdateTitle mutates a session's Line-1 lifecycle state (TitleState) under the meta lock and persists on change. The server's title code (server_title.go) uses this to buffer user prompts, flip Meaningful on first confident title, bump Attempts on UNCLEAR, and reset on a detected topic change. Persisting TitleState means a daemon restart mid-session keeps the naming context. No-op for an unknown session.
func (*Manager) ValidateSessionToken ¶
type Meta ¶
type Meta struct {
ID string `json:"session_id"`
Name string `json:"name"`
NameIsCustom bool `json:"name_is_custom"`
Agent AgentKind `json:"agent"`
Cwd string `json:"path"`
Shell string `json:"command"` // the spawn command (shell path or agent CLI)
Active bool `json:"active"`
AutoTitle string `json:"auto_title,omitempty"` // OSC 0/2 captured title; empty until shell emits one
ModelTitle string `json:"model_title,omitempty"` // model-derived session title (Line 1); precedence: custom > model > OSC
Summary string `json:"summary,omitempty"` // Foundation Model "what it's doing now" line; empty → fall back to auto_title
Stats SessionStats `json:"stats,omitempty"` // Line 3: model · msgs · turns · tokens · last-updated (agent-agnostic)
Title TitleState `json:"title,omitempty"` // Line 1 lifecycle state (prompt buffer + flags); persisted across restart
TitleSessionID string `json:"title_session_id,omitempty"` // agent session id the current ModelTitle/Title belongs to; persists across quit so resume-vs-new-conversation is detectable
ShortName string `json:"short_name,omitempty"` // peer-messaging alias (#cc1); assigned once, never changed
WorktreeID string `json:"worktree_id,omitempty"`
BindingHealth string `json:"binding_health,omitempty"`
Index int `json:"-"` // position in the tab strip (0-based); recomputed by List callers
SessionToken string `json:"-"` // in-memory capability injected into this PTY only
// AgentSessionIDs is the single canonical per-agent session-id binding
// (SessionStart sets, SessionEnd / ClearAgentOnExit clears). Exposed on
// the wire as agent_session_ids; the skills bar / restart auto-resume read
// it after a force-quit-reopen when no fresh 0x08 frame has arrived yet.
// Keyed generically by agent id — no per-agent struct mirrors.
AgentSessionIDs map[agents.ID]string `json:"-"`
Profile string `json:"profile,omitempty"` // agent-profile name tag (summon dedup)
ProfileAgent AgentKind `json:"-"`
ProfileModel string `json:"-"`
ProfileEffort string `json:"-"`
// Orchestration (in-memory only; not persisted — a daemon restart re-runs
// any summon fresh, and stale reply bindings must not survive). The
// Stop-hook auto-reply fires for a session while Summoner is set and
// SummonedAt is within summonReplyWindow. LastReplyAt advances whenever a
// peer message is sent FROM this session, enabling `wait --until replied`.
Summoner string `json:"-"`
SummonedAt time.Time `json:"-"`
LastReplyAt time.Time `json:"-"`
// contains filtered or unexported fields
}
Meta is the per-tab metadata layered over a live pty.Session. The PTY process details (PID, ring, subscribers) live in pty.Session; Meta holds what the REST API + iOS tab strip need: display name, agent kind, cwd.
func (Meta) AgentSessionIDMap ¶
AgentSessionIDs returns the canonical open agent→session-id map (empty values dropped). AgentSessionIDs is the single source of truth; there are no per-agent struct matches to fall back to.
type SessionStats ¶
type SessionStats struct {
Model string `json:"model,omitempty"`
Messages int `json:"messages,omitempty"`
Turns int `json:"turns,omitempty"`
Tokens int64 `json:"tokens,omitempty"`
LastUpdatedMs int64 `json:"last_updated_ms,omitempty"`
}
SessionStats is the Line-3 payload: a compact, agent-agnostic stats summary rendered as `model · msgs · turns · tokens · updated Xm ago`. All fields omitempty so a fresh session serializes to {} until the first hook event.
Counting is event-driven (works for every agent without parsing their token-schema JSON): UserPromptSubmit → Messages++, Stop/AfterModel → Turns++. Tokens stays 0 until a per-agent usage path lands (phase-05 refinement); LastUpdatedMs is the observation time (when the daemon last updated this struct), distinct from session mtime.
type TitleState ¶
type TitleState struct {
Prompts []string `json:"prompts,omitempty"`
Meaningful bool `json:"meaningful,omitempty"`
Attempts int `json:"attempts,omitempty"`
}
TitleState is the Line-1 lifecycle state, persisted to state.json so a daemon restart doesn't lose naming context for a long-running session. Driven by server_title.go: prompts accumulate as naming input until the model returns a confident title (Meaningful=true, then locked). Attempts bounds the UNCLEAR retries before a best-effort call forces a name.