How to Debug Session Issues Using the Kimi Code Transcript System
The Kimi Code transcript system serves as the single source of truth for session state, exposing APIs to inspect live stores, rebuild cold snapshots, and monitor operation streams to diagnose missing turns, journal gaps, and synchronization errors.
The transcript system in the MoonshotAI/kimi-code repository captures every interaction within a session—from turns, steps, and frames to attachments, todos, and metadata—providing immutable state management through a hierarchy of specialized stores and services. When sessions exhibit unexpected behavior such as missing turns, out-of-order operations, or stale agent data, developers can leverage the transcript APIs to trace exactly how the engine constructed the current state and where synchronization may have failed.
Understanding the Transcript Architecture
TranscriptStore (Per-Session Root)
Located in packages/transcript/src/store/transcriptStore.ts, the TranscriptStore acts as the root container for each session. It lazily creates an AgentTranscript for every participating agent and maintains the agent roster through methods like ensureAgent(), which guarantees the existence of an agent transcript, and agents(), which exposes roster changes and provides snapshots.
AgentTranscript (Per-Agent L1 Store)
Found in packages/transcript/src/store/agentTranscript.ts, the AgentTranscript holds the immutable AgentState and applies operations via apply(). It emits change events through onChange and provides read-only access through getItems(), getTurn(id), and snapshot(options). Crucially, the apply() method returns { accepted, gap }, where the gap property indicates missing sequence numbers that could signal truncated journals or missed appends.
TranscriptService (Server Layer)
Located in packages/kap-server/src/services/transcript/transcriptService.ts, the TranscriptService bridges the engine to persistent storage and manages three critical paths:
- Live path:
forSessionLive()creates or retrieves active stores;whenReady()waits for back-fill completion before the store is considered ready. - Cold path:
readColdSnapshot()rebuilds state from persistedwire.jsonlvia thesnapshotReaderinpackages/kap-server/src/snapshot/snapshotReader.ts;readColdRoster()restores agents fromstate.json. - Op handling:
onSessionOps()subscribes to mapped operation batches;getOpsSince()returns journal slices for catch-up;journalOps()maintains per-agent sequence watermarks;healEndedTurns()reconciles persisted turns after completion to recover missing frames.
Debugging Workflow
1. Determine Live vs. Cold State
First, check if the session is actively managed in memory or requires file-based reconstruction.
const store = transcriptService.forSessionLive(sessionId);
if (store) {
// Live session: operations are being journaled in real-time
} else {
// Cold session: must fall back to file-based snapshot
}
2. Inspect the Live Transcript
Access specific agent data to verify turn counts and recent activity using the AgentTranscript API.
const agent = store.ensureAgent(agentId);
console.log('Turn count:', agent.getItems().filter(i => i.kind === 'turn').length);
console.log('Specific turn:', agent.getTurn('t123'));
console.log('Snapshot (last 20 turns):', agent.snapshot({ tailTurns: 20 }));
3. Examine the Op-Batch Journal
Verify operation continuity using sequence watermarks to detect truncation.
const watermark = transcriptService.getSeqWatermark(sessionId, agentId);
const catchup = transcriptService.getOpsSince(sessionId, agentId, watermark - 10);
if (catchup?.complete) {
console.log('Recent ops:', catchup.batches);
} else {
console.warn('Journal truncated – request full refresh');
}
4. Subscribe to Live Operations
Monitor real-time operation streams to detect missing or unexpected sequences as they arrive.
const disposable = transcriptService.onSessionOps(sessionId, (event, seq) => {
console.log(`Seq ${seq} – ${event.agentId} ops:`, event.ops);
});
// Cleanup subscription when done investigating
disposable?.dispose();
5. Force a Cold Rebuild
Compare live state against persisted history to identify divergence or staleness.
const snapshot = await transcriptService.readColdSnapshot(sessionId, agentId);
if (snapshot) {
console.log('Cold turn count:', snapshot.items.filter(i => i.kind === 'turn').length);
}
6. Detect Journal Gaps
Identify missing append operations when targets are already closed, which prevents placement.
const result = agent.apply([]);
if (result.gap) {
console.warn('Append gap detected:', result.gap);
}
7. Heal Ended Turns
Trigger reconciliation for recently completed turns to recover tool frames that may have been missed during live projection.
// Replace 'ordinal' with the completed turn's ordinal number
await transcriptService['healEndedTurns'](sessionId, agentId, new Set([ordinal]));
8. Verify Agent Roster
Check persisted agent descriptors when UI pickers appear empty or missing expected participants.
const roster = await transcriptService.readColdRoster(sessionId);
console.log('Agent roster:', roster);
Common Debug Scenarios
Missing Recent Turns
Likely caused by journal capacity limits (TRANSCRIPT_OPS_JOURNAL_CAPACITY = 2000). Call getOpsSince() with a sinceSeq near the current watermark; if complete returns false, the batches have been evicted and you must request a full snapshot via readColdSnapshot().
Duplicate Turns
Indicates a race condition where back-fill occurred after a live turn.upsert. Compare the live AgentTranscript snapshot with the cold snapshot; the healTurnOps logic (lines 992-1040) ensures live state wins on overlapping fields while merging persisted data.
Tool Result Never Appears
Occurs when the projector missed the frame during a mid-turn attachment, persisting the result only to disk. Verify that journalOps recorded the batch, then run healEndedTurns() manually; the healTurnOps function (lines 731-791) will re-read persisted turns and re-emit missing tool frames.
Empty Roster Picker
The live store has not been seeded with the persisted roster. Ensure backfillMain() succeeded (it calls store.describeAgent() for each persisted agent at lines 329-342). If the roster remains empty, inspect the output of readColdRoster() to verify the underlying data exists.
Gap Reported on apply()
An append operation could not be placed because the target turn was already closed. Inspect the gap object returned from apply() (see AgentTranscript.apply at lines 61-68); the gap target indicates which specific turn was missing from the sequence.
Practical Code Examples
Retrieve the Last Five Turns from a Live Session
const store = transcriptService.forSessionLive(sessionId);
if (!store) throw new Error('Session not live');
const transcript = store.ensureAgent(agentId);
const recent = transcript.snapshot({ tailTurns: 5 });
console.log('Last 5 turns:', recent.items.filter(i => i.kind === 'turn'));
Detect Sequence Number Gaps in Real-Time
let expected = 1;
transcriptService.onSessionOps(sessionId, (event, seq) => {
if (seq !== expected) {
console.warn(`Gap detected: expected ${expected}, got ${seq}`);
}
expected = seq + 1;
});
Compare Live State Against Cold Rebuild
const live = transcriptService.forSessionLive(sessionId)!.ensureAgent(agentId);
const liveSnap = live.snapshot();
const coldSnap = await transcriptService.readColdSnapshot(sessionId, agentId);
if (coldSnap) {
console.log('Live vs cold turn counts:',
liveSnap.items.filter(i => i.kind === 'turn').length,
'vs',
coldSnap.items.filter(i => i.kind === 'turn').length);
}
Trigger Manual Post-Turn Healing
// Assuming turn ordinal 42 just completed
await (transcriptService as any).healEndedTurns(sessionId, agentId, new Set([42]));
console.log('Heal complete – missing frames should be recovered');
Retrieve Cold Session Roster
const roster = await transcriptService.readColdRoster(sessionId);
if (roster) {
roster.forEach(d => console.log(`${d.agentId} – ${d.type ?? 'sub'}`));
}
Summary
- The transcript system in MoonshotAI/kimi-code provides immutable state tracking through
TranscriptStore,AgentTranscript, andTranscriptService, with clear separation between live in-memory state and cold persisted storage. - Use
forSessionLive()to distinguish between active and cold sessions, falling back toreadColdSnapshot()frompackages/kap-server/src/snapshot/snapshotReader.tswhen the journal is truncated or the session is not resident. - Monitor operation integrity via
getOpsSince()andonSessionOps()to detect sequence gaps, and inspect thegapproperty returned byAgentTranscript.apply()to identify missing append targets. - Apply the
healEndedTurns()mechanism to recover tool frames and turns that may have been missed during live projections, particularly when projectors fail to attach mid-turn results. - Validate agent roster consistency using
readColdRoster()and ensurebackfillMain()completes successfully when UI components fail to display expected agents.
Frequently Asked Questions
What does the apply() method return when there's a gap in operations?
The apply() method in AgentTranscript returns an object containing accepted and gap properties. When a gap exists—typically because an append operation targets a turn that has already closed—the gap field contains metadata identifying the missing sequence target, allowing you to pinpoint exactly where the operation stream broke down.
How does the system handle missing tool results that occur during mid-turn attachments?
When projectors miss frames during active turn processing, the system persists these results to disk only, omitting them from the live transcript. To recover them, invoke healEndedTurns() after the turn completes; the healTurnOps logic (lines 731-791) reads the persisted wire.jsonl and re-emits the missing tool frames into the live transcript store.
Why would the op journal return incomplete when calling getOpsSince()?
The journal maintains a fixed capacity of 2000 operations defined by TRANSCRIPT_OPS_JOURNAL_CAPACITY. If the requested range exceeds this buffer, getOpsSince() returns complete: false, indicating that older batches have been evicted and you must request a full cold snapshot via readColdSnapshot() rather than relying on incremental catch-up.
What causes duplicate turns to appear in the transcript?
Duplicates typically result from race conditions where a back-fill operation executes after a live turn.upsert has already populated the store. The transcript service resolves these conflicts through healTurnOps (lines 992-1040), which ensures that live state takes precedence on overlapping fields while safely merging non-conflicting persisted data.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →