@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…

X AI KOLs Timeline Tools

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.

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.
Original Article
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.

SelectorSelected 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 pointInputOutput
parse(provider, input)Explicit file or acquired JSONLComplete Session snapshot
stream(provider, input)Acquired JSONLOne consumption of incremental SessionFrame values
open(ref)Explicit file/store referenceReplayable 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.

ProviderImplemented storesCompatibility evidence
Claude Code (claude)Projects/sidechain JSONL, split and usage-only recordsResearch
Codex (codex)Active/archived JSONL and Zstandard rollouts, both record streamsResearch
Cursor (cursor)IDE KV/bubbles, conversationMap, ItemTable indexes and CLI transcript JSONLResearch
OpenCode (opencode)SQLite message/part and session_message, empirical session_v2, historical filesystem JSONResearch
Pi (pi)Version 1/2/3 JSONL and complete session trees; future records remain visibleResearch
OAR (oar)Voyage/3 recordings and RawEvent JSONL from any harnessResearch
ACP (acp)Recorded stable v1/v2 session notifications and JSON-RPC JSONLResearch
Kimi Code (kimi)Per-agent wire.jsonl with native session metadataResearch
Grok Build (grok)Native updates.jsonl and summary.json; xAI extensions retainedResearch
Antigravity CLI (antigravity)Partial observed SQLite/Protobuf steps; native bytes retainedResearch
Mister Morph (morph)Topic discovery and ordered native task journal snapshotsResearch
GitHub Copilot CLI (copilot)Flat/session-state events JSONL; mirrored calls and native usageResearch
Hermes Agent (hermes)Current state.db, historical JSON snapshots and JSONL captures/exportsResearch
OpenClaw (openclaw)Current per-agent SQLite transcripts, compressed payloads and legacy JSONLResearch
Qwen Code (qwen)Project chat JSONL, native Google message parts and system recordsResearch
Devin CLI (devin)SQLite session metadata and all message-tree nodes with main-chain markersResearch
fx (fx)Manifest and checkpoint JSON snapshots; event tail not replayedResearch
Cline CLI/Desktop (cline)Version-1 manifests and adjacent messages JSONResearch
DeepSeek Harness (deepseek)v0–v4 immutable JSONL generations and Zstandard logsResearch
Factory Droid (droid)Legacy interactive JSONL and captured stream-json recordsResearch
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

@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...

X AI KOLs Timeline

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.