Transcripts
Every agent run rupu executes writes a transcript: an append-only JSON Lines file with one event per line, recording what was sent, what came back, every tool call and what it did. This page is the schema, for anyone who reads transcripts with their own tools.
Where transcripts live
Each agent run writes one file, named after its run id:
<transcripts-dir>/run_<ULID>.jsonl # an active transcript <transcripts-dir>/archive/run_<ULID>.jsonl # after rupu transcript archive
| Directory | Used when |
|---|---|
<project>/.rupu/transcripts/ | The directory exists in the project the run started in. |
~/.rupu/transcripts/ (or $RUPU_HOME/transcripts/) | Otherwise. |
- The file name is the run id.
run_plus a 26-character ULID, so names sort by start time. Onlyrun_startandrun_completerepeat the id inside the file. - Every agent run writes one: a standalone
rupu run, each workflow step and fan-out unit, each child started withdispatch, and each session turn. A workflow run’s own records (run.json,step_results.jsonl,events.jsonl) live in the run store and point at each step’s transcript path. - Not every
.jsonlin the directory is an agent transcript.run:step andaction:step logs and per-run netflow ledgers use the same naming with their own shapes. An agent transcript’srun_startis written before the agent loop starts, so it survives every tolerated damage mode; a file with norun_startanywhere is one of the others, and rupu’s readers skip it. rupu transcript listreads only therun_startevent of each file for metadata, which is what lets it sort thousands of transcripts cheaply; it then reads just the--limitmost recent end to end (run_completeat the tail carries the status and token totals).
File format
One JSON object per line, UTF-8, newline-terminated. Every line has the same envelope:
{"type": "<event>", "data": { … }}
typeis the event name insnake_case; so are all field names.- Optional fields are omitted, not
null, when they have no value, and boolean flags that default tofalseare omitted when false. The one exception isrun_start.customer, wherenullis meaningful. Treat a missing key as its default. - Files are only ever appended to. Several writers (the agent loop, the
tool_audithook, the netflow sink) append to one file, each line whole.
Schema versions
run_start.schema says which writer produced the file. Current rupu writes 2; a transcript with no schema key is version 1.
| Version | What it records |
|---|---|
| 1 (no key) | The conversation as text. Reasoning, if any, is a plain thinking string on assistant_message, with no provider payload. |
| 2 | Enough to rebuild the exact conversation sent to the provider: the effective system_prompt on run_start, the user_message, each reasoning block as a thinking event with the provider’s byte-exact raw block, the seed the run started from, every compaction, and assistant_block for reply blocks with no event of their own. |
Fields and events have been added within version 2 since it shipped: outcome, recovery, assistant_block, customer, codename, cache_write_tokens, purpose, skill_index, skill_load, launch_skills, launch_preload_skills, launch_args, message and injected. A reader should accept a missing optional key and an unknown event type.
Reading rules
- No
run_completemeans the run was killed or crashed (or is still going). Treat it asaborted, the statusrupu transcript listgives it. Everything before the end is still valid, and often diagnostic. - A truncated last line is a write cut off by a crash. Skip it.
- Empty lines: skip them.
- A malformed line mid-file is an error for that line only; keep reading the rest.
- An unknown
typeis not an error. See Unknown events.
Event order
Within one file:
run_start notice? # model_limits tool_grant # the tools offered, and why notice* # tool_unavailable, one per named tool the run can't serve skill_index? # the skills bound, when the run has any (skill_load | notice)* # preload, continuation and operator loads; skill_not_indexed, skill_missing seed? # a run seeded from earlier history user_message? ( turn_start ( message* injected )* # what the collectors delivered before this turn's model call usage+ # one per provider call; a compaction call adds one with purpose outcome? # a non-normal reply, ahead of the turn's content ( thinking_delta* thinking | assistant_delta* assistant_message | assistant_block )* ( tool_call tool_result ( skill_load notice* )? file_edit? command_run? ( message | notice )? tool_audit )* turn_end recovery* ( skill_load | notice )* compaction? )* net_flow* # interleaved anywhere, as connections close run_complete
Reasoning, text, tool calls and other blocks appear in the order the model produced them. An outcome follows the turn’s usage and precedes its content; its recovery events follow it, usually after turn_end. A notice can appear wherever the runtime intervened; a hop to a fallback model writes a model_limits notice just before its fell_back recovery, and a note sent back to the model is a user_message. A successful skills.load writes its skill_load right after the call’s tool_result, then any skill_changed or skill_tools_missing notice. Skills put back after a compaction are skill_load lines with via: reattach, alongside any skill_changed, skill_missing or skill_reattach_truncated notice. A session’s compact pseudo-turn (the compaction a running session’s worker does on request) writes the same lines after its compaction usage; it has no compaction event.
Event reference
Fields marked opt are omitted when empty.
run_start
The first line of every agent transcript.
| Field | Type | Meaning |
|---|---|---|
run_id | string | Same as the file name without .jsonl. |
workspace_id | string | The workspace (project) record, ws_…. |
agent | string | Agent name from its frontmatter. |
provider / model | string | What the run was configured to use. A fallback hop appears later as a notice and a recovery, not here. |
started_at | RFC 3339 | Start time, UTC. |
mode | ask | bypass | readonly | The permission mode: prompt before write/bash tool calls; execute all tools without prompting; deny write/bash and allow read/grep/glob only. |
schema | integer, opt | 2 today; absent on version 1. |
system_prompt | string, opt | The system prompt actually sent, after rupu appended its own sections (coverage catalog, run target, finding guidance). |
codename | string, opt | The agent instance’s human codename, e.g. amber-lantern/heron or cobalt-harbor/heron#412; absent on transcripts that predate codenames. |
customer | string | null, opt | The customer slug. null means recorded as no customer; no key means the transcript predates customers. |
launch_skills | string[], opt | The skill entries the launch granted on top of the agent’s (a workflow step’s skills:, rupu run --skill), as written: * stays *. Absent when the launch named none, and on transcripts that predate the field. rupu run --continue of a transcript without launch_args passes them again. |
launch_preload_skills | string[], opt | The launch’s preload_skills: / --preload-skill entries, likewise. |
launch_args | string[], opt | A rupu run’s own arguments, the tokens after run, with the run’s effective values: no prompt, no --continue, and for a continued run the merged command. rupu run --continue restores them, so a continue keeps every original flag and a flag given on it replaces only its own setting. Absent for other runs and on transcripts that predate the field. |
{"type":"run_start","data":{"run_id":"run_01JB7Q2M4X8T6V0K3N5R9C1D2E","workspace_id":"ws_01JB7PZ0A1B2C3D4E5F6G7H8J9","agent":"code-reviewer","provider":"anthropic","model":"claude-sonnet-4-6","started_at":"2026-10-07T09:14:03Z","mode":"readonly","schema":2,"codename":"amber-delta/kestrel","customer":null}}
run_complete
The last line of a run that ended. Its absence means the run never finished.
| Field | Type | Meaning |
|---|---|---|
run_id | string | Same as run_start. |
status | ok | error | aborted | Completed normally; ended with a provider or internal error; killed or crashed before run_complete. |
total_tokens | integer | Tokens across all turns. |
duration_ms | integer | Wall-clock duration. |
error | string, opt | The error, when status is error. |
outcome | OutcomeRecord, opt | The outcome the run failed on, when it ended because the recovery ladder ran out; or, on an aborted run stopped by SIGTERM, the stop: class stopped_by_signal (warning). A workflow run.json’s cause carries the same record when its run failed while its process was stopped by a signal, or runner_died when the reaper finalized a run whose runner had died. A run whose cause is one of these did not fail on its own: whoever continues its parent continues it. |
turn_start, turn_end and usage
| Event | Fields |
|---|---|
turn_start | turn_idx — zero-based turn counter. |
turn_end | turn_idx; opt tokens_in, tokens_out; opt stop_reason (the provider’s own wire stop value, e.g. end_turn, max_output_tokens); opt response_id; opt stop, a StopRecord: reason (normalized: end_turn, max_tokens, refusal, pause_turn, …), wire (provider name, raw value, extras), opt refusal, opt served_by (the model that answered when a server-side fallback served the turn) — absent on transcripts written before the response-outcomes work; opt discarded: true when the turn’s content was thrown away (a refused, blocked or retried turn) — replay drops it and it never reached the model again. |
usage | One per provider call, right after the call returns and before the response’s content events: provider, model (requested; used for pricing), opt served_model (if the provider served another), input_tokens, output_tokens (billable, reasoning tokens folded in — there is no separate reasoning-token field), cached_tokens (prompt-cache reads, part of input_tokens, default 0), opt cache_write_tokens (prompt-cache writes in input_tokens; Anthropic only, omitted when 0), opt purpose: "compaction" for the context-compaction summarizer — real, billed spend that is not an agent turn (turn counters skip it; token and cost totals include it). |
{"type":"usage","data":{"provider":"anthropic","model":"claude-sonnet-4-6","input_tokens":18240,"output_tokens":612,"cached_tokens":16384}}
{"type":"turn_end","data":{"turn_idx":3,"tokens_in":18240,"tokens_out":612,"stop_reason":"tool_use"}}
Conversation events
| Event | Fields | Notes |
|---|---|---|
user_message | content | The user turn this run added. Absent on a run that only continues a seed. A note rupu sends back to the model mid-run (after an interruption, or a recovery note) is also a user_message. |
assistant_message | content; opt thinking | One complete text block. thinking is the version 1 reasoning string; version 2 writers record reasoning as separate thinking events. |
assistant_delta | content | A streamed chunk ahead of its assistant_message. Ignore it unless you’re rendering live. |
thinking | opt text, provider, model, raw | One reasoning block, in its real position among the turn’s other blocks. raw is the provider’s byte-exact block, signatures included, kept for replay; never display it. No text means the reasoning was redacted or its display omitted. |
thinking_delta | content | A streamed reasoning chunk; the following thinking event has the whole block. |
assistant_block | block; opt abandoned | A reply content block with no event of its own, in its position among the turn’s other events: {"type":"fallback","from_model":…,"to_model":…} (an Anthropic server-side fallback boundary) or {"type":"unknown","provider":…,"raw":…} (a block rupu does not model, with the provider’s raw payload). abandoned: true marks a block the API discarded at a mid-reply server-side fallback: never sent back to the model, skipped by replay. |
seed | message_count, sha256; opt source_transcript or messages | The conversation this run started from (a rupu run --continue, a session’s earlier turns, a workflow resume), stored once. Exactly one of source_transcript (a transcript whose replay rebuilds exactly this seed; chains can be several deep) and messages (the seed inline, reasoning raw intact) is present. sha256 is a hash of the canonical seed JSON, so a reader can verify a reference chain. |
compaction | seq, summarized_messages, backup_path, messages | Context compaction rewrote the conversation. messages is the whole conversation afterwards, which the following turns were built from. The summarizer call itself is billed under a usage line with purpose: "compaction". |
Tool events
| Event | Fields | Notes |
|---|---|---|
tool_call | call_id, tool, input | The model asked for a tool. call_id is provider-assigned; input is the JSON it sent. |
tool_result | call_id, output, opt error, duration_ms, opt structured | Pairs with its tool_call by call_id. structured is a machine-readable payload some tools add next to the text, e.g. ast_grep’s matches with ranges and metavariable bindings; absent on older transcripts and for tools that add none. |
file_edit | path, kind (create | modify | delete), diff | Written alongside a write_file / edit_file result, so you can index edits without parsing tool inputs. path is absolute; diff is unified. |
command_run | argv, cwd, exit_code, stdout_bytes, stderr_bytes | Written alongside a bash result. |
action_emitted | kind, payload, allowed, applied, opt reason | A standalone workflow action: step’s catalog call: kind is the tool name as the step wrote it (issues.create, scm.prs.comment), payload its rendered with:. applied is true whenever the call reached the connector (whether or not the connector call succeeded) and false only when it was denied before reaching it; allowed is false only when permission was denied; reason explains either. Always followed immediately by a tool_audit for the same call. Old transcripts may carry free-form verbs (log_finding, propose_edit) with applied: false that no current code path can produce. |
tool_audit | tool, declared, granted, blocked, restricted, opt reason, opt decision | The audit line for one tool call: every call ends in exactly one, whatever the tool (core, coverage/findings, connector, agentiflow) and whatever happened to it. Written by the agent loop right after the call’s tool_result (before run_complete when the operator stops the run at the call’s prompt), and after an action: node’s action_emitted. It is what the control plane’s transcript panel renders a badge from (blocked / not granted). See below. |
tool_audit fields:
| Field | Meaning |
|---|---|
tool | The tool name as the model called it (canonical or a legacy alias, matching its tool_call); for an action: node, the step’s tool. |
declared | Whether the step’s actions: names tool. false both when actions: is empty or absent (unrestricted — not a violation) and when it doesn’t name this tool; use restricted to tell those apart. |
granted | Whether the run’s grant covered tool before any actions: narrowing. Always true for an action: node. |
blocked | Whether the call was denied (decision is not allowed). |
restricted | Whether the step declared a non-empty actions: allowlist at all. |
reason (opt) | Why the tool was in the grant, as comma-separated reasons (the tool_grant entry’s), or action for an action: node. Absent when the grant didn’t cover the call, and on older lines. |
decision (opt) | allowed, denied:readonly, denied:operator (answered no at the prompt), denied:operator_stop, denied:tool (the tool’s own gate refused), or not_granted (the model named a tool the run doesn’t offer). Absent on lines written before every call was audited. |
# a write refused by readonly mode {"type":"tool_audit","data":{"tool":"write_file","declared":false,"granted":true,"blocked":true,"restricted":false,"reason":"declared","decision":"denied:readonly"}} # an action: step that created an issue {"type":"action_emitted","data":{"kind":"issues.create","payload":{"title":"null pointer in parser.rs:142","body":"..."},"allowed":true,"applied":true}}
tool_grant
The tools the run offered its model, and why. Written once, right after run_start and its model_limits notice. entries is exactly the model’s tool list.
| Field | Type | Meaning |
|---|---|---|
entries | array | {tool, reasons} per offered tool: its canonical name and every reason it is there — declared (named in tools:), declared:<wildcard> (matched *, core.*, scm.*, …), default (the agent has no tools:), ambient:concerns, ambient:engagement, ambient:messaging_default (the messaging tools every run with a message space gets unless its agent, workflow, agentiflow or parent turns them off), ambient:workflow_messaging (a workflow’s messaging: true or { tools: [...] }), origin:flow_lead / origin:flow_unit (an agentiflow lead’s or unit’s coordination tools; a lead’s include dispatch, join and workflows.generate), implied:dispatch (join, added because dispatch is granted). |
narrowed | array, opt | Connector tools the step’s actions: removed. |
unavailable | array, opt | {tool, missing}: tools named exactly in tools: that this run can’t serve, with the services it lacks. Each also gets a tool_unavailable notice. |
skipped | array, opt | {tool, missing}: tools a wildcard or the default grant matched that this run can’t serve. Not offered, no notice. |
Services a tool can be missing: scm (configured connectors), launcher (dispatch / join, every child kind), findings, engagement, coverage (a concerns: block), message_bus, run_status, catalog, workflow_generator (workflows.generate), netflow.
{"type":"tool_grant","data":{"entries":[{"tool":"read_file","reasons":["declared"]},{"tool":"findings.report","reasons":["ambient:concerns"]}],"unavailable":[{"tool":"board.post","missing":["message_bus"]}]}}
skill_index and skill_load
A run’s skills leave two kinds of line. skill_index is the skills the run bound, written once, right after tool_grant and its notices, and only when the run has a skill: skills is an array with one row per bound skill.
| Row field | Type | Meaning |
|---|---|---|
name | string | The name the skill binds in this run. |
layer | global | project | Which skills directory it came from. |
stamp | string | The skill’s stamp at run start: a sha256 over its files’ paths, sizes, mtimes and modes, no contents. A later load compares against it to notice an edit. In a session turn it is the session’s baseline, recorded when rupu session start froze the set (or when session send --skill added the skill), so an edit made since then is noticed in every later turn. |
preload | bool | The skill’s body is in the system prompt. |
indexed | bool | The skill is listed in the model’s skill index. false for a preloaded skill and for one marked disable-model-invocation. |
reason | string | Why the run has it: agent (the agent’s skills: / preloadSkills:), launch (the launch’s own: a workflow step’s keys or rupu run --skill), session (rupu session start --skill / --preload-skill) or operator (a skill rupu session send --skill invoked in an earlier turn; never indexed). A transcript written before the rename says step for launch; readers take the value as written. |
shipped | bool, opt | The skill came from the coordinator of a placed unit, not from the host’s installed layers. Absent when false. |
skill_load records that a skill’s content entered the conversation, for every load, whichever path made it:
| Field | Type | Meaning |
|---|---|---|
name | string | The skill’s name in this run. |
layer | global | project | |
content_hash | string | sha256 over the skill’s files’ paths, modes and bytes: the version that was loaded. A preload load’s is taken when the run resolves its skills, so it is the version of the body in run_start.system_prompt. |
base_dir | string | The skill’s directory, which its relative file references resolve against. |
via | string | model (a skills.load call), preload (at run start), operator (a /name token at the start of the prompt, or rupu session send --skill, put its body in the turn’s message; written at run start), reattach (put back after a compaction) or continuation (carried over from the run this one continues). |
{"type":"skill_index","data":{"skills":[{"name":"pdf","layer":"global","stamp":"9f2c…","preload":false,"indexed":true,"reason":"agent"}]}}
{"type":"skill_load","data":{"name":"pdf","layer":"global","content_hash":"4b1e…","base_dir":"/home/me/.rupu/skills/pdf","via":"model"}}
message and injected
A run with a message space records what it sent and received. message is one message, sent or received: sent is written right after the messaging tool’s tool_result; received is written when a collector delivers it, right before the model call that sees it, one event per message, ahead of the injected event that carries it.
| Field | Type | Meaning |
|---|---|---|
id | string | The message’s id in the space’s log.jsonl (msg_<ULID>). |
direction | sent | received | |
from | string | The sender’s address (lynx#1, lead, operator). |
from_codename | string, opt | The sender’s full codename. |
to | string, opt | The address it was sent to, as recorded (role:recon, lynx#2, broadcast); absent for an unaddressed post. |
kind | string | post, direct, broadcast, directive, retract or operator. |
body | string | The text. |
urgent | bool | Absent unless true (operator messages). |
via | tool | injected | The run called a messaging tool, or a collector delivered it. |
delivery | object, opt | On sent: {resolved: [address…], undelivered: [{to, reason}…]} as logged. |
injected is context a collector put in front of the model. Every once injection is written, at the point it joined the conversation, so replay re-inserts it. An every_turn injection is written only when its content changed since the turn before, so a standing directive is one line, not one per turn.
| Field | Type | Meaning |
|---|---|---|
source | string | Who it is from (mailbox:lynx#1, broadcast:lynx#1, directive:board, operator, roster, children). |
kind | string | message, directive, operator, observation or status. operator is framed as the operator’s instruction, everything else as data. |
cadence | once | every_turn | Kept in the conversation, or sent with each request and never kept. |
content | string | The collector’s text, before the framing the model sees (an urgent operator message starts URGENT: ). |
message_ids | string[] | The messages it delivers; absent when none. |
{"type":"message","data":{"id":"msg_01J…","direction":"sent","from":"lynx#1","to":"role:recon","kind":"direct","body":"port 22 is open","via":"tool","delivery":{"resolved":["otter#1"]}}}
{"type":"injected","data":{"source":"mailbox:otter#1","kind":"message","cadence":"once","content":"from lynx#1 at 2026-10-13T…: port 22 is open","message_ids":["msg_01J…"]}}
outcome and recovery
When a reply isn’t a normal one, or the provider returns an error, rupu classifies it and records what it did about it. The classes and the recovery ladder are explained in Recovery & reliability.
outcome has turn_idx and an outcome object (an OutcomeRecord; the same shape as run_complete.outcome):
| Field | Type | Meaning |
|---|---|---|
id | string | Unique in the run (oc_1, oc_2, …). Recovery events point back to it. |
class | string | pause_turn, max_tokens, context_window_exceeded, refusal, safety, malformed_tool_call, incomplete, empty_reply, unrecognized_stop, unreported_stop or provider_error. A string, so a class a newer writer adds still parses. |
severity | info | warning | error | |
title | string | One line, e.g. refused · cyber. |
detail | string, opt | Longer text: a refusal explanation, the tool call that was cut off, the provider’s error message. |
error_class | string, opt | For provider_error: rate_limited, overloaded, server, timeout, context_overflow, quota, not_found, policy, auth, permission, invalid_request, too_large or unrecognized. |
wire | object, opt | The provider’s own stop value or error body, kept verbatim. |
recovery is one recovery action taken for an outcome, written after its outcome:
| Field | Type | Meaning |
|---|---|---|
outcome_id | string | The outcome it answers. |
rung | integer | 0 in place, 1 fallback on the same provider, 2 fallback on another provider, 3 nothing left: the run fails. |
action | string | continued, retried, compacted, fell_back, served_by_fallback, skipped, asked, parked or failed. asked and parked are reserved for operator-decided recovery and are not written yet. Treat an action you don’t know as other. |
attempt / budget | integer, opt | Which rung-0 attempt this is, against the rung-0 budget for this outcome’s turn. |
provider / model | string, opt | The fallback, for fell_back, served_by_fallback and skipped. |
reason | string, opt | Why: the hint on failed, the build error on skipped. |
merge_into_previous | bool, opt | The next turn’s assistant content joins the previous assistant message (a pause_turn continuation). |
continues_output | bool, opt | The next turn’s text continues this turn’s answer (a truncation continuation), so the final answer joins across the boundary. |
{"type":"outcome","data":{"turn_idx":5,"outcome":{"id":"oc_2","class":"refusal","severity":"error","title":"refused · cyber","wire":{"provider":"anthropic","value":"refusal"}}}}
{"type":"recovery","data":{"outcome_id":"oc_2","rung":1,"action":"fell_back","provider":"anthropic","model":"claude-opus-5"}}
notice
A runtime intervention worth showing that isn’t part of the conversation: a kind and a human-readable message. Expect new kinds; render an unknown one by its message.
| Kind | Meaning |
|---|---|
model_limits | The run’s input and output limits and where each came from, written at start and on a fallback hop. |
model_limits_clamped, context_trim, provider_retry, server_side_fallback_disabled | Context and provider interventions; see Recovery & reliability. |
permission_mode_degraded | Ask mode with no operator to ask. |
tool_unavailable | A tool named in tools: that this run can’t serve; one per tool, right after tool_grant. |
skill_not_indexed | A granted skill marked disable-model-invocation that isn’t preloaded, so the model can’t load it. |
skill_changed | A skill loaded at a newer version than the run’s skill_index recorded. |
skill_missing | A loaded skill no longer installed when it was due to be re-attached, or, at a session turn’s start, a skill the session froze that can’t be given now. |
skill_tools_missing | A loaded skill’s allowed-tools names tools the run wasn’t granted. |
skill_reattach_truncated | Loaded skills that didn’t fit under the re-attach cap after a compaction. |
messaging_full | The run’s message log reached [messaging].log_max_mb, so a send failed; written after that call’s tool_result. |
{"type":"notice","data":{"kind":"provider_retry","message":"overloaded; retrying in 2s (attempt 1/3)"}}
net_flow
One outbound network connection the run made: {"flow": <FlowRecord>}, with the host, port, scheme, method and query-stripped path, status and outcome, bytes, timing, and a fidelity that says how much of it was observed. Written as flows are recorded, so these lines can appear anywhere in the file. The record is described on the Netflow page.
gate_requested
Defined (gate_id, prompt, opt decision — approved or rejected —, opt decided_by) but not written by current rupu: workflow approval gates are recorded on the workflow run (run.json, events.jsonl), not in agent transcripts. Readers still parse and render it so older or hand-made files display.
Unknown events
A line whose type a reader doesn’t know is not corruption. rupu reads it as an unknown event that keeps the raw type and data, and writes it back unchanged, so a transcript from a newer rupu survives being read and copied by an older one. rupu itself never writes an unknown event. rupu transcript show prints it as unrecognized event · <type> and the control plane shows an “unrecognized event” block. Your own reader should do the same: skip or pass through, never fail.
What is corruption: a line that isn’t a JSON object, has no type, or has a type that isn’t a string.
Working with transcripts
| Command | What it does |
|---|---|
rupu transcript list | Project and global transcripts, newest first, with status and tokens. --limit N (default 50; every row beyond it costs a full read of its transcript), --archived or --all. |
rupu transcript show <run> | Render one transcript. <run> is a run id, a unique prefix of one, or a codename. --view focused|compact|full picks the density; --pager / --no-pager, --no-color. |
rupu transcript archive <run> | Move a standalone run’s transcript and its metadata into archive/. --ignore-liveness skips the still-running check. |
rupu transcript delete <run> --force | Delete a standalone transcript permanently (--force is required; --ignore-liveness is separate and also required for one that still looks live). |
rupu transcript prune --older-than 30d | Delete archived standalone transcripts older than the cutoff (30d, 12h, 1w); --dry-run previews. |
For your own tooling, the global --format jsonl on show prints the parsed events one per line in the schema above, and --format json wraps them in one transcript_show envelope. Or read the file directly:
# every bash command a run executed, with its exit code jq -c 'select(.type=="command_run") | [.data.exit_code, (.data.argv|join(" "))]' \ .rupu/transcripts/run_01JB7Q2M4X8T6V0K3N5R9C1D2E.jsonl # total spend per model across a run, cache reads included jq -s 'map(select(.type=="usage").data) | group_by(.model) | map({model: .[0].model, input: (map(.input_tokens)|add), output: (map(.output_tokens)|add)})' \ .rupu/transcripts/run_01JB7Q2M4X8T6V0K3N5R9C1D2E.jsonl # tool calls that were denied or not granted, with the decision jq -c 'select(.type=="tool_audit" and .data.blocked) | [.data.tool, .data.decision]' .rupu/transcripts/*.jsonl
Replay
A version 2 transcript is enough to rebuild the exact conversation the provider saw, and rupu does exactly that when it continues a run: rupu run --continue <run-id>, rupu workflow resume continuing an interrupted step, and a session’s next turn. The rules, if you want to rebuild one yourself:
- Start from the
seed, if any. Asource_transcriptseed means “rebuild that transcript first”; itssha256must match what was rebuilt. - Add the
user_message. Per turn, foldthinking(itsrawblock),assistant_message,tool_callandassistant_blockevents, in file order, into one assistant message, skippingabandonedblocks; the turn’stool_results become the next user message. - Drop a turn with no
turn_end(it was in flight) and a turn whoseturn_endisdiscarded. Arecoverywithmerge_into_previousjoins the next turn’s reply onto the previous assistant message. - An
injectedevent withcadence: onceadds itscontent, framed as the runner framed it, as a user message at its position. Anevery_turnone was sent with that turn’s request only, so it adds nothing. - A
compactionreplaces everything so far with itsmessages.
A version 1 transcript rebuilds without reasoning blocks, since it never kept the provider’s payload. Deltas, usage, notice, outcome, tool_grant, tool_audit, skill_index, skill_load, message, net_flow and unknown events don’t affect the conversation. The skills a run loaded are folded from its skill_load events separately, so a continued run knows what to re-attach.