session

package
v0.3.1 Latest Latest
Warning

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

Go to latest
Published: Jun 19, 2026 License: MIT Imports: 12 Imported by: 0

Documentation

Overview

Package session loads, mutates, and writes coding-agent transcripts.

The package defines a tool-agnostic Turn / Session model plus a single Excise(set<turn_id>) operation that preserves four invariants:

  1. Removing a turn that owns tool_use blocks removes the paired tool_result turns.
  2. Removing a tool_result turn requires (or warns about) removing the originating tool_use turn.
  3. The ordering of surviving turns is preserved; their stable IDs are preserved as well.
  4. Writes are atomic: snapshot first, write to a tmp file, rename.

Two concrete loaders are provided: claude.go (Claude Code JSONL) and cursor.go (Cursor state.vscdb sqlite).

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func DefaultCursorPath

func DefaultCursorPath() (string, error)

DefaultCursorPath returns the platform-specific path to state.vscdb.

func DiscoverNewestClaude

func DiscoverNewestClaude() (string, error)

DiscoverNewestClaude returns the most recently-modified Claude Code session jsonl under ~/.claude/projects/, searching across every project directory.

It is the zero-arg invocation backbone of `excise`.

func SortByTimestamp

func SortByTimestamp(turns []Turn)

SortByTimestamp ensures a stable display order. We rely on it after loading any format to make the TUI consistent.

Types

type ClaudeLoader

type ClaudeLoader struct{}

ClaudeLoader reads ~/.claude/projects/<dir>/<session>.jsonl files.

Schema (verified against real samples on the target machine, 2026-05-18):

{
  "type": "user" | "assistant" | "system" | ...,
  "uuid": "...",
  "parentUuid": "..." | null,
  "timestamp": "2026-05-17T12:32:37.576Z",
  "sessionId": "...",
  "message": {
    "role": "user" | "assistant",
    "content": "string" | [{"type": "text" | "thinking" | "tool_use" | "tool_result", ...}, ...]
  }
}

We tolerate every Claude-specific extension by capturing the *raw* line bytes and re-emitting them on write, only re-encoding the small set of fields we actually parse for the TUI.

func (*ClaudeLoader) Detect

func (l *ClaudeLoader) Detect(path string) bool

Detect returns true iff path ends in .jsonl AND the first non-empty line parses as a Claude-shaped object. We do not require a Claude path prefix so that the user can `excise testdata/foo.jsonl` outside ~/.claude.

func (*ClaudeLoader) Load

func (l *ClaudeLoader) Load(path string) (*Session, error)

Load reads a Claude JSONL file into a Session.

type ClaudeWriter

type ClaudeWriter struct{}

ClaudeWriter implements Writer for Claude JSONL sessions. It re-emits each surviving turn's Raw bytes verbatim, separated by single newlines, and performs an atomic snapshot-then-tmp-then-rename to preserve the invariants documented on the package.

func (*ClaudeWriter) Write

func (w *ClaudeWriter) Write(s *Session) error

Write enforces invariant 4 (atomic) for Claude sessions. The caller is responsible for any snapshot strategy (see internal/safety).

type CursorLoader

type CursorLoader struct{}

CursorLoader reads Cursor's chat data.

Cursor stores composers and individual chat bubbles inside

~/Library/Application Support/Cursor/User/globalStorage/state.vscdb

(linux: ~/.config/Cursor/...; windows: %APPDATA%/Cursor/...).

Two table layouts exist:

  1. The live `state.vscdb` (sqlite) — `cursorDiskKV` table — keys of shape `bubbleId:<composerId>:<bubbleId>` whose value is a JSON blob with fields like `text`, `type` (numeric), `bubbleId`, `toolResults`, `tokenCount`, etc. This is the format verified against the real install on this machine (Cursor build 2025-04).

  2. A fixture-only JSON-lines export, used for unit tests and for users who do not have Cursor installed locally. Each line is one bubble JSON object plus an envelope:

    {"composerId": "...", "bubble": { ...the same value as above... }}

    Detection: file extension is .jsonl AND first non-empty line has a top-level "bubble" object.

We deliberately do NOT add a CGO sqlite driver because the dependency cost (and Linux/Windows cross-compile friction) outweighs the benefit for v0.1. Instead we shell out to the `sqlite3` CLI for sqlite reads. If sqlite3 is not on PATH, Load returns a clear error pointing the user at the fixture path.

IMPORTANT: this is read-only for sqlite in v0.1. Cursor's state.vscdb is frequently held open by the Cursor process; mutating it from the outside while Cursor is running risks corruption. The writer therefore refuses to write directly back to state.vscdb and instead emits a side-car .excised file the user can manually copy back (or, in v0.2, we will add a Cursor-must-be-closed safety prompt).

func (*CursorLoader) Detect

func (l *CursorLoader) Detect(path string) bool

Detect returns true for either format.

func (*CursorLoader) Load

func (l *CursorLoader) Load(path string) (*Session, error)

Load picks the sqlite or jsonl branch by suffix.

type CursorWriter

type CursorWriter struct{}

CursorWriter writes Cursor sessions. For sqlite sources it emits a side-car `.excised.jsonl` next to the database; for jsonl sources it does an atomic snapshot-then-tmp-then-rename, matching ClaudeWriter.

func (*CursorWriter) Write

func (w *CursorWriter) Write(s *Session) error

type DependencyGraph

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

DependencyGraph models the tool_use → tool_result edge.

invariant 1 of the Excise primitive — "removing a turn with tool_calls removes its paired tool_result turns" — is enforced here.

func BuildGraph

func BuildGraph(turns []Turn) *DependencyGraph

BuildGraph indexes a session's turns into a DependencyGraph.

func (*DependencyGraph) Closure

func (g *DependencyGraph) Closure(turns []Turn, seeds map[string]bool) map[string]bool

Closure computes the full set of turn ids that must be removed when `seeds` are excised, honoring invariants 1 and 2.

  • invariant 1: removing a turn with tool_use blocks pulls in the matching tool_result turns.
  • invariant 2 (warn-mode, see Verify): removing a turn that contains only a tool_result without its owner is allowed but flagged.

The walk is iterative so it terminates even on pathological self-cycles (which a well-formed transcript should never contain).

func (*DependencyGraph) Verify

func (g *DependencyGraph) Verify(turns []Turn, toCut map[string]bool) []Warning

type Loader

type Loader interface {
	Detect(path string) bool
	Load(path string) (*Session, error)
}

Loader is the contract every format reader satisfies.

func Auto

func Auto(path string) (Loader, error)

Auto picks a Loader by sniffing path / extension. Falls back to Claude.

type Role

type Role string

Role is the speaker of a single Turn. We collapse Claude's `system` / `tool_result` types and Cursor's numeric types down to a small enum so the TUI does not need to know about format-specific edge cases.

const (
	RoleUser      Role = "user"
	RoleAssistant Role = "assistant"
	RoleTool      Role = "tool"   // a turn whose payload is purely a tool_result
	RoleSystem    Role = "system" // meta / system / queue rows
)

type Session

type Session struct {
	Tool       Tool
	SourcePath string // file path (Claude) or sqlite path (Cursor)
	ComposerID string // Cursor only: bubble group id
	SessionID  string // Claude only: session uuid; Cursor: composer uuid
	Turns      []Turn
}

Session is an ordered list of Turn plus the metadata needed to write it back to disk in the same format it was loaded from.

func LoadAuto

func LoadAuto(path string) (*Session, error)

LoadAuto is a convenience helper for the CLI.

func LoadWithTool

func LoadWithTool(tool Tool, path string) (*Session, error)

LoadWithTool forces a specific loader.

type Tool

type Tool string

Tool identifies which coding-agent format a Session originated from.

const (
	ToolClaude  Tool = "claude"
	ToolCursor  Tool = "cursor"
	ToolUnknown Tool = "unknown"
)

type ToolCall

type ToolCall struct {
	ID   string `json:"id"`
	Name string `json:"name"`
}

ToolCall represents a single tool_use block owned by a Turn.

type ToolResult

type ToolResult struct {
	ToolUseID string `json:"tool_use_id"`
}

ToolResult represents a single tool_result block owned by a Turn.

type Turn

type Turn struct {
	ID          string // stable id (Claude uuid; Cursor bubbleId)
	ParentID    string // parentUuid / parentBubbleId, "" if root
	Role        Role
	Timestamp   time.Time
	Preview     string       // first ~120 chars of textual content
	TokenEst    int          // very rough token estimate (chars / 4)
	ToolCalls   []ToolCall   // tool_use blocks owned by this turn
	ToolResults []ToolResult // tool_result blocks owned by this turn
	Raw         []byte       // verbatim original payload
	Meta        any          // format-specific metadata (loader's own struct)
}

Turn is one logical entry in a transcript. ID is stable across edits.

Raw holds the original on-disk JSON line (Claude) or sqlite value (Cursor) so writer.go can re-emit untouched bytes for surviving turns and avoid lossy round-trips through our model.

func Excise

func Excise(turns []Turn, toCut map[string]bool) []Turn

Excise returns a new []Turn with `toCut` (and any dependents) removed. The original slice is not mutated.

This is the core mutation referenced by the Excise primitive in the MVP plan (section 2). The returned slice preserves ordering (invariant 3) and surviving turns keep their stable IDs.

type Warning

type Warning struct {
	TurnID string
	Reason string
}

Verify returns warnings about turns in `toCut` that would leave dangling tool refs. invariant 2 says removing a tool_result requires its owner; we surface this as a warning rather than refusing so the user with --force can override.

type Writer

type Writer interface {
	Write(s *Session) error
}

Writer is the contract every format writer satisfies. The implementation is responsible for honoring the atomic-write invariant.

func WriterFor

func WriterFor(s *Session) (Writer, error)

WriterFor returns the right Writer implementation for the session's tool. We keep this separate from each loader file so the CLI can do a single dispatch.

Jump to

Keyboard shortcuts

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