Files
stack/packages/conversation/src/framing.mjs
T
jason.woltjeandClaude Opus 5.5 243e153c8b feat(conversation): CHAT-03 I1, mediated control of a sealed headless Pi (#1507)
Controller, claim store, live-session guard, engine link and seal,
turn tracker, cohort force stop and recovery, client library,
transcript and mediated terminal, with the fake engine and tests.
Fixtures only; no live cutover.

Dewey built it. Darkwing (comment 26690) and Filbert (comment 26694)
approved round 2. Manifest I1-r2-manifest.sha256 (2b48e333, 27 files).
Suites on an export: conversation 152/152, control-board 124, webui 14,
seat 19, chat-00/01/01c checks, and all nine scripts/test-*.sh green.
Follow-ups for I3 are in DEFERRED. Gate E stays with Jason.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-04 15:47:53 -05:00

69 lines
2.1 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// JSONL framing for the engine pipe and the control socket (#1507, CHAT-03 §1).
//
// Records are split on LF (0x0A) only, and a trailing CR is stripped (rpc.md
// lines 30–38). Node's readline is not used, because it also splits on U+2028
// and U+2029, which JSON strings may carry raw (E2). Splitting happens on
// bytes, before UTF-8 decoding, so a multibyte character split across two
// chunks is never damaged.
export const MAX_LINE_BYTES = 64 * 1024 * 1024;
export class LineSplitter {
constructor(onLine, { maxBytes = MAX_LINE_BYTES, onOverflow = null } = {}) {
this.onLine = onLine;
this.maxBytes = maxBytes;
this.onOverflow = onOverflow;
this.parts = [];
this.size = 0;
this.overflowed = false;
}
push(chunk) {
if (this.overflowed) return;
let start = 0;
for (;;) {
const lf = chunk.indexOf(0x0a, start);
if (lf === -1) break;
this.parts.push(chunk.subarray(start, lf));
const line = Buffer.concat(this.parts);
this.parts = [];
this.size = 0;
const end = line.length > 0 && line[line.length - 1] === 0x0d ? line.length - 1 : line.length;
this.onLine(line.subarray(0, end).toString("utf8"));
start = lf + 1;
}
if (start < chunk.length) {
const rest = chunk.subarray(start);
this.size += rest.length;
if (this.size > this.maxBytes) {
this.overflowed = true;
this.parts = [];
this.onOverflow?.();
return;
}
this.parts.push(Buffer.from(rest));
}
}
// Bytes left without a terminating LF when the stream ends. They are never
// parsed as a record: a partial line is a transport gap, not data.
pending() {
return this.size;
}
}
export function encodeLine(value) {
return JSON.stringify(value) + "\n";
}
// Parses one line. Returns {value} or {error}; never throws.
export function parseLine(line) {
try {
const value = JSON.parse(line);
if (value === null || typeof value !== "object" || Array.isArray(value)) return { error: "not-an-object" };
return { value };
} catch {
return { error: "unparseable" };
}
}