agentstore

package
v0.8.2 Latest Latest
Warning

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

Go to latest
Published: Aug 18, 2026 License: MIT Imports: 13 Imported by: 0

Documentation

Overview

Package agentstore reads the conversation transcripts that coding agents — Claude Code, Codex, Pi — keep under the user's home directory, and serves them back as one normalized shape: a summary per session, pages of messages, and search hits.

The trust model is read-only, and read-only over stores this package does not own. Every file here belongs to another program, is written in a format that program has never promised to keep, and may be mid-append at the moment it is read. So nothing in this package writes into a store, nothing treats a line it cannot parse as an error, and nothing treats the end of a file as the end of a session: an unknown line type is a future version of the tool, a malformed line is a crash mid-write, and both are skipped rather than failed on, because failing would make this viewer's availability depend on the stability of three formats nobody versioned. A file that cannot be read at all simply is not listed.

The one thing this package does write is its own index, a cache of the summaries it has computed, kept under the flue config directory. Summaries name working directories, first prompts and session titles — the same order of sensitivity as the scrollback in a session snapshot — so the index inherits the config directory's discipline: 0700 directory, 0600 file, written to a fresh inode and renamed into place. A corrupt or missing index is rebuilt from the transcripts, never fatal; the transcripts are the truth and the index only ever a summary of them.

Index

Constants

This section is empty.

Variables

View Source
var (
	// ErrUnknownTool is a tool name outside the three this package reads.
	ErrUnknownTool = errors.New("agentstore: unknown tool")
	// ErrNotFound is a (tool, id) the index does not hold, or one whose file
	// vanished between the index sweep and the read.
	ErrNotFound = errors.New("agentstore: no such session")
	// ErrBadQuery is a search with nothing to search for.
	ErrBadQuery = errors.New("agentstore: empty query")
)

Errors the callers translate onto the wire. Two rather than several, because the client has two moves: a name outside the protocol is the client's bug (bad_message), and a session that is not there is the ordinary answer for a store another program prunes (not_found).

Functions

func ToolAllowed added in v0.8.1

func ToolAllowed(tool Tool, filter []string) bool

ToolAllowed is toolAllowed for the daemon, which applies the agents verb's tool filter to History the way Snapshot applies it to sessions.

Types

type HistoryDay added in v0.8.1

type HistoryDay struct {
	Tool Tool `json:"tool"`
	// Date is the day in the tool's own calendar, YYYY-MM-DD — local to the
	// machine that wrote it, which is also the machine serving it.
	Date     string `json:"date"`
	Sessions int    `json:"sessions"`
}

HistoryDay is one day of one tool's own aggregate accounting: how many sessions began that day, by the tool's count of it.

type HistoryTotals added in v0.8.2

type HistoryTotals struct {
	Tool   Tool       `json:"tool"`
	Model  string     `json:"model"`
	Tokens TokenUsage `json:"tokens"`
}

HistoryTotals is one model's share of one tool's lifetime accounting, in the same four buckets the summaries carry — Claude Code's modelUsage keeps them split, so unlike the per-day figures these merge cleanly with the transcript-derived numbers. Lifetime only: the aggregate offers no way to slice them by range, which is why a client applies them to its all-time view alone.

type Hit

type Hit struct {
	Tool  Tool   `json:"tool"`
	ID    string `json:"id"`
	Cwd   string `json:"cwd"`
	Title string `json:"title,omitempty"`
	Ts    string `json:"ts,omitempty"`
	Role  string `json:"role"`
	// Snippet is the match with ~80 characters of context each side, folded
	// onto one line, no markup.
	Snippet string `json:"snippet"`
	Offset  int64  `json:"offset"`
}

Hit is one search match: enough of the session to label the result, and enough of the message to open it — Offset pages straight to the line.

type Index

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

Index is the in-memory map of every transcript the sweep has met, keyed by path, persisted as one JSON file so a daemon restart does not re-parse a hundred sessions to answer its first list.

func New

func New(persistDir, home string) *Index

New returns an index over home's stores, persisting under persistDir. The persisted file is loaded if it is there and readable; a corrupt or missing one costs a rebuild on the first sweep and nothing else — the transcripts are the truth and the file only a cache of reading them.

func (*Index) History added in v0.8.1

func (x *Index) History() []HistoryDay

History reads the per-day backfill the stores' own aggregates offer — today, Claude Code's stats-cache.json. Absent, unreadable or malformed files are an empty history, never an error: backfill is a bonus over the transcripts, not a dependency. The parse is cached against the file's stat, so the sweep-per-poke pattern costs one stat rather than one parse.

func (*Index) HistoryTotals added in v0.8.2

func (x *Index) HistoryTotals() []HistoryTotals

HistoryTotals reads the lifetime per-model accounting the same aggregate keeps, on the same cache and the same fail-to-empty terms as History.

func (*Index) ReadPage

func (x *Index) ReadPage(tool Tool, id string, offset int64, dir string, limit int) (Page, error)

ReadPage answers one agentRead. dir is "forward" or "backward" (the caller's to validate, forward being the default); limit is clamped to the package bounds, with zero meaning the default. A tool outside the protocol is ErrUnknownTool — the caller's bug — and an id the index does not hold, or one whose file has vanished since the sweep, is ErrNotFound — the ordinary kind of miss over a store another program prunes.

func (*Index) Resolve

func (x *Index) Resolve(tool Tool, id string) (string, Summary, bool)

Resolve is the only way a (tool, id) becomes a path: reads and searches go through it, so a path never has to cross the wire to name a transcript.

func (*Index) Search

func (x *Index) Search(ctx context.Context, query string, tools []string, cwd string, limit int) (hits []Hit, building, truncated bool, err error)

Search answers one agentSearch. tools and cwd filter the way the agents verb filters; limit is clamped, zero meaning the default. building reports whether a sweep is still running — a client that wants the stragglers re-asks, the way it re-asks the list. An empty query is ErrBadQuery, the caller's bug: matching everything is an answer nobody is asking for.

ctx is the caller's interest in the answer, checked between files and on every line: the budget is sixty-four megabytes, and a connection that closes mid-scan should stop costing this machine file reads the moment nobody is left to read the answer. A cancellation caught between files is ctx.Err(); one caught mid-file surfaces as an ordinary truncated result, which is fine — the one caller that cancels has already stopped listening.

func (*Index) Snapshot

func (x *Index) Snapshot(tools []string, cwd string) ([]Summary, bool)

Snapshot answers the agents verb: the sessions the index holds right now — filtered, newest first — and whether a sweep is still running. It also pokes the sweep, so looking at the list is what keeps the list fresh. tools filters by tool name when non-empty; a name that matches no adapter matches no session, which is what a filter means. cwd filters by exact working directory.

type Message

type Message struct {
	// Role is "user", "assistant" or "system". System is where each adapter
	// puts the lines a harness injected — hook context, command echoes,
	// environment preambles — so a viewer can fold them away and the message
	// count does not inflate with text no human wrote.
	Role string `json:"role"`
	// Kind is "text", "thinking", "tool_call" or "tool_result".
	Kind string `json:"kind"`
	// Ts is RFC 3339, or absent when the line carries no timestamp.
	Ts    string `json:"ts,omitempty"`
	Model string `json:"model,omitempty"`
	// Text is the message body: prose for text and thinking, the input JSON
	// for a tool_call, the result body for a tool_result.
	Text string `json:"text"`
	// ToolName is set on tool_call always, and on tool_result when the
	// transcript names the tool there — Claude and Pi only record the call id
	// on results, so those arrive without one.
	ToolName string `json:"toolName,omitempty"`
	// Sidechain marks a subagent's message, which Claude keeps in the same
	// file as the conversation that spawned it.
	Sidechain bool `json:"sidechain,omitempty"`
	// Truncated says Text was cut to the per-block cap on its way into a
	// page. The transcript still holds the whole thing.
	Truncated bool  `json:"truncated,omitempty"`
	Offset    int64 `json:"offset"`
}

Message is one normalized entry of a transcript page.

Offset is the byte offset of the line the message came from, and it is the stable anchor: a client keys on it, and hands it back as agentRead.offset to page from here. One line can normalize to several messages — an assistant line holding a thinking block and a tool call — and they all carry the line's offset, so the anchor names the line, not the message.

type Page

type Page struct {
	Messages []Message
	// Start is the offset of the first returned message's line; Next is the
	// offset parsing would continue from. Forward pages advance with Next,
	// backward pages load earlier with Start.
	Start    int64
	Next     int64
	Eof      bool
	FileSize int64
}

Page is one agentRead answer: a window of messages and where the window sits in the file. Eof says the parse reached the end of the file as it was at read time — not that the session is over, since the file may still be growing; the same doctrine as the read verb's eof.

type Resume

type Resume struct {
	Cmd []string `json:"cmd"`
	Cwd string   `json:"cwd"`
}

Resume is the command that picks a session back up, with the directory to run it in. It is a hint printed for a human, never something this package executes; a session whose working directory is unknown carries no Resume at all, because a resume command run in the wrong directory is worse than none.

type Summary

type Summary struct {
	ID   string `json:"id"`
	Tool Tool   `json:"tool"`
	Cwd  string `json:"cwd"`
	// Title is the best name the transcript offers, by each tool's own
	// precedence — a human-set title beats a generated one beats the first
	// prompt. FirstPrompt is kept beside it rather than folded in, so a client
	// can show both the name and the opening ask.
	Title       string `json:"title,omitempty"`
	FirstPrompt string `json:"firstPrompt,omitempty"`
	// StartedAt and EndedAt are the first and last message timestamps —
	// messages, not bookkeeping lines, so a title rewritten hours later does
	// not stretch the session.
	StartedAt     time.Time  `json:"startedAt"`
	EndedAt       time.Time  `json:"endedAt"`
	MessageCount  int        `json:"messageCount"`
	ToolCallCount int        `json:"toolCallCount"`
	Models        []string   `json:"models"`
	Tokens        TokenUsage `json:"tokens"`
	// CostUsd is present only when the transcript itself records dollar cost,
	// which today is Pi alone. Absent is "not recorded", not "free": this
	// package reports what the store says and estimates nothing.
	CostUsd  float64 `json:"costUsd,omitempty"`
	FileSize int64   `json:"fileSize"`
	Resume   *Resume `json:"resume,omitempty"`
	// Missing says the transcript file has been pruned by its own tool —
	// Claude Code deletes transcripts past its cleanup period — and this
	// summary is a tombstone: it still counts in every aggregate, but there
	// is no file left to page or resume, so a client keeps it out of the
	// list of openable transcripts.
	Missing bool `json:"missing,omitempty"`
}

Summary is one session as the list shows it: identity, where it ran, what it was about, and what it cost. Everything here is computed from the transcript alone.

type TokenUsage

type TokenUsage struct {
	Input      int64 `json:"input"`
	Output     int64 `json:"output"`
	CacheRead  int64 `json:"cacheRead"`
	CacheWrite int64 `json:"cacheWrite"`
}

TokenUsage is one session's token consumption, in the four buckets every tool's accounting maps onto. The mapping is per-adapter — Claude sums lines deduped by requestId, Codex reports a cumulative total this copies, Pi sums per-message — and this struct is where the three spellings converge.

type Tool

type Tool string

Tool names one of the transcript stores this package reads. The values are wire strings — a client sends them in agentRead and reads them off every summary — so they are spelled the way the wire spells them.

const (
	ToolClaude Tool = "claude"
	ToolCodex  Tool = "codex"
	ToolPi     Tool = "pi"
)

Jump to

Keyboard shortcuts

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