Help us shape the future of Go! Take the 2026 Go Developer Survey

session

package
v1.3.4 Latest Latest
Warning

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

Go to latest
Published: Oct 18, 2026 License: MIT Imports: 15 Imported by: 0

Documentation

Overview

This file implements session export/import (US-008, #124): a session can be exported to a self-contained JSONL or HTML file, and a JSONL export can be imported back as a fresh, resumable session. The JSONL form is the same role-discriminated schema the store persists, so an export → import round-trip is lossless (message contents and the id/parentId tree survive verbatim); the HTML form is a read-only, self-contained transcript with inline styles and no external network resources, suitable for sharing.

This file holds the HTML rendering helpers for session export (US-008, #124). The output is a single self-contained document: all CSS is inlined in a <style> block, there are no <script> tags, no external fonts, and no network requests, so the transcript renders identically offline and cannot phone home. Every piece of session-derived text is escaped via html.EscapeString before it reaches the document.

Package session implements local JSONL session persistence and resume (US-024, #43). A session is stored as a single append-only JSONL file: the first line is a SessionHeader (schema version + metadata), and every subsequent line is one persisted message (user / assistant / toolResult), using the same "role"-discriminated encoding as agentcore.MessageList.

The format is internally self-consistent and deliberately NOT wire-compatible with pi's session files (spec #16, session-format decision #5): golder owns the schema and versions it via SessionHeader.Version so future migrations have a hook.

A persisted session round-trips into an agentcore.AgentContext via Load, so a run can be resumed by feeding the reconstructed context to a fresh run and the transcript replays correctly in the REPL.

This file implements the shareable session export (issue #570, ported from pi's session-sharing practice): a Markdown rendering of a session suitable for archiving or publishing, with credential redaction ON by default so a transcript that touched real keys can be shared without leaking them.

Redaction covers the credential shapes pi-share-hf warns about and more: sk-… API tokens, GitHub tokens (ghp_/gho_/ghu_/ghs_/ghr_), AWS AKIA ids, Slack xox… tokens, and env-shaped assignments (…_API_KEY=…, …_TOKEN=…, …_SECRET=…). Redaction applies to every text block in every message; the session header's system prompt is dropped unless explicitly included (it frequently carries environment details the user does not want published).

The JSONL format stays the lossless resumable export (export.go); sharing a JSONL with redaction is allowed but breaks the round-trip by design — the flag exists for archival of transcripts that touched real secrets.

Index

Constants

View Source
const SchemaVersion = 3

SchemaVersion is the current session file schema version. It is written into every SessionHeader and checked on read; an unknown (higher) version is a hard error so a newer file is never silently misread by an older binary.

v2 adds the inline "compaction" message role (US-003): a compaction checkpoint persisted as one message line.

v3 gives every persisted entry an id/parentId, forming a tree (US-005, #121) — the prerequisite for fork/clone/tree navigation. Each message line is wrapped as {"id","parentId","timestamp","message":{…}}. v1/v2 files (bare message lines) remain fully readable: readSession migrates them on load by synthesizing ids and chaining parentId to the previous entry, so old sessions still load and resume.

Variables

This section is empty.

Functions

func ExportFormatFromPath

func ExportFormatFromPath(outPath string) string

ExportFormatFromPath infers a share format from an output path's extension.

func FileName

func FileName(id string) string

FileName returns the on-disk file name for a session id (id + ".jsonl").

func NewID

func NewID(now time.Time) string

NewID returns a time-ordered session id: a UTC timestamp stem that sorts lexicographically by creation time (e.g. "20260710-142530-uniq"). The suffix disambiguates sessions created within the same second.

func ProjectRoot added in v1.2.16

func ProjectRoot(dir string) string

ProjectRoot returns the directory that identifies dir's project: the nearest ancestor of dir (dir itself included) holding a .git entry, else dir itself.

The probe is a filesystem stat, not a git subprocess: it costs nothing worth caching, it works on a machine without git installed, and it needs no accounting for a missing or broken git. Nested checkouts therefore collapse to the innermost repository (the submodule boundary), which is also where git itself draws the line.

dir is made absolute before the walk, so a relative launch directory matches the absolute path a session recorded. An empty or unresolvable dir yields "".

func ReadJSONL

func ReadJSONL(r io.Reader) (SessionHeader, []Entry, error)

ReadJSONL decodes a JSONL export (as produced by WriteJSONL or a raw session file) into a header and entries, migrating v1/v2 bare-message files the same way LoadEntries does. It is the import counterpart to WriteJSONL.

func RedactText

func RedactText(s string) string

RedactText strips credential-shaped substrings, returning shareable text.

func SameProject added in v1.2.16

func SameProject(a, b string) bool

SameProject reports whether directories a and b belong to the same project. Two unresolvable paths are never "the same project": attribution requires a real directory on both sides.

func WriteHTML

func WriteHTML(w io.Writer, header SessionHeader, entries []Entry) error

WriteHTML writes a self-contained HTML transcript of the session: inline CSS only (no external stylesheets, fonts, scripts, or network resources), role color-coding, and tool-call/result blocks. All message text is HTML-escaped so a transcript containing markup or a crafted "</script>" cannot break out of its container or inject active content (defensive against a hostile session).

func WriteJSONL

func WriteJSONL(w io.Writer, header SessionHeader, entries []Entry) error

WriteJSONL writes header + entries as JSONL in the store's on-disk schema (header line first, then one entry line each, ids/parentIds preserved). It is the export counterpart to writeSessionEntries and the exact input ReadJSONL expects, so a WriteJSONL → ReadJSONL round-trip is lossless.

func WriteMarkdown

func WriteMarkdown(w io.Writer, header SessionHeader, entries []Entry, opts ShareOptions) error

WriteMarkdown renders header + entries as a shareable Markdown transcript.

Types

type Entry

type Entry struct {
	// ID is this entry's stable identifier (see newEntryID). Unique within a file.
	ID string
	// ParentID is the ID this entry descends from; empty for a root entry.
	ParentID string
	// Timestamp is when the entry was persisted (RFC 3339, UTC).
	Timestamp time.Time
	// Message is the wrapped agent message (user / assistant / toolResult / …).
	Message agentcore.Message
}

Entry wraps one persisted message with the tree metadata introduced in schema v3 (US-005, #121): a stable ID plus the ParentID it descends from. A linear session is the degenerate tree where every entry's ParentID is the previous entry's ID; the first entry has an empty ParentID (a root). The wrapper is what lets a session fork/clone later — PathToLeaf walks the ParentID chain to reconstruct the linear conversation feeding any leaf.

func PathToLeaf

func PathToLeaf(entries []Entry, leafID string) []Entry

PathToLeaf walks the ParentID chain from the entry identified by leafID back to its root and returns the entries in root→leaf order — the linear conversation that feeds the given leaf (US-005 acceptance criterion c). An empty leafID (or an unknown one) yields nil. If the chain references a missing parent the walk stops at the last resolvable ancestor rather than failing, so a partially corrupt file still yields the recoverable prefix.

func (Entry) MarshalJSON

func (e Entry) MarshalJSON() ([]byte, error)

MarshalJSON emits the entry as {"id","parentId","timestamp","message":{…}}.

func (*Entry) UnmarshalJSON

func (e *Entry) UnmarshalJSON(data []byte) error

UnmarshalJSON decodes an entry line, decoding the inner message with the same discriminated logic as agentcore.MessageList (by wrapping it in a one-element array).

type SessionHeader

type SessionHeader struct {
	// Version is the schema version (SchemaVersion at write time).
	Version int `json:"version"`
	// ID is the session identifier, also the file stem (see FileName).
	ID string `json:"id"`
	// CreatedAt is when the session file was created (RFC 3339, UTC).
	CreatedAt time.Time `json:"createdAt"`
	// UpdatedAt is when the session was last appended to.
	UpdatedAt time.Time `json:"updatedAt"`
	// Model / Provider record what the session ran against, for display (the
	// /resume list, exports, /status's session section). They are history, not a
	// configuration: resuming a session serves it with the live provider/model of
	// the process doing the resuming, exactly as a launch does. Optional.
	Model    string `json:"model,omitempty"`
	Provider string `json:"provider,omitempty"`
	// SystemPrompt is the system prompt the session ran under. Persisted so the
	// resumed context is faithful. Optional.
	SystemPrompt string `json:"systemPrompt,omitempty"`
	// BaseInstruction and AppendInstructions are the USER-AUTHORED halves of
	// SystemPrompt: --system-prompt's replacement text (empty when the default
	// guide was used) and the --append-system-prompt blocks. They are recorded
	// separately because a resumed session rebuilds its prompt: our own text
	// (guide, environment block, AGENTS.md, skills) describes the CURRENT binary
	// and working tree and is recomputed, while these two are the user's words
	// and are carried over verbatim. Optional and additive: sessions written
	// before these fields existed omit them and are rebuilt from the current
	// defaults.
	BaseInstruction    string   `json:"baseInstruction,omitempty"`
	AppendInstructions []string `json:"appendInstructions,omitempty"`
	// ParentSession is the id of the session this one was forked/cloned from
	// (US-006, #122). Empty for a session created from scratch. It records
	// lineage only; a fork is otherwise a fully independent session file.
	ParentSession string `json:"parentSession,omitempty"`
	// Cwd is the absolute working directory the session ran in, recorded so a
	// session can be attributed to a project (its stable project id derives from
	// this path). Used by /dream distillation to select the current project's
	// recent sessions (SPEC §5.3 / §11.3). Optional and additive: older schemas
	// omit it and still load; a session with an empty Cwd is treated as
	// unattributed and never matches a project-scoped distill.
	Cwd string `json:"cwd,omitempty"`
	// ContextFrom is the id of the session this one inherited its collapsed
	// context from (#480, "infinite context"). Empty when the session started
	// with no inherited checkpoint. Optional and additive: older schemas (v1/v2/v3)
	// omit it and still load.
	ContextFrom string `json:"contextFrom,omitempty"`
	// ContextWatermark is the message index up to which context was collapsed
	// into an inherited checkpoint (#480). Messages before this index live in the
	// checkpoint summary rather than the replayed transcript. Optional/additive.
	ContextWatermark int `json:"contextWatermark,omitempty"`
}

SessionHeader is the first line of a session file: schema version plus the metadata needed to list and resume a session without reading its messages.

type ShareOptions

type ShareOptions struct {
	// Redact strips credential-shaped substrings (default true in the CLI).
	Redact bool
	// IncludeSystemPrompt keeps the session's system prompt in the export. It
	// is dropped by default: it often embeds environment details.
	IncludeSystemPrompt bool
}

ShareOptions controls one shareable export.

type Store

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

Store persists sessions as JSONL files under a directory (typically ~/.golder/sessions). The zero value is unusable; construct with NewStore.

func NewStore

func NewStore(dir string) (*Store, error)

NewStore returns a Store rooted at dir, creating the directory if needed.

func (*Store) Append

func (s *Store) Append(id string, updatedAt time.Time, messages agentcore.MessageList) error

Append adds messages to an existing session file and bumps its UpdatedAt to updatedAt. It is the incremental-persistence primitive: a driver that wants to grow a session turn-by-turn appends only the newly produced messages rather than rewriting the whole file itself. If the session does not exist it is an error — use Save to create one first.

Because the header lives on the first line and JSONL is otherwise append-only, updating UpdatedAt requires rewriting the file; Append does a load-modify-save under the hood, which is simple and correct for the session sizes golder produces.

func (*Store) AppendBranch

func (s *Store) AppendBranch(header SessionHeader, parentLeafID string, messages agentcore.MessageList) (string, error)

AppendBranch appends messages as a chain descending from parentLeafID, preserving every existing entry — and therefore any other branches — in the session file (US-007, #123). It is the tree-aware counterpart to Append (which rewrites the file as a single linear chain): where Append flattens, AppendBranch grows the on-disk tree, so switching the active leaf to a historical entry and continuing produces a real sibling branch rather than truncating history.

Each message becomes a fresh entry (new id, ParentID chained to the previous one, first chained to parentLeafID). An empty parentLeafID roots the new chain. header (Version forced to SchemaVersion, UpdatedAt as the caller set it) is rewritten as line 1. It returns the id of the new leaf — the last appended entry — so the caller can track the active branch. If the session file does not yet exist it is created (the fresh-session first-turn case).

func (*Store) Dir

func (s *Store) Dir() string

Dir returns the store's root directory.

func (*Store) Export

func (s *Store) Export(id, outPath string) (int, error)

Export writes the session identified by id to outPath. The format is chosen by outPath's extension: ".html"/".htm" produces a self-contained HTML transcript; anything else (including ".jsonl") produces JSONL. The parent directory of outPath must already exist. It returns the number of entries written so a caller can report progress.

func (*Store) ExportShare

func (s *Store) ExportShare(id, outPath, format string, opts ShareOptions) (int, error)

ExportShare writes the session identified by id to outPath in the share format named by format ("md" or "markdown" for Markdown, "json"/"jsonl" for the redactable JSONL, "html" for the self-contained HTML transcript). The parent directory of outPath must already exist. It returns the number of entries written.

func (*Store) Fork

func (s *Store) Fork(sourceID, leafID string, now time.Time) (SessionHeader, []Entry, error)

Fork creates a new session whose contents are the linear path from the root down to leafID in the source session, copied verbatim (each entry keeps its id/parentId). The new session gets a fresh id (derived from now) and its header carries ParentSession = sourceID so the lineage is recorded. It returns the new session's header and its entries.

This is the primitive behind /fork and /clone (US-006, #122):

  • /clone passes the current leaf id → the entire current conversation is duplicated into an independent session (position "at").
  • /fork passes a historical user message's PARENT id → the new session holds everything up to but excluding that message, so the user re-prompts from that point on a fresh branch (position "before").

Because the copy lands in a brand-new file, appending to either the source or the fork never touches the other — the two branches are fully isolated. An empty leafID copies nothing but the header (an empty new session rooted at the source), which is the correct behavior for forking before the very first message.

func (*Store) Import

func (s *Store) Import(inPath string, now time.Time) (SessionHeader, []Entry, error)

Import reads a JSONL export at inPath and materializes it as a fresh session in the store: a new id (derived from now) is assigned, the original id is recorded as ParentSession for lineage, and the entries are written verbatim (ids/parentIds preserved) so the tree — and thus PathToLeaf/resume — behaves exactly as in the source. It returns the new header and the imported entries. An HTML file (or any non-JSONL input) fails to parse and returns an error rather than importing garbage.

func (*Store) List

func (s *Store) List() ([]SessionHeader, error)

List returns the headers of all sessions in the store, sorted by UpdatedAt descending (most recently used first). Files that fail to parse are skipped rather than failing the whole listing, so one corrupt session does not hide the rest.

func (*Store) Load

Load reads a session file and returns its header and messages. It validates the schema version (an unknown version is rejected) and decodes each entry line, returning the messages in file order (root→leaf for a linear session). Old v1/v2 files (bare message lines) are migrated transparently. Load is the linear view; LoadEntries exposes the id/parentId tree metadata.

func (*Store) LoadEntries

func (s *Store) LoadEntries(id string) (SessionHeader, []Entry, error)

LoadEntries reads a session file and returns its header plus the tree entries (id/parentId + message) in file order. v1/v2 files are migrated on load: every bare message line is wrapped in an Entry with a synthesized id and its ParentID chained to the previous entry, so old sessions load and resume exactly as they did before (US-005 acceptance criterion b/d).

func (*Store) Save

func (s *Store) Save(header SessionHeader, messages agentcore.MessageList) error

Save writes header and messages to a fresh session file, overwriting any existing file for the same id. The header's Version is forced to SchemaVersion; CreatedAt/UpdatedAt are left as the caller set them. Save is the whole-session write; Append adds messages to an existing file.

func (*Store) SaveEntries

func (s *Store) SaveEntries(header SessionHeader, entries []Entry) error

SaveEntries writes header plus the given entries verbatim — preserving each entry's id/parentId — to a fresh session file, overwriting any existing file for header.ID. Unlike Save (which generates fresh ids and a linear chain from a MessageList), SaveEntries persists an already-known tree, which is what Fork needs: it copies a path of existing entries into a new session file without disturbing their identifiers. The header's Version is forced to SchemaVersion.

type TreeLine

type TreeLine struct {
	Entry Entry
	Text  string
	// Current is true for the entry whose id == leafID; callers render their own
	// tag for it (the REPL appends "← current", the TUI picker marks the row).
	Current bool
}

TreeLine pairs one entry with its rendered display line, so a caller can print the tree AND map a 1-based selection index back to the entry it refers to (the slice order is the render order — a stable pre-order DFS). See RenderTreeLines.

func RenderTreeLines

func RenderTreeLines(entries []Entry, leafID string) []TreeLine

RenderTreeLines renders the entry forest as human-readable lines for a pure line REPL — no TUI, no cursor control (US-007, #123). Each entry becomes one line in render order (element i ↔ the i-th printed line, so 1-based selector n → [n-1]); TreeLine.Current marks the entry behind leafID.

Indentation is chain-compressed: a node with exactly one child continues that child in the same column (no connector, no extra indent), so a linear conversation — the common case — renders as a flat list and its width never grows with the message count. ├─/└─ connectors appear only where the tree actually forks, which keeps the indent proportional to the number of fork points on the path (capped at treeMaxIndentCells), never to session length. Roots (entries with an empty or dangling ParentID) anchor the forest; children are ordered by timestamp then id for stable output.

Jump to

Keyboard shortcuts

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