RUGitHubInstall

Trace — the turn journal

The core writes one JSONL line per event: from the update the Bridge accepts to the answer Outbox delivers. Two readers consume it — the trace plugin viewer and iva trace — so this file, not the code, is the contract (ADR-0010). Terms follow CONTEXT.md.

File: data/trace/YYYY-MM-DD.jsonl under the data directory (ASSISTANT_DATA_DIR, ./data by default). Two processes append to it: the agent and the Bridge. Append-only, one line per write.

Event schema

Exactly seven fields, always in this order:

FieldMeaning
tsISO-8601 UTC, the moment of writing
turnturn key — three cases, see below
sessionEve session id (empty until the turn starts)
sourcetelegram, bridge, web, http, rollup, watch, brief, insight, cron, unknown
kindgroup: bridge, inbound, gate, context, turn, eve, outbox, stop
namethe specific event inside the group
dataobject: names, timings, sizes, content

source is unknown when an Eve event arrives without a channel kind. Journals of 0.4.11 and earlier may also carry digest. Note that ts is UTC while the day file is named after the installation timezone (ASSISTANT_TIMEZONE): near midnight the first lines of a file can carry a UTC timestamp that belongs to the previous UTC day. That is deliberate — the journal splits days the way the vault does.

A line is never longer than 16 KB in UTF-8 bytes. An event that does not fit is rebuilt with every content field at 2000 characters; only if that still does not fit does it lose its content (data.traceTrimmed: true). Names, timings and sizes always survive.

The three key spaces of turn

  1. Before the turn exists — the update key tg:<chatId>:<messageId>. Both the Bridge and the core can compute it; update_id is invisible to the core.
  2. After the turn starts — the Eve turnId (turn_0, turn_1, …). Subagent steps carry a suffix: turn_3#custom-node, the same key data/usage.jsonl uses.
  3. Night turns have no turn key at all. Rollup and other cron deliveries go through the Eve client, which exposes only a session id, so their gate.outbound and outbox.* lines carry turn: "" with a non-empty session and source in {rollup, cron}. Watch, Brief and Insight parts are sent by the proactive tick itself: their lines carry turn: "", no session and source watch, brief or insight. The Eve events of that same night turn still carry turn_N from the hook, because the hook runs inside the agent.

How a reader stitches one turn

Naming one turn. <session>/<turn>, e.g. wrun_01M…/turn_0, is what iva trace show prints and accepts, what the Insight failure list gives and what iva diagnose --turn takes. A bare turn_N names the newest turn with that number.

Callback updates (/menu buttons) get the key tg:<chatId>:cb:<callbackId>, and only the Bridge produces it: callbacks never reach runTelegramInbound, so those bridge.* lines stay orphans with no turn.bound and no inbound.*. Native draft stops (stopped_message_generation) use tg:<chatId>:stop:<update_id> at intake; stop.* records the verified session and turn pair.

Event catalogue

kind.namedata
bridge.admitted / bridge.droppedupdateId, chatId, messageId, kind (message/callback), decision (owned/terminal-drop/unownable/write-failed)
bridge.queuedupdateId, chatId, messageId, kind, count (now waiting in the chat queue), parts; the event time is the queueing time
bridge.delivered / bridge.rejectedthe same fields plus accepted (true/false/"handled"), ms, parts (messages in the delivery: a queue batch carries several)
inbound.receivedchatId, chatType, messageId, userId, allowlisted; content: text
inbound.accepted / inbound.droppedchatId, chatKey, parts, partChars[]; content: context[]
gate.inbound / gate.websurface, blocked, reason, flags[], truncatedChars, chars
turn.boundchatKey, updateKey
turn.retiredreplayMs
context.partscore, persona, moc, daily — memory file sizes in bytes, unit, approximate
eve.turn.started / turn.completed / turn.cancelledsequence
eve.turn.failed / eve.step.failedsequence, stepIndex, code; content: message, details
eve.step.startedsequence, stepIndex
eve.step.completedsequence, stepIndex, finishReason, usage {in,out,cacheRead,cacheWrite,costUsd?}
eve.actions.requestedsequence, stepIndex, actions[{kind,callId,toolName|name}]; content: args[], index-aligned with actions
eve.action.resultsequence, stepIndex, status, callId, toolName, name? (the skill of load_skill), subagentName?, isError, errorCode?, exitCode?, failure?, outChars?; content: result (one JSON string), error (only when it differs from result)
eve.message.completedsequence, stepIndex, finishReason; content: message
eve.message.receivedsequence, parts; content: message
eve.reasoning.completedsequence, stepIndex; content: reasoning
eve.subagent.started / subagent.completed / subagent.calledcallId, subagentName, name, childSessionId; content: output
Eve events inside a subagentthe same fields plus subagent, parentCallId
gate.outboundclean, findings[] (type:name, no secret preview), chars; content: text — already after redaction
outbox.delivered / outbox.failedok, delivered, fellBack, error, chars, ms
stop.requested / stop.idle / stop.failedchatKey, outcome

Any Eve event may also carry sessionId in data (whenever its payload has one) and input in content (with inputChars); the hook copies both by name.

failure marks a failed tool call with its class, never with text: isError (eve or the tool flagged the answer as an error), status:failed (the answer named its own code and message; errorCode holds the code), ok:false, error, timeout, exit <code> (bash, only with a non-empty stderr). rejected and a cancelled call are not failures. A failure hidden by a pipe or 2>&1 has no failure. A string answer that holds a JSON object is read as that object, as eve and the repeat guard read it. Lines written before this change carry result as an object and no failure.

gate.outbound can appear without a matching outbox.*. Every service reply the channel sends (a draft frame, an error explanation, a media notice) passes the same outbound gate but not the Outbox seam, so it produces a gate line alone. Do not wait for a delivery event after each gate verdict.

outbox.delivered can carry fellBack: true and an error. The reply reached the chat, but not in the form the text asked for: a chunk went out as plain text, or Telegram refused the rich message and the reply went by the HTML path without its buttons. In the second case error reads buttons dropped: sendRichMessage 400: <Telegram's reason>.

Writer markers in data: traceTrimmed — the event went without content; traceUnreadable — the payload could not be serialized. Markers inside values: …[truncated] — a string or list was cut, …[deep] — nesting deeper than four levels, …[keys] — how many fields the truncated object had, …[unreadable] — a field could not be read. A bash result that fits the field is written whole; one that does not keeps the end of stdout and the last 1000 characters of stderr: …[truncated] at the start of the field means the head was cut. Other answers put ok, error, message, isError, code and exitCode first, so a cut end keeps them. outChars is the size of the answer the model got. An event rebuilt at 2000 characters (below) cuts every field from the end, a bash result too.

What is not in the journal

Content: the toggle and the caps

data/settings.json, field captureContent (on by default):

{ "captureContent": false }

Turned off it keeps names, timings and sizes — the turn stays fully visible, only without text. Every content field is capped at 4096 characters with the …[truncated] marker.

Sizes are written for string content fields only, as <key>Chars (text → textChars). Array content — args[], context[] — gets no size; their counts already live in data as actions[] and parts.

Retention

30 days: today’s file and the 29 before it. Pruning goes by the date in the file name, never by mtime (ADR-0002: file time lies after a copy or a restore), and runs on the first journal write of a process and again whenever the day file changes. Files whose names do not match YYYY-MM-DD.jsonl are left alone.

Guarantees for a reader

Historical Trace entries such as turn_3#planner retain their original meaning.

View original Markdown ↗