@volter/supercode-interchange 0.1.1 → 0.1.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (44) hide show
  1. package/README.md +88 -0
  2. package/hermes-store.mjs +5 -2
  3. package/message.mjs +85 -0
  4. package/native-json.mjs +94 -7
  5. package/package.json +16 -3
  6. package/session-claude-append.mjs +107 -0
  7. package/session-claude-content.mjs +209 -0
  8. package/session-claude-files.mjs +78 -0
  9. package/session-claude-graph.mjs +285 -0
  10. package/session-claude-index.mjs +173 -0
  11. package/session-claude.mjs +73 -0
  12. package/session-codec-utils.mjs +115 -0
  13. package/session-codecs.d.ts +64 -0
  14. package/session-codecs.mjs +19 -0
  15. package/session-codex-context.mjs +68 -0
  16. package/session-codex-display-items.mjs +89 -0
  17. package/session-codex-display.mjs +81 -0
  18. package/session-codex-items.mjs +117 -0
  19. package/session-codex-user-history.mjs +62 -0
  20. package/session-codex.mjs +135 -0
  21. package/session-display.mjs +135 -0
  22. package/session-document.mjs +19 -0
  23. package/session-file.mjs +27 -0
  24. package/session-follower.mjs +181 -0
  25. package/session-gemini.mjs +93 -0
  26. package/session-goose.mjs +139 -0
  27. package/session-grok.mjs +72 -0
  28. package/session-hermes.mjs +116 -0
  29. package/session-lines.mjs +20 -0
  30. package/session-model.d.ts +114 -0
  31. package/session-model.mjs +70 -0
  32. package/session-native-residue.mjs +109 -0
  33. package/session-native.mjs +62 -0
  34. package/session-opencode-content.mjs +115 -0
  35. package/session-opencode-store.mjs +101 -0
  36. package/session-opencode.mjs +115 -0
  37. package/session-path.mjs +129 -0
  38. package/session-pi-content.mjs +129 -0
  39. package/session-pi.mjs +86 -0
  40. package/session-read.d.ts +12 -0
  41. package/session-read.mjs +104 -0
  42. package/session-source.mjs +30 -0
  43. package/session-time.mjs +46 -0
  44. package/session-views.mjs +171 -0
package/README.md CHANGED
@@ -7,6 +7,94 @@ The port branch routes discovery and index windows through this library and the
7
7
  cover selected small homes; publication, all-format and large-home qualification remain open.
8
8
  Full session translation codecs remain at their later boundary.
9
9
 
10
+ The `./session-model` subpath supplies the step-5 read service's typed message
11
+ and retained-session representation, native raw-line framing, normalized views,
12
+ projected message windows, inline-media elision and recorded profile history.
13
+ Source metadata is separate from provider-facing message serialization. Bounding
14
+ a display view keeps its raw records and imported count; a view is not an export
15
+ or a continuation artifact. The port branch's public load/follow handlers now
16
+ use these readers through caller-owned worker lanes; shared Rust models remain for
17
+ the native consumers named in the architecture plan. No runtime qualification
18
+ is claimed for these new helpers.
19
+
20
+ The read-codec slice has named document and store readers in
21
+ `./session-codecs`. They preserve raw text, native
22
+ message extensions, tool pairing and source metadata; the Goose store read
23
+ uses one read-only DB/WAL snapshot and refuses a changed source. They do not
24
+ own a native parsing process. The `./session-read` service selects a structural
25
+ reader and never falls back to Rust. These readers
26
+ have source and syntax checks only, with no runtime
27
+ qualification claimed.
28
+ Hermes selects only native active rows for replay while retaining inactive rows
29
+ in its reconstructed raw container. Its lineage, trigger, surface and profile
30
+ interpretation is shared with the metadata reader; recorded lifecycle and
31
+ message timestamps retain their separate native rounding rules. No Hermes
32
+ write tier is introduced.
33
+ Grok separates user/system envelopes, assistant tools and screenshot results
34
+ from raw reasoning/backend records. Its portable residue is verified using the
35
+ existing BLAKE3 digest contract before restoration; missing-record summaries
36
+ name the lost residue. Path/summary overlays belong to the read service slice.
37
+ Claude selects its active graph branch and compacted history before decoding
38
+ messages, retaining exact reasoning blocks, tool outcomes, media and creating
39
+ record provenance. Semantic views name disconnected or degraded graph segments;
40
+ strict reads refuse a reconstruction they cannot establish. Its file reader
41
+ attaches native subagent transcripts and verifies the main and child file
42
+ identities. Its scoped index retains graph references and ordering/count
43
+ skeletons while materializing only requested assistant groups and summary
44
+ messages. The file handle closes before the result leaves the scope. The
45
+ public read/follow handoff has source and syntax checks only, with no runtime
46
+ qualification claimed yet.
47
+ Codex has separate model-context and bounded display-history readers. The
48
+ model reader retains compaction replacement, turn grouping, reasoning and
49
+ native typed-input pairing; the display reader projects durable TurnItems or
50
+ legacy typed events with human-boundary anchors. Context and hook classification
51
+ is shared between both. Hook XML is parsed by the pinned `saxes` dependency;
52
+ source checks do not establish XML or display runtime parity. These additions
53
+ are used by the public load/follow path on this port branch.
54
+ Pi's active parent chain retains compaction flags, native signatures and
55
+ message clocks, custom messages and source-native entry ids. Its OpenClaw
56
+ dialect follows recorded leaf redirects, excludes side appends from default
57
+ leaf selection and keeps vendor references inert. Unknown Pi image shapes
58
+ remain raw-only, following the existing reader's refusal to synthesize image
59
+ data it cannot establish. These are read-library additions, with no new
60
+ OpenClaw write tier or runtime qualification.
61
+ OpenCode's envelope and export-document readers share creation/id ordering,
62
+ compaction boundaries, portable message extensions and task-child provenance.
63
+ Completed tool parts split into calls/results, retaining masked original output
64
+ and attachment content. Its SQLite reader reconstructs native columns, reads
65
+ the external revert sidecar and verifies both identities; malformed JSON columns
66
+ remain diagnostic. Legacy JSON-tree directories retain their native
67
+ unsupported-source refusal. No runtime qualification is claimed for this
68
+ checkpoint.
69
+ The path layer selects formats structurally, pins native store selectors and
70
+ reads compressed transcripts through Node's zstd support. Native sidecars
71
+ validate framing and retain malformed appended turns as raw parse loss.
72
+ Grok summaries and OpenClaw path profiles keep their source meanings. Display
73
+ reads retain the native byte-window, human-anchor and fixed-byte-anchor rules;
74
+ those bounded views remain unsuitable as continuation artifacts. Public
75
+ load/follow uses `./session-read` on this branch. Its TypeScript follower pins
76
+ the store selector, compares filesystem identities and reports snapshot,
77
+ append, rewrite and recoverable-error events. Cold follower opening performs
78
+ one selection read followed by a fingerprint-bracketed pinned read. Changed
79
+ full-history Claude files can reuse a conservative append projection after
80
+ comparing every old prefix byte. Partial, tool, branch, assistant-chunk and
81
+ companion changes retain the canonical full loader. This optimization has
82
+ source/syntax checks only, with no runtime qualification yet.
83
+
84
+ The npm stdio router and the machine's harness streams use caller-owned load
85
+ and follow worker lanes from `@volter/supercode-teams/session-read-client`.
86
+ Large parsing jobs do not run on their routing thread. Each stream owns its
87
+ subscriptions; response admission precedes activation, and close terminates
88
+ its workers. The machine rechecks operator authority before content leaves
89
+ an asynchronous read. Runtime-state events use authenticated loopback
90
+ `frontend.v2.describe` with an `observe` narrowing header, not transcript
91
+ activity. This read-only probe does not retire runtime receipts; the retained
92
+ registry owns that behavior until its later replacement. No Windows caller
93
+ authority implementation or runtime qualification is implied by this slice.
94
+ The SDK checks the read capability before moved requests; an older service
95
+ refuses explicitly instead of supplying a native read. Its catalog and other
96
+ retained operations remain independently available on the same client.
97
+
10
98
  `resolveHarnessHomes` follows native HOME-first defaults and preserves explicitly empty harness-specific
11
99
  home values. `projectMetadata` applies workspace scope, native lineage, repository-family, profile, time and
12
100
  metadata-search filters with stable newest-first ordering. It clones raw rows before descendant roll-up.
package/hermes-store.mjs CHANGED
@@ -37,8 +37,8 @@ function nouns(row, relationship, config, extended) {
37
37
  const source = nonempty(row.source) ?? '';
38
38
  let trigger = relationship === 'delegate' ? 'parent' : sourceTrigger(source);
39
39
  let recurrence = null;
40
- const cron = /^cron_(.+)_([0-9]{8})_([0-9]{6})$/.exec(row.id);
41
- if (cron) { trigger = 'cron'; recurrence = { job_id: cron[1], kind: 'cron' }; }
40
+ const cron = /^cron_(.+)_([0-9]{8})_([0-9]{6})$/s.exec(row.id);
41
+ if (cron?.[0].length === row.id.length) { trigger = 'cron'; recurrence = { job_id: cron[1], kind: 'cron' }; }
42
42
  recurrence ??= mirrorRecurrence(config);
43
43
  let surface = {}, profile = null;
44
44
  if (extended) {
@@ -66,6 +66,9 @@ function nouns(row, relationship, config, extended) {
66
66
  ...(state != null ? { cross_surface: { state, ...(platform != null ? { platform } : {}), ...(error != null ? { error } : {}) } } : {}) };
67
67
  }
68
68
 
69
+ // Transcript and metadata readers share these native row interpretations.
70
+ export { lineage as hermesLineageKind, nouns as hermesSessionNouns };
71
+
69
72
  export async function readHermesStore(path, { metrics = {} } = {}) {
70
73
  return readStore(path, (db) => {
71
74
  statement(db, 'SELECT count(*) FROM sessions').get();
package/message.mjs ADDED
@@ -0,0 +1,85 @@
1
+ // Provider-neutral messages. Source provenance lives beside the provider wire
2
+ // fields; serialization to a model never sends it as another instruction.
3
+ import { cloneNativeJson, parseNativeJson, stringifyNativeJsonMember } from './native-json.mjs';
4
+ import { nativeTrim } from './text.mjs';
5
+
6
+ export const ROLES = Object.freeze(['system', 'user', 'assistant', 'tool']);
7
+ export const TOOL_ERROR_METADATA_KEY = 'sc.tool_error';
8
+ export const TOOL_OUTCOME_UNKNOWN_METADATA_KEY = 'sc.tool_outcome_unknown';
9
+
10
+ const object = (value) => value != null && typeof value === 'object' && !Array.isArray(value);
11
+ const invalid = (field) => { throw new TypeError(`invalid canonical message: ${field}`); };
12
+ const optionalString = (value, key) => {
13
+ if (value[key] == null) return null;
14
+ if (typeof value[key] !== 'string') invalid(`${key} must be a string`);
15
+ return value[key];
16
+ };
17
+
18
+ /** Internal representation: null and an explicitly empty array are distinct. */
19
+ export function createMessage(role, content = null) {
20
+ if (!ROLES.includes(role)) invalid('role must be system, user, assistant or tool');
21
+ if (content !== null && typeof content !== 'string') invalid('text content must be a string');
22
+ return { role, content, content_parts: null, tool_calls: null, tool_call_id: null, name: null, metadata: Object.create(null) };
23
+ }
24
+
25
+ /** The provider-shaped decoder deliberately ignores metadata, as ChatMessage's
26
+ * Deserialize implementation does. Native-turn readers restore it separately. */
27
+ export function deserializeMessage(value) {
28
+ if (!object(value)) invalid('expected an object');
29
+ const message = createMessage(value.role);
30
+ if (typeof value.content === 'string') message.content = value.content;
31
+ else if (Array.isArray(value.content)) message.content_parts = cloneNativeJson(value.content);
32
+ else if (value.content != null) message.content = stringifyNativeJsonMember(value, 'content');
33
+ message.tool_call_id = optionalString(value, 'tool_call_id');
34
+ message.name = optionalString(value, 'name');
35
+ if (value.tool_calls != null) {
36
+ if (!Array.isArray(value.tool_calls)) invalid('tool_calls must be an array');
37
+ message.tool_calls = value.tool_calls.map((call) => {
38
+ if (!object(call) || typeof call.id !== 'string') invalid('tool call id must be a string');
39
+ const kind = Object.hasOwn(call, 'type') ? call.type : 'function';
40
+ if (typeof kind !== 'string') invalid('tool call type must be a string');
41
+ if (!object(call.function) || typeof call.function.name !== 'string' || typeof call.function.arguments !== 'string') {
42
+ invalid('tool call function needs string name and arguments');
43
+ }
44
+ return { id: call.id, type: kind, function: { name: call.function.name, arguments: call.function.arguments } };
45
+ });
46
+ }
47
+ return message;
48
+ }
49
+
50
+ /** Model-facing serialization, omitting absent fields and source metadata. */
51
+ export function messageWire(message) {
52
+ const value = { role: message.role };
53
+ if (message.content_parts != null) value.content = cloneNativeJson(message.content_parts);
54
+ else if (message.content != null) value.content = message.content;
55
+ if (message.tool_calls != null) value.tool_calls = cloneNativeJson(message.tool_calls);
56
+ if (message.tool_call_id != null) value.tool_call_id = message.tool_call_id;
57
+ if (message.name != null) value.name = message.name;
58
+ return value;
59
+ }
60
+
61
+ /** Session-read serialization adds the complete source provenance map. */
62
+ export function messageJson(message) {
63
+ return { ...messageWire(message), metadata: cloneNativeJson(message.metadata) };
64
+ }
65
+
66
+ export function parsedArguments(call) {
67
+ const text = nativeTrim(call.function.arguments);
68
+ return text === '' ? {} : parseNativeJson(text);
69
+ }
70
+
71
+ export function markToolError(message) {
72
+ delete message.metadata[TOOL_OUTCOME_UNKNOWN_METADATA_KEY];
73
+ message.metadata[TOOL_ERROR_METADATA_KEY] = 'true';
74
+ }
75
+
76
+ export function markToolOutcomeUnknown(message) {
77
+ delete message.metadata[TOOL_ERROR_METADATA_KEY];
78
+ message.metadata[TOOL_OUTCOME_UNKNOWN_METADATA_KEY] = 'true';
79
+ }
80
+
81
+ export function isToolError(message) { return message.metadata[TOOL_ERROR_METADATA_KEY] === 'true'; }
82
+ export function toolOutcome(message) {
83
+ if (isToolError(message)) return 'known_error';
84
+ return message.metadata[TOOL_OUTCOME_UNKNOWN_METADATA_KEY] === 'true' ? 'unknown' : 'known_success';
85
+ }
package/native-json.mjs CHANGED
@@ -123,7 +123,25 @@ export function parseNativeJson(text) {
123
123
  return result.value;
124
124
  }
125
125
 
126
- export function nativeObjectKeys(value) { return facts.get(value)?.keys ?? Object.keys(value); }
126
+ export function nativeObjectKeys(value) {
127
+ const keys = facts.get(value)?.keys;
128
+ if (!keys) return Object.keys(value);
129
+ const known = new Set(keys);
130
+ return [...keys.filter((key) => Object.hasOwn(value, key)), ...Object.keys(value).filter((key) => !known.has(key))];
131
+ }
132
+
133
+ // Transcript projections clone JSON before bounding strings or eliding media.
134
+ // Keep the source's numeric tokens and insertion order with those values.
135
+ export function cloneNativeJson(value) {
136
+ if (value == null || typeof value !== 'object') return value;
137
+ const result = Array.isArray(value) ? [] : Object.create(Object.getPrototypeOf(value) === null ? null : Object.prototype);
138
+ for (const key of nativeObjectKeys(value)) {
139
+ Object.defineProperty(result, key, { value: cloneNativeJson(value[key]), enumerable: true, writable: true, configurable: true });
140
+ }
141
+ const meta = facts.get(value);
142
+ if (meta) facts.set(result, { keys: [...meta.keys], numbers: new Map(meta.numbers) });
143
+ return result;
144
+ }
127
145
 
128
146
  export function nativeInteger(value, key, { signed = false } = {}) {
129
147
  if (value == null || typeof value !== 'object') return null;
@@ -132,6 +150,30 @@ export function nativeInteger(value, key, { signed = false } = {}) {
132
150
  const whole = fact.integer;
133
151
  return whole >= (signed ? intMin : 0n) && whole <= (signed ? intMax : uintMax) ? whole : null;
134
152
  }
153
+ export function nativeNumberKind(value, key) {
154
+ const fact = facts.get(value)?.numbers.get(String(key));
155
+ return fact && Object.is(value[key], fact.value) ? fact.kind : null;
156
+ }
157
+
158
+ /** Native SQLite INTEGER columns enter JSON without a lossy Number round
159
+ * trip. This helper records their exact i64/u64 value for the serializer. */
160
+ export function setNativeInteger(value, key, integer) {
161
+ if (typeof integer !== 'bigint' || integer < intMin || integer > uintMax) throw new RangeError('not a native JSON integer');
162
+ let meta = facts.get(value);
163
+ if (!meta) { meta = { keys: Object.keys(value), numbers: new Map() }; facts.set(value, meta); }
164
+ if (!Object.hasOwn(value, key)) meta.keys.push(String(key));
165
+ Object.defineProperty(value, key, { value: Number(integer), enumerable: true, writable: true, configurable: true });
166
+ meta.numbers.set(String(key), { value: Number(integer), kind: 'integer', integer });
167
+ }
168
+
169
+ export function setNativeFloat(value, key, number) {
170
+ let meta = facts.get(value);
171
+ if (!meta) { meta = { keys: Object.keys(value), numbers: new Map() }; facts.set(value, meta); }
172
+ if (!Object.hasOwn(value, key)) meta.keys.push(String(key));
173
+ value[key] = Number.isFinite(number) ? number : null;
174
+ if (value[key] == null) meta.numbers.delete(String(key));
175
+ else meta.numbers.set(String(key), { value: number, kind: 'float' });
176
+ }
135
177
 
136
178
  function formatFloat(number) {
137
179
  if (number === 0) return Object.is(number, -0) ? '-0.0' : '0.0';
@@ -149,12 +191,57 @@ function formatFloat(number) {
149
191
  }
150
192
 
151
193
  export function stringifyNativeJson(value) {
194
+ if (typeof value === 'bigint') return String(value);
152
195
  if (value == null || typeof value !== 'object') return JSON.stringify(value);
153
- const meta = facts.get(value);
154
- const child = (key) => {
155
- const fact = meta?.numbers.get(key);
156
- return fact ? fact.kind === 'integer' ? String(fact.integer) : formatFloat(fact.value) : stringifyNativeJson(value[key]);
196
+ const child = (key) => stringifyNativeJsonMember(value, key);
197
+ if (Array.isArray(value)) return `[${Array.from({ length: value.length }, (_, at) => child(String(at)) ?? 'null').join(',')}]`;
198
+ return `{${nativeObjectKeys(value).flatMap((key) => {
199
+ const encoded = child(key); return encoded === undefined ? [] : [`${JSON.stringify(key)}:${encoded}`];
200
+ }).join(',')}}`;
201
+ }
202
+
203
+ /** serde_json Value equality ignores object order but distinguishes integers
204
+ * from floats. Keep those facts when comparing transcript content; ordinary
205
+ * deep JS equality cannot distinguish an exact u64 from its rounded Number. */
206
+ export function nativeJsonEqual(left, right) {
207
+ const number = (parent, key) => {
208
+ const value = parent[key], fact = facts.get(parent)?.numbers.get(String(key));
209
+ if (fact && Object.is(value, fact.value)) return fact;
210
+ return typeof value === 'number' ? { kind: Number.isInteger(value) && !Object.is(value, -0) ? 'integer' : 'float',
211
+ value, integer: Number.isInteger(value) ? BigInt(value) : null } : null;
212
+ };
213
+ const equal = (a, b) => {
214
+ if (a == null || b == null || typeof a !== 'object' || typeof b !== 'object') return a === b;
215
+ if (Array.isArray(a) !== Array.isArray(b)) return false;
216
+ const keys = Object.keys(a);
217
+ if (keys.length !== Object.keys(b).length) return false;
218
+ for (const key of keys) {
219
+ if (!Object.hasOwn(b, key)) return false;
220
+ const first = number(a, key), second = number(b, key);
221
+ if (first || second) {
222
+ if (!first || !second || first.kind !== second.kind
223
+ || (first.kind === 'integer' ? first.integer !== second.integer : first.value !== second.value)) return false;
224
+ } else if (!equal(a[key], b[key])) return false;
225
+ }
226
+ return true;
157
227
  };
158
- if (Array.isArray(value)) return `[${value.map((_, at) => child(String(at))).join(',')}]`;
159
- return `{${nativeObjectKeys(value).map((key) => `${JSON.stringify(key)}:${child(key)}`).join(',')}}`;
228
+ return equal(left, right);
229
+ }
230
+
231
+ export function stringifyNativeJsonMember(value, key) {
232
+ const fact = facts.get(value)?.numbers.get(String(key));
233
+ return fact && Object.is(value[key], fact.value) ? fact.kind === 'integer' ? String(fact.integer) : formatFloat(fact.value) : stringifyNativeJson(value[key]);
234
+ }
235
+
236
+ export function stringifyNativeJsonPretty(value, depth = 0) {
237
+ if (value == null || typeof value !== 'object') return stringifyNativeJson(value);
238
+ const keys = nativeObjectKeys(value), array = Array.isArray(value);
239
+ if (!keys.length) return array ? '[]' : '{}';
240
+ const padding = ' '.repeat(depth + 1);
241
+ const lines = keys.map((key) => {
242
+ const child = value[key] != null && typeof value[key] === 'object'
243
+ ? stringifyNativeJsonPretty(value[key], depth + 1) : stringifyNativeJsonMember(value, key);
244
+ return padding + (array ? child : `${JSON.stringify(key)}: ${child}`);
245
+ });
246
+ return `${array ? '[' : '{'}\n${lines.join(',\n')}\n${' '.repeat(depth)}${array ? ']' : '}'}`;
160
247
  }
package/package.json CHANGED
@@ -1,13 +1,25 @@
1
1
  {
2
2
  "name": "@volter/supercode-interchange",
3
- "version": "0.1.1",
3
+ "version": "0.1.3",
4
4
  "type": "module",
5
- "description": "Read-only harness metadata and discovery projection for Supercode's daemon-owned session index",
5
+ "description": "Read-only harness metadata, transcript codecs and session projections for Supercode",
6
6
  "license": "MIT OR Apache-2.0",
7
7
  "exports": {
8
8
  ".": {
9
9
  "types": "./index.d.ts",
10
10
  "import": "./index.mjs"
11
+ },
12
+ "./session-model": {
13
+ "types": "./session-model.d.ts",
14
+ "import": "./session-model.mjs"
15
+ },
16
+ "./session-codecs": {
17
+ "types": "./session-codecs.d.ts",
18
+ "import": "./session-codecs.mjs"
19
+ },
20
+ "./session-read": {
21
+ "types": "./session-read.d.ts",
22
+ "import": "./session-read.mjs"
11
23
  }
12
24
  },
13
25
  "files": [
@@ -21,6 +33,7 @@
21
33
  "node": ">=22.15.0"
22
34
  },
23
35
  "dependencies": {
24
- "@noble/hashes": "2.4.0"
36
+ "@noble/hashes": "2.4.0",
37
+ "saxes": "6.0.0"
25
38
  }
26
39
  }
@@ -0,0 +1,107 @@
1
+ // Conservative native append optimization. Every retained prefix byte is
2
+ // compared before reuse. Complex graph/tool/chunk changes use the full codec.
3
+ import { withSessionFile } from './file-io.mjs';
4
+ import { decodeSessionUtf8 } from './session-file.mjs';
5
+ import { parseNativeJson, cloneNativeJson, nativeJsonEqual } from './native-json.mjs';
6
+ import { nativeTrim } from './text.mjs';
7
+ import { field, stringField } from './session-codec-utils.mjs';
8
+ import { ClaudeReplayIndex } from './session-claude-graph.mjs';
9
+ import { captureClaudeMeta } from './session-claude.mjs';
10
+ import { pushClaudeUser, captureClaudeProvenance, mergeClaudeAssistantChunk, flushClaudeAssistant } from './session-claude-content.mjs';
11
+
12
+ const messageId = (value) => stringField(field(value, 'message'), 'id');
13
+ function simpleMessage(value) {
14
+ if (value != null && typeof value === 'object' && Object.keys(value).some((key) => key.startsWith('_supercode_'))) return false;
15
+ if (!['user', 'assistant'].includes(stringField(value, 'type'))) return false;
16
+ const content = field(field(value, 'message'), 'content');
17
+ return typeof content === 'string' || Array.isArray(content)
18
+ && content.every((block) => ['text', 'image', 'thinking', 'redacted_thinking', 'fallback'].includes(stringField(block, 'type')));
19
+ }
20
+ function* rawChunks(raw) {
21
+ for (const line of raw) {
22
+ for (let at = 0; at < line.length;) {
23
+ let end = Math.min(line.length, at + 32 * 1024);
24
+ const last = line.charCodeAt(end - 1);
25
+ if (end < line.length && last >= 0xd800 && last <= 0xdbff) end--;
26
+ yield Buffer.from(line.slice(at, end)); at = end;
27
+ }
28
+ yield Buffer.from('\n');
29
+ }
30
+ }
31
+ async function prefixMatches(reader, raw) {
32
+ let position = 0, available = Buffer.alloc(0), used = 0;
33
+ for (const expected of rawChunks(raw)) {
34
+ for (let at = 0; at < expected.length;) {
35
+ if (used === available.length) { available = await reader.read(position, 64 * 1024); used = 0; }
36
+ const length = Math.min(available.length - used, expected.length - at);
37
+ if (!length || !expected.subarray(at, at + length).equals(available.subarray(used, used + length))) return false;
38
+ used += length; at += length; position += length;
39
+ }
40
+ }
41
+ return true;
42
+ }
43
+
44
+ export class ClaudeAppendState {
45
+ static fromSession(session, fidelity) {
46
+ if (session.meta.source !== 'claude_code' || !session.raw_is_verbatim || !session.raw_trailing_newline
47
+ || session.parse_error_lines !== 0 || session.subagents.length) return null;
48
+ const graph = new ClaudeReplayIndex(); let bytes = 0;
49
+ session.raw.forEach((line, index) => {
50
+ bytes += Buffer.byteLength(line) + 1;
51
+ if (nativeTrim(line) !== '') graph.observe(index, parseNativeJson(line));
52
+ });
53
+ const selection = graph.clone().selectLines(fidelity);
54
+ if (!nativeJsonEqual(selection.residue, session.load_residue)) return null;
55
+ let lastAssistant = null;
56
+ if (selection.lines.length) {
57
+ const last = parseNativeJson(session.raw[selection.lines.at(-1)]);
58
+ if (stringField(last, 'type') === 'assistant') lastAssistant = messageId(last);
59
+ }
60
+ return new ClaudeAppendState(graph, selection.lines, bytes, lastAssistant);
61
+ }
62
+ constructor(graph, replay, bytes, lastAssistant) { this.graph = graph; this.replay = replay; this.bytes = bytes; this.lastAssistant = lastAssistant; }
63
+ async append(path, current, fidelity, { metrics } = {}) {
64
+ return (await withSessionFile(path, async (reader) => {
65
+ if (reader.size <= this.bytes || reader.size - this.bytes > 8 * 1024 * 1024) return null;
66
+ if (!await prefixMatches(reader, current.raw)) return null;
67
+ const appended = decodeSessionUtf8(await reader.read(this.bytes, reader.size - this.bytes));
68
+ if (!appended.endsWith('\n')) return null;
69
+ const lines = appended.slice(0, -1).split('\n'), values = [], graph = this.graph.clone(), meta = cloneNativeJson(current.meta);
70
+ for (let at = 0; at < lines.length; at++) {
71
+ const line = lines[at]; if (nativeTrim(line) === '') { values.push(null); continue; }
72
+ let value; try { value = parseNativeJson(line); } catch { return null; }
73
+ if (!simpleMessage(value)) return null;
74
+ graph.observe(current.raw.length + at, value); captureClaudeMeta(value, meta, line); values.push(value);
75
+ }
76
+ const selection = graph.clone().selectLines(fidelity);
77
+ if (selection.lines.length < this.replay.length || this.replay.some((index, at) => selection.lines[at] !== index)) return null;
78
+ const added = selection.lines.slice(this.replay.length);
79
+ if (added.some((index) => index < current.raw.length)) return null;
80
+ if (added.length && this.lastAssistant != null && messageId(values[added[0] - current.raw.length]) === this.lastAssistant) return null;
81
+ const messages = []; let pending = null;
82
+ for (const index of added) {
83
+ const value = values[index - current.raw.length];
84
+ if (stringField(value, 'type') === 'assistant') {
85
+ if (field(value, 'isApiErrorMessage') === true) { flushClaudeAssistant(pending, messages); pending = null; continue; }
86
+ if (pending != null) {
87
+ if (messageId(pending) != null && messageId(pending) === messageId(value)) { mergeClaudeAssistantChunk(pending, value); continue; }
88
+ flushClaudeAssistant(pending, messages);
89
+ }
90
+ pending = cloneNativeJson(value);
91
+ } else {
92
+ flushClaudeAssistant(pending, messages); pending = null;
93
+ const before = messages.length; pushClaudeUser(value, messages); captureClaudeProvenance(value, messages, before);
94
+ }
95
+ }
96
+ flushClaudeAssistant(pending, messages);
97
+ const last = added.length ? values[added.at(-1) - current.raw.length] : null;
98
+ const lastAssistant = added.length ? stringField(last, 'type') === 'assistant' ? messageId(last) : null : this.lastAssistant;
99
+ const session = cloneNativeJson(current), count = (current.imported_message_count ?? current.messages.length) + messages.length;
100
+ session.meta = meta;
101
+ for (const message of messages) session.messages.push(message);
102
+ for (const line of lines) session.raw.push(line);
103
+ session.imported_message_count = count; session.load_residue = selection.residue;
104
+ return { session, state: new ClaudeAppendState(graph, selection.lines, reader.size, lastAssistant) };
105
+ }, metrics)).result;
106
+ }
107
+ }