name: trace-clankie description: Use when tracing what Clankie said, did, or observed after the fact — operator console chat, Discord presence, play sessions, or service state — and you need to know which durable trail holds it and how to read it safely.
Trace Clankie
Every surface leaves a durable trail. Find the right one, read it (everything is append-only JSONL or plain files now), never write to it.
The trail map
| What you want | Where it lives | Shape |
|---|---|---|
| Operator console chat (the TUI dialogue) | ~/.clankie/captain/conversations/<conversationId>/ | meta.json (title, revision, session state) + append-only events.jsonl (message role/text, reasoning, tool, turn phases). Cursors are zero-padded line counts. ADR 0111 prunes this directory. |
| The seat's turns (Claude Code as Clankie) | ~/.claude/projects/<cwd-as-dashes>/<sessionId>.jsonl, folded into ~/.clankie/captain/conversations/global-default/events.jsonl | Claude Code's own session log is the source; while a herdr pane named clankie is up, the service projects its settled turns into the default global conversation as captain messages and tool events (ADR 0152), which is what the app's Clankie thread shows. Replay global-default over /operator/v1/dispatch; resume from resetCursor on cursor_expired. Channel events (<channel source="clankie">) are not operator messages there. |
| Per-turn tool-shape metrics | ~/.clankie/captain/turn-settled.jsonl, read with clankie metrics or GET /v1/captain/turn-metrics | One captain.turn.settled JSON object per settled operator/Discord turn: tool-name counts, first mutating tool, outcome, context tokens, the model/provider/effort that executed it, and reported totalTokens with the number of reports. Never args, outputs, or message text. Survives conversation prune. Absorbed steers do not write their own line. Presence captain.turn.settled in events.jsonl is a different payload (idle/waiting_user). |
| The pi session behind a conversation | ~/.clankie/captain/conversations/<conversationId>/pi/ | pi JSONL session trees; durable room sessions live the same way — voice under ~/.clankie/captain/voice/<sessionKey>/, Discord text under ~/.clankie/captain/rooms/<sessionKey>/ (ADR 0118). |
| What he heard/said per room | ~/.clankie/captain/lanes/<lane>~<encoded-target>.jsonl | One JSONL file per lane+target; observe_room and the TUI lanes view read the same files. |
| Tool calls he made in a room | ~/.clankie/captain/rooms/<sessionKey>/, ~/.clankie/captain/voice/<sessionKey>/, ~/.clankie/captain/turns/<lane>~<encoded-target>/*.jsonl | Social and trusted system lanes are durable under rooms/ or voice/; system-lane keys end in :authority:system (ADR 0133). turns/ holds one pi tree per actor-level privileged one-shot in a shared, untrusted room, with full toolCall/toolResult args and results. ADR 0107. |
| Presence + system events | ~/.clankie/events.jsonl (override: CLANKIE_EVENT_LOG) | One DomainEvent per line, full JSON. Heartbeats are not persisted; everything else is. Replayed at boot to rebuild presence. |
| Goal decision journal | ~/.clankie/captain/goal-journal/<encoded-conversation>.jsonl | One entry per real choice made while working a goal: at, goalCreatedAt, decision, why, optional evidence and autonomous. Append-only; survives goal clearing and conversation prune. get_goal returns only the current goal's tail (filtered by goalCreatedAt) — read the file for earlier goals. ADR 0132. |
| Durable memory | ~/.clankie/memory/discord-people/*.json, captain-episodes/*.jsonl | Approved person facts grouped by guild/user plus one global bounded episode ring stored across source-lane files. /memory status reads the same store through the operator-only API. |
| Play sessions (GBA) | ~/.local/state/clankie/gba-play/*.jsonl, .screenshots/<journal-stem>/*.png | V1/V2/V3 headers and V1/V2 turn lines. V3 adds the stable journey, environment, and venue that join sittings; V2 binds causal evidence. Selected turns and summaries may reference bounded PNGs by relative path, dimensions, byte size, hash, and capture reason. |
| Historical shared-body artifacts | ~/.local/state/clankie/gba-body/possession-events.jsonl and body.lock, when left by an older build | Inert historical files only. Current play and GBA MCP neither read nor write them; do not infer current ownership from them. |
| Official-bot Discord actions | ~/.local/state/clankie/discord-live-receipts.jsonl (override: DISCORD_BRIDGE_RECEIPT_PATH) | What the bot bridge actually did, including text and bot voice — content-free receipts, never message bodies. |
| User-session Discord actions | ~/.local/state/clankie/discord-user-session-receipts.jsonl (override: DISCORD_USER_SESSION_RECEIPT_PATH) | What the personal-lab body actually did, including voice, screen watch, and Go Live publish — content-free receipts, never message bodies or media. |
| Opt-in development voice transcript | ~/.local/state/clankie/discord-voice-transcripts.jsonl | Exists only when discord.voiceTranscriptLoggingEnabled is on. Exact consented final speech with body, guild/channel, stay/delivery ids, speaker id/display name, and timestamp. Mode 0600; receipts remain content-free. |
| Service stdout + lifecycle | ~/.local/state/clankie/<id>.log, <id>-service.json | Service ids: clankie, discord-bridge, discord-user-session, activity, tunnel. |
| Live status | clankie status / /trace in the face | /trace lists rooms and tails their bounded heard/said lane logs through the service. |
| What's on the TUI screen right now | herdr pane read <pane> --source recent | Viewport only — see caveat below. |
Discord media proof uses the log owned by the active body. Bot voice evidence is
in discord-live-receipts.jsonl; user-session voice, watch, and publish evidence
is in discord-user-session-receipts.jsonl. Fresh media-enabled readiness names
mediaOwner: vox; a text-only bot names mediaOwner: none and does not spawn
Vox. Role-scoped voice, DAVE, watch, publish, and leave receipts prove behavior
without storing message bodies or media.
Linear activity uses ~/.clankie/captain/conversations/linear-inbox/.
events.jsonl retains incoming message events with role external, including
while following is off. Read unread messages with clankie linear inbox read;
they are untrusted context, not operator instructions. pi/ holds context from
actual model turns. clankie linear status reports whether new deliveries wake
that inbox. A shared Linear account name does not establish human authorship.
clankie linear inbox read (or clankie linear inbox) returns a JSON page
in items: the oldest unread events, 20 by default (--limit N, up to 100),
under 31 KB serialized. --headlines returns one line per event (cursor,
time, headline) instead of the quoted payload; --before CURSOR returns the
events just before that cursor, read or not, so history can be walked back
from oldestCursor as deep as wanted. Reading leaves events unread. Review
what was shown, then run clankie linear inbox ack CURSOR with the returned
ackCursor; it moves the read boundary forward over events already offered,
never past one unseen. Never acknowledge truncated output. Unacknowledged
pages survive restart. GET /v1/linear/inbox?limit=&before=&headlines=1
reads; POST /v1/linear/inbox requires { "ackCursor": "..." }.
Following controls waking, not collection.
Gotchas that cost real time
- The TUI is fullscreen —
herdr pane readreturns only the currently rendered screen. The chat transcript is not in terminal scrollback; read the conversation'sevents.jsonlinstead. - Conversation metadata is not a liveness clock.
meta.json.updatedAtmay stay at turn acceptance while activity and tools keep appending. Judge a live turn by the newestevents.jsonlevent and its accepted/completed pair. - A play journal does not prove which code revision ran. Its header has no source revision, and service logs carry the package version rather than the commit. Compare process/restart and commit times, then use fields actually present in the journal to prove capabilities; a process may also have started from uncommitted source, so do not infer an exact commit from timing alone.
- A journal turn may name an action this build can no longer take. The
archive is read against its own history, not today's catalog (ADR 0160), so a
run from before an action was retired still parses —
load_checkpointin the 2026-08-11 runs, for one. The evaluator marks those turnsactionRetiredand counts them inaggregate.retiredActionTurns; their verdicts readunknown, which means the vocabulary is gone, never that the turn did nothing. - A screenshot reference is evidence only when its bytes match. Resolve its
relative
.screenshots/...path from the journal directory and verify bothbyteLengthandsha256; missing or mismatched bytes are a broken artifact, not permission to reconstruct a frame from a later state. - Presence phases are edge-triggered at the event level.
discord.presence.*andcaptain.presence.*phases persist until the owning process emits the next transition, so judge liveness by the age of the last event for that session id, never by the stored phase alone. The console keys presence rows by bot binding (a successor's first event retires its predecessor's row) and stamps each row· since <t>— a live phase with an old stamp is a dead process that never got a successor. - The agent roster only sees Herdr panes. Clankie leads coding agents
through the herdr CLI; there is no worker protocol reporting to the service.
Inside Herdr the console lists panes from
herdr pane listas[<agent> · herdr]rows; outside Herdr an empty roster only means "no visibility" — checkherdr pane listyourself. - A turn with no tree never answered. Pi holds a session file back until the
first assistant message, so a one-shot that timed out or failed before he
replied leaves nothing under
turns/. Absence is evidence; pair it with thediscord.text.ingressreceipt that has no matchingdiscord.text.reply. - A provider failure can resolve with no reply. For
captain_model_failedorcaptain_usage_limit_reached, read the terminal assistant'sstopReasonanderrorMessagein the matching Pi tree. Oldercaptain_response_missingreceipts can hide the same failure; do not infer an empty successful run from that code alone. - An
acceptedreceipt with no terminal one is a turn still running, not a lost one. The terminal receipt lands whenever the turn settles, which for a wedged turn is at the 3-minute deadline — outside any window you picked from the accepted timestamp. Widen the window before concluding a turn vanished. absorbedis notdeclined. A message folded into a live run reportsabsorbed(ADR 0118): he answered, the answer just rode the delivery that owned the run. Onlydeclinedmeans he read it and chose silence.- A restart does not clear Discord conversation context. The next ingress prompt can feed Clankie his own earlier replies from channel history, so a removed tool cue may still be copied after the new process starts. When exact wording survives a restart, inspect the turn's initial user message for that wording before concluding the running code is stale.
- Typed input can belong to the active voice room. A text-only message in
the voice channel's attached chat does not start a
discord_presencecaptain turn while that exact guild/channel has a live voice session. Finddiscord.voice.text_input, then join itsdeliveryIdtodiscord.voice.floor_decision,model_response,realtime_tool, andresponse. The receipt is content-free; exact text remains in Discord, and the opt-in voice transcript log stays speech-only. - Realtime voice tool calls are receipts only.
discord.voice.realtime_toolnames the tool (ask_clankie,look_at_screen,music_*) and its phase, never its arguments or result — the content fence applies. To see arguments, followask_clankieinto the captain: the durable channel tree under~/.clankie/captain/voice/, orturns/when that handoff was privileged. - A voice capability denial may never have reached the captain. Join the
room delivery to
model_response,realtime_tool, andresponse. A settled fast-path response with noask_clankiereceipt means the realtime mouth answered alone; verify the underlying host separately before blaming its permissions or availability. - Voice leaves a room transcript only when the owner enables the development
toggle.
get_self_state.voiceHistoryis closed stays only (join/leave), and it is empty while he is still in the channel.get_self_state.recentVoiceSpeechis the content-free projection: spoken vs suppressed, trigger, latency, tokens, stay id.observe_roomondiscord_voiceis only captainask_clankiehandoffs (heard/saidin~/.clankie/captain/lanes/discord_voice~…jsonland the matching~/.clankie/captain/voice/<sessionKey>/tree) — not the Discord conversation. Receipts stay content-free. Withdiscord.voiceTranscriptLoggingEnabledoff, exact ambient speech remains memory-only; with it on, read~/.local/state/clankie/discord-voice-transcripts.jsonl. The samedeliveryIdjoins the utterance, transcription outcome, floor decision, realtime response, and tool call/result; music continues undercallIdthrough queue,yt-dlp, FFmpeg, first-audio, and player checkpoints. These receipts contain ids, counts, phases, timings, and exit codes — never the transcript, search query, URL, model text, or PCM. Join a play turn to audio withspeechDeliveryIdon the GBA journal line and the samedeliveryIdon the submission / response / suppressed receipts. Human words persist only in the opt-in transcript log; otherwise they live in the bridge's in-memory window and live provider call. Fast-path model text and exact audible wording are not added to that log. A journalspeechDeliveryIdis only a join key: only a matching response, suppression, refusal, or settledmodel_responsereceipt proves the outcome. Absence ofdiscord.voice.responsedoes not mean the narration was lost — that receipt is emitted only when audio actually played, so a response that settled without speaking leavesmodel_responsephase: "completed"and nothing else. V2narrationEventis the bounded game event offered to the room, not generated voice wording; exact audible wording remains unknown by policy. - Words with no audio is the mute-mouth signature. A
model_responsephase: "completed"carryingtextCharacters > 0whosedeliveryIdnever reaches adiscord.voice.responseis a reply the room never heard: he wrote it, synthesis dropped it.discord.voice.failedwith stagespeech_synthesisnames the throw. Join bydeliveryId, never by adjacency — anask_clankieround trip spends onedeliveryIdon two responses (the "let me check" and the answer), so a naive join credits the answer with the filler's audio and hides exactly the turn worth looking at. - An ElevenLabs byte-limit failure can follow audible speech.
discord.voice.failedwith codeelevenlabs_context_audio_exceeded_the_byte_limitmeans synthesized PCM hit the per-utterance safety fence, not that Discord disconnected. Earlier audio still plays and leaves adiscord.voice.response; the room hears only a prefix. Join both records bydeliveryIdand check the code revision's cap. The external-voice adapter adds a conversation marker for this incomplete speech so the next response knows the suffix was not audible and that the exact cutoff is unknown. - A readiness probe that skipped paid synthesis can coexist with a mute mouth. Probe the TTS boundary directly when receipts show the mute-mouth signature; do not treat a READY-shaped credential check as audible speech.
- Vox process readiness is not Discord media readiness.
process_readymust carry the exactVOX_IPC_PROTOCOL_VERSIONbefore the client accepts any command, but it still proves only that the one child accepts IPC. Primaryready,connection_state,transport_state,dave_state, and transport error events must carry the caller's currentconnectionId. For voice, requiretransport_state=readyfor rolevoice, positive role-scopeddave_state=ready, then adiscord.voice.joinedreceipt withdaveProtocolVersion > 0. Fresh app readiness must also setmediaOwnertovox; otherwise the evidence may predate the sole-owner migration. - Buffered TTS is not audible TTS.
tts_playback_state=bufferedonly means PCM entered Vox's queue.startedis the first audible TTS-containing RTP frame successfully transmitted and starts floor occupancy.drainedfollowsfinish_tts_playbackonly after PCM, a held partial tail, and trailing output frames cross the sender. Join all three byplaybackId. - Watch and publish are separate proofs. A decoded watch still proves
stream_watch, notstream_publish. A qualifyingdiscord.stream.publish_startedproves Discord accepted OP18 and OP22, thestream_publishtransport and positive DAVE were ready, and Vox emitted the firststream_publish_media_startedH264 event for the current connection/source generations. Never accept a generic ready line. - A local leave event is not a completed leave. Qualifying
discord.voice.leftevidence recordsgatewayConfirmed: trueandmediaOwner: voxonly after the account gateway confirms that the body is detached from voice. A local session close without those fields is not the clean-leave proof. - A clean voice leave must not kill another role. In the user body, primary
voice, screen watch, and publish share one child but have separate leases. A
valid leave closes
voicewhile active watch/publish evidence continues; only body shutdown closes all roles and the child. If process inspection finds two Vox children or a Node voice media owner, the sole-owner proof fails. A text-only official-bot process is the intentional exception: it recordsmediaOwner: noneand spawns no Vox child. - A missing play summary is not automatically an incomplete mystery. Join
the journal header
runIdtoembodiment.session.stoppedorembodiment.session.failedin~/.clankie/events.jsonl. A matching terminal event accounts for the run with its real outcome (includinglease_lapsed) but never becomes a synthetic summary. - A hosted play has two session ids. The play journal header's
environmentSessionIdis the Clankie embodiment id used for lifecycle joins. The PokeAgents session id (ses_...) lives in each V2 turn'sevidence.*.provenance.sessionId; use that id to find the independent host journal under~/.pokeagent-mmo/world/players/*/games/*/journal/. - A journey is not a session. New V3 journal headers carry
journeyId; group those files to reconstruct Clankie's story across sittings.runId,environmentSessionId, hostedses_..., and checkpoint ids still name one execution or saved state. V1/V2 journals predate this join and must not be assigned a journey from timing alone. - There is no current GBA possession trail. Clankie's play host and every
GBA MCP process own separate emulator/runtime instances. Trace Clankie's play
through
gba-play/*.jsonlplus embodiment lifecycle events; trace an MCP harness through its own stdio results and configured checkpoint directory. Oldbody.lockandpossession-events.jsonlfiles are inert and intentionally neither migrated nor deleted. world_unreachableis usually a missing process, not a crash. The hosted world is a separatepokeagentsserver reached over a unix socket, so a refusal milliseconds afterembodiment.session.claimedmeans nothing was listening. Checkps aux | grep pokeagentsand compare its start time (ps -p <pid> -o lstart=) against the refusal — a join that lands before the server is up refuses, and the retry seconds later succeeds.refusedis notfailed: he never started, so there is no journal and nothing crashed.
Queries that answered real questions
Last N chat messages in a conversation:
tail -n 40 ~/.clankie/captain/conversations/<id>/events.jsonl | jq -c '{type, role, text}'
Per-turn survey vs implementation shape (survives conversation prune):
jq -c '{runId, lane, outcome, toolCount, firstMutatingTool, mutatingCount, surveyToolCountBeforeFirstMutation}' \
~/.clankie/captain/turn-settled.jsonl | tail -n 20
What ran a turn and what it cost in reported tokens. Prefer the read surfaces —
clankie metrics [--run ID] [--limit N] or GET /v1/captain/turn-metrics — which
answer the same rows newest first with execution and usage explicitly null
when unknown:
clankie metrics --limit 5 | jq -c '.items[] | {runId, outcome, execution, usage}'
execution is the model/provider/effort that actually executed the turn, not the
current configuration; it is null for turns settled before that capture existed.
usage.totalTokens is what the provider reported, summed over usage.reports
assistant messages, and is null when nothing was reported — never zero. Never read
contextTokensStart/contextTokensEnd as usage or as a charge: they are context
occupancy, and no cost is recorded anywhere.
Every tool he ran in a room, newest last (rooms/ and voice/ hold durable
social or trusted-system lanes; turns/ holds actor-level privileged
one-shots):
jq -c 'select(.type=="message" and .message.role=="assistant")
| {at: .timestamp, tools: [.message.content[] | select(.type=="toolCall") | .name]}
| select(.tools | length > 0)' \
~/.clankie/captain/rooms/*/*.jsonl ~/.clankie/captain/voice/*/*.jsonl \
~/.clankie/captain/turns/discord_presence~*/*.jsonl
Swap .name for the whole block to see arguments, and grep the same files for
"role":"toolResult" to see what came back.
What happened tonight, minus presence noise:
jq -c 'select(.type | startswith("captain.presence") | not) | {type, occurredAt}' ~/.clankie/events.jsonl | tail -n 60
Does durable memory contain anything, without printing its contents:
find ~/.clankie/memory -type f -maxdepth 2 -exec wc -l {} +
Is a presence session real or a ghost:
grep '<session-id-prefix>' ~/.clankie/events.jsonl | tail -n 5 | jq -c '{occurredAt, phase: .data.phase, reason: .data.reason}'
Did he speak this stay, and are play reports dropped:
jq -c 'select(.type == "discord.voice.response" or .type == "discord.voice.play_narration_suppressed" or .type == "discord.voice.left") | {type, at: .occurredAt, stayId: .data.stayId, deliveryId: .data.deliveryId, trigger: .data.trigger, reason: .data.reason, spoken: .data.spokenCount, suppressed: .data.narrationSuppressed, tokens: {in: .data.inputTokens, out: .data.outputTokens}}' ~/.local/state/clankie/discord-live-receipts.jsonl | tail -n 40
What exact consented speech did Discord transcribe in development:
tail -n 40 ~/.local/state/clankie/discord-voice-transcripts.jsonl | jq -c '{at: .occurredAt, body, guildId, channelId, stayId, deliveryId, speakerId, displayName, text}'
Where did one voice/music turn stop:
jq -c --arg id '<delivery-or-call-id>' 'select(.data.deliveryId == $id or .data.callId == $id) | {type, at: .occurredAt, data: .data}' ~/.local/state/clankie/discord-live-receipts.jsonl
Checkout-only live proofs (watch-live-proof, publish-live-proof,
gameplay:evaluate-journal, pnpm discord:voice-readiness) live in
verify-clankie, which is present only in a source checkout.
