@wibus_wee: Now, no matter what we want to build, we always have to first study the Session formats of N different Agent families a…
Summary
Huihua is a TypeScript open-source library that provides one consistent API for reading local session data produced by 20+ different Coding Agents (Codex, Claude, Cursor, Kimi, Qwen Code, Cline, etc.), normalizing evidence-preserving records and events so developers no longer need to parse each agent's session format separately.
View Cached Full Text
Cached at: 10/06/26, 01:40 AM
Now, no matter what we want to build, we always have to first study the Session formats of N different Agent families and parse them all over again. We might need a dedicated library just for reading sessions. This is: Huihua, preserving the original data and differences as much as possible, letting the upper layer decide how to use it. Thanks to the open-source community for providing a wealth of analysis and ideas for reference.
wibus-wee/huihua
Source: https://github.com/wibus-wee/huihua
汇话 · Huihua
One API for Coding Agent session data.
Huihua is a TypeScript library for reading local session data produced by different Coding Agents through one consistent API.
Supported Providers
Codex Claude Pi Cursor OpenCode OAR ACP Morph Kimi Grok Antigravity Copilot CLI Hermes OpenClaw Qwen Code Devin CLI fx Cline DeepSeek Harness Droid
Installation
npm install huihua
pnpm install huihua
bun install huihua
yarn add huihua
Read evidence, project what you need
import { jsonOf, sessions } from 'huihua'
import {
conversationOf,
eventsOf,
fileChangesOf,
subagentsOf,
toolCallsOf,
toolResultsOf,
} from 'huihua/observe'
const { refs } = await sessions.scan({ providers: ['codex'] })
if (refs[0]) {
const session = await sessions.read(refs[0])
const conversation = conversationOf(session)
const calls = toolCallsOf(session)
const results = toolResultsOf(session)
const tools = eventsOf(session, 'tool_call', 'tool_result')
const changes = fileChangesOf(session)
const agents = subagentsOf(session)
const json = jsonOf(session) // JSON.stringify(session) works as well
}
A session uses agent-session/v1: ordered records contain native evidence once; normalized
events refer to their record sequence.
Messages, reasoning, tools, commands, usage, system and
unknown records retain order.
Repeated IDs, mirrored events, native tool names and Pi branches
survive.
Unknown data is retained rather than dropped.
Missing facts remain absent.
Projections
are disposable views of events, with evidence associations intact.
| Selector | Selected events |
|---|---|
| eventsOf(session, …types) | One or more explicit event types, with a narrowed result type |
| conversationOf(session) | User and assistant messages |
| toolCallsOf(session) | Tool calls |
| toolResultsOf(session) | Tool results |
| fileChangesOf(session) | File changes |
| subagentsOf(session) | Subagent lifecycle events |
Selectors retain order, duplicates and original event objects; they do not merge messages or resolve relationships. An empty result means no matching normalized events were found, not that the source format lacks the capability. The architecture contract defines selection and evidence semantics.
Keep the full SessionRef: an ID alone can be ambiguous across stores. diagnostics explains partial
parses and unsupported records.
Raw text preserves JSON numeric spellings outside JavaScript’s
safe integer range; use it when exact native numeric lexemes matter.
Scan local sources
const { refs, failures } = await sessions.scan()
for (const failure of failures)
console.warn(failure.provider, failure.source?.path, failure.message)
Scanning preserves refs when another source or provider fails, including refs found before a database scan fails midway. Missing stores produce no refs or failures. Invalid requests and caller cancellation still throw. A discovered ref does not guarantee a later read will succeed.
for await (const event of sessions.scanStream()) {
if (event.type === 'ref')
console.log(event.ref)
else
console.warn(event.failure)
}
scanStream emits discoveries and failures as they become available; scan collects the same events and sorts its refs. Both deduplicate by provider and complete source, preserving equal IDs from different stores. See the scanning contract for failure scope, cancellation and third-party SPI migration.
Parse supplied data without discovery
| Entry point | Input | Output |
|---|---|---|
parse(provider, input) | Explicit file or acquired JSONL | Complete Session snapshot |
stream(provider, input) | Acquired JSONL | One consumption of incremental SessionFrame values |
open(ref) | Explicit file/store reference | Replayable handle with frames and snapshots |
import { sessions } from 'huihua'
// A known JSONL file: no detect(), scan(), directory traversal or header pre-read.
const fromFile = await sessions.parse('codex', { path: '/backups/rollout.jsonl' })
// Already acquired JSONL: string, Uint8Array or AsyncIterable<Uint8Array>.
const fromText = await sessions.parse('claude', {
jsonl: '{"type":"user","sessionId":"example","message":{"content":"hello"}}\n',
source: 'upload:example',
})
JSONL acquisition is supported by Codex, Claude, Pi, Cursor’s CLI adapter, OAR, ACP, Kimi, Grok, Morph, Copilot, OpenClaw, Qwen, Droid, DeepSeek and Hermes captures/exports. File reads and supplied data use the same provider mapping, raw evidence, ordering, diagnostics and record limits. The provider is explicit; content and file extensions are not used to guess it. The source label is provenance only and is never opened as a path. Without an explicit id, the first native identity wins; conflicting later identities are preserved and diagnosed. If none exists, a labeled source identity is returned. File inputs default to JSONL; compressed files require format: ‘jsonl_zstd’.
A database contains multiple sessions; specify the exact format and native selector:
const fromDatabase = await sessions.parse('cursor', {
path: '/backups/state.vscdb',
format: 'cursor_sqlite',
id: 'composer-id',
locator: { storage: 'modern' }, // Use 'legacy' for ItemTable-backed stores.
})
const fromOpenCode = await sessions.parse('opencode', {
path: '/backups/opencode.db',
format: 'opencode_sqlite',
id: 'session-id',
// locator: { table: 'session_v2' } selects that empirical schema; default is 'session'.
})
OpenCode’s historical session metadata files use format: ‘opencode_files’ and retain their message/part directory layout. OpenCode does not consume arbitrary JSONL exports; unsupported acquired input fails explicitly. parse() returns a full snapshot; a supplied byte stream is consumed once, incrementally, while the returned Session accumulates in memory. Use stream() to consume acquired JSONL without collecting that snapshot:
// bytes is a caller-supplied AsyncIterable<Uint8Array> from an already acquired object.
for await (const frame of sessions.stream('codex', {
jsonl: bytes,
source: 'stored-object:rollout',
})) {
if (frame.type === 'event') {
// Await your batch writes here; the parser follows consumer backpressure.
}
}
stream() also accepts text or Uint8Array and yields the same record/event/diagnostic/metadata frames as file-backed open(). Each returned sequence permits one consumer; concurrent consumption or replay throws TypeError. To replay, call stream() again with text/bytes or a fresh byte iterable. Early return closes the producer’s iterator; cancellation is checked between frames and chunks. Producers waiting on their own I/O must also handle the supplied AbortSignal. Prefixes remain provisional until successful completion, and early return does not validate an unread suffix. The direct acquisition contract defines lifecycle and capability boundaries. For incremental event consumption from a known file, construct a SessionRef and call open(); refs need not originate from scan().
Stream large transcripts
const opened = await sessions.open(refs[0]!)
console.log(opened.readMode) // 'incremental' or 'buffered', selected by this source's adapter.
for await (const frame of opened.stream()) {
if (frame.type === 'event') {
// Consume incrementally; record, diagnostic and metadata frames remain available too.
}
}
const snapshot = await opened.snapshot() // A fresh read, collected into a complete Session
opened.events() and opened.records() filter the same stream.
JSONL reads are incremental and
close on early return or AbortSignal cancellation. read/snapshot collects the full result;
SQL ordering can buffer a selected session’s rows.
readMode makes that buffering visible; it describes iteration, while snapshot always collects the full session.
A streamed prefix remains provisional until
completion, because an unread suffix may contain corruption.
Details and resource limits live in
the architecture contract.
Providers and custom roots
const { refs } = await sessions.scan({
providers: ['codex'],
roots: { codex: ['/backups/codex/sessions'] },
})
Explicit roots replace defaults and accept a supported file or directory.
Explicit homeDir
isolates discovery from the process environment.
Default discovery honors CODEX_HOME,
CLAUDE_CONFIG_DIR, absolute XDG_CONFIG_HOME/XDG_DATA_HOME and PI_CODING_AGENT_DIR.
Kimi, Grok, Antigravity and Morph honor the store roots listed in the coverage contract.
Copilot, Hermes, OpenClaw, Qwen, Devin, fx, Cline, DeepSeek and legacy Droid discover their native roots listed in the coverage contract.
OAR and ACP recordings require explicit input or roots.
| Provider | Implemented stores | Compatibility evidence |
|---|---|---|
Claude Code (claude) | Projects/sidechain JSONL, split and usage-only records | Research |
Codex (codex) | Active/archived JSONL and Zstandard rollouts, both record streams | Research |
Cursor (cursor) | IDE KV/bubbles, conversationMap, ItemTable indexes and CLI transcript JSONL | Research |
OpenCode (opencode) | SQLite message/part and session_message, empirical session_v2, historical filesystem JSON | Research |
Pi (pi) | Version 1/2/3 JSONL and complete session trees; future records remain visible | Research |
OAR (oar) | Voyage/3 recordings and RawEvent JSONL from any harness | Research |
ACP (acp) | Recorded stable v1/v2 session notifications and JSON-RPC JSONL | Research |
Kimi Code (kimi) | Per-agent wire.jsonl with native session metadata | Research |
Grok Build (grok) | Native updates.jsonl and summary.json; xAI extensions retained | Research |
Antigravity CLI (antigravity) | Partial observed SQLite/Protobuf steps; native bytes retained | Research |
Mister Morph (morph) | Topic discovery and ordered native task journal snapshots | Research |
GitHub Copilot CLI (copilot) | Flat/session-state events JSONL; mirrored calls and native usage | Research |
Hermes Agent (hermes) | Current state.db, historical JSON snapshots and JSONL captures/exports | Research |
OpenClaw (openclaw) | Current per-agent SQLite transcripts, compressed payloads and legacy JSONL | Research |
Qwen Code (qwen) | Project chat JSONL, native Google message parts and system records | Research |
Devin CLI (devin) | SQLite session metadata and all message-tree nodes with main-chain markers | Research |
fx (fx) | Manifest and checkpoint JSON snapshots; event tail not replayed | Research |
Cline CLI/Desktop (cline) | Version-1 manifests and adjacent messages JSON | Research |
DeepSeek Harness (deepseek) | v0–v4 immutable JSONL generations and Zstandard logs | Research |
Factory Droid (droid) | Legacy interactive JSONL and captured stream-json records | Research |
const voyage = await sessions.parse('oar', { path: '/backups/run.jsonl' })
const acp = await sessions.parse('acp', { path: '/backups/acp.jsonl' })
const agy = await sessions.parse('antigravity', {
path: '/backups/conversation.db',
format: 'antigravity_sqlite',
})
const morph = await sessions.parse('morph', {
path: '/backups/morph-state',
format: 'morph_journal',
id: 'topic-id',
})
ACP defines a protocol rather than a universal export container; client-specific archives need their own evidence.
Antigravity’s native adapter covers an observed CLI database subset; Google’s separate ACP-server/IDE storage is not certified.
Use oar or acp for recordings of that server.
Kimi reads extracted native directories, not ZIP or Markdown exports.
Morph preserves repeated task snapshots, and OAR preserves submitted requests alongside runtime echoes.
For the new structured stores, parse() requires an explicit format:
const hermes = await sessions.parse('hermes', {
path: '/backups/state.db',
format: 'hermes_sqlite',
id: 'session-id',
})
const devin = await sessions.parse('devin', {
path: '/backups/sessions.db',
format: 'devin_sqlite',
id: 'session-id',
})
const openclaw = await sessions.parse('openclaw', {
path: '/backups/openclaw-agent.sqlite',
format: 'openclaw_sqlite',
id: 'session-id',
})
const cline = await sessions.parse('cline', {
path: '/backups/session/session.json',
format: 'cline_json',
})
// Also supported: hermes_json and fx_json, using the exact snapshot/manifest path.
JSON snapshots are bounded complete-file reads and report buffered mode. Devin retains abandoned branches; filter on native on_main_chain metadata when you need the selected chain. fx reads checkpoint history and diagnoses its unconsumed event tail. Cline ignores exported external paths and reads only the adjacent messages file. DeepSeek selects the highest generation per session directory and retains surface changes without replaying migrations. OpenClaw cold archives and newer private schemas outside the documented tables remain unsupported.
Cursor CLI private store.db protobuf is not decoded. SQLite ingestion supports unencrypted rowid tables, overflow and committed WAL without source writes; virtual/generated-column/WITHOUT ROWID schemas fail explicitly. Active database changes and nonempty rollback journals can return PartialParse. Zstandard windows are capped at 32 MiB and dictionary frames are unsupported. These are fixture-backed boundaries, not a promise to decode every private future product format.
Compose your own registry
import { createSessionRegistry, defineProvider } from 'huihua'
import { codexProvider } from 'huihua/providers/codex'
const registry = createSessionRegistry([codexProvider])
const { refs } = await registry.scan()
const provider = registry.require('codex')
Third-party adapters use defineProvider({ id, detect, scan, read }); scan returns AsyncIterable<ScanEvent> with ref and failure events.
Optionally implement open for file streaming, parse for acquired snapshots and stream for acquired frames.
The registry knows no builtin identities; duplicate registration fails.
registry.require(id) resolves a registered provider handle or throws ProviderNotFound;
capability support is member presence on that handle, such as provider.open.
The public
SPI is thin.
Supported exports are the root, /observe,
/testing and /providers/{claude,codex,cursor,opencode,pi,oar,acp,kimi,grok,antigravity,morph,copilot,hermes,openclaw,qwen,devin,fx,cline,deepseek,droid}.
Internal paths are not package exports.
Development and releases
Use the pinned pnpm version and run pnpm check before submitting changes.
The quality and release automation contract describes CI, version tags and npm Trusted Publisher setup.
The opt-in pnpm bench command measures synthetic large JSONL reads; it is not part of pnpm check.
License
MIT © 2026 wibus-wee.
Similar Articles
I made it so any agent can agent can use any context form another agent. Claude learns from codex, visa versa.
Lore is a local tool that indexes AI agent sessions into a SQLite database and serves them over MCP, allowing different agents (e.g., Claude and Codex) to share and retrieve each other's context across sessions.
@geekbb: An open-source AI agent meta-orchestration framework that provides a unified orchestration layer for multiple agents including Claude Code, Codex, Cursor, OpenCode, Hermes, and Pi. Users can seamlessly switch sessions across devices (terminal, browser, mobile, desktop app) to enable multi-agent...
Omnigent is an open-source meta-orchestration framework that provides a unified orchestration layer over multiple AI agents like Claude Code, Codex, and Cursor. It enables seamless cross-device session switching, multi-agent collaboration, policy enforcement, and cloud sandbox execution.
@TiagerBao: Nowadays, many Agents' so-called "long-term memory" is really just storing past chats and searching through them again.…
Hindsight 是 Vectorize 开源的 Agent 记忆系统,超越简单的聊天记录检索,通过 Retain/Recall/Reflect 三个动作让 Agent 从过往经验中形成新判断,支持 Claude Code、Codex、Cursor 等并内置 MCP Server。
@claudeai: New in Claude Code: agent view. One list of all your sessions, available today as a research preview.
Anthropic has released a new 'agent view' feature in Claude Code, providing a unified list of all sessions, currently available as a research preview.
What I'm learning trying to ensure context continuity for different agents across different sessions
The author introduces AICTX, an open-source tool that preserves structured operational state across coding agent sessions, reducing the need for agents to rediscover repository context each time.