Replace internal task tracking (TODO.md, FINDINGS.md) with structured documentation in docs/. Trim CLAUDE.md to agent instructions only. Co-Authored-By: Virgil <virgil@lethean.io>
231 lines
11 KiB
Markdown
231 lines
11 KiB
Markdown
# Architecture
|
|
|
|
Module: `forge.lthn.ai/core/go-session`
|
|
|
|
## Overview
|
|
|
|
go-session parses Claude Code JSONL transcript files into structured `Event` arrays, computes analytics over those events, and renders them as self-contained HTML timelines or MP4 video files via VHS. The package has no external runtime dependencies — only the standard library.
|
|
|
|
## JSONL Transcript Format
|
|
|
|
Claude Code stores sessions as newline-delimited JSON (JSONL) files. Each line is a top-level entry:
|
|
|
|
```json
|
|
{"type":"assistant","timestamp":"2026-02-20T10:00:01Z","sessionId":"abc123","message":{...}}
|
|
{"type":"user","timestamp":"2026-02-20T10:00:02Z","sessionId":"abc123","message":{...}}
|
|
```
|
|
|
|
The `type` field is either `"assistant"` or `"user"`. The `message` field contains a `role` and a `content` array of content blocks. Content blocks carry a `type` of `"text"`, `"tool_use"`, or `"tool_result"`.
|
|
|
|
Tool calls appear as a two-entry sequence:
|
|
1. An `assistant` entry with a `tool_use` content block (carries the tool name, unique ID, and input parameters).
|
|
2. A subsequent `user` entry with a `tool_result` content block referencing the same tool ID (carries the output and an `is_error` flag).
|
|
|
|
## Parsing Pipeline
|
|
|
|
### `ParseTranscript(path string) (*Session, *ParseStats, error)`
|
|
|
|
`ParseTranscript` streams the file line by line using `bufio.Scanner` with a 4 MB buffer, which handles large sessions without loading the entire file into memory.
|
|
|
|
The parser maintains a `pendingTools` map (keyed by tool ID) to correlate `tool_use` blocks with their corresponding `tool_result` blocks. When a result arrives, the parser computes the round-trip duration (`result.timestamp - toolUse.timestamp`), assembles an `Event`, and removes the entry from the map. Any tool IDs remaining in `pendingTools` after the scan completes are orphaned tool calls.
|
|
|
|
Malformed JSON lines are skipped without aborting the parse. The line number and a 100-character preview are appended to `ParseStats.Warnings`. A truncated final line is detected by checking whether the last scanned line failed to unmarshal.
|
|
|
|
The session's `ID` is derived from the filename (the `.jsonl` suffix is stripped). `StartTime` and `EndTime` are set from the first and last successfully parsed RFC3339Nano timestamps.
|
|
|
|
### `ListSessions(projectsDir string) ([]Session, error)`
|
|
|
|
`ListSessions` globs for `*.jsonl` files in the given directory and performs a lightweight scan of each file to extract the first and last timestamps. Sessions are returned sorted newest-first by `StartTime`. If no valid timestamps are found in a file (e.g. it is entirely malformed), `StartTime` falls back to the file's modification time.
|
|
|
|
## Event Types
|
|
|
|
The `Event` struct is the central data type:
|
|
|
|
```go
|
|
type Event struct {
|
|
Timestamp time.Time
|
|
Type string // "tool_use", "user", "assistant"
|
|
Tool string // "Bash", "Read", "Edit", "Write", "Grep", "Glob", "Task"
|
|
ToolID string
|
|
Input string // human-readable summary of the tool input
|
|
Output string // truncated tool output (2000 chars max)
|
|
Duration time.Duration // time from tool_use to tool_result
|
|
Success bool
|
|
ErrorMsg string // populated when Success is false
|
|
}
|
|
```
|
|
|
|
Non-tool events (`user` and `assistant`) carry only `Timestamp`, `Type`, and `Input` (the text content, truncated to 500 characters).
|
|
|
|
## Tool Input Extraction
|
|
|
|
`extractToolInput` converts the raw JSON input of a tool call into a readable string. Each recognised tool type has its own struct and formatting rule:
|
|
|
|
| Tool | Input format |
|
|
|------|-------------|
|
|
| Bash | `command # description` (description omitted if empty) |
|
|
| Read | `file_path` |
|
|
| Edit | `file_path (edit)` |
|
|
| Write | `file_path (N bytes)` |
|
|
| Grep | `/pattern/ in path` (path defaults to `.`) |
|
|
| Glob | `pattern` |
|
|
| Task | `[subagent_type] description` (falls back to truncated prompt) |
|
|
|
|
For unknown tools, the fallback extracts and sorts the JSON field names from the raw input object.
|
|
|
|
## Result Content Extraction
|
|
|
|
`extractResultContent` handles three forms that `tool_result` content can take:
|
|
|
|
- **String**: returned as-is.
|
|
- **Array of objects**: each object's `text` field is extracted and joined with newlines.
|
|
- **Map with a `text` key**: the text field is returned.
|
|
- **Anything else**: formatted with `fmt.Sprintf("%v", content)`.
|
|
|
|
## ParseStats
|
|
|
|
```go
|
|
type ParseStats struct {
|
|
TotalLines int
|
|
SkippedLines int
|
|
OrphanedToolCalls int
|
|
Warnings []string
|
|
}
|
|
```
|
|
|
|
`ParseStats` is always returned alongside `*Session`. Callers may discard it with `_`. Warnings include per-line skipped-line notices (with line number and preview) and orphaned tool call IDs.
|
|
|
|
## Analytics
|
|
|
|
`Analyse(sess *Session) *SessionAnalytics` is a pure function (no I/O) that iterates events and computes:
|
|
|
|
- `Duration`: `EndTime - StartTime`
|
|
- `ActiveTime`: sum of all tool call durations
|
|
- `ToolCounts`, `ErrorCounts`: per-tool-name call and error tallies
|
|
- `SuccessRate`: `(totalCalls - totalErrors) / totalCalls`
|
|
- `AvgLatency`, `MaxLatency`: per-tool average and maximum durations
|
|
- `EstimatedInputTokens`, `EstimatedOutputTokens`: approximated at 1 token per 4 characters of input and output text respectively
|
|
|
|
`FormatAnalytics(a *SessionAnalytics) string` renders these fields as a tabular plain-text summary suitable for CLI display.
|
|
|
|
## Cross-Session Search
|
|
|
|
`Search(projectsDir, query string) ([]SearchResult, error)` iterates every `*.jsonl` file in the directory, parses each with `ParseTranscript`, and performs a case-insensitive substring match against the concatenation of each tool event's `Input` and `Output` fields. Only `tool_use` events are searched. Results carry the session ID, timestamp, tool name, and the matched input as context.
|
|
|
|
## HTML Timeline Rendering
|
|
|
|
`RenderHTML(sess *Session, outputPath string) error` generates a fully self-contained HTML file. All CSS and JavaScript are written inline; the output has no external dependencies and can be opened directly in any browser.
|
|
|
|
### Visual Design
|
|
|
|
The timeline uses a dark theme with CSS custom properties:
|
|
|
|
| Variable | Value | Purpose |
|
|
|----------|-------|---------|
|
|
| `--bg` | `#0d1117` | Page background |
|
|
| `--bg2` | `#161b22` | Header and event header background |
|
|
| `--bg3` | `#21262d` | Hovered elements, input fields |
|
|
| `--fg` | `#c9d1d9` | Primary text |
|
|
| `--dim` | `#8b949e` | Secondary text (timestamps, durations) |
|
|
| `--accent` | `#58a6ff` | Tool names, links, focused borders |
|
|
| `--green` | `#3fb950` | Bash tool label, success icons |
|
|
| `--red` | `#f85149` | Error tool label, error event border, failure icons |
|
|
| `--yellow` | `#d29922` | User message label |
|
|
| `--border` | `#30363d` | Default card borders |
|
|
|
|
### Event Colour Coding
|
|
|
|
Tool type determines the CSS class applied to the `.tool` label span:
|
|
|
|
| Event type / tool | CSS class | Colour |
|
|
|------------------|-----------|--------|
|
|
| Bash tool | `.bash` | green |
|
|
| Failed tool | `.error` (on `.event`) | red border |
|
|
| User message | `.user` | yellow |
|
|
| Assistant message | `.assistant` | dim |
|
|
| All other tools | lowercase tool name | accent (default) |
|
|
|
|
Success or failure of a `tool_use` event is indicated by a Unicode check mark (U+2713) in green or cross (U+2717) in red.
|
|
|
|
### Collapsible Panels
|
|
|
|
Each event is rendered as a `<div class="event">` containing:
|
|
- `.event-header`: always visible; shows timestamp, tool label, truncated input (120 chars), duration, and status icon.
|
|
- `.event-body`: hidden by default; shown on click via the `toggle(i)` JavaScript function which toggles the `open` class.
|
|
|
|
The arrow rotates 90 degrees (CSS `transform: rotate(90deg)`) when the panel is open. Output text in `.event-body` is capped at 400 px height with `overflow-y: auto`.
|
|
|
|
Input label semantics per tool:
|
|
|
|
| Tool | Label |
|
|
|------|-------|
|
|
| Bash | Command |
|
|
| Read, Glob, Grep | Target |
|
|
| Edit, Write | File |
|
|
| User | Message |
|
|
| Assistant | Response |
|
|
|
|
### Search and Filter
|
|
|
|
A sticky header contains a text input and a `<select>` filter. Both call `filterEvents()` on change. The filter adds or removes the `hidden` class from each `.event` element based on:
|
|
|
|
1. The type filter dropdown (`all`, `tool_use`, `errors`, `Bash`, `user`).
|
|
2. A case-insensitive substring match against the `data-text` attribute, which holds the lowercased concatenation of `Input` and `Output` for each event.
|
|
|
|
Pressing `/` focuses the search input (keyboard shortcut), unless an input element is already focused.
|
|
|
|
### XSS Protection
|
|
|
|
All user-controlled content is passed through `html.EscapeString` before being written into the HTML output. This covers event input text, output text, tool labels, and the `data-text` attribute used for client-side search. Raw strings are never interpolated directly into HTML.
|
|
|
|
## MP4 Video Rendering
|
|
|
|
`RenderMP4(sess *Session, outputPath string) error` generates a VHS tape script (`.tape` format for `github.com/charmbracelet/vhs`) from the session events and invokes the `vhs` binary to render it as an MP4.
|
|
|
|
`generateTape` constructs the tape script:
|
|
- Configuration: 1400x800, 16pt font, Catppuccin Mocha theme, 30 ms typing speed.
|
|
- Title frame: session short ID and start date, with a 2-second pause.
|
|
- Per Bash event: simulates typed command and abbreviated output (200 chars max), followed by a `# OK` or `# FAILED` comment and a 1-second pause.
|
|
- Read, Edit, Write events: a comment line showing tool name and truncated input, with a 500 ms pause.
|
|
- Task events: a comment line showing the agent description with a 1-second pause.
|
|
- Grep and Glob events are omitted from the tape (no visual output to simulate).
|
|
|
|
`extractCommand` strips the description suffix from a Bash input string by splitting on the first ` # ` separator.
|
|
|
|
`RenderMP4` is only usable when the `vhs` binary is present on `$PATH`; it returns an actionable error message if it is not.
|
|
|
|
## Truncation Limits
|
|
|
|
| Field | Limit |
|
|
|-------|-------|
|
|
| User / assistant text | 500 characters |
|
|
| Tool output | 2000 characters |
|
|
| Error message | 500 characters |
|
|
| HTML event header input | 120 characters |
|
|
| MP4 command output | 200 characters |
|
|
| Task description fallback | 80 characters |
|
|
| Malformed-line warning preview | 100 characters |
|
|
|
|
## Data Flow Diagram
|
|
|
|
```
|
|
JSONL file
|
|
|
|
|
v
|
|
bufio.Scanner (4 MB buffer)
|
|
|
|
|
+-- rawEntry (type, timestamp, sessionId, message)
|
|
| |
|
|
| +-- assistant/tool_use --> pendingTools[id]
|
|
| +-- user/tool_result --> match pendingTools[id] --> Event
|
|
| +-- assistant/text --> Event (assistant)
|
|
| +-- user/text --> Event (user)
|
|
|
|
|
v
|
|
Session{ID, Path, StartTime, EndTime, Events[]}
|
|
|
|
|
+-- Analyse() --> SessionAnalytics
|
|
+-- RenderHTML() --> .html (self-contained)
|
|
+-- RenderMP4() --> .mp4 (via vhs)
|
|
+-- Search() --> []SearchResult (cross-session)
|
|
```
|