Skip to content
wibus-weePublic

About

One API for Coding Agent session data.

Resources

Code of conduct

Contributing

Stars

31 stars

Watchers

0 watching

Forks

Repository files navigation

汇话 · 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.

Note

Under heavy development. APIs are not yet stable and may change in ways that break compatibility between releases.

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

Quick start

import { sessions } from 'huihua'
import { conversationOf, toolCallsOf } from 'huihua/observe'

const { refs, failures } = await sessions.scan({ providers: ['codex'] })
for (const failure of failures)
  console.warn(failure.provider, failure.source?.path, failure.message)

if (refs[0]) {
  const session = await sessions.read(refs[0])
  conversationOf(session) // user and assistant messages, in source order
  toolCallsOf(session) // tool calls
}

A Session uses the agent-session/v1 schema: records hold each native record once in read order, normalized events refer back to their record, and diagnostics explain partial parses. Unknown data is retained rather than dropped; missing facts stay absent. Keep the whole SessionRef when passing sessions around — an ID alone can be ambiguous across stores, and source identifies the exact file or selector.

Selector Selected events
eventsOf(session, ...types) 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 keep order, duplicates and the original event objects; they never merge messages or resolve relationships. millisOf(timestamp) converts a native Timestamp to epoch milliseconds, leaving absent or unparseable input absent. Serialize with jsonOf(session) or JSON.stringify(session); record.text preserves JSON numeric spellings beyond JavaScript's safe integer range.

Reading paths

Entry point Input Returns
sessions.scan(options?) Provider ids, roots, homeDir, signal { refs, failures }; refs sorted
sessions.scanStream(options?) Same AsyncIterable<ScanEvent>; refs and failures as discovered
sessions.read(ref) SessionRef Complete Session snapshot
sessions.open(ref) SessionRef Replayable handle: stream(), events(), records(), snapshot()
sessions.parse(provider, input) { path, format?, id?, locator? } or { jsonl, source?, id? } Complete Session snapshot
sessions.stream(provider, input) { jsonl } acquired input Single-consumer AsyncIterable<SessionFrame>
sessions.detect(options?) homeDir, roots Per-provider discovered roots
sessions.require(id) Provider id Its registered handle, or ProviderNotFound
sessions.providers() — Registered provider handles

Errors split by caller versus data: invalid requests and cancellation throw, while source problems become in-band failures and diagnostics that never retract already emitted refs. Iterables are lazy, follow consumer backpressure and check cancellation between frames; a streamed prefix is provisional until the iterator completes. The architecture contract owns the full semantics.

Scanning local stores

const { refs, failures } = await sessions.scan({
  providers: ['codex'],
  roots: { codex: ['/backups/codex/sessions'] },
})

Explicit roots replace defaults and accept supported files or directories; homeDir isolates discovery from the process environment. Default roots honor each provider's environment variables — CODEX_HOME, CLAUDE_CONFIG_DIR(S), XDG_CONFIG_HOME/XDG_DATA_HOME and the per-provider variables in the coverage contract. Missing stores produce no refs and no failures; a discovered ref does not guarantee that a later read succeeds.

Reading sessions

const opened = await sessions.open(refs[0]!)
opened.readMode // 'incremental' or 'buffered': the adapter's iteration strategy
for await (const frame of opened.stream()) {
  // 'record' | 'event' | 'diagnostic' | 'metadata' frames, in source order
}
const snapshot = await opened.snapshot() // a fresh read, collected into a Session

Each call opens a fresh read-only source and closes it on completion, early return or cancellation, so replays observe file changes. read is equivalent to open().snapshot(). File-backed handles may expose optional capabilities — select(selection), consume(selection, fn) and, where the provider certifies enough usage context, consumeUsage(fn)/consumeUsageFacts(fn); support is member presence on the handle. Refs need not originate from scan(): construct one with provider, id and source (path, format, optional locator) and call open or read directly.

Supplying input directly

parse/stream skip discovery — no detect(), directory walk or header pre-read:

// A known file:
const fromFile = await sessions.parse('codex', { path: '/backups/rollout.jsonl' })

// Already acquired text, bytes or AsyncIterable<Uint8Array>:
const fromText = await sessions.parse('claude', {
  jsonl: '{"type":"user","sessionId":"example","message":{"content":"hello"}}\n',
  source: 'upload:example', // provenance label only; never opened as a path
})

// Structured stores need their explicit format and native selector:
const fromDatabase = await sessions.parse('cursor', {
  path: '/backups/state.vscdb',
  format: 'cursor_sqlite',
  id: 'composer-id',
  locator: { storage: 'modern' }, // 'legacy' selects ItemTable-backed stores
})

File inputs default to format: 'jsonl'; compressed files take 'jsonl_zstd'. The provider is explicit — content and file extensions are never used to guess it. Without an explicit id, the first native identity wins and later conflicting identities are preserved and diagnosed. Acquired JSONL is supported by every line-based provider; opencode, cline, fx and devin require file input, and unsupported input fails UnsupportedSchema. Antigravity accepts acquired CLI JSONL; SQLite and Markdown sources require their explicit file formats.

huihua/browser mirrors every entry in this table and adds mount helpers, so the same sessions can be read from files a browser holds in memory; see the portable guide.

sessions.stream(provider, input) consumes acquired JSONL incrementally without collecting a snapshot. Each returned sequence accepts one consumer — including for string and byte-array input — and a second iterator throws TypeError; call stream again to replay.

Browser and portable parsing

huihua/browser is the same parser graph built for a host that has Blob instead of node:fs, so a page or a worker can read session folders the user hands over without uploading them:

import { allProviderRoots, entriesFromDrop, mount, sessions } from 'huihua/browser'

const store = mount(await entriesFromDrop(event.dataTransfer), { as: 'home' })
const { refs, failures } = await sessions.scan({ homeDir: store.homeDir })

store.describe(refs[0]!.source.path) // synthetic evidence path -> the relative hand-off path
store.dispose()

A home-shaped hand-off scans through the provider defaults (~/.claude/projects, ~/.codex/sessions and the rest), because the mount places it under a synthetic home. A flat hand-off mounts as explicit roots instead: sessions.scan({ roots: allProviderRoots(store.paths) }). Mounts are zero-copy, streaming reads keep their byte bounds, and the portable artifact contains no Node import and no network API. The portable guide owns the gestures, limits and known differences.

Provider coverage

Provider Implemented stores Compatibility evidence
Claude Code (claude) Projects/sidechain and Desktop/Cowork 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, CLI JSONL and chat/ACP meta/blob stores 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 update journal or derived chat history, with bounded native sidecars Research
Antigravity CLI (antigravity) Observed SQLite/Protobuf steps, CLI brain JSONL and bounded Markdown artifacts 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) V2 manager journals, conversation frames and legacy checkpoint snapshots 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

The linked research owns each provider's native-format evidence and limitations; the coverage contract owns discovery roots and mapping boundaries. OAR and ACP define recordings rather than native stores, so they require explicit input or roots. All stores share the same engine limits: unencrypted rowid SQLite with committed WAL, bounded records (16 MiB default) and 32 MiB Zstandard windows. Coverage is fixture-backed — not a promise to decode every private future format. The compatibility acceptance report records the Agent Sessions comparison, focused before/after reproductions and real local-data checks.

Custom registries and adapters

import { codexProvider } from 'huihua/providers/codex'
import { createSessionRegistry } from 'huihua/registry'

const registry = createSessionRegistry([codexProvider])
const { refs } = await registry.scan()

The root sessions composes every builtin provider; huihua/registry exposes the same registry without loading them. The public SPI is thin — defineProvider({ id, detect, scan, read }), plus optional open, parse and stream capabilities. huihua/ingest re-exports the bounded primitives builtin adapters are built on — jsonlProvider, the JSON and SQLite store factories, Ingestion, openFrom, framing, traversal, failure and value helpers — so third-party adapters conform to the same scan, ordering, cancellation and evidence rules without duplicating them. Supported exports are the root, /registry, /observe, /ingest, /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.

Decoding Pi extension records

Configure decoders on an independent Pi provider and register it with your own registry. The builtin piProvider has no decoders. This example recognizes an application-owned custom entry; it does not describe a pi-subagents format.

import type { SessionDecoder } from 'huihua/ingest'
import { object } from 'huihua/ingest'
import { createPiProvider } from 'huihua/providers/pi'
import { createSessionRegistry } from 'huihua/registry'

const decoder: SessionDecoder = {
  id: 'my-extension/tasks',
  create() {
    return {
      decode(input) {
        if (input.type === 'gap')
          return []
        const native = object(input.record.native)
        const data = object(native.data)
        if (native.type !== 'custom' || native.customType !== 'my-extension/task-started' || typeof data.agentId !== 'string')
          return []
        return [{
          type: 'event',
          record: input.record,
          event: {
            type: 'subagent',
            data: { agentId: data.agentId, kind: 'started', metadata: data },
          },
        }]
      },
    }
  },
}

const sessions = createSessionRegistry([
  createPiProvider({ decoders: [decoder] }),
])
const session = await sessions.parse('pi', {
  jsonl: '{"type":"custom","customType":"my-extension/task-started","data":{"agentId":"worker"}}',
})
console.log(session.events)

create() runs separately for every replay; keep mutable correlation state inside it. decode() receives each original RawRecord, including its text/bytes when record delivery is disabled. Malformed rows arrive as gap inputs so a decoder can reset its correlation state. Optional finish() runs only after successful source EOF, never on cancellation, early return or errors. All callbacks are synchronous and trusted: inputs and returned contributions must not be mutated. Huihua passes evidence by reference, without cloning or freezing it.

Each contribution references a record from that replay, including contributions returned by finish(). An event contribution adds an existing canonical event body; core assigns its envelope, sequence and providerMetadata.decoder. A metadata contribution publishes a complete extension-owned snapshot under metadata.decoders[id].metadata as { record, data }; the latest contribution becomes that decoder's final snapshot. A parent_session contribution carries an explicit parent ID and is retained under metadata.decoders[id].parentSessionIds with its evidence record number. Core publishes parentSessionId at EOF only when decoder candidates agree and native lineage is absent. Decoder metadata and candidate provenance are aggregated privately and published once at successful EOF. Event contributions are delivered incrementally; early return or failure does not publish a final aggregate. When metadata is unselected, core skips namespace construction while still validating lineage and contributions. Native lineage takes precedence; disagreement produces a diagnostic and preserves candidates. metadata.decoders is reserved for this integration when decoders are configured.

Decoders append in configured order and do not replace builtin events or suppress unknown diagnostics. They run through read, parse, stream, open and selective/callback delivery; scanning stays bounded and does not invoke them. The same optional decoders configuration is available to third-party jsonlProvider adapters. Pi titles, paths and entry parentId fields still do not establish native subagent lineage.

Boundaries

Huihua reads local session evidence and nothing else: it never executes agents, connects to networks, writes to or repairs stores, and never opens a database through a SQLite engine or creates sidecars and locks. It provides no runtime control, search, indexing or UI, and does not infer facts that a source does not establish. The portable entry adds mount helpers for handed-over files; it still ships no visual surface.

Example packages

Name Description
@huihua/usage A derivative package that uses Huihua to report daily token usage and model breakdowns from local coding-agent sessions. See the Usage guide.

Development

Use the pinned pnpm version and run pnpm check before submitting changes; the contributing guide describes the validation pipeline, fixture rules and review policy, and the architecture contract owns layer and API semantics. The opt-in pnpm bench command measures synthetic large JSONL reads; it is not part of pnpm check.

License

MIT © 2026 wibus-wee.

About

One API for Coding Agent session data.

Resources

Code of conduct

Contributing

Stars

31 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages