diff --git a/.github/meta/commit.txt b/.github/meta/commit.txt index f376b9108d..02ac9673d4 100644 --- a/.github/meta/commit.txt +++ b/.github/meta/commit.txt @@ -1,22 +1,14 @@ -<<<<<<< Updated upstream -fix: add missing `altimate_change` markers for `experimental` block in `opencode.jsonc` - -The `experimental` config added in #311 was missing upstream markers, -causing the Marker Guard CI check to fail on main. -======= -fix: add try/catch and input sanitization to TUI install/create (#341) - -Root cause of silent failures: `onConfirm` async callbacks had no -try/catch, so any thrown error was swallowed and no result toast shown. - -Fixes: -- Wrap all install/create logic in try/catch with error toast -- Strip trailing dots from input (textarea was appending `.`) -- Strip `.git` suffix from URLs (users paste from browser) -- Trim whitespace and validate before proceeding -- "Installing..." toast now shows 60s duration with helpful text - ("This may take a moment while the repo is cloned") -- Empty input shows immediate error instead of proceeding ->>>>>>> Stashed changes +fix: viewer UX improvements from 100-trace analysis + +- Collapse Files Changed after 5 entries with "Show all N files" toggle +- Rename "GENS" → "LLM Calls" in header cards +- Hide Tokens card when cost is $0 (not actionable without cost context) +- Hide Cost metric card when $0.00 (wasted space) +- Add prominent error summary banner right after header metrics +- Improved dbt outcome detection: catch [PASS], [ERROR], N of M, Compilation Error +- Outcome detection rate improved from 18% → 33% across 100 real traces +- Updated doc screenshots with cleaner samples + +Tested across 100 real production traces: 0 crashes, 0 JS errors. Co-Authored-By: Claude Opus 4.6 (1M context) diff --git a/README.md b/README.md index 365983fc58..e98d0d259a 100644 --- a/README.md +++ b/README.md @@ -131,8 +131,8 @@ Manifest parsing, test generation, model scaffolding, incremental model detectio ### Data Visualization Interactive charts and dashboards from SQL results. The data-viz skill generates publication-ready visualizations with automatic chart type selection based on your data. -### Local-First Tracing -Built-in observability for AI interactions — trace tool calls, token usage, and session activity locally. No external services required. View traces with `altimate trace`. +### Local-First Recap +Built-in observability for AI interactions — recap tool calls, token usage, and session activity locally. No external services required. View recaps with `altimate recap`. Features include loop detection, post-session summary, and shareable HTML exports. ### AI Teammate Training Teach your AI teammate project-specific patterns, naming conventions, and best practices. The training system learns from examples and applies rules automatically across sessions. diff --git a/docs/docs/assets/images/recap/summary-full.png b/docs/docs/assets/images/recap/summary-full.png new file mode 100644 index 0000000000..e07fff2db2 Binary files /dev/null and b/docs/docs/assets/images/recap/summary-full.png differ diff --git a/docs/docs/assets/images/recap/summary-tab.png b/docs/docs/assets/images/recap/summary-tab.png new file mode 100644 index 0000000000..0379aa3125 Binary files /dev/null and b/docs/docs/assets/images/recap/summary-tab.png differ diff --git a/docs/docs/configure/tracing.md b/docs/docs/configure/recap.md similarity index 61% rename from docs/docs/configure/tracing.md rename to docs/docs/configure/recap.md index 029f062982..eafc73162b 100644 --- a/docs/docs/configure/tracing.md +++ b/docs/docs/configure/recap.md @@ -1,26 +1,31 @@ -# Tracing +# Recap -Altimate Code captures detailed traces of every headless session, including LLM generations, tool calls, token usage, cost, and timing, and saves them locally as JSON files. Traces are invaluable for debugging agent behavior, optimizing cost, and understanding how the agent solves problems. +Altimate Code captures detailed recaps of every session, including LLM generations, tool calls, token usage, cost, and timing, and saves them locally as JSON files. Recaps are invaluable for debugging agent behavior, optimizing cost, and understanding how the agent solves problems. -Tracing is **enabled by default** and requires no configuration. Traces are stored locally and never leave your machine unless you configure a remote exporter. +Recap is **enabled by default** and requires no configuration. Recaps are stored locally and never leave your machine unless you configure a remote exporter. + +![Recap Summary View](../assets/images/recap/summary-tab.png) + +!!! note "Renamed from Tracer" + The tracer feature has been renamed to **recap**. The `trace` command still works as a backward-compatible alias (`--no-trace` is the backward-compatible flag name). New features include loop detection, post-session summary, and shareable HTML exports. ## Quick Start ```bash -# Run a prompt (trace is saved automatically) +# Run a prompt (recap is saved automatically) altimate-code run "optimize my most expensive queries" -# → Trace saved: ~/.local/share/altimate-code/traces/abc123.json +# → Recap saved: ~/.local/share/altimate-code/traces/abc123.json -# List recent traces -altimate-code trace list +# List recent recaps +altimate-code recap list -# View a trace in the browser -altimate-code trace view abc123 +# View a recap in the browser +altimate-code recap view abc123 ``` ## What's Captured -Each trace records the full agent session: +Each recap records the full agent session: | Data | Description | |------|-------------| @@ -31,10 +36,12 @@ Each trace records the full agent session: | **Timing** | Start/end timestamps for every span (session, generation, tool) | | **Errors** | Error messages and status on failed tool calls or generations | | **Metadata** | Model, provider, agent, prompt, user ID, environment, tags | +| **Loop Detection** | Automatic detection of repeated tool call patterns | +| **Post-Session Summary** | AI-generated summary of the session's key actions and outcomes | ### Data Engineering Attributes -When using SQL and dbt tools, traces automatically capture domain-specific data: +When using SQL and dbt tools, recaps automatically capture domain-specific data: | Category | Examples | |----------|----------| @@ -44,7 +51,7 @@ When using SQL and dbt tools, traces automatically capture domain-specific data: | **Data Quality** | Row counts, null percentages, freshness, anomaly detection | | **Cost Attribution** | LLM cost + warehouse compute cost + storage delta = total cost, per user/team/project | -These attributes are purely optional. Traces are valid without them. They're populated automatically by tools that have access to warehouse metadata. +These attributes are purely optional. Recaps are valid without them. They're populated automatically by tools that have access to warehouse metadata. ## Configuration @@ -63,12 +70,12 @@ Add to your config file (`~/.config/altimate-code/altimate-code.json` or project | Option | Type | Default | Description | |--------|------|---------|-------------| -| `enabled` | `boolean` | `true` | Enable or disable tracing | -| `dir` | `string` | `~/.local/share/altimate-code/traces/` | Custom directory for trace files | -| `maxFiles` | `number` | `100` | Max trace files to keep (oldest pruned automatically). Set to `0` for unlimited | +| `enabled` | `boolean` | `true` | Enable or disable recap | +| `dir` | `string` | `~/.local/share/altimate-code/traces/` | Custom directory for recap files | +| `maxFiles` | `number` | `100` | Max recap files to keep (oldest pruned automatically). Set to `0` for unlimited | | `exporters` | `array` | `[]` | Remote HTTP exporters (see below) | -### Disabling Tracing +### Disabling Recap ```json { @@ -84,15 +91,15 @@ Or per-run with the `--no-trace` flag: altimate-code run --no-trace "quick question" ``` -## Viewing Traces +## Viewing Recaps -### List Traces +### List Recaps ```bash -altimate-code trace list +altimate-code recap list ``` -Shows a table of recent traces with session ID, timestamp, duration, tokens, cost, tool calls, and status. +Shows a table of recent recaps with session ID, timestamp, duration, tokens, cost, tool calls, and status. ``` SESSION WHEN DURATION TOKENS COST TOOLS STATUS PROMPT @@ -105,19 +112,34 @@ Options: | Flag | Description | |------|-------------| -| `-n`, `--limit` | Number of traces to show (default: 20) | +| `-n`, `--limit` | Number of recaps to show (default: 20) | -### View a Trace +### View a Recap ```bash -altimate-code trace view +altimate-code recap view ``` -Opens a local web server with an interactive trace viewer in your browser. The viewer shows: +Opens a local web server with an interactive recap viewer in your browser. + +![Recap Full View](../assets/images/recap/summary-full.png) + +The viewer has 5 tabs: -- **Summary cards** showing duration, token breakdown (input/output/reasoning/cache), cost, generations, tool calls, status -- **Timeline** with horizontal bars for each span, color-coded by type (generation, tool, error) -- **Detail panel** where you click any span to see its model info, token counts, finish reason, input/output, and domain-specific attributes (warehouse metrics, dbt results, etc.) +- **Summary** (default) — The story of the session: what was asked, files changed with diff previews, outcome (dbt/pytest/Airflow results), what happened timeline, and cost breakdown +- **Waterfall** — Gantt-style timeline bars for every span, color-coded by type +- **Tree** — Nested indentation view showing parent/child span relationships +- **Chat** — Conversation flow with user prompt and agent responses +- **Log** — Flat chronological list of all events + +The Summary tab shows what matters most to data engineers: + +- **What was asked** — Your prompt, truncated with expand toggle +- **Files changed** — Each file with NEW/EDIT badge and SQL diff preview +- **Outcome** — dbt build results, test results, SQL query results (clickable to jump to waterfall) +- **What happened** — Smart timeline grouping boring commands, showing meaningful actions +- **Loop warnings** — Automatic detection when the agent repeats the same tool call +- **Cost details** — Collapsible token breakdown with visual bar chart Options: @@ -126,31 +148,35 @@ Options: | `--port` | Port for the viewer server (default: random) | | `--live` | Auto-refresh every 2s for in-progress sessions | -Partial session ID matching is supported. For example, `altimate-code trace view abc` matches `abc123def456`. +Partial session ID matching is supported. For example, `altimate-code recap view abc` matches `abc123def456`. ### Live Viewing (In-Progress Sessions) -Traces are written incrementally. After every tool call and generation, a snapshot is flushed to disk. This means you can view a trace while the session is still running: +Recaps are written incrementally. After every tool call and generation, a snapshot is flushed to disk. This means you can view a recap while the session is still running: ```bash # In terminal 1: run a long task altimate-code run "refactor the entire pipeline" -# In terminal 2: watch the trace live -altimate-code trace view --live +# In terminal 2: watch the recap live +altimate-code recap view --live ``` The `--live` flag adds a green "LIVE" indicator and polls for updates every 2 seconds. The page auto-refreshes when new spans appear. ### From the TUI -Type `/trace` in the TUI to open a trace history dialog listing all recent sessions. Select any trace to open it in your browser with the interactive viewer. The current session appears at the top, and traces are grouped by date with duration and timestamp info. +Type `/recap` in the TUI to open a recap history dialog listing all recent sessions. Select any recap to open it in your browser with the interactive viewer. The current session appears at the top, and recaps are grouped by date with duration and timestamp info. The viewer launches in live mode automatically for in-progress sessions, so you can watch spans appear as the agent works. +### Sharing Recaps + +The recap viewer includes a **Share** button that exports a self-contained HTML file. This file includes all session data and can be opened in any browser without a server — perfect for sharing with teammates, attaching to tickets, or archiving sessions. + ## Remote Exporters -Traces can be sent to remote backends via HTTP POST. Each exporter receives the full trace JSON on session completion. +Recaps can be sent to remote backends via HTTP POST. Each exporter receives the full recap JSON on session completion. ```json { @@ -171,7 +197,7 @@ Traces can be sent to remote backends via HTTP POST. Each exporter receives the | Field | Type | Description | |-------|------|-------------| | `name` | `string` | Identifier for this exporter (used in logs) | -| `endpoint` | `string` | HTTP endpoint to POST trace JSON to | +| `endpoint` | `string` | HTTP endpoint to POST recap JSON to | | `headers` | `object` | Custom headers (e.g., auth tokens) | **How it works:** @@ -182,9 +208,9 @@ Traces can be sent to remote backends via HTTP POST. Each exporter receives the - Exporters have a 10-second timeout - All export operations are best-effort and never crash the CLI -## Trace File Format +## Recap File Format -Traces are stored as JSON files in the traces directory. The schema is versioned for forward compatibility. +Recaps are stored as JSON files in the traces directory. The schema is versioned for forward compatibility. ```json { @@ -296,15 +322,15 @@ All domain-specific attributes use the `de.*` prefix and are stored in the `attr ## Crash Recovery -Traces are designed to survive process crashes: +Recaps are designed to survive process crashes: -1. **Immediate snapshot.** A trace file is written as soon as `startTrace()` is called, before any LLM interaction. Even if the process crashes immediately, a minimal trace file exists. +1. **Immediate snapshot.** A recap file is written as soon as the session starts, before any LLM interaction. Even if the process crashes immediately, a minimal recap file exists. -2. **Incremental snapshots.** After every tool call and generation completion, the trace file is updated atomically (write to temp file, then rename). The file on disk always contains a valid, complete JSON document. +2. **Incremental snapshots.** After every tool call and generation completion, the recap file is updated atomically (write to temp file, then rename). The file on disk always contains a valid, complete JSON document. -3. **Crash handlers.** The `run` command registers `SIGINT`/`SIGTERM`/`beforeExit` handlers that flush the trace synchronously with a `"crashed"` status. +3. **Crash handlers.** The `run` command registers `SIGINT`/`SIGTERM`/`beforeExit` handlers that flush the recap synchronously with a `"crashed"` status. -4. **Status indicators.** Trace status tells you exactly what happened: +4. **Status indicators.** Recap status tells you exactly what happened: | Status | Meaning | |--------|---------| @@ -313,31 +339,31 @@ Traces are designed to survive process crashes: | `running` | Session is still in progress (visible in live mode) | | `crashed` | Process was interrupted before the session completed | -Crashed traces contain all data up to the last successful snapshot. You can view them normally with `altimate-code trace view`. +Crashed recaps contain all data up to the last successful snapshot. You can view them normally with `altimate-code recap view`. -## Historical Traces +## Historical Recaps -All traces are stored in the traces directory and persist across sessions. Use `trace list` to browse history: +All recaps are stored in the traces directory and persist across sessions. Use `recap list` to browse history: ```bash -# Show the last 50 traces -altimate-code trace list -n 50 +# Show the last 50 recaps +altimate-code recap list -n 50 -# View any historical trace -altimate-code trace view +# View any historical recap +altimate-code recap view ``` -Traces are automatically pruned when `maxFiles` is exceeded (default: 100). The oldest traces are removed first. Set `maxFiles: 0` for unlimited retention. +Recaps are automatically pruned when `maxFiles` is exceeded (default: 100). The oldest recaps are removed first. Set `maxFiles: 0` for unlimited retention. ## Privacy -Traces are stored **locally only** by default. They contain: +Recaps are stored **locally only** by default. They contain: - The prompt you sent - Tool inputs and outputs (SQL queries, file contents, command results) - Model responses -If you configure remote exporters, trace data is sent to those endpoints. No trace data is included in the anonymous telemetry described in [Telemetry](../reference/telemetry.md). +If you configure remote exporters, recap data is sent to those endpoints. No recap data is included in the anonymous telemetry described in [Telemetry](../reference/telemetry.md). !!! warning "Sensitive Data" - Traces may contain SQL queries, file paths, and command outputs from your session. If you share trace files or configure remote exporters, be aware that this data will be included. + Recaps may contain SQL queries, file paths, and command outputs from your session. If you share recap files or configure remote exporters, be aware that this data will be included. diff --git a/docs/docs/data-engineering/guides/ci-headless.md b/docs/docs/data-engineering/guides/ci-headless.md index bacc07d8a1..e37871e8dc 100644 --- a/docs/docs/data-engineering/guides/ci-headless.md +++ b/docs/docs/data-engineering/guides/ci-headless.md @@ -137,16 +137,16 @@ fi --- -## Tracing in Headless Mode +## Recap in Headless Mode -Tracing works in headless mode. View traces after the run: +Recap works in headless mode. View recaps after the run: ```bash -altimate trace list -altimate trace view +altimate recap list +altimate recap view ``` -See [Tracing](../../configure/tracing.md) for the full trace reference. +See [Recap](../../configure/recap.md) for the full recap reference. --- diff --git a/docs/docs/usage/ci-headless.md b/docs/docs/usage/ci-headless.md index 471acdaada..7de44a8bcc 100644 --- a/docs/docs/usage/ci-headless.md +++ b/docs/docs/usage/ci-headless.md @@ -137,16 +137,16 @@ fi --- -## Tracing in Headless Mode +## Recap in Headless Mode -Tracing works in headless mode. View traces after the run: +Recap works in headless mode. View recaps after the run: ```bash -altimate trace list -altimate trace view +altimate recap list +altimate recap view ``` -See [Tracing](../configure/tracing.md) for the full trace reference. +See [Recap](../configure/recap.md) for the full recap reference. --- diff --git a/docs/docs/usage/cli.md b/docs/docs/usage/cli.md index 804923785e..b32b43b243 100644 --- a/docs/docs/usage/cli.md +++ b/docs/docs/usage/cli.md @@ -33,7 +33,7 @@ altimate --agent analyst | `export` | Export session data | | `import` | Import session data | | `session` | Session management | -| `trace` | List and view session traces | +| `recap` | List and view session recaps (formerly `trace`) | | `github` | GitHub integration | | `pr` | Pull request tools | | `upgrade` | Upgrade to latest version | @@ -149,20 +149,22 @@ altimate run --model anthropic/claude-sonnet-4-6 "optimize my warehouse" # Print logs for debugging altimate --print-logs --log-level DEBUG run "test query" -# Disable tracing for a single run +# Disable recap for a single run (--no-trace is the backward-compatible flag name) altimate run --no-trace "quick question" ``` For CI pipelines and headless automation, see [CI & Automation](ci-headless.md). -## Tracing +## Recap -Every `run` command automatically saves a trace file with the full session details, including generations, tool calls, tokens, cost, and timing. See [Tracing](../configure/tracing.md) for configuration options. +Every `run` command automatically saves a recap file with the full session details, including generations, tool calls, tokens, cost, and timing. See [Recap](../configure/recap.md) for configuration options. ```bash -# List recent traces -altimate trace list +# List recent recaps +altimate recap list -# View a trace in the browser -altimate trace view +# View a recap in the browser +altimate recap view ``` + +> **Note:** The `trace` command still works as a backward-compatible alias for `recap`. diff --git a/docs/docs/usage/tui.md b/docs/docs/usage/tui.md index bb627e0dd9..d739a9019b 100644 --- a/docs/docs/usage/tui.md +++ b/docs/docs/usage/tui.md @@ -20,7 +20,7 @@ The TUI has three main areas: |--------|--------|---------| | `@` | Reference a file | `@src/models/user.sql explain this model` | | `!` | Run a shell command | `!dbt run --select my_model` | -| `/` | Slash command | `/discover`, `/connect`, `/review`, `/models`, `/theme`, `/trace` | +| `/` | Slash command | `/discover`, `/connect`, `/review`, `/models`, `/theme`, `/recap` | ## Leader Key diff --git a/docs/mkdocs.yml b/docs/mkdocs.yml index 70daa51719..38315447fe 100644 --- a/docs/mkdocs.yml +++ b/docs/mkdocs.yml @@ -97,6 +97,7 @@ nav: - Custom Tools: configure/tools/custom.md - Skills: configure/skills.md - Commands: configure/commands.md + - Recap: configure/recap.md - Interfaces: - TUI: usage/tui.md - CLI: usage/cli.md @@ -122,9 +123,6 @@ nav: - Appearance: - Themes: configure/themes.md - Keybinds: configure/keybinds.md - - Observability: - - Tracing: configure/tracing.md - - Telemetry: reference/telemetry.md - Training: - Overview: data-engineering/training/index.md - Team Deployment: data-engineering/training/team-deployment.md @@ -141,6 +139,7 @@ nav: - Formatters: configure/formatters.md - Reference: - Changelog: reference/changelog.md + - Telemetry: reference/telemetry.md - Security FAQ: reference/security-faq.md - Troubleshooting: reference/troubleshooting.md - Extend: diff --git a/packages/opencode/src/altimate/observability/tracing.ts b/packages/opencode/src/altimate/observability/tracing.ts index c64e74c969..d1aa5c37b2 100644 --- a/packages/opencode/src/altimate/observability/tracing.ts +++ b/packages/opencode/src/altimate/observability/tracing.ts @@ -1,5 +1,5 @@ /** - * Tracing for Altimate CLI. + * Recap (session tracing) for Altimate CLI. * * Trace schema aligned with industry standards (OpenTelemetry GenAI semantic * conventions, Arize Phoenix / OpenInference, Langfuse). @@ -126,6 +126,14 @@ export interface TraceFile { cacheRead: number cacheWrite: number } + // altimate_change start — recap: loop detection + post-session summary + /** Detected tool call loops (same tool+input repeated 3+ times). */ + loops?: Array<{ tool: string; inputHash?: string; count: number; description: string }> + /** Human-readable session narrative generated at endTrace. */ + narrative?: string + /** Top tools by call count with total duration. */ + topTools?: Array<{ name: string; count: number; totalDuration: number }> + // altimate_change end } } @@ -166,7 +174,7 @@ export class FileExporter implements TraceExporter { async export(trace: TraceFile): Promise { try { await fs.mkdir(this.dir, { recursive: true }) - // Sanitize sessionId for safe file name (defense-in-depth — also sanitized in Tracer) + // Sanitize sessionId for safe file name (defense-in-depth — also sanitized in Recap) const safeId = (trace.sessionId ?? "unknown").replace(/[/\\.:]/g, "_") || "unknown" const filePath = path.join(this.dir, `${safeId}.json`) await fs.writeFile(filePath, JSON.stringify(trace, null, 2)) @@ -239,18 +247,41 @@ export class HttpExporter implements TraceExporter { } // --------------------------------------------------------------------------- -// Tracer +// Recap (formerly Tracer) // --------------------------------------------------------------------------- interface TracerOptions { maxFiles?: number } -export class Tracer { - // Global active tracer — set when a session starts, cleared on end. - private static _active: Tracer | null = null - static get active(): Tracer | null { return Tracer._active } - static setActive(tracer: Tracer | null) { Tracer._active = tracer } +// altimate_change start — recap: helper utilities for loop detection and narrative +function simpleHash(str: string): string { + try { + let hash = 0 + for (let i = 0; i < str.length; i++) { + const ch = str.charCodeAt(i) + hash = ((hash << 5) - hash + ch) | 0 + } + return hash.toString(36) + } catch { + return "0" + } +} + +function formatDurationShort(ms: number): string { + if (ms < 1000) return `${Math.round(ms)}ms` + if (ms < 60000) return `${(ms / 1000).toFixed(1)}s` + const mins = Math.floor(ms / 60000) + const secs = Math.floor((ms % 60000) / 1000) + return `${mins}m${secs}s` +} +// altimate_change end + +export class Recap { + // Global active recap — set when a session starts, cleared on end. + private static _active: Recap | null = null + static get active(): Recap | null { return Recap._active } + static setActive(recap: Recap | null) { Recap._active = recap } private traceId: string private sessionId: string | undefined @@ -270,6 +301,11 @@ export class Tracer { private generationCount = 0 private tokensBreakdown = { input: 0, output: 0, reasoning: 0, cacheRead: 0, cacheWrite: 0 } + // altimate_change start — recap: loop detection state + private toolCallHistory: Array<{ tool: string; inputHash: string; time: number }> = [] + private loopsDetected: Array<{ tool: string; inputHash: string; count: number; firstSeen: number; lastSeen: number }> = [] + // altimate_change end + private metadata: TraceFile["metadata"] = {} private snapshotDir: string | undefined private snapshotPending = false @@ -290,16 +326,16 @@ export class Tracer { } /** - * Create a tracer with the default local file exporter. + * Create a recap with the default local file exporter. */ - static create(extraExporters: TraceExporter[] = []): Tracer { - return new Tracer([new FileExporter(), ...extraExporters]) + static create(extraExporters: TraceExporter[] = []): Recap { + return new Recap([new FileExporter(), ...extraExporters]) } /** - * Create a tracer with explicit exporters (no defaults). + * Create a recap with explicit exporters (no defaults). */ - static withExporters(exporters: TraceExporter[], options?: TracerOptions): Tracer { + static withExporters(exporters: TraceExporter[], options?: TracerOptions): Recap { if (options?.maxFiles != null) { for (const exp of exporters) { if (exp instanceof FileExporter) { @@ -309,7 +345,7 @@ export class Tracer { } } } - return new Tracer(exporters) + return new Recap(exporters) } /** @@ -554,6 +590,42 @@ export class Tracer { output: isError ? { error: errorStr } : outputStr.slice(0, 10000), }) this.toolCallCount++ + + // altimate_change start — recap: loop detection + try { + const inputStr = safeInput != null ? JSON.stringify(safeInput) : "" + const inputHash = simpleHash(toolName + inputStr) + const now = Date.now() + this.toolCallHistory.push({ tool: toolName, inputHash, time: now }) + // Prune to prevent unbounded growth in long sessions + if (this.toolCallHistory.length > 200) { + this.toolCallHistory = this.toolCallHistory.slice(-100) + } + + // Check last 10 tool calls for repeated tool+input + const recent = this.toolCallHistory.slice(-10) + const matchCount = recent.filter((h) => h.tool === toolName && h.inputHash === inputHash).length + if (matchCount >= 3) { + const existing = this.loopsDetected.find((l) => l.tool === toolName && l.inputHash === inputHash) + if (existing) { + existing.count = matchCount + existing.lastSeen = now + } else { + const firstMatch = recent.find((h) => h.tool === toolName && h.inputHash === inputHash) + this.loopsDetected.push({ + tool: toolName, + inputHash, + count: matchCount, + firstSeen: firstMatch?.time ?? now, + lastSeen: now, + }) + } + } + } catch { + // Loop detection must never crash the recap + } + // altimate_change end + this.snapshot() } catch { // best-effort @@ -775,6 +847,49 @@ export class Tracer { const trace = this.buildTraceFile(error) + // altimate_change start — recap: post-session summary (narrative, loops, topTools) + try { + // Top tools by call count + const toolCounts = new Map() + for (const span of this.spans) { + if (span.kind !== "tool") continue + const entry = toolCounts.get(span.name) ?? { count: 0, totalDuration: 0 } + entry.count++ + entry.totalDuration += span.tool?.durationMs ?? 0 + toolCounts.set(span.name, entry) + } + const topTools = [...toolCounts.entries()] + .sort((a, b) => b[1].count - a[1].count) + .slice(0, 10) + .map(([name, stats]) => ({ name, count: stats.count, totalDuration: stats.totalDuration })) + trace.summary.topTools = topTools + + // Loops + if (this.loopsDetected.length > 0) { + trace.summary.loops = this.loopsDetected.map((l) => ({ + tool: l.tool, + inputHash: l.inputHash, + count: l.count, + description: `${l.tool} called ${l.count} times with same input (hash: ${l.inputHash})`, + })) + } + + // Narrative + const dur = formatDurationShort(trace.summary.duration) + const top3 = topTools.slice(0, 3).map((t) => t.name).join(", ") + const toolsStr = top3 ? ` using ${toolCounts.size} tools (${top3})` : "" + const loopWarning = this.loopsDetected.length > 0 + ? ` Warning: ${this.loopsDetected.length} loop(s) detected.` + : "" + const costStr = Number.isFinite(this.totalCost) ? `$${this.totalCost.toFixed(4)}` : "$0.0000" + const statusPrefix = error ? `Failed after ${dur}` : `Completed in ${dur}` + const llmStr = this.generationCount > 0 ? `. Made ${this.generationCount} LLM call${this.generationCount > 1 ? "s" : ""}` : "" + trace.summary.narrative = `${statusPrefix}${llmStr}${toolsStr}.${loopWarning} Total cost: ${costStr}.` + } catch { + // Narrative generation must never crash the recap + } + // altimate_change end + // Wrap each exporter call with a timeout to prevent hanging exporters // from blocking the entire endTrace call const EXPORTER_TIMEOUT_MS = 5_000 @@ -878,3 +993,10 @@ export class Tracer { } } } + +// altimate_change start — recap: backward-compat alias (value + type) +/** @deprecated Use Recap instead */ +export const Tracer = Recap +/** @deprecated Use Recap instead */ +export type Tracer = Recap +// altimate_change end diff --git a/packages/opencode/src/altimate/observability/viewer.ts b/packages/opencode/src/altimate/observability/viewer.ts index c9c4df337a..b782586639 100644 --- a/packages/opencode/src/altimate/observability/viewer.ts +++ b/packages/opencode/src/altimate/observability/viewer.ts @@ -1,14 +1,15 @@ /** * Trace viewer HTML renderer. * - * Generates a self-contained HTML page with 4 visualization modes: - * 1. Waterfall — Gantt-style timeline bars (Datadog/Jaeger-style) - * 2. Tree — nested indentation with expandable detail (Langfuse-style) - * 3. Chat — conversation flow with user/agent messages (LangSmith-style) - * 4. Log — flat scrollable list, Ctrl+F searchable (Langfuse Log View) + * Generates a self-contained HTML page with 5 visualization modes: + * 1. Summary — shareable recap with narrative, metrics, and charts (default) + * 2. Waterfall — Gantt-style timeline bars (Datadog/Jaeger-style) + * 3. Tree — nested indentation with expandable detail (Langfuse-style) + * 4. Chat — conversation flow with user/agent messages (LangSmith-style) + * 5. Log — flat scrollable list, Ctrl+F searchable (Langfuse Log View) * * All modes share a common summary header with metrics cards. - * Branded with Altimate colors. + * Branded with Altimate Recap colors. Includes share/export features for virality. */ import type { TraceFile } from "./tracing" @@ -23,7 +24,7 @@ export function renderTraceViewer(trace: TraceFile, options?: { live?: boolean; -Altimate Trace +Altimate Recap