session

package
v0.116.3 Latest Latest
Warning

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

Go to latest
Published: Jul 9, 2026 License: MIT Imports: 24 Imported by: 0

Documentation

Overview

Package session is the runtime persistence layer for live PTY sessions (this binary's runtime state).

State today (post-Phase-2c-2 slice 28):

  • The runtime canonical store is a kv row at (namespace="sessions", key="all", scope=local, type=json, value=JSON([]SessionInfo)). scope=local because session state is per-peer PTY metadata that is never replicated — same semantic the v0 sessions.json file had.
  • On first Load after upgrade from a pre-slice-28 v1 install or from v0, Store falls back to the legacy file at <configdir.Path()>/sessions.json (v1 dir) and, if absent, at <configdir.V0Path()>/sessions.json (the v0 dir — tmux session names live there, attach is still wire-compatible because `kojo_<id>` naming and `tmux attach-session` semantics did not change between v0 and v1). The selected file's contents are mirrored into kv via an IfMatchAny PutKV (collision-safe vs a same-peer concurrent writer; scope=local means the row is never replicated, so the only race here is inside the same kojo process tree). After a successful mirror the v1-dir legacy file is best-effort unlinked; the v0-dir legacy file is NEVER unlinked from here — that is `kojo --clean v0`'s job, and leaving it in place lets a roll-back to v0 still find its own sessions.json. The pattern matches LoadCronPaused / loadOrCreateOwner from internal/agent and internal/auth — a stray legacy file from a v1→v0→v1 round trip is treated as non-authoritative once kv has the row, but only after the row passes the row-shape gate (Type=json, Scope=local, Secret=false) and JSON parses. A malformed kv row leaves the legacy file in place so the operator can repair (delete the row) and let the next Load re-mirror cleanly.
  • The Phase 2c-1 sessions importer (internal/migrate/importers/sessions.go) writes rows into the DB `sessions` table with status forced to 'archived' (per design §5.5). Those archived rows live in a different storage site than the runtime kv row and are NOT consulted by this Store; they exist so a future cutover that promotes the DB `sessions` table to the runtime canonical site can replay them. Reconciling the status vocabulary mismatch (runtime "exited" vs DB CHECK 'stopped'/'archived') is also deferred to that future cutover; the kv row preserves the runtime vocabulary as-is.

Implication for legacy file cleanup: a stray <configdir.Path()>/sessions.json after slice 28 is a remnant of the legacy file → kv migration; the runtime self-heals on its next Load. Adding it to `kojo --clean legacy`'s sweep list is safe but unnecessary — see cmd/kojo/clean_legacy.go's per-kind list, which omits it because the runtime path is already idempotent.

Index

Constants

This section is empty.

Variables

View Source
var (
	ErrSessionNotFound    = errors.New("session not found")
	ErrSessionRunning     = errors.New("session is still running")
	ErrSessionNotRunning  = errors.New("session not running")
	ErrToolNotFound       = errors.New("tool not found")
	ErrUnsupportedTool    = errors.New("unsupported tool")
	ErrHasRunningChildren = errors.New("cannot remove session with running children")
	ErrNotTerminal        = errors.New("not a terminal session")
	ErrNoTmuxID           = errors.New("session has no tmux ID")
)

Sentinel errors for the session package.

Functions

func ShellToolName added in v0.5.0

func ShellToolName() string

ShellToolName returns the internal tool name for terminal sessions.

func ShutdownSignals added in v0.5.0

func ShutdownSignals() []os.Signal

ShutdownSignals returns the OS signals for graceful shutdown.

func ToolAvailability

func ToolAvailability() map[string]ToolInfo

ToolAvailability checks which user-facing tools are available on this system.

Types

type Attachment added in v0.5.0

type Attachment struct {
	Path      string `json:"path"`
	Name      string `json:"name"`
	Size      int64  `json:"size"`
	Mime      string `json:"mime"`
	ModTime   string `json:"modTime"`
	CreatedAt string `json:"createdAt"`
}

Attachment represents a media file detected in session output.

type Manager

type Manager struct {

	// callback for session events
	OnSessionExit func(s *Session)
	// contains filtered or unexported fields
}

func NewManager

func NewManager(logger *slog.Logger, db *store.Store, opts ManagerOptions) *Manager

NewManager constructs a session.Manager. db is the kv-backed persistence layer (Phase 2c-2 slice 28); pass nil to disable persistence (test scaffolding that exercises Manager methods without a configured store). The runtime path always passes a real *store.Store via server.Config.

func (*Manager) Create

func (m *Manager) Create(tool, workDir string, args []string, yoloMode bool, parentID string) (*Session, error)

func (*Manager) FindChildSession added in v0.4.0

func (m *Manager) FindChildSession(parentID, tool string) (*Session, bool)

FindChildSession returns a child session of the given parent with the specified tool.

func (*Manager) Get

func (m *Manager) Get(id string) (*Session, bool)

func (*Manager) List

func (m *Manager) List() []*Session

func (*Manager) Remove added in v0.4.4

func (m *Manager) Remove(id string) error

Remove removes an exited session and its internal children from memory and persists the change.

func (*Manager) Restart

func (m *Manager) Restart(id string) (*Session, error)

func (*Manager) SaveAll

func (m *Manager) SaveAll()

SaveAll persists all sessions to disk. Called on shutdown.

func (*Manager) SetCustomBaseURL added in v0.10.1

func (m *Manager) SetCustomBaseURL(baseURL string)

SetCustomBaseURL configures the base URL for custom Anthropic API sessions.

func (*Manager) Stop

func (m *Manager) Stop(id string) error

func (*Manager) StopAll

func (m *Manager) StopAll()

func (*Manager) TmuxAction added in v0.4.3

func (m *Manager) TmuxAction(id, action string) error

TmuxAction executes a whitelisted tmux action on a terminal session.

type ManagerOptions added in v0.101.0

type ManagerOptions struct {
	// V0LegacyDir is the v0 config directory (configdir.V0Path()).
	// Empty means: do not consult the v0 dir at all — the runtime
	// opted out (e.g. --fresh) or there is no v0 install to fall
	// back to. Non-empty enables the v0-side fallback inside
	// internal/session.Store.Load(): kv miss → v1 dir → v0 dir.
	V0LegacyDir string
}

ManagerOptions tunes Manager construction. Zero-value is the minimal-effects posture: no v0 → v1 session fallback. Callers that know migration is complete (the startup gate confirmed v1Complete) supply a non-empty V0LegacyDir so the v0 dir's sessions.json can be picked up on first Load.

type RingBuffer

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

func NewRingBuffer

func NewRingBuffer(size int) *RingBuffer

func (*RingBuffer) Bytes

func (r *RingBuffer) Bytes() []byte

func (*RingBuffer) Write

func (r *RingBuffer) Write(p []byte)

type Session

type Session struct {
	ID              string
	Tool            string
	WorkDir         string
	Args            []string
	PTY             io.ReadWriteCloser
	Cmd             *exec.Cmd
	CreatedAt       time.Time
	Status          Status
	ExitCode        *int
	YoloMode        bool
	Internal        bool   // internal session (e.g. tmux), not user-facing
	ToolSessionID   string // tool-specific session ID for resume
	ParentID        string // parent session ID (e.g. tmux child of a CLI session)
	TmuxSessionName string // tmux session name (kojo_<id>) for tmux-backed sessions
	// contains filtered or unexported fields
}

func (*Session) Attachments added in v0.5.0

func (s *Session) Attachments() []*Attachment

Attachments returns all tracked attachments, removing any whose files no longer exist.

func (*Session) BroadcastAttachments added in v0.5.0

func (s *Session) BroadcastAttachments(attachments []*Attachment)

func (*Session) BroadcastYoloDebug

func (s *Session) BroadcastYoloDebug(tail string)

func (*Session) CaptureToolSessionID added in v0.2.0

func (s *Session) CaptureToolSessionID(data []byte)

CaptureToolSessionID tries to parse a tool-specific session ID from PTY output. Only captures once (when ToolSessionID is still empty). Accumulates data across chunk boundaries to handle split reads.

func (*Session) CheckAttachments added in v0.5.0

func (s *Session) CheckAttachments(data []byte) []*Attachment

CheckAttachments appends data to the trailing buffer, scans for media file paths, and returns newly detected attachments. Caller should broadcast the result.

func (*Session) CheckYolo

func (s *Session) CheckYolo(data []byte) (*YoloApproval, string)

CheckYolo appends data to a trailing buffer and checks for approval patterns. Returns non-nil YoloApproval if a match is found. Caller should write the response to PTY.

func (*Session) Done

func (s *Session) Done() <-chan struct{}

func (*Session) HasAttachment added in v0.5.0

func (s *Session) HasAttachment(path string) bool

HasAttachment checks if a path is tracked as an attachment.

func (*Session) Info

func (s *Session) Info() SessionInfo

func (*Session) InfoForSave added in v0.5.0

func (s *Session) InfoForSave() SessionInfo

InfoForSave returns session info including attachment metadata for persistence.

func (*Session) IsYoloMode

func (s *Session) IsYoloMode() bool

func (*Session) RemoveAttachment added in v0.5.0

func (s *Session) RemoveAttachment(path string)

RemoveAttachment removes an attachment from tracking (does not delete the file).

func (*Session) Resize

func (s *Session) Resize(cols, rows uint16) error

func (*Session) SetYoloMode

func (s *Session) SetYoloMode(enabled bool)

func (*Session) Subscribe

func (s *Session) Subscribe() (chan []byte, []byte)

func (*Session) SubscribeAttachments added in v0.5.0

func (s *Session) SubscribeAttachments() chan []*Attachment

func (*Session) SubscribeYoloDebug

func (s *Session) SubscribeYoloDebug() chan string

func (*Session) Unsubscribe

func (s *Session) Unsubscribe(ch chan []byte)

func (*Session) UnsubscribeAttachments added in v0.5.0

func (s *Session) UnsubscribeAttachments(ch chan []*Attachment)

func (*Session) UnsubscribeYoloDebug

func (s *Session) UnsubscribeYoloDebug(ch chan string)

func (*Session) Write

func (s *Session) Write(data []byte) (int, error)

type SessionInfo

type SessionInfo struct {
	ID              string        `json:"id"`
	Tool            string        `json:"tool"`
	WorkDir         string        `json:"workDir"`
	Args            []string      `json:"args,omitempty"`
	Status          Status        `json:"status"`
	ExitCode        *int          `json:"exitCode,omitempty"`
	YoloMode        bool          `json:"yoloMode"`
	Internal        bool          `json:"internal,omitempty"`
	CreatedAt       string        `json:"createdAt"`
	ToolSessionID   string        `json:"toolSessionId,omitempty"`
	ParentID        string        `json:"parentId,omitempty"`
	TmuxSessionName string        `json:"tmuxSessionName,omitempty"`
	LastOutput      string        `json:"lastOutput,omitempty"`
	LastCols        uint16        `json:"lastCols,omitempty"`
	LastRows        uint16        `json:"lastRows,omitempty"`
	Attachments     []*Attachment `json:"attachments,omitempty"`
}

type Status

type Status string
const (
	StatusRunning Status = "running"
	StatusExited  Status = "exited"
)

type Store

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

Store persists session metadata as a single JSON-typed kv row.

See the package doc comment for the post-slice-28 state: kv is canonical, the legacy <configdir.Path()>/sessions.json file is mirrored into kv on first Load and best-effort unlinked. db may be nil — that path keeps Save/Load as no-ops, which is the test posture (the session_test.go suite constructs Sessions directly without going through NewManager).

func (*Store) Load

func (st *Store) Load() ([]SessionInfo, error)

Load reads persisted sessions, filtering out entries older than maxAge. Returns (nil, nil) when the kv row is absent and no legacy file exists (first run on a fresh install). Returns (nil, err) on read/parse errors so callers can distinguish "no sessions" from "failed to load" (important for orphan cleanup).

Branches:

  • valid kv hit (Type=json, Scope=local, Secret=false, JSON parses): return kv contents, opportunistically unlink any stray legacy file from a v1→v0→v1 round trip.
  • malformed kv hit (row-shape gate fails OR JSON parse fails): leave the legacy file in place so the operator can repair (delete the bad row); return legacy-parsed contents if a legacy file exists, or errMalformedKVNoLegacy if it does not (the latter prevents the caller's orphan cleanup from wiping live sessions on a (nil, nil) misread).
  • kv miss + legacy file present: parse legacy file, mirror into kv via IfMatchAny PutKV; on success the legacy file is unlinked. On collision (a same-peer concurrent writer inserted the row first) re-read the winner and unlink only when the winner row also passes the row-shape + parse gate.
  • kv miss + no legacy file: fresh install, return (nil, nil).

func (*Store) Save

func (st *Store) Save(infos []SessionInfo)

Save upserts the kv row holding all sessions. Errors are logged (this matches the pre-slice-28 contract — Save returned no error either; the live runtime cannot meaningfully react to a persistence failure mid-tick) but never propagated.

type ToolInfo

type ToolInfo struct {
	Available bool   `json:"available"`
	Path      string `json:"path"`
}

type YoloApproval

type YoloApproval struct {
	Matched string `json:"matched"`
}

YoloApproval is broadcast when yolo auto-approves a prompt.

Jump to

Keyboard shortcuts

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