ENGitHubУстановить

Trace - журнал хода

Ядро пишет одну строку JSONL на событие: от апдейта, который принял Bridge, до ответа, который доставил Outbox. Читателей двое - вьюер плагина trace и iva trace, - поэтому контракт описан здесь, а не в коде (ADR-0010). Термины - по CONTEXT.md.

Файл: data/trace/YYYY-MM-DD.jsonl в каталоге данных (ASSISTANT_DATA_DIR, по умолчанию ./data). Пишут два процесса: агент и мост. Append-only, одна строка на запись.

Схема события

Полей ровно семь, порядок всегда такой:

ПолеЧто это
tsISO-8601 UTC, момент записи
turnключ хода - три случая, см. ниже
sessionсессия eve (пустая, пока ход не начался)
sourcetelegram, bridge, web, http, rollup, watch, brief, insight, cron, unknown
kindгруппа: bridge, inbound, gate, context, turn, eve, outbox, stop
nameконкретное событие внутри группы
dataобъект: имена, тайминги, размеры, содержимое

source равен unknown, когда событие eve пришло без вида канала. В журналах 0.4.11 и раньше встречается и digest. ts записан в UTC, а файл дня назван по часовому поясу установки (ASSISTANT_TIMEZONE): около полуночи первые строки файла несут метку предыдущих UTC-суток. Так и задумано - журнал режет дни так же, как их режет vault.

Строка не длиннее 16 КБ в байтах UTF-8. Событие, которое не влезло, пересобирается со всеми полями содержимого по 2000 знаков; только если не влезло и так, оно теряет содержимое (data.traceTrimmed: true). Имена, тайминги и размеры остаются всегда.

Три пространства ключа turn

  1. До появления хода - ключ апдейта tg:<chatId>:<messageId>. Его умеют посчитать и мост, и ядро; update_id ядру не виден.
  2. После старта хода - turnId eve (turn_0, turn_1, …). Шаги субагента идут с суффиксом: turn_3#custom-node, тот же ключ, что в data/usage.jsonl.
  3. У ночного хода ключа хода нет вовсе. Свёртка и прочие cron-отправки идут через клиент eve, а он отдаёт только id сессии, поэтому их строки gate.outbound и outbox.* несут turn: "", непустую session и source из {rollup, cron}. Части Watch, Brief и Insight шлёт сам тик проактивности: их строки несут turn: "", без session, и source watch, brief или insight. События eve того же ночного хода при этом несут turn_N - их пишет хук внутри агента.

Как читатель собирает один ход

Имя одного хода. <session>/<turn>, например wrun_01M…/turn_0, - это то, что печатает и принимает iva trace show, что даёт список сбоев Insight и что берёт iva diagnose --turn. Голый turn_N называет новейший ход с этим номером.

Колбэки (кнопки /menu, прежняя iva_cancel) получают ключ tg:<chatId>:cb:<callbackId>, и делает его только мост: до runTelegramInbound колбэк не доходит, поэтому такие строки bridge.* остаются сиротами - ни turn.bound, ни inbound.* у них не будет. Нативный стоп черновика (stopped_message_generation) получает ключ tg:<chatId>:stop:<update_id> на входе; stop.* хранит сверенную пару сессии и хода.

Каталог событий

kind.namedata
bridge.admitted / bridge.droppedupdateId, chatId, messageId, kind (message/callback), decision (owned/terminal-drop/unownable/write-failed)
bridge.queuedupdateId, chatId, messageId, kind, count (сколько теперь ждёт в очереди чата), parts; время события и есть время постановки
bridge.delivered / bridge.rejectedте же поля плюс accepted (true/false/"handled"), ms, parts (сколько сообщений в доставке: пачка из очереди несёт несколько)
inbound.receivedchatId, chatType, messageId, userId, allowlisted; содержимое: text
inbound.accepted / inbound.droppedchatId, chatKey, parts, partChars[]; содержимое: context[]
gate.inbound / gate.websurface, blocked, reason, flags[], truncatedChars, chars
turn.boundchatKey, updateKey
context.partscore, persona, moc, daily - размеры файлов памяти в байтах, unit, approximate
eve.turn.started / turn.completed / turn.cancelledsequence
eve.turn.failed / eve.step.failedsequence, stepIndex, code; содержимое: 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}]; содержимое: args[] позиция в позицию с actions
eve.action.resultsequence, stepIndex, status, callId, toolName, name? (скилл load_skill), subagentName?, isError, errorCode?, exitCode?, failure?, outChars?; содержимое: result (одна строка JSON), error (только если отличается от result)
eve.message.completedsequence, stepIndex, finishReason; содержимое: message
eve.message.receivedsequence, parts; содержимое: message
eve.reasoning.completedsequence, stepIndex; содержимое: reasoning
eve.subagent.started / subagent.completed / subagent.calledcallId, subagentName, name, childSessionId; содержимое: output
события eve внутри субагентате же поля плюс subagent, parentCallId
gate.outboundclean, findings[] (тип:имя, без превью секрета), chars; содержимое: text - уже после редактуры
outbox.delivered / outbox.failedok, delivered, fellBack, error, chars, ms
stop.requested / stop.idle / stop.failedchatKey, outcome

У любого события eve в data может оказаться sessionId (если он есть в payload), а в содержимом - input (и inputChars): хук копирует оба по имени.

failure помечает упавший вызов инструмента классом, никогда не текстом: isError (eve или инструмент пометил ответ ошибкой), status:failed (ответ назвал свои code и message; код лежит в errorCode), ok:false, error, timeout, exit <код> (bash, только с непустым stderr). rejected и отменённый вызов - не сбой. Сбой, спрятанный конвейером или 2>&1, failure не получает. Строковый ответ, в котором лежит JSON-объект, читается как этот объект, как его читают eve и сторож повторов. Строки, записанные до этой правки, несут result объектом и без failure.

gate.outbound бывает без парного outbox.*. Служебные реплики канала (статус «Работаю…», объяснение сбоя, пометка про медиа) проходят тот же outbound-гейт, но мимо шва Outbox, и дают строку гейта в одиночку. Не жди события доставки после каждого вердикта.

outbox.delivered бывает с fellBack: true и error. Ответ дошёл до чата, но не в том виде, что просил текст: кусок ушёл без разметки или Telegram отверг rich-сообщение, и ответ ушёл HTML-путём без кнопок. Во втором случае error такой: buttons dropped: sendRichMessage 400: <причина от Telegram>.

Пометки писателя в data: traceTrimmed - событие поехало без содержимого; traceUnreadable - payload не сериализуется. Пометки в значениях: …[truncated] - строка или список обрезаны, …[deep] - вложенность глубже четырёх уровней, …[keys] - сколько полей было у обрезанного объекта, …[unreadable] - поле не прочиталось. Результат bash, который влезает в поле, пишется целиком; который не влезает, хранит конец stdout и последние 1000 знаков stderr: …[truncated] в начале поля значит, что срезано начало. Остальные ответы ставят ok, error, message, isError, code и exitCode первыми, поэтому обрезка с конца их не теряет. outChars - размер ответа, который получила модель. Событие, пересобранное по 2000 знаков (ниже), режет каждое поле с конца, и результат bash тоже.

Чего в журнале нет

Содержимое: тумблер и обрезка

data/settings.json, поле captureContent (по умолчанию включено):

{ "captureContent": false }

Выключенный тумблер оставляет имена, тайминги и размеры - ход виден целиком, только без текста. Каждое поле содержимого обрезано 4096 знаками с пометкой …[truncated].

Размеры пишутся только для строковых полей содержимого, как <ключ>Chars (text → textChars). У массивов - args[], context[] - размера нет: их количество уже лежит в data как actions[] и parts.

Хранение

30 дней: сегодняшний файл и 29 предыдущих. Чистка идёт по дате в имени файла, никогда по mtime (ADR-0002: время файла врёт после копирования и восстановления), и запускается на первой записи журнала в процессе и потом на каждой смене файла дня. Файлы с именами не по шаблону YYYY-MM-DD.jsonl не трогаются.

Что гарантировано читателю

Исторические события Trace вроде turn_3#planner сохраняют прежний смысл.

Исходный Markdown ↗