Every OpenCode 2 session leaves behind a rich event ledger — prompts, tool calls, retries, errors, compactions, tokens and cost — that is painful to reconstruct by hand. This tool opens that ledger locally and read-only: no telemetry, no writes to your data.
A local-first, read-only observability layer for OpenCode 2 sessions. It adds two views:
- Trajectory — a faithful, filterable ledger of every recorded session event: prompts, assistant steps, reasoning, tool calls/results, subagents, retries, errors, compactions, context notices, model/agent/location switches and session provenance markers.
- Statistics — a dense per-turn/per-step table with context size, tokens, cache reuse, cost, model/tool/residual time, output speed and status, plus distinct compaction transactions.
The implementation is original and targets OpenCode 2.2.x. It was built against the installed data format (session_v2, session_message) and the published OpenCode 2 plugin API (@opencode/plugin 2.2.6).
The analysis is read-only. The SQLite source opens the database with
SQLITE_OPEN_READONLYand setsPRAGMA query_only = ON. Nothing in the app writes sessions, messages or configuration.
| Area | Delivered |
|---|---|
| Data adapter | OpenCode 2 SQLite (session_v2, session_message), OpenCode server API (ctx.session.*), in-memory fixture source |
| Normalization | Typed NormalizedEvent stream for every persisted message type and assistant content block |
| Projection | Turns, steps, tool calls, subagents, compactions, non-step events, unattached events |
| Metrics | Prompt/context tokens, cache hit rate, model/tool/residual/total time, output speed, cost, context deltas |
| UI | Local browser app: Trajectory + Statistics, Basic/Advanced modes, OpenCode themes, project-grouped session tree, settings, filters, full-text search, detail drawer, copy, export |
| Integration | OpenCode 2 server plugin with read-only RPC + TUI commands: native /statistics (alias /stats) and native /trajectory//observability fullscreen panels, with --web/-w/web opening the browser, plus /statistics:web (alias /stats:web) |
| Export | Self-contained HTML, Markdown and JSON |
| Tests | 305 tests: parsing, projection, metrics, paging, scroll preservation, redaction, authentication, adapters, HTTP API, RPC, exports, settings/theme persistence, TUI routing seams, native panels, TUI columns/status helpers, plugin entry shape, read-only guarantees |
Screenshots of both views in dark and light mode (1440×900):
- Node.js 24.x (tested with 24.18).
node:sqliteis built in. Node 22.18+ may require--experimental-sqlitedepending on the exact release. - OpenCode 2.2.x session data. The tested database was produced by
@opencode/cli2.2.5; the plugin types were checked against@opencode/plugin2.2.6. - A local browser and/or the OpenCode 2 TUI.
The app does not support the OpenCode 1 schema (session/message tables). It detects that schema and reports it clearly instead of reading incompatible data.
npm install
npm startnpm start discovers an OpenCode 2 database automatically (see below), starts a local server on http://127.0.0.1:4517, and prints the URL and database path.
By default the server generates an access token, prints the tokenized URL, and writes a per-port token file so the TUI can open the authenticated UI without copying the token.
Open the printed URL. The token is carried in the URL fragment, removed from the address bar after the UI loads, and kept in sessionStorage for that tab.
Disable authentication on loopback:
npm start -- --no-authUse a stable explicit token:
npm start -- --token=<value>Explicit tokens are not written to disk. Configure the TUI with the token plugin option or OPENCODE_OBSERVABILITY_TOKEN.
Explicit database and port:
npm start -- --db "C:/Users/me/.opencode2/data/opencode/opencode.db" --port 4517 --openOther flags:
--db <path> OpenCode 2 SQLite database
--host <host> Bind address (default 127.0.0.1)
--port <port> Port (default 4517; 0 picks a free port)
--reveal Allow the explicit reveal-secrets mode
--no-reveal Keep reveal disabled (the default)
--token Generate a per-run access token (default) and print it
--token=<value> Use an explicit access token (at least 16 characters)
--no-auth Disable the access token (loopback only)
--allow-remote Allow a non-loopback --host; authentication stays required
--open Open the browser after starting
--help, -v Help / version
--token and --no-auth are mutually exclusive. A non-loopback bind is refused when authentication is disabled.
Run the checks:
npm run check # typecheck + 305 tests (node:test)
npm run audit:prod # production dependency audit (0 advisories)The CLI looks for opencode.db in this priority order and picks the first OpenCode 2 database:
--db <path>(authoritative; must be an OpenCode 2 database)OPENCODE_OBSERVABILITY_DBOPENCODE_DB$XDG_DATA_HOME/opencode/opencode.db~/.opencode2/data/opencode/opencode.db(isolated OpenCode 2 profile, e.g.opencode2.cmd)~/.local/share/opencode/opencode.db- Windows:
%LOCALAPPDATA%\opencode\opencode.db,%APPDATA%\opencode\opencode.db - macOS:
~/Library/Application Support/opencode/opencode.db - Linux:
~/.config/opencode/opencode.db
If only an OpenCode 1 database is found, the app exits with an explicit error. Use --db to point at the OpenCode 2 store.
The server never reads auth.json, service.json or provider credentials. It opens only the session database.
The package is both a standalone app and an OpenCode 2 plugin with a server part and a TUI part:
exports: "." -> server plugin (RPC)
"./tui" -> prebuilt native panel (dist-tui/tui.mjs, typed by src/plugin/tui.ts)
"./rpc" -> typed RPC contract
The ./tui entry resolves to the prebuilt dist-tui/tui.mjs bundle and is typed by the src/plugin/tui.ts sidecar, which also re-exports the slash-command helpers. src/plugin/tui.tsx is a typechecked-only OpenTUI scaffold that exposes NativeObservabilityView without mounting it; it is not bundled. The browser UI stays the primary surface.
Why this shape? OpenCode 2.2.x plugins can add tools, hooks, RPC methods, commands, agents, TUI routes/slots and experimental GUI panels. They do not expose arbitrary HTTP routes for a third-party web page. The supported integration path is therefore:
- the server part registers a read-only RPC namespace over the OpenCode client;
- the TUI part registers
/observabilityand/trajectory(browser deep links) plus/statistics(native fullscreen panel;--webfor the browser) and/statistics:web(the browser Statistics deep link); - the web UI runs as a separate local read-only server, so it works from the TUI, a browser, or any local client.
For a local checkout, copy opencode.jsonc.example into your OpenCode config and adjust the path:
For a published package:
{
"plugins": ["opencode-session-observability"]
}The TUI loads the TUI parts of plugins its server loads. /statistics renders in a native fullscreen panel; the other commands open the browser. None of them inject a prompt into the session.
The TUI reads the per-port token file written by npm start. The optional token plugin option, or the OPENCODE_OBSERVABILITY_TOKEN environment variable, overrides it. An explicit --token=<value> is never written to disk, so it must be provided through one of those two settings. The TUI never prints the token in a toast.
Two example configs ship with the repo:
opencode.jsonc.example— the full plugin entry with theurl,allowReveal,themeandtokenoptions, as shipped inopencode.jsonc.example. Use it for a local checkout or to change the port or allow reveal.cli.json.example— the minimal registration-only list for the bare CLI config, when the defaults are enough (loopbackhttp://127.0.0.1:4517, reveal off, theme forwarded by the TUI, token read from the per-port file):
{
"$schema": "https://opencode.ai/config.json",
"plugins": ["opencode-session-observability"]
}Start the local UI once:
npm startThen in the OpenCode 2 TUI:
/observability open the native Trajectory panel (alias /obs)
/trajectory open the native Trajectory panel
/trajectory --web open the browser Trajectory view instead (also -w, web)
/statistics open the native fullscreen Statistics panel (alias /stats)
/statistics --web open the browser Statistics view instead (also -w, web)
/statistics:web open the browser Statistics view (alias /stats:web)
/statistics renders the banner, dense turn table and turn accordion inside the terminal; the web commands use the browser. If the UI is not running, a browser command shows a toast with the command to start it. If the UI requires a token and the plugin has none, it shows how to provide one instead of opening an unauthenticated page. The URL is configurable with the plugin option url; it must point at a loopback address (127.0.0.1, localhost or ::1). Native /statistics opens the built-in fullscreen panel through the session.panel slot. /trajectory and /observability open the native fullscreen Trajectory panel the same way: a turn ledger with the per-turn step accordion. Without a selected session they toast once and open the browser Trajectory view, and --web forces the browser. The router-page seam (registerNativeView with STATISTICS_ROUTE) is deprecated: it has no production caller, stays exported only for host compatibility, and new views should claim session.panel instead. The TUI also contributes the status badge in the prompt.footer.status slot, showing the session cost and cache hit rate (n/a when the local UI is offline or unauthenticated); the badge never toasts.
The native panel ships precompiled: npm run build:tui produces the committed dist-tui/tui.mjs, which is the import target of exports["./tui"]. Node hosts load that ESM artifact, so the raw .tsx sources are never required at runtime (Bun-only raw-TypeScript loading is not relied on). Rebuild the artifact after any change under src/plugin/tui/; CI verifies it is fresh. @opentui/*, solid-js and vite are dev dependencies only — the package keeps zero runtime dependencies, as shown by the exports map in package.json.
session-observability exposes these methods (read-only):
| Method | Purpose |
|---|---|
health |
source, version, reveal policy, read-only flag |
sessions |
list/search sessions |
session |
session metadata + parent/children family |
analysis |
turns, steps, compactions, totals, event index |
eventPage |
filtered/keyset-paged event rows |
event |
one full event payload |
export |
JSON / Markdown / HTML report |
reveal defaults to false. The plugin option allowReveal also defaults to false; set it to true to permit reveal for RPC callers.
Desktop GUI note. The experimental
./gui-experimentalpanel API is not shipped. Desktop users use the same browser UI. The server RPC is available to future GUI or external clients.
- Events are ordered by the recorded per-session sequence, not by a reconstructed conversation. Timestamp regressions are flagged, never re-sorted.
- Rows are grouped by turn and step. Unattached events appear in recorded order under an Unattached header.
- Badges: error, retry, compaction, context, subagent, uncertain. A
~before a timestamp means the block timestamp is approximate; the recorded order is still exact. - Filters: full-text search, kind, agent, model, provider, tool, status, time range, prompt-token range, and shortcut chips for errors/tools/compactions/retries/subagents/context.
- Click an event to open the detail drawer: full payload JSON, text/reasoning, tool input/output/error/metadata/diff, subagent links, copy JSON/text/metrics/turn chain.
- Parent/child sessions are linked in the sidebar family section and in subagent tool results.
- One row per turn; click a turn to expand its steps. Click a step to expand its inline detail (Markdown response, reasoning, tool calls, subagent links).
Context basisswitches the turn context column between Latest (final prompt of the turn) and Cumulative (sum of step prompts). The session total row always shows both final and cumulative context independently.Show event rowsadds non-step rows (system/context notices, switches, retries, errors, compactions, synthetic/skill/shell events) to expanded turns and lists unattached events. These rows are never summed into turn/step totals.- Compactions are shown as distinct transactions with before/after context, removed tokens, summary size, compaction usage and cost. Duration is marked unavailable because OpenCode records only a compaction timestamp.
- Click “Open in Trajectory” on a compaction or “Open step in Trajectory” on a step to jump to the exact event.
Recorded by OpenCodecompares computed totals with the session-row totals.
| Key | Action |
|---|---|
/ |
Focus event search |
1 / 2 |
Trajectory / Statistics |
e t c r s |
Toggle errors / tools / compactions / retries / subagents |
j / k |
Next / previous event |
Enter |
Open selected event |
g / G |
Top / bottom |
? |
Shortcut dialog |
Esc |
Close drawer/dialog |
http://127.0.0.1:4517/#/s/<sessionID>/trajectory
http://127.0.0.1:4517/#/s/<sessionID>/statistics
http://127.0.0.1:4517/#/s/<sessionID>/trajectory?event=<eventID>
The Export menu downloads:
- JSON — normalized session, analysis, event index and (by default) full normalized events;
- Markdown — summary, turns, steps, compactions, trajectory and limitations;
- HTML — one self-contained report with no external assets.
Exports are redacted by default. Turn on Reveal secrets (with confirmation) to export raw payloads. Reveal is disabled on the server unless it was started with --reveal.
| Metric | Definition | Basis |
|---|---|---|
| Prompt tokens | input + cache.read + cache.write |
derived |
| Cache hit rate | cache.read / prompt, token-weighted |
derived |
| Model time | time.created → time.streamed |
derived |
| Step total | time.created → time.completed |
derived |
| Tool time | sum of tool.time.ran → tool.time.completed |
derived |
| Residual | step total − model − tool; negative means overlap | derived |
| Turn total | last completed step end − first step start | derived |
| Output speed | output tokens / model-generation seconds | derived |
| Context (turn) | latest final prompt or sum of step prompts | derived |
| Context delta | current final prompt − previous turn final prompt | derived |
| Compaction before/after | compaction request prompt / next step prompt | derived |
| Cost | sum of recorded step + compaction costs | derived |
| TTFT | not persisted | unavailable |
| Approvals / request headers / tool-set changes | not persisted | unavailable |
See docs/METRICS.md for formulas, caveats and worked examples.
- Read-only connection:
DatabaseSync(path, { readOnly: true }),PRAGMA query_only = ON,enableForeignKeyConstraints: false(no writes). - No mutation APIs: the adapter interface has only
listSessions,getSession,listMessages,messageBounds,getFamily. - No session prompts: the plugin and TUI commands never call
session.prompt,compact,interruptor any action method. - Access token (default on loopback): the server generates a 256-bit per-run token. Every
/apiroute except/api/healththen requiresAuthorization: Bearer <token>orX-Observability-Token. The token is printed in the URL fragment, stripped from the address bar, and kept insessionStorage; it is never written to logs or error bodies. A per-port token file with user-only permissions lets the TUI reuse it (%LOCALAPPDATA%\opencode-observabilityon Windows,$XDG_RUNTIME_DIRor~/.config/opencode-observabilityelsewhere).--no-authdisables authentication on loopback only.--token=<value>uses an explicit token that is not persisted. - Redaction: credential-shaped keys and strings are replaced with
[REDACTED]by default. Token counts are metrics and are preserved. - Reveal: disabled by default; enable with
--reveal(server) orallowReveal: true(plugin). The browser confirmation is a UX safeguard, not an access control: any client that can reach the API, is allowed to reveal and has a valid token (when enabled) receives raw data. The UI shows a persistent warning while active. - Network: bind
127.0.0.1by default. Non-loopback binds require--allow-remoteand authentication;--no-authis refused off loopback. Requests are rejected unless theHost(and anyOrigin) belongs to the listener, which blocks DNS rebinding; CORS is not relied on. - Exports: redacted by default; HTML/Markdown/JSON include a warning when redacted. When a token is enabled, download requests carry the same header.
Test coverage includes a SHA-256 before/after check proving analysis leaves the database file unchanged, and a read-only write-rejection test.
The honest list (details in docs/CHECKLIST.md):
- TTFT is not persisted in OpenCode 2.2.x. It is always
—. - Permission/approval decisions are not stored in historical session data. There are no false “awaiting approval” rows.
- Request headers are available only to live
http.requesthooks; they are not in session storage. - Tool-set changes are not persisted.
- Exact compaction duration is not persisted; only a completion timestamp is available.
- Session-row totals can include auxiliary model calls (title, generation, compaction) and therefore can differ slightly from the sum of visible steps.
- Parallel tool calls are summed; the sum can exceed wall-clock. The UI notes this.
- Desktop GUI panel is not shipped; the browser UI is primary.
src/
core/
types.ts normalized model and DTOs
normalize.ts message -> ordered NormalizedEvent[]
project.ts turns/steps/compactions/totals/capabilities
metrics.ts token/cache/duration/speed formulas
service.ts dataset loading, sanitization, filters, pagination, export
export.ts JSON / Markdown / HTML
redact.ts/util.ts credential redaction
paging.ts page merge, scroll anchor, virtual range
adapters/
sqlite.ts OpenCode 2 SQLite, read-only
api.ts OpenCode server API source
memory.ts fixtures/tests
paths.ts database discovery
server/
index.ts CLI
http.ts local HTTP API + static UI
auth.ts access-token generation and constant-time comparison
token-file.ts per-port token file read by the TUI
ui/
index.html
styles.css
app.js views, state, filters, drawer, shortcuts
auth.js fragment token, sessionStorage and headers
virtual.js variable-height virtual list
markdown.js safe Markdown subset
format.js formatting + badges
paging.js UI-side page merge (mirrors core)
plugin/
index.ts OpenCode 2 server part (RPC)
define.ts identity shims for the SDK's define helpers
rpc.ts typed RPC contract
rpc-define.ts local Rpc.define equivalent
service.ts RPC handlers
tui.ts TUI commands
tui.tsx typed OpenTUI JSX sidecar (provide-only)
test/
fixtures/build.ts synthetic OpenCode 2 fixtures
*.test.ts 305 tests
docs/
DATA-SOURCES.md
ARCHITECTURE.md
METRICS.md
UI.md
CHECKLIST.md
npm run check # typecheck + 305 tests
npm run audit:prod # production dependency auditThe suite is at 305 tests across 34 files and grows with each new suite.
The fixtures cover: a simple session, multiple tool calls (including parallel and error), subagents, retry + error, compaction, a long paging session, an incomplete/interrupted run, out-of-order timestamps, secrets, and an empty session. The HTTP tests start the real server on an ephemeral port against a synthetic SQLite database.
The OpenCode 2 SDK is a devDependency used only for types: the runtime plugin uses local identity shims for Plugin.define and Rpc.define, so the SDK's packaging toolchain is never loaded by this app. npm run audit:prod reports no advisories. The overrides in package.json pin glob@14.0.4 and minimatch@11.0.5 to clear the SDK's transitive packaging advisories, so a full npm audit also reports no vulnerabilities. Remove those overrides when OpenCode pins patched releases upstream.
A real-database smoke check:
npm start -- --db "%USERPROFILE%/.opencode2/data/opencode/opencode.db" --port 4517or on a POSIX shell:
npm start -- --db "$HOME/.opencode2/data/opencode/opencode.db" --port 4517MIT. See LICENSE.
{ "$schema": "https://opencode.ai/config.json", "plugins": [ { "package": "<path-to-repo>", "options": { "url": "http://127.0.0.1:4517", "allowReveal": false // "token": "optional override; the TUI reads the run token file by default" } } ] }