JSONL Core — Conversation Parsing Infrastructure
Architecture
All parsing logic lives in ${CLAUDE_PLUGIN_ROOT}/scripts/echolib/ — a Python package (stdlib only, Python 3.6+). User-facing tools are sd-recall.py (search/list/stats), index-builder.py (FTS index), topic-segmenter.py (segmentation), trend-engine.py (longitudinal aggregation), skill-gap-finder.py (pain-point mining), format-detector.py (unknown agent format detection), topic_classify.py (topic routing), chat-profiles.py (group chat participant extraction), and remember.py (auto memory generation).
Data Locations
- Session index (fast path):
~/.claude/projects/<encoded-path>/sessions-index.json - Fallback index (built by session-digger):
~/.claude/projects/<encoded-path>/.session-digger-index.json - SQLite unified index:
~/.claude/.session-digger/index.db— auto-maintained byindex-builder.py, stores session stats, tool usage JSON, timestamps for all environments - Full conversations:
~/.claude/projects/<encoded-path>/<uuid>.jsonl - Subagent conversations:
~/.claude/projects/<encoded-path>/<uuid>/subagents/agent-<id>.jsonl - Global prompt history:
~/.claude/history.jsonl
Important: Only ~10% of projects have sessions-index.json. The scripts automatically build a fallback index from raw .jsonl files for the remaining 90%, cached in .session-digger-index.json.
Strategy: Fast Path First
Always start with the index before opening any .jsonl file:
python3 ${CLAUDE_PLUGIN_ROOT}/scripts/sd-recall.py sessions --scope current --limit 20
python3 ${CLAUDE_PLUGIN_ROOT}/scripts/sd-recall.py search "search term" --limit 10
python3 ${CLAUDE_PLUGIN_ROOT}/scripts/sd-recall.py sessions --scope path --limit 20
Output is tab-separated: SESSION_ID CREATED MODIFIED MSG_COUNT BRANCH SUMMARY FIRST_PROMPT PROJECT_PATH FULL_PATH
The FULL_PATH field (9th column) is the absolute path to the .jsonl file. Use this to pass to other scripts.
Only open the full .jsonl when you need message-level detail.
Primary Tools
All parsing lives in ${CLAUDE_PLUGIN_ROOT}/scripts/echolib/. Use these front-ends:
# Build fast FTS search index (one-time, ~1-2s)
python3 ${CLAUDE_PLUGIN_ROOT}/scripts/index-builder.py build
# Search across all sessions (FTS, <50ms with index)
python3 ${CLAUDE_PLUGIN_ROOT}/scripts/sd-recall.py search "keyword" --limit 10
# List sessions
python3 ${CLAUDE_PLUGIN_ROOT}/scripts/sd-recall.py sessions --scope all --limit 20
# Aggregate statistics
python3 ${CLAUDE_PLUGIN_ROOT}/scripts/sd-recall.py stats
# Topic segmentation per session
python3 ${CLAUDE_PLUGIN_ROOT}/scripts/topic-segmenter.py <file.jsonl>
# Pattern analysis (retries, errors, user corrections)
bash ${CLAUDE_PLUGIN_ROOT}/scripts/analyze-session.sh <file.jsonl>
Direct Python API
import echolib
for msg in echolib.extract_messages("file.jsonl", role="user", limit=20):
print(msg["text"])
for tool in echolib.extract_tools("file.jsonl", errors_only=True):
print(tool["name"], tool["status"])
stats = echolib.session_stats("file.jsonl")
Subagent Discovery
Sessions with subagent work have a <session-uuid>/subagents/ directory. Check for it:
ls "$(dirname <full_path>)/$(basename <full_path> .jsonl)/subagents/" 2>/dev/null
Subagent files follow the same JSONL format and can be parsed with the same scripts.
Performance Notes
- With FTS index: keyword search <50ms regardless of total sessions
index-builder.pyincremental update: only re-parses changed filesextract_messages(limit=N)enables early exit — near-instant for small Niter_records(skip_noise=True)avoidsjson.loadson noise lines- For files > 10MB:
json.loadsis the CPU bottleneck (63% of time), not I/O extract_files_changed()uses reverse-read for large filessession_stats()single-pass: counts errors, tokens, compactions together
When Grep Is Enough
For targeted searches when no FTS index is built yet:
# Find user messages containing a keyword
Grep pattern='"type":"user"' path="<file.jsonl>" output_mode="content"
# Find error results
Grep pattern='"is_error"\s*:\s*true'
# Find specific tool usage
Grep pattern='"name"\s*:\s*"ToolName"'
# Find decisions (AskUserQuestion usage)
Grep pattern='"name"\s*:\s*"AskUserQuestion"'
Record Type Quick Reference
See references/record-types.md for the complete schema. For WorkBuddy's tree-structured format, see references/workbuddy-record-types.md. The essential types:
| Type | What It Contains | When to Use |
|---|---|---|
user (string content) |
Human's actual request | Understanding intent, finding topics |
assistant (text blocks) |
Claude's responses and reasoning | Finding decisions, explanations |
assistant (tool_use blocks) |
Tool invocations | Understanding what actions were taken |
file-history-snapshot |
Files edited with version counts | Knowing which files were touched |
summary |
AI-generated session title | Quick identification (also in sessions-index.json) |
system (compact_boundary) |
Context compaction marker | Session was long enough to need compaction |
Finding the Right Session Directory
To map a project path to its Claude session directory:
- Take the absolute project path (e.g.,
/Users/joker/github/myproject) - Replace all
/with-→Users-joker-github-myproject - Prepend
-→-Users-joker-github-myproject - Look in
~/.claude/projects/-Users-joker-github-myproject/
Use sd-recall.py sessions which handles lookup automatically:
Noise Filtering
When reading raw .jsonl, skip these:
- Records where
typeisprogressorqueue-operation(streaming/internal bookkeeping) - User records with
isMeta: true(slash command injection) - User records with
isCompactSummary: true(auto-generated context, not human input) - Assistant records with
model: "<synthetic>"(passthrough, not real inference) - User records where
contentis an array oftool_resultblocks (tool outputs, not human messages)
This is handled automatically by echolib/ functions (skip_noise=True by default).
Schema Evolution Awareness
Claude Code evolves rapidly. The JSONL format has changed across versions:
- New record types appear silently (e.g.,
progressat v2.1.14,pr-linklater) - New optional fields are added to existing records (~3-5 per minor version)
- Some record types (
summary,pr-link,file-history-snapshot) lack common fields likeversionoruuid - The directory encoding is lossy for Unicode paths — use
sessions-index.json'soriginalPathfield as ground truth
When parsing results look unexpected, use echolib.detect_schema("file.jsonl") or the schema-scout agent to check for unknown record types.
Multi-Agent Support
Session-digger supports parsing sessions from multiple agents. Use detect_agent_type() to identify the agent:
python3 -c "from echolib import detect_agent_type; print(detect_agent_type('/path/to/session'))"
Grok Build Session Format
Grok stores sessions under ~/.grok/sessions/<url-encoded-cwd>/<session-id>/ with a different file layout:
$GROK_HOME/sessions/<encoded-cwd-or-slug>/<session-id>/ # GROK_HOME defaults to ~/.grok
summary.json # metadata: title, timestamps, model, message count
chat_history.jsonl # raw messages sent to the model
updates.jsonl # authoritative ACP session/update stream (resume source)
events.jsonl # internal event stream (tool_started/completed, turns)
signals.json # pre-aggregated stats (toolFailureCount, toolCallCount, …)
plan.json # in-session TODO state (when present)
rewind_points.jsonl # file snapshots for /rewind
compaction_checkpoints/ # auto-compact saved state
subagents/ # child session metadata; child transcripts live in sessions tree
.cwd # (group dir only) original cwd when path encoding is rewritten
Key differences from Claude Code:
- Data root: honour
GROK_HOME(official CLI), not only~/.grok. - Tool calls in chat_history.jsonl: embedded
tool_callson assistant messages +tool_resultrows. Usegrok_extract_tools()(events.jsonl supplements timestamps/outcome). - Stats source of truth:
signals.jsonfor activity counters (tools/messages/errors);updates.jsonl→params.update.usagefor billable tokens (inputTokens/outputTokens/cachedReadTokens). Usage snapshots are run-cumulative; a drop inmodelCallsstarts a new run — digger sums last-of-each-run. Per-model legs fromusage.modelUsageuse the same rule and land instats["model_usage"](sum of models == session totals). Do not usesignals.contextTokensUsedas billable input (that is window occupancy). - 主会话 / 子代理分账:
grok_family_usage_report(session_dir)— 读subagents/*/meta.json;自动判定rollup/separate;轻量 token profile + updates 单次流式聚合 + mtime 缓存。跨会话汇总grok_aggregate_model_usage(mode="family"):有子代理的父会话只计家族一次(rollup 用父、separate 用父+子),子会话不双计;mode="raw"才允许调试双计。 - No file-history-snapshot: use
rewind_points.jsonlfor file change tracking. - FTS5 search:
session_search.sqliteunder the sessions root. Usegrok_list_sessions()with keyword. - Outcome field: tool_completed uses
outcome: "success"|"error"(also accept"failure"). - No gitBranch field: Grok sessions don't track git branch info.
Grok adapter functions in echolib/:
detect_agent_type(path)— returns "claude", "grok", "both", or "unknown"grok_list_sessions(cwd, limit, keyword)— list/search Grok sessionsgrok_session_stats(session_dir)— read pre-aggregated statsgrok_extract_messages(session_dir, role, limit)— extract from chat_history.jsonlgrok_extract_tools(session_dir, tool_filter, errors_only, limit)— extract tool calls from chat_history.jsonlgrok_session_path(cwd, session_id)— find a Grok session directory
ZCode Session Format
- Agents root:
~/.zcode/cli/agents/sess_*/agent_*/transcript.jsonl - Official billable source:
~/.zcode/cli/db/db.sqlitetablemodel_usage(one row per model call:model_id, input/output/cache/reasoning tokens). - digger prefers SQL
SUM+GROUP BY model_id→stats["model_usage"]; falls back to summing transcriptmodel_complete.usage. - Subagents use distinct
session_id(sess_subagent_agent_*, alsometadata.childSessionId); no parent rollup double-count in this table. - Global model totals:
zcode_aggregate_model_usage(). - Cache hit rate (Grok + ZCode):
cache_hit_rate = cache_read_tokens / input_tokenson session stats and eachmodel_usageleg (Noneif no input). Helper:compute_cache_hit_rate/attach_cache_hit_rates.
Kimi Code Session Format
Kimi Code stores sessions under ~/.kimi/sessions/<project-hash>/<session-uuid>/:
~/.kimi/sessions/<project-hash>/<session-uuid>/
wire.jsonl # raw event stream (TurnBegin, ToolCall, ToolResult, ContentPart, ...)
context.jsonl # context management records (_checkpoint, compaction boundaries)
metadata.json # session_id, title, wire_mtime
state.json # optional: model, agent state
context_sub_N.jsonl # sub-context files from compaction
subagents/ # child session directories
Key differences from Claude Code:
- Top-level type field: Kimi stores message type in
message.type(e.g.TurnBegin,ToolCall,ContentPart), not in the record-leveltype. - User input in TurnBegin: User messages are nested in
TurnBegin.payload.user_input[].text. Usekimi_extract_messages(). - Tool calls by ID: ToolCall and ToolResult are matched by
tool_call_id(exact match, not proximity). Usekimi_extract_tools(). - wire_mtime for timestamps: Session timestamp comes from
metadata.json'swire_mtimefield (Unix float), not from individual message timestamps. - No is_error field: Kimi's ToolResult doesn't expose error status. Tool failures are not distinguishable from successes in the wire format.
- No gitBranch field: Kimi sessions don't track git branch info.
Kimi adapter functions in echolib/:
kimi_list_sessions(cwd, limit, keyword)— list/search Kimi sessionskimi_session_stats(session_dir)— count messages, tools from wire.jsonlkimi_extract_messages(session_dir, role, limit)— extract from wire.jsonlkimi_extract_tools(session_dir, tool_filter, errors_only, limit)— extract tool calls from wire.jsonlkimi_session_path(cwd, session_id)— find a Kimi session directory
Cross-Tool Unified Interface
For analysis across all agents, use the unified functions:
cross_tool_list_sessions(limit, keyword, agent_filter)— returns merged list of dicts withagent,session_id,created,summary,first_prompt,msg_count,full_pathcross_tool_session_stats(session_path)— auto-detects agent type and dispatchesscan_all_environments_parallel()— probes all known agent directories in parallel, returns status per environmentdispatch_resolve_agent(path)— maps a path to its agent type (claude, grok, kimi_code, codex, workbuddy, trae_cn, zcode, dim, reasonix, universal)
Use sd-recall.py:
# List across all agents
python3 ${CLAUDE_PLUGIN_ROOT}/scripts/sd-recall.py sessions --scope all --agent cross --limit 20
# List only Kimi sessions
python3 ${CLAUDE_PLUGIN_ROOT}/scripts/sd-recall.py sessions --scope all --agent kimi --limit 20
下游协作
| 触发条件 | 推荐 |
|---|---|
| 解析完会话数据,需要提炼经验 | experience-synthesis |
| 需要管理/归档记忆文件 | memory-management |
| 需要结合 git 历史交叉分析 | git-mining |
DO NOT
- 从零创建新 skill →
skill-creator - 需要提炼经验/教训 →
experience-synthesis(jsonl-core 只做解析,不做分析) - 需要管理 MEMORY.md →
memory-management