transcript

package
v0.20.0 Latest Latest
Warning

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

Go to latest
Published: Oct 4, 2026 License: MIT Imports: 11 Imported by: 0

Documentation

Overview

Package transcript reads Claude Code's own session transcript — the append-only NDJSON file whose path the hooks receive as `transcript_path` — for the two facts `brigade sessions` shows beside a session's name and activity: the model it runs and how much of its context window is in use. It is read LOCALLY by the detached watcher (never by a hook, whose budget is seconds; never by an adapter) and only the two derived facts ever leave the machine, as the `model` and `context_used_tokens` heartbeat members (4.4.4); the transcript's contents, its path and the native session id stay where they are (T10, U-22).

The shape is Claude Code's to change (measured on 2.1.267): records of type `attachment` with attachment.type "model" carry the FULL model id in attachment.identity.modelId ("claude-opus-5[1m]"), written at session start and on every /model switch; records of type `assistant` carry the BARE id in message.model ("claude-opus-5") and the response's usage in message.usage — input_tokens, cache_creation_input_tokens and cache_read_input_tokens, whose sum is the context occupancy. Every record has isSidechain; a sidechain (a subagent's turn) says nothing about this session's context, and neither does the `<synthetic>` assistant record Claude Code writes for a turn the API refused (its usage is all zeros). Anything unexpected — a line that is not JSON, a member of another type, an unknown record type, a usage with a negative count — is skipped, never a failure: the facts simply stay what they were. Two things are decoded leniently rather than refused, because Claude Code legitimately writes them: invalid UTF-8 and a lone surrogate escape (JSON.stringify's rendering of an unpaired surrogate) become U+FFFD, and a duplicated member takes its last value, as the JSON.parse that wrote the file reads it back. The count the reader yields is never negative and never wraps (a sum that would overflow saturates); the wire's upper bound on it is the caller's (4.4.4).

The reader is incremental: it keeps a byte offset and the facts so far, and each Refresh reads only what was appended, complete lines only. A line can be hundreds of KB (a tool result) and a file tens of MB, so one line is capped at MaxLineBytes and a longer one is skipped whole; a file shorter than the offset (truncated, or replaced by another) starts the reader over. Nothing here can block the watcher: the path must name a regular file (a FIFO, a device or a directory is refused before any read), and the errors are diagnostic only — fixed text, never the path.

Index

Constants

View Source
const MaxLineBytes = 64 << 20

MaxLineBytes caps one transcript line, newline excluded. A real line is at most hundreds of KB; a longer one is skipped whole and the offset advanced past it, so a runaway record can cost at most this much memory and never stalls the facts behind it.

Variables

View Source
var (
	// ErrMissing: the transcript does not exist (yet, or any more).
	ErrMissing = errors.New("transcript does not exist")
	// ErrNotRegular: the path names something other than a regular file
	// (a FIFO, a device, a directory); nothing was opened for reading.
	ErrNotRegular = errors.New("transcript is not a regular file")
	// ErrUnreadable: a stat, open, seek or read failure other than
	// "missing".
	ErrUnreadable = errors.New("transcript cannot be read")
)

The errors Refresh returns. They are diagnostic only: fixed text, no path, no underlying error — the watcher logs them at debug level and the last facts stand.

Functions

This section is empty.

Types

type Facts

type Facts struct {
	// Model is the model identity as Claude Code wrote it — the full id
	// from the latest model attachment ("claude-opus-5[1m]") unless an
	// assistant record since then names a bare id the attachment does not
	// begin with (a switch the attachment missed), or the bare id alone
	// when no attachment was seen; "" when neither was. It is RAW: the
	// caller sanitises (protocol.SanitizeModel) before it travels.
	Model string
	// ContextUsedTokens is input_tokens + cache_creation_input_tokens +
	// cache_read_input_tokens of the latest non-sidechain assistant record
	// that carries a usage with non-negative counts; meaningful only when
	// HasContext. It is never negative and never a wrapped sum (a corrupt
	// usage saturates at math.MaxInt); the wire's upper bound is the
	// caller's to apply.
	ContextUsedTokens int
	// HasContext reports whether any usage was seen.
	HasContext bool
}

Facts are the two facts the transcript yields.

type Reader

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

A Reader is the incremental reader of one transcript file. It is not safe for concurrent use: the watcher drives it from one goroutine.

func NewReader

func NewReader(path string) *Reader

NewReader returns a reader positioned at the start of path. Nothing is opened until Refresh.

func (*Reader) Path

func (r *Reader) Path() string

Path is the transcript path this reader follows.

func (*Reader) Refresh

func (r *Reader) Refresh() (Facts, error)

Refresh reads whatever was appended since the last call and returns the facts so far. A file shorter than the offset is treated as truncated: the offset and the facts start over. On an error the facts returned are the last known ones (a transcript that vanished keeps reporting what it said), and the error is one of ErrMissing, ErrNotRegular and ErrUnreadable — diagnostic only, never the path.

Jump to

Keyboard shortcuts

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