You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Two converging pressures push burn toward a Rust rewrite:
Ingest at scale is too slow. A real user ledger at 1.5GB takes ~60s to do a full archive rebuild today. The hot loop (packages/ledger/src/archive.ts:1224-1248) is createReadStream → JSON.parse per line, single-threaded; a full burn state rebuild all re-streams the entire ledger three times (archive.ts, index-sidecar.ts:189, file-adapter.ts:481). Node's parser ceiling is ~50-100 MB/s on JSON-heavy lines, so the wall-clock cost grows linearly with ledger size and there's no incremental fix that closes the gap.
Wash (AgentWorkforce/wash) is going Rust. Issue Publish relayburn/sdk programmatic surface for embedded use #218 was scoped as a Node-importable SDK so wash could esbuild-bundle it; with wash itself going Rust, that framing is obsolete. The right SDK is a burn-sdk crate on crates.io that wash adds to Cargo.toml, plus a thin napi-rs wrapper published as @relayburn/sdk on npm so existing TS/Node consumers keep working without spawning the CLI.
Rust is the source of truth; the npm package is a generated binding layer over it. This issue supersedes #218 (which proposed a TS-native SDK with better-sqlite3); #218 stays open as the Node-only option of record until this issue lands, then closes.
Goal
Port the six-package TS workspace to a Rust Cargo workspace that:
Cuts burn state rebuild archive --full on a 1.5GB ledger from ~60s to ~5-10s (SQLite-bound, not parser-bound).
Ships a single static burn binary per (os, arch) triple.
Publishes burn-sdk to crates.io as the supported embedding surface for Rust consumers (wash).
Build order: reader → ledger → analyze → mcp → cli, with sdk re-exporting from the lower crates and sdk-node depending on sdk. packages/sdk-node is the only TS package that survives the port — it's mostly generated.
Crate-by-crate translation
TS today
Rust target
Notes
zod schemas in reader/types.ts
serde + serde_json with #[derive(Deserialize)]
TurnRecord, ContentRecord, LedgerLine become tagged enums; v schema field stays.
Async boundary:ingest and watch-loop are async (tokio); summary / hotspots are sync — they're CPU-bound queries against an open handle. Wash's MCP handlers wrap them in spawn_blocking.
Handle vs free fn: offer both. Free fn for one-shot use; LedgerHandle::summary for embedded paths (wash's MCP server keeps a long-lived handle).
SDK shape — TypeScript (@relayburn/sdk on npm, day-1 deliverable)
burn-sdk-node exposes the same surface via napi-rs:
Errors:Result<T, E> → throws on the JS side; E becomes a typed BurnError with code and cause.
Numbers: u64 token counts → bigint in TS; cost (f64 USD) → number. Document the bigint boundary in the README.
Async: Rust async fn → Promise<T> automatically via napi's tokio runtime integration.
Codegen:.d.ts is generated by napi-rs; never hand-edited. The TS package is index.js (loader) + index.d.ts (generated) + prebuilt .node binaries. Source-of-truth single direction: Rust → TS, never the reverse.
The TS facade in packages/sdk-node is small enough (loader + re-exports + a few JS-ergonomics helpers like converting ISO strings to Date) that it's effectively zero ongoing maintenance.
Distribution
burn binary: GitHub Releases with prebuilt static binaries for darwin-{arm64,x64}, linux-{arm64,x64}, optionally windows-x64. cargo install burn-cli as a fallback for users with Rust toolchains.
burn-sdk: crates.io. Wash adds burn-sdk = "1" to Cargo.toml and statically links it.
@relayburn/sdk (npm): prebuilt .node binaries via napi-rs's standard CI recipe — @relayburn/sdk-darwin-arm64, -darwin-x64, -linux-arm64-gnu, -linux-x64-gnu, optionally -win32-x64-msvc. The umbrella @relayburn/sdk package picks the right native package via npm's optionalDependencies selector. esbuild-bundles cleanly. No node-gyp, no compile-on-install.
relayburn (npm, legacy): keep on npm as a postinstall shim that downloads the right prebuilt burn binary, so npm i -g relayburn keeps working through the transition. Phase out post-1.0 if no one's using it.
Wash plugin install (/plugin install relaywash@…): wash binary ships the MCP server; plugin manifest points at the platform-specific binary or a 20-line Node shim that execves it. Same model tokscale uses.
Sequencing
Land the SDK refactor in TS first (the burn-cli → burn-sdk direction, even though the SDK is still TS). Establishes a clean port boundary and keeps users on a stable CLI through the migration.
Port burn-reader + burn-ledger + burn-analyze behind the same JSON contract. Use existing *.test.ts fixtures as conformance tests — every Rust crate has to produce byte-identical output to the TS version on the fixture corpus.
Stand up burn-sdk-node and @relayburn/sdk with napi-rs as soon as burn-sdk is functional. Don't defer this — landing the napi-rs CI matrix early surfaces bindings issues while the surface is small.
Port burn-cli with clap. Cut over the published binary; npm relayburn package becomes a download shim.
Decommission TS packages once nothing depends on them. Only packages/sdk-node survives.
Risk register
Parser surface is the most volatile code in the repo.claude.ts / codex.ts / opencode.ts change with every new harness/tool/skill shape. Two options: freeze TS feature work during the port (slow), or run TS and Rust in parallel with a fixture-conformance gate (more work, safer). Recommend the latter.
Activity classifier rule tables are the most likely site of subtle behavior drift. Add a property-based test on the fixture corpus that asserts category assignments match TS exactly.
SQLite schema compatibility must be preserved — archive.sqlite files in the wild need to keep working without a forced rebuild, or we ship a one-time migration in burn state rebuild archive --full.
Lock semantics across processes: Node's flock and Rust's fs2::FileExt use the same OS primitive (flock(2) on Linux/mac), so cross-process locks between mid-migration TS and Rust tools work. Verify on Windows.
napi-rs CI matrix: 4-5 platforms × build × test, plus npm publish per-platform. Use napi-rs's standard GitHub Actions templates as a starting point — they handle the matrix and the optionalDependencies trick correctly.
Lockstep semver: crates.io burn-sdk and npm @relayburn/sdk ship with the same version, always, via a single release workflow. No independent npm patches between Rust releases — that path leads to drift.
TS ergonomics across the FFI:bigint for u64 token counts is the right call but will surprise consumers; document loudly. Date types are passed as ISO strings; the TS facade converts where helpful.
Changelog/release machinery: workflow-driven today. Port to cargo-release + napi publish + the same [Unreleased] promotion logic applied to both crates/*/CHANGELOG.md and packages/sdk-node/CHANGELOG.md.
Acceptance
burn state rebuild archive --full on a 1.5GB ledger completes in ≤10s on M-series silicon (target: 5-10s).
Every *.test.ts fixture passes against the Rust crate that owns it (golden conformance gate).
burn-sdk published on crates.io with documented public API; wash builds against it.
@relayburn/sdk published on npm, esbuild-bundles cleanly, runs on Node ≥ 20.11 across darwin-{arm64,x64} and linux-{arm64,x64} without compile-on-install.
Context
Two converging pressures push burn toward a Rust rewrite:
packages/ledger/src/archive.ts:1224-1248) iscreateReadStream → JSON.parseper line, single-threaded; a fullburn state rebuild allre-streams the entire ledger three times (archive.ts,index-sidecar.ts:189,file-adapter.ts:481). Node's parser ceiling is ~50-100 MB/s on JSON-heavy lines, so the wall-clock cost grows linearly with ledger size and there's no incremental fix that closes the gap.AgentWorkforce/wash) is going Rust. Issue Publishrelayburn/sdkprogrammatic surface for embedded use #218 was scoped as a Node-importable SDK so wash could esbuild-bundle it; with wash itself going Rust, that framing is obsolete. The right SDK is aburn-sdkcrate on crates.io that wash adds toCargo.toml, plus a thin napi-rs wrapper published as@relayburn/sdkon npm so existing TS/Node consumers keep working without spawning the CLI.Rust is the source of truth; the npm package is a generated binding layer over it. This issue supersedes #218 (which proposed a TS-native SDK with
better-sqlite3); #218 stays open as the Node-only option of record until this issue lands, then closes.Goal
Port the six-package TS workspace to a Rust Cargo workspace that:
burn state rebuild archive --fullon a 1.5GB ledger from ~60s to ~5-10s (SQLite-bound, not parser-bound).burnbinary per(os, arch)triple.burn-sdkto crates.io as the supported embedding surface for Rust consumers (wash).@relayburn/sdkto npm as a napi-rs wrapper overburn-sdkfor TS/Node consumers — first-class deliverable, day one. Replaces Publishrelayburn/sdkprogrammatic surface for embedded use #218.burn-clicold-start from ~80ms (Node) to ~5ms.burn ingest --watchRSS from ~80MB to ~10MB; switches the watch loop from stat-polling tonotify(inotify/FSEvents).Capability parity, not new features. No
burncommand added or removed during the port.Workspace shape
Build order:
reader → ledger → analyze → mcp → cli, withsdkre-exporting from the lower crates andsdk-nodedepending onsdk.packages/sdk-nodeis the only TS package that survives the port — it's mostly generated.Crate-by-crate translation
zodschemas inreader/types.tsserde+serde_jsonwith#[derive(Deserialize)]TurnRecord,ContentRecord,LedgerLinebecome tagged enums;vschema field stays.claude.ts(2255),codex.ts(1506),opencode.ts+opencode-stream.ts(1882)serde_json::Deserializer::from_reader().into_iter()per line*.test.tsfixtures as Rust acceptance tests.classifier.tsrule tablesphfstatic maps +matcharmsledger/file-adapter.ts+lock.tsfs2::FileExt::lock_exclusive+BufWriterwithLock('ledger', …)becomes a typedLedgerLockguard the type system enforces.rusqliteWAL+synchronous=NORMALfor the rebuild path.mcp/server.tsrmcpcrateburn --jsonper #210. Recommend folding #210 into this epic.cliclapv4 deriveharnesses/*.tsHarnessAdapterwithasync fn plan/before_spawn/after_exitphftable.notify+ interval fallbackSDK shape — Rust (
burn-sdkon crates.io)Two design knobs:
ingestand watch-loop areasync(tokio);summary/hotspotsare sync — they're CPU-bound queries against an open handle. Wash's MCP handlers wrap them inspawn_blocking.LedgerHandle::summaryfor embedded paths (wash's MCP server keeps a long-lived handle).SDK shape — TypeScript (
@relayburn/sdkon npm, day-1 deliverable)burn-sdk-nodeexposes the same surface via napi-rs:Generated TS surface (auto-emitted
.d.ts):Binding rules:
Result<T, E>→ throws on the JS side;Ebecomes a typedBurnErrorwithcodeandcause.bigintin TS; cost (f64 USD) →number. Document thebigintboundary in the README.async fn→Promise<T>automatically via napi's tokio runtime integration..d.tsis generated by napi-rs; never hand-edited. The TS package isindex.js(loader) +index.d.ts(generated) + prebuilt.nodebinaries. Source-of-truth single direction: Rust → TS, never the reverse.The TS facade in
packages/sdk-nodeis small enough (loader + re-exports + a few JS-ergonomics helpers like converting ISO strings toDate) that it's effectively zero ongoing maintenance.Distribution
burnbinary: GitHub Releases with prebuilt static binaries fordarwin-{arm64,x64},linux-{arm64,x64}, optionallywindows-x64.cargo install burn-clias a fallback for users with Rust toolchains.burn-sdk: crates.io. Wash addsburn-sdk = "1"toCargo.tomland statically links it.@relayburn/sdk(npm): prebuilt.nodebinaries via napi-rs's standard CI recipe —@relayburn/sdk-darwin-arm64,-darwin-x64,-linux-arm64-gnu,-linux-x64-gnu, optionally-win32-x64-msvc. The umbrella@relayburn/sdkpackage picks the right native package via npm'soptionalDependenciesselector. esbuild-bundles cleanly. Nonode-gyp, no compile-on-install.relayburn(npm, legacy): keep on npm as a postinstall shim that downloads the right prebuiltburnbinary, sonpm i -g relayburnkeeps working through the transition. Phase out post-1.0 if no one's using it./plugin install relaywash@…): wash binary ships the MCP server; plugin manifest points at the platform-specific binary or a 20-line Node shim thatexecves it. Same modeltokscaleuses.Sequencing
burn-cli → burn-sdkdirection, even though the SDK is still TS). Establishes a clean port boundary and keeps users on a stable CLI through the migration.burn-reader+burn-ledger+burn-analyzebehind the same JSON contract. Use existing*.test.tsfixtures as conformance tests — every Rust crate has to produce byte-identical output to the TS version on the fixture corpus.burn-sdk-nodeand@relayburn/sdkwith napi-rs as soon asburn-sdkis functional. Don't defer this — landing the napi-rs CI matrix early surfaces bindings issues while the surface is small.burn-cliwithclap. Cut over the published binary; npmrelayburnpackage becomes a download shim.burn-mcp(and resolve mcp: refactor@relayburn/mcpas a thin wrapper overburn <verb> --jsonso the MCP surface tracks the CLI automatically #210 — standalone server vsburn --jsonshell — in this epic).burn-sdk1.0 to crates.io and@relayburn/sdk1.0 to npm in lockstep. Wash bumps to it. Closes Publishrelayburn/sdkprogrammatic surface for embedded use #218.packages/sdk-nodesurvives.Risk register
claude.ts/codex.ts/opencode.tschange with every new harness/tool/skill shape. Two options: freeze TS feature work during the port (slow), or run TS and Rust in parallel with a fixture-conformance gate (more work, safer). Recommend the latter.archive.sqlitefiles in the wild need to keep working without a forced rebuild, or we ship a one-time migration inburn state rebuild archive --full.flockand Rust'sfs2::FileExtuse the same OS primitive (flock(2)on Linux/mac), so cross-process locks between mid-migration TS and Rust tools work. Verify on Windows.burn-sdkand npm@relayburn/sdkship with the same version, always, via a single release workflow. No independent npm patches between Rust releases — that path leads to drift.bigintfor u64 token counts is the right call but will surprise consumers; document loudly. Date types are passed as ISO strings; the TS facade converts where helpful.cargo-release+ napi publish + the same[Unreleased]promotion logic applied to bothcrates/*/CHANGELOG.mdandpackages/sdk-node/CHANGELOG.md.Acceptance
burn state rebuild archive --fullon a 1.5GB ledger completes in ≤10s on M-series silicon (target: 5-10s).*.test.tsfixture passes against the Rust crate that owns it (golden conformance gate).burn-sdkpublished on crates.io with documented public API; wash builds against it.@relayburn/sdkpublished on npm, esbuild-bundles cleanly, runs on Node ≥ 20.11 acrossdarwin-{arm64,x64}andlinux-{arm64,x64}without compile-on-install.import { ingest, summary } from '@relayburn/sdk', esbuild-bundle, and run with no Node-side native build step. (Closes Publishrelayburn/sdkprogrammatic surface for embedded use #218.)burnavailable as prebuilt static binary for the four primary(os, arch)targets.Sub-issues to file
claude/rust-rewrite-exploration-RaBtKburn-readerparsers with TS fixture conformance gateburn-ledger(JSONL + lock + sqlite archive)burn-analyze(pricing, cost derivation, compare aggregator)burn-sdk-nodewith napi-rs (bindings crate + CI matrix + first prebuilt artifact)@relayburn/sdknpm package wrappingburn-sdk-node(loader + .d.ts + optionalDependencies)burn-cli(clap surface matching today's flags)burn-mcp(resolve mcp: refactor@relayburn/mcpas a thin wrapper overburn <verb> --jsonso the MCP surface tracks the CLI automatically #210 in this scope)burn-sdk1.0 to crates.io +@relayburn/sdk1.0 to npm in lockstep (closes Publishrelayburn/sdkprogrammatic surface for embedded use #218)relayburnpackage becomes a download-shim