Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -272,3 +272,14 @@ communities load. Do not expose credentials in logs or responses. If the identit
or community loading cannot be verified, report that limitation before presenting
the build as ready to test. Follow the worktree bootstrap instructions above;
do not hardcode identity values into tracked files.

## Inspect a live agent session

When asked to inspect or debug an Agent conversation, open its live session viewer
for the human:

```sh
bin/node scripts/session-debug.mjs '<Buzz thread ID or link>'
```

Keep it running for live updates. See [details](docs/session-debugging.md) if needed.
5 changes: 4 additions & 1 deletion docs/communities.md
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,10 @@ community origin and viewer; channel IDs alone are not sufficient keys.
## Session lifetime

Selecting a saved membership lazily acquires its session. Sessions survive page
navigation and switching; startup opens only the selected one. Opened communities
navigation and switching; startup opens only the selected one. Once its agent
list loads, Agents2 also opens each joined community where the viewer has an
agent, through `open`, which connects without selecting so those agents keep
running, and retries one whose connection failed while it is not selected. Opened communities
retain their existing per-session query/cache budgets. Arbitrary destinations mean
there is **no longer a two-session maximum**: retained session count grows with
communities opened until app disposal. This slice does not add background eviction
Expand Down
110 changes: 110 additions & 0 deletions docs/session-debugging.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
# Inspect a live agent session

From a buzz-app checkout:

```sh
bin/node scripts/session-debug.mjs '<thread-id or buzz://message link>'
```

The command opens a private loopback URL in your browser and follows Codex or
Claude JSONL records once a second. Leave the command running. Ctrl+C stops it.
It does not resume an agent, execute transcript content, or send data to a relay.
For a known file use `--file /absolute/session.jsonl`. `--no-open` prints the URL
without opening it. A partial last JSONL line is retried on the next refresh;
malformed complete records produce a visible error, not silent omission.

The left rail is one vertical wall-time chart for the whole session, with time
running downward. Active time is proportional: inference, tools, inference
overlapping a tool, and **unattributed** gaps (a run is active but no model or
tool activity is recorded). **Idle** time between runs, when the agent waits for
the next prompt, collapses to a labeled break. The legend totals each category;
overlap is counted once, never added to wall time. Tool time is call-to-result
elapsed time, not subprocess CPU time. Beside each inference, a thin mark shows
its new (uncached) tokens; hover it for the full context window and cache share.
Hover or focus a bar segment for its timing and tool names; click either to jump
the conversation to that record.

Native transcripts do not expose exact provider request start times. Inference
bars are explicitly estimated from the preceding logged input/result through
observed model output, and can include queue, prefill and dispatch overhead. With
no usable output boundary, the chart shows an untimed inference and leaves that
time unattributed. An explicit running turn extends to now; legacy sessions with
no completion record end at their last recorded message. Claude Code's zero-usage
`<synthetic>` replies and queue bookkeeping are not counted as activity. The chart
uses UTC to match the transcript timestamps.

Each inference shows new-input, cache-read, cache-write (when nonzero), and output
tokens. Token counts are the default cost view. Optional dollar estimates appear
only when rates are supplied; missing prices do not produce placeholder warnings.
For Codex, cached tokens are part of input and are subtracted to find new input.
Claude already reports new input separately. Neither provider's cache usage is
counted twice.

The conversation stays on the right, under one line naming the harness, model
and reasoning effort (when the transcript records it). Messages the agent posted
to Buzz are highlighted; plain assistant text was never sent. Search filters the
conversation. The view follows new records while it is scrolled to the bottom
and leaves your position alone otherwise; expanded disclosures survive updates.
These are recorded transcript views, not exact reconstructed HTTP inference
requests. When multiple sessions match a thread, only one is shown; open a
specific one with `--file`. Thread discovery checks user delivery envelopes,
rather than matches in tool outputs that merely quoted a thread. Channel-scoped
sessions and DMs may require `--file`, and can contain multiple Buzz threads.

## Recorded fields and cost

**Prompt** opens what the model received before its first response. Instructions
are tagged with the role they were sent as (System, Developer, User, or Settings
for run configuration) and render as Markdown: the system prompt first, then
the rest in recorded order. Tools show one row per tool, opening to its
description and JSON schema. Context injected later, such as Claude Code's
reminders, stays in the conversation as faint rows.

Codex supplies `session_meta.base_instructions`, `dynamic_tools`, developer/user
messages, response items, lifecycle events, and token usage when supported by its
CLI version. Run settings (approval, sandbox, collaboration mode, effort) come from
`turn_context` and show again when they change. Codex does not record its native
tool schemas, such as shell and apply_patch. The viewer prefers per-response `token_usage_record`, deduplicates
response IDs, and falls back to changes in cumulative token counts. It does not
add those two sources together. The chart uses timestamp intervals to account for
wall time; recorded `duration_ms` remains available in raw records.

Claude's native transcript supplies messages and usage. Claude Code also writes a
`prompt_snapshot` with the system prompt and tool schemas, shown once per change,
and the rendered text and role of each reminder it injects (environment, skills,
CLAUDE.md, hook context). Request framing, such as billing and SDK system blocks,
beta headers and deferred tool schemas, is not recorded. Content blocks can repeat
the same message ID and usage; the viewer counts each response once. Legacy turn
boundaries are inferred from user messages; tool results do not start turns.
Without a completion event the viewer does not pretend that the last token's
arrival proves completion. Inferred boundaries are labeled.

Dollar costs are unavailable unless reported or a rate file is supplied. They are
never represented as zero merely because the account uses a subscription. Claude
result totals are process-cumulative; the viewer derives differences within each
captured launch and labels the result. Exact billing can differ from API estimates.

To estimate costs, create a JSON object keyed by exact model ID with USD per
million token rates, then pass `--rates /path/to/rates.json`:

```json
{"your-model-id":{"input":2,"output":10,"cached":0.2,"written":2.5}}
```

These are illustrative rates, not current pricing. Supply all four rates. No
models or prices are guessed. The UI labels estimates. For Claude, input excludes
cache reads and writes; for Codex those are subtracted from inclusive input before
pricing. Models with tiered context, service tiers, or cache TTL pricing need rates
appropriate to the inspected usage; the simple estimator does not infer tiers.

## Storage and older exports

Discovery reads `$CODEX_HOME/sessions`, `$CODEX_HOME/archived_sessions` (default
`~/.codex`) and `$CLAUDE_CONFIG_DIR/projects` (default `~/.claude`). Compressed
`.jsonl.zst` rollouts must be decompressed to a local file first and opened with
`--file`. The server is bound to 127.0.0.1, uses an unguessable path, rejects foreign
Host/Origin requests, and serves only its three assets and selected transcript.

For standalone snapshots, the existing
[Codex](view-codex-session.md) and [Claude](view-claude-session.md) exporters remain
available. A snapshot does not update.
5 changes: 5 additions & 0 deletions docs/view-claude-session.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
# View a Claude Code session started by Buzz

For the live viewer, run `bin/node scripts/session-debug.mjs '<Buzz thread ID or link>'`
from the repository root. See [session debugging](session-debugging.md) for wall
time, token use and live updates. The commands below remain useful for
standalone snapshots and manual transcript inspection.

This guide is for agents investigating a Buzz conversation. All commands run
non-interactively and read the saved transcript without invoking Claude or tools.

Expand Down
5 changes: 5 additions & 0 deletions docs/view-codex-session.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
# View a Codex session started by Buzz

For the live viewer, run `bin/node scripts/session-debug.mjs '<Buzz thread ID or link>'`
from the repository root. See [session debugging](session-debugging.md) for wall
time, token use and live updates. The commands below remain useful for
standalone snapshots and manual transcript inspection.

This guide is for agents investigating a Buzz conversation. All commands run
non-interactively and read the saved transcript without invoking Codex or tools.

Expand Down
92 changes: 87 additions & 5 deletions scripts/export-claude-session.mjs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
#!/usr/bin/env node
import { entry, exportSession } from "./session-viewer.mjs";
import { pathToFileURL } from "node:url";
import { entry, exportSession, text } from "./session-viewer.mjs";

// Tool results are a string or a list of text/image content blocks.
const resultText = (content) =>
Expand All @@ -16,7 +17,7 @@ const target = (input) =>
.map((key) => input?.[key])
.find((value) => typeof value === "string") ?? "";

exportSession((records) => {
export function buildClaude(records) {
const messages = records.filter((record) => record?.message != null);
const results = new Map(
messages.flatMap((record) =>
Expand All @@ -29,7 +30,79 @@ exportSession((records) => {
);
const entries = [];
let model;
for (const { timestamp, type, isMeta, message } of messages) {
// Claude Code writes `prompt_snapshot`s in pairs: the first, before a
// response, lacks the CLI prefix and tools that the second fills in. Show the
// pair as one, at the first, and again only when the prompt or tools change.
let system;
let tools;
const deferred = records.some(
(record) => record?.attachment?.type === "deferred_tools_delta",
);
for (const record of records) {
const { timestamp, type, isMeta, message, attachment } = record;
if (type === "attachment" && attachment?.type === "prompt_snapshot") {
const key = JSON.stringify(attachment.systemPrompt);
const blocks = [
attachment.cliPrefix,
...(attachment.systemPrompt ?? []),
].filter((block) => typeof block === "string" && block.trim());
const prompt = blocks.join("\n\n");
const row = (at) =>
entry.instructions(at, "System prompt", prompt, {
scope: "system",
blocks,
});
if (prompt && key !== system?.key) {
system = { key, prefixed: !!attachment.cliPrefix, at: entries.length };
entries.push(row(timestamp));
} else if (prompt && attachment.cliPrefix && !system.prefixed) {
system.prefixed = true;
entries[system.at] = row(entries[system.at].timestamp);
}
// `schema` is the wire definition; name and description show apart.
const defs = (attachment.tools ?? []).map((tool) => {
const {
name: _,
description: __,
...schema
} = tool.schema ?? tool.input_schema ?? {};
return { name: tool.name, description: tool.description, schema };
});
if (defs.length && JSON.stringify(defs) !== tools) {
tools = JSON.stringify(defs);
const row = (at) =>
entry.tools(at, `Tools · ${defs.length}`, defs, {
note: deferred
? "Deferred tools load on demand; only their names are recorded, in the deferred tools reminder."
: null,
});
if (system && !system.tools)
entries.splice(system.at + 1, 0, row(entries[system.at].timestamp));
else entries.push(row(timestamp));
}
if (system && defs.length) system.tools = true;
continue;
}
// Other attachments that reached the model carry their rendered text.
if (type === "attachment") {
const body = (record.rendered ?? [])
.map((part) => text(part?.content))
.join("\n\n")
.trim();
if (body)
entries.push(
entry.instructions(
timestamp,
[record.renderedRole, attachment?.type?.replaceAll("_", " ")]
.filter(Boolean)
.join(" · ") || "context",
body,
{ scope: record.renderedRole },
),
);
continue;
}
if (message == null) continue;
if (message.model && message.model !== model) {
model = message.model;
entries.push(entry.model(timestamp, model));
Expand All @@ -46,7 +119,7 @@ exportSession((records) => {
if (body)
entries.push(
isMeta
? entry.instructions(timestamp, "meta", body)
? entry.instructions(timestamp, "meta", body, { scope: "user" })
: entry.user(timestamp, body, buzzRequest(body)),
);
for (const block of blocks)
Expand All @@ -60,6 +133,12 @@ exportSession((records) => {
else if (block?.type === "thinking") {
if (block.thinking?.trim())
entries.push(entry.thinking(timestamp, block.thinking.trim()));
} else if (
block?.type === "tool_use" &&
/(^|__)buzz__send$/.test(block.name) &&
typeof block.input?.text === "string"
) {
entries.push(entry.sent(timestamp, block.input.text));

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P2] Confirm Claude delivery before rendering a send as posted

This branch replaces every buzz__send invocation with a “posted to Buzz” card without consulting the results map. If publication is rejected, the recorded tool_result.is_error and its explanation are therefore discarded; a still-pending send is also shown as delivered. That makes the diagnostic viewer report success precisely when someone is investigating a missing reply. Preserve the pending/failed tool row and show a posted card only when the recorded result confirms success. The analogous Codex-side issue is already covered in this finding.

} else if (block?.type === "tool_use") {
const result = results.get(block.id);
entries.push(
Expand Down Expand Up @@ -88,4 +167,7 @@ exportSession((records) => {
],
entries,
};
});
}

if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href)
exportSession(buildClaude);
Loading
Loading