session

package
v0.37.0 Latest Latest
Warning

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

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

Documentation

Overview

Package session manages on-disk session entries at `<BaseDir>/sessions/<id>/`. A session is one Slack thread or one UI conversation: routing key, status, agent registry, log files, and an optional reference to a project whose folder is the agent cwd.

Sessions own no project files of their own — a project is a shared bundle (see internal/agents/project) whose folder the pool resolves to a cwd at spawn time. See agents-design.md §0.2 for the refactor that removed the per-session git worktree, and the project design (internal/planning/archive/project/design.md) for the Workspace→Project rename.

Files in this package:

  • session.go — Meta + lifecycle (Create/Load/Save/Delete/SetProject)
  • agents.go — per-session AgentEntry + Add/SetActive

Index

Constants

This section is empty.

Variables

View Source
var ErrNoAgentName = errors.New("session: agent name is empty")

ErrNoAgentName rejects a blank agent name.

The setters below create an entry when the name does not match, which is how a single blank name turned into a growing list of unaddressable rows: nothing can ever match "" again, so every later call appended another one, and the session appeared to hold a second agent nobody created. A blank name is always a caller bug, so it fails loudly instead of writing.

Functions

func AbandonGoal added in v0.35.2

func AbandonGoal(layout agentconfig.Layout, id, note string) error

AbandonGoal marks the open latch abandoned. No open goal → no-op.

func ActiveRunTarget added in v0.36.2

func ActiveRunTarget(layout config.Layout, id string) (providerKey, modelID string, ok bool)

ActiveRunTarget reports the provider instance + model a session is actually running on, read from its active agent entry.

This is the pair a delegated sub-agent inherits when its role names no provider of its own, so it must read the SAME fields the pool reads on spawn — Provider and ModelID off ONE entry — rather than reconstructing the choice from elsewhere. Returned as two strings rather than a provider.RunTarget because provider imports this package; the caller pairs them.

The entry is picked by Meta.ActiveAgent, falling back to the first one: a session created outside the UI flow may never have had an active agent set, and treating that as "runs on nothing" would silently drop the inheritance the caller asked for.

ok=false means the session could not be read or has no agent entries — distinct from an entry that names no provider, which is a real answer ("this session has no opinion") and returns ok=true with an empty providerKey.

func AddAgent

func AddAgent(layout config.Layout, id, name, provider string) error

AddAgent appends a new agent entry. Errors on duplicate name within the same session.

func CompleteGoal added in v0.35.2

func CompleteGoal(layout agentconfig.Layout, id, note string) error

CompleteGoal marks the open latch done. No open goal → no-op.

func Delete

func Delete(_ context.Context, layout config.Layout, id string) error

Delete removes the session folder. The project it points at is left untouched — projects are shared, so deleting a session must not delete its files.

func HasOpenGoal added in v0.35.2

func HasOpenGoal(layout agentconfig.Layout, id string) bool

HasOpenGoal reports whether the session has an open goal latch.

func HasOpenGoalDir added in v0.35.2

func HasOpenGoalDir(sessionDir string) bool

HasOpenGoalDir is the engine-side form of HasOpenGoal.

func List

func List(layout config.Layout) ([]string, error)

List returns every top-level session ID, sorted. Sub-agent sessions live inside their parent's folder and are deliberately NOT returned — this is what keeps them out of the conversation list.

func ListAll added in v0.36.1

func ListAll(layout config.Layout) ([]string, error)

ListAll returns every session ID, sub-agents included, parents before their children.

Callers that hydrate runtime state — the boot registry, the workspace sweeper — must use this rather than List: a sub-agent is a real session with a real transcript and real connector instances, and skipping it would leave it unrecoverable after a restart and its instances unreaped. Anything that renders the conversation list wants List.

func ListChildren added in v0.36.1

func ListChildren(layout config.Layout, parentID string) ([]string, error)

ListChildren returns the IDs of the sub-agent sessions delegated directly by parentID, sorted. Empty for a session that never delegated.

func OpenGoal added in v0.35.2

func OpenGoal(layout agentconfig.Layout, id, goal, note string) error

OpenGoal writes/replaces an open latch.

func SaveAgents

func SaveAgents(layout config.Layout, id string, agents []AgentEntry) error

SaveAgents atomically rewrites sessions/<id>/agents.json. nil becomes an empty array on disk so consumers don't have to handle `null`.

func SaveMeta

func SaveMeta(layout config.Layout, id string, meta Meta) error

SaveMeta atomically rewrites sessions/<id>/meta.json.

func SetActiveAgent

func SetActiveAgent(layout config.Layout, id, name string) error

SetActiveAgent updates meta.json's active_agent field. The named agent must already exist in agents.json.

func SetAgentProvider added in v0.36.2

func SetAgentProvider(layout config.Layout, id, name, providerKey string) error

SetAgentProvider repoints an existing agent entry at another provider instance, leaving the rest of the entry (resume id, caps) intact.

Distinct from AddAgent, which refuses a name that already exists. Used to repair an entry created with no provider: the spawn path has to fill one in, and re-adding would drop the resume id the entry already carries. No-op when the entry doesn't exist.

func SetCLISessionID added in v0.15.5

func SetCLISessionID(layout config.Layout, id, name, cliID string) error

SetCLISessionID writes (or clears, with "") the CLI resume id on the agent entry. No-op when the entry doesn't exist.

func SetMaxTurns added in v0.15.5

func SetMaxTurns(layout config.Layout, id, name string, maxTurns int) error

SetMaxTurns persists the per-spawn turn cap on the agent entry, creating it if missing. 0 = unlimited (provider default).

func SetModelID added in v0.34.0

func SetModelID(layout config.Layout, id, name, modelID string) error

SetModelID persists the pinned model id on the agent entry, creating it if missing. Empty = unset (the active provider's own default applies).

func SetModelIDIfEmpty added in v0.36.2

func SetModelIDIfEmpty(layout config.Layout, id, name, modelID string) error

SetModelIDIfEmpty pins a model only when the entry carries no pin.

Inherited defaults use this rather than SetModelID: a session already pinned to a model chose that deliberately, and a project default must not silently move it. No-op when the entry doesn't exist or already has a pin.

func SetProject added in v0.14.21

func SetProject(_ context.Context, layout config.Layout, id, newProjectID string) error

SetProject changes the project a session points at. No filesystem work — the next agent spawn will resolve the new project's folder as cwd. Conversation, agent registry, and logs are preserved. Empty newProjectID unscopes the session.

func SetSystemAddon added in v0.36.0

func SetSystemAddon(layout config.Layout, id, addon string) error

SetSystemAddon replaces a session's free-text system-prompt addon.

Takes effect on the next spawn, like a preset edit. Used by delegation to give a sub-agent its role's system prompt: a role is not a named preset, so writing one would put a per-role file into the shared preset list where nobody expects it.

func SetThinkingTokens added in v0.18.7

func SetThinkingTokens(layout config.Layout, id, name, v string) error

SetThinkingTokens persists the resolved MAX_THINKING_TOKENS env value on the agent entry, creating it if missing. Empty = unset (provider default, thinking on); "0" = disabled; "<n>" = budget. Always persisted (including "") so switching a reused session back to full thinking clears a prior value.

Types

type AgentEntry

type AgentEntry struct {
	Name             string            `json:"name"`
	Provider         string            `json:"provider"`
	CLISessionID     string            `json:"cli_session_id,omitempty"`
	Status           string            `json:"status"`
	CreatedAt        time.Time         `json:"created_at"`
	LastActive       time.Time         `json:"last_active,omitempty"`
	ProviderSessions map[string]string `json:"provider_sessions,omitempty"`
	// MaxTurns caps agentic turns on the next spawn (--max-turns).
	// 0 = unlimited (provider default). Set per workflow agent node.
	MaxTurns int `json:"max_turns,omitempty"`
	// ThinkingTokens is the resolved MAX_THINKING_TOKENS env value applied on
	// the next spawn (claude). Empty = unset (provider default, thinking on);
	// "0" = disabled; "<n>" = budget. Set per workflow agent node from its
	// thinking + max_thinking_tokens inputs.
	ThinkingTokens string `json:"thinking_tokens,omitempty"`
	// ModelID pins a specific model id this agent uses on its next spawn,
	// scoped to whichever provider Provider currently points at. Empty =
	// use that provider instance's own default-model resolution. Currently
	// only meaningful for wick (WickModel.ID); other provider types ignore
	// it. Cleared on switching to a provider type that doesn't recognize
	// it, so a later switch back doesn't resurrect a stale/foreign pin.
	ModelID string `json:"model_id,omitempty"`
}

AgentEntry is one row in sessions/<id>/agents.json. CLISessionID is the resume key — wick captures it from the first stream-json event emitted by the CLI and persists it so subsequent spawns can pass `--resume <id>`. See agents-design.md §5.2.

ProviderSessions maps "type/name" provider keys to their last-known CLI session ID. When switching providers, the outgoing resume ID is saved here so switching back can resume the old conversation.

type CreateOptions

type CreateOptions struct {
	ID        string
	ProjectID string
	Origin    Origin
	ChannelID string
	// Preset is the preset name to associate with this session.
	// Stored in meta.json; factory loads content from presets/<name>/agent.md on spawn.
	Preset string
	// UserID is the wick user who is creating this session. Stored in
	// meta.json for ownership checks in MCP handlers.
	UserID string
	// ParentSessionID marks this session as a delegated sub-agent's
	// isolated context. See Meta.ParentSessionID.
	ParentSessionID string
}

CreateOptions describes a new session. ProjectID is optional — a session may be created without one, in which case the pool falls back to the tools-config default project (or a per-session temp dir if no default is set).

type Goal added in v0.35.2

type Goal struct {
	Goal      string     `json:"goal"`
	Status    GoalStatus `json:"status"`
	Note      string     `json:"note,omitempty"`
	UpdatedAt time.Time  `json:"updated_at"`
}

Goal is the on-disk record.

func LoadGoal added in v0.35.2

func LoadGoal(layout agentconfig.Layout, id string) (*Goal, error)

LoadGoal reads the session's goal latch. Missing file → (nil, nil).

func LoadGoalDir added in v0.35.2

func LoadGoalDir(sessionDir string) (*Goal, error)

LoadGoalDir reads goal.json from an absolute session directory (engine path — it already holds SessionDir, not layout+id).

type GoalStatus added in v0.35.2

type GoalStatus string

GoalStatus is the lifecycle of one session goal.

const (
	GoalOpen      GoalStatus = "open"
	GoalDone      GoalStatus = "done"
	GoalAbandoned GoalStatus = "abandoned"
)

type Meta

type Meta struct {
	// ProjectID references the project this session belongs to. Empty =
	// unscoped (pool falls back to the tools-config default project).
	ProjectID    string    `json:"project_id,omitempty"`
	Origin       Origin    `json:"origin"`
	ChannelID    string    `json:"channel_id,omitempty"`
	ActiveAgent  string    `json:"active_agent,omitempty"`
	Status       Status    `json:"status"`
	CreatedAt    time.Time `json:"created_at"`
	LastActive   time.Time `json:"last_active"`
	PendingInput []string  `json:"pending_input,omitempty"`
	// Label is the session title shown in the sidebar. By default it is
	// the first user message truncated to 60 runes (see
	// pool.setLabelIfEmpty), cached here so sidebar rendering never needs
	// to open conversation.jsonl. Once a human or the agent sets an
	// explicit title (TitleCustom=true) the auto-derived label no longer
	// overwrites it.
	Label string `json:"label,omitempty"`
	// TitleCustom marks Label as an explicit title set by a human or the
	// agent (via the wick_set_title MCP tool), as opposed to the
	// auto-derived first-message label. When true, the first-user-message
	// auto-label is skipped so it never clobbers the chosen title.
	TitleCustom bool `json:"title_custom,omitempty"`
	// Preset is the name of the preset active for this session.
	// Factory reads the preset content from presets/<name>/agent.md on
	// every spawn so edits to the preset take effect on next respawn.
	Preset string `json:"preset,omitempty"`
	// SystemAddon is free-text system-prompt content appended after the
	// preset on every spawn. Unlike Preset it is not a named file, so it
	// carries prompt text that belongs to THIS session and has no reason
	// to appear in the shared preset list — a sub-agent role's system
	// prompt, or a project's default addon.
	//
	// Read by the pool on each spawn, so an edit takes effect on the next
	// respawn exactly like a preset edit does.
	SystemAddon string `json:"system_addon,omitempty"`
	// Subscribers is the list of user IDs that opted in to receive
	// browser push notifications for this session's lifecycle
	// transitions (queue → working → idle). Persisted in meta.json so
	// the subscription survives wick restart. Sessions are shared
	// across users (anyone can open them) but pushes target only the
	// IDs in this list — no auto-subscribe on session create.
	Subscribers []string `json:"subscribers,omitempty"`
	// UserID is the wick user ID that created this session. Empty for
	// legacy sessions created before ownership tracking was added.
	// When non-empty, only the owning user (or app owner) may access it.
	UserID string `json:"user_id,omitempty"`
	// AutoReply marks a Slack channel thread as auto-reply: while true,
	// replies in the thread are dispatched to the agent without an
	// @mention. Set when the thread is created (the bot also drops a 🤖
	// marker on the parent) and toggled by adding/removing that marker.
	// Persisted here so the switch survives a wick restart even though
	// the channel's in-memory state is lost. Only meaningful for Slack
	// channel sessions; absent/false everywhere else.
	AutoReply bool `json:"auto_reply,omitempty"`
	// ParentSessionID links a sub-agent's isolated session back to the
	// session that delegated it. Non-empty = this is a child: hidden
	// from the conversation list and surfaced in the parent's Sub-agents
	// rail panel instead.
	//
	// This relation is the ONLY marker of "is a child" — there is
	// deliberately no separate `type: subagent` field, because two
	// sources of truth for the same fact can disagree. Absent on every
	// session created before sub-agents existed, which reads correctly
	// as "not a child".
	ParentSessionID string `json:"parent_session_id,omitempty"`
}

Meta is the persisted-on-disk shape of a session. PendingInput is the message buffer that survives wick restart so a session that was queued at shutdown gets its messages drained on next boot.

func (*Meta) AddSubscriber added in v0.15.0

func (m *Meta) AddSubscriber(userID string) bool

AddSubscriber appends userID to Subscribers if not already present. Returns true if the list changed (i.e. caller should persist Meta).

func (*Meta) IsSubscribed added in v0.15.0

func (m *Meta) IsSubscribed(userID string) bool

IsSubscribed returns true when userID has opted in to receive lifecycle push notifications for this session.

func (*Meta) RemoveSubscriber added in v0.15.0

func (m *Meta) RemoveSubscriber(userID string) bool

RemoveSubscriber drops userID from Subscribers. Returns true if the list changed.

type Origin

type Origin string

Origin identifies where the session was first created from. Slack threads use thread_ts as the session ID; UI/API sessions use a UUID generated by wick.

const (
	OriginSlack    Origin = "slack"
	OriginTelegram Origin = "telegram"
	OriginREST     Origin = "rest"
	OriginUI       Origin = "ui"
)

type Session

type Session struct {
	ID     string       `json:"id"`
	Meta   Meta         `json:"meta"`
	Agents []AgentEntry `json:"agents"`
}

Session is the in-memory view: ID + meta + agent registry. Mirrors the trio of files at sessions/<id>/.

func Create

func Create(_ context.Context, layout config.Layout, opt CreateOptions) (Session, error)

Create materializes sessions/<id>/: meta.json, agents.json (empty array), agent.md snapshot. No filesystem work touches the project folder — project folders live under projects/<id>/ and are shared across sessions; the session simply stores the project id as a reference.

func Load

func Load(layout config.Layout, id string) (Session, error)

Load reads meta.json + agents.json. Missing agents.json is treated as an empty list.

type Status

type Status string

Status is the high-level pool view: idle (no subprocess), queued (waiting for a slot), running (subprocess alive).

const (
	StatusIdle    Status = "idle"
	StatusQueued  Status = "queued"
	StatusRunning Status = "running"
)

Jump to

Keyboard shortcuts

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