@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
@@ -0,0 +1,173 @@
1
+ import { withSessionFile } from './file-io.mjs';
2
+ import { sessionFileLines, decodeSessionUtf8 } from './session-file.mjs';
3
+ import { parseNativeJson, cloneNativeJson } from './native-json.mjs';
4
+ import { createSession } from './session-model.mjs';
5
+ import { messageHasContent } from './session-views.mjs';
6
+ import { nativeTrim } from './text.mjs';
7
+ import { field, stringField, ensureToolResultsPaired } from './session-codec-utils.mjs';
8
+ import { captureNativeResidue, claudeResidueKind } from './session-native-residue.mjs';
9
+ import { captureClaudeMeta } from './session-claude.mjs';
10
+ import { ClaudeReplayIndex } from './session-claude-graph.mjs';
11
+ import { captureClaudeProvenance, pushClaudeUser, pushClaudeAttachment, pushClaudeSystem, mergeClaudeAssistantChunk, flushClaudeAssistant, reorderClaudeToolResults } from './session-claude-content.mjs';
12
+ import { sourceStat } from './store-registry.mjs';
13
+ import { looksLikeSessionSqlite } from './session-path.mjs';
14
+ import { detectSessionSource } from './session-source.mjs';
15
+
16
+ const REFERENCE = 'supercode.internal.read_index';
17
+
18
+ export async function supportsClaudeReadIndex(path, { metrics } = {}) {
19
+ if (!(await sourceStat(path))?.isFile() || await looksLikeSessionSqlite(path, metrics)) return false;
20
+ return (await withSessionFile(path, async (reader) => {
21
+ for await (const line of sessionFileLines(reader)) {
22
+ const source = detectSessionSource(line.text);
23
+ if (source != null) return source === 'claude_code';
24
+ }
25
+ return false;
26
+ }, metrics)).result;
27
+ }
28
+
29
+ class ClaudeReadIndex {
30
+ #reader;
31
+ #records = [];
32
+ #groups = [];
33
+ #skeleton = [];
34
+ #meta = createSession('claude_code').meta;
35
+ #parseErrors = 0;
36
+ #residue = [];
37
+
38
+ constructor(reader) { this.#reader = reader; }
39
+ get length() { return this.#skeleton.length; }
40
+ get rawRecordCount() { return this.#records.length; }
41
+
42
+ async open(fidelity) {
43
+ const graph = new ClaudeReplayIndex(), residueRecords = [];
44
+ for await (const line of sessionFileLines(this.#reader)) {
45
+ const index = this.#records.length;
46
+ const record = { offset: line.offset, length: line.length, assistant: false, error: false, messageId: null };
47
+ if (nativeTrim(line.text) !== '') {
48
+ let value;
49
+ try { value = parseNativeJson(line.text); } catch { this.#parseErrors++; }
50
+ if (value !== undefined) {
51
+ captureClaudeMeta(value, this.#meta, line.text.endsWith('\n') ? line.text.slice(0, -1) : line.text);
52
+ graph.observe(index, value);
53
+ record.assistant = stringField(value, 'type') === 'assistant';
54
+ record.error = field(value, 'isApiErrorMessage') === true;
55
+ record.messageId = stringField(field(value, 'message'), 'id');
56
+ if (claudeResidueKind(value) != null) residueRecords.push(index);
57
+ }
58
+ }
59
+ this.#records.push(record);
60
+ }
61
+ const selection = graph.selectLines(fidelity); this.#residue = selection.residue;
62
+ for (const index of residueRecords) {
63
+ const text = await this.#raw(index), raw = text.endsWith('\n') ? text.slice(0, -1) : text;
64
+ const value = parseNativeJson(raw);
65
+ captureNativeResidue(this.#meta, 'claude_code', index, raw, value, claudeResidueKind(value));
66
+ }
67
+ let pending = [], pendingId = null;
68
+ for (const index of selection.lines) {
69
+ const record = this.#records[index];
70
+ if (record.assistant && !record.error) {
71
+ if (pending.length && !(pendingId != null && pendingId === record.messageId)) { await this.#addGroup(pending); pending = []; }
72
+ pendingId = record.messageId; pending.push(index);
73
+ } else {
74
+ await this.#addGroup(pending); pending = []; pendingId = null;
75
+ if (!record.error || !record.assistant) await this.#addGroup([index]);
76
+ }
77
+ }
78
+ await this.#addGroup(pending);
79
+ reorderClaudeToolResults(this.#skeleton); ensureToolResultsPaired(this.#skeleton);
80
+ }
81
+
82
+ async #raw(index) {
83
+ const record = this.#records[index];
84
+ return decodeSessionUtf8(await this.#reader.read(record.offset, record.length));
85
+ }
86
+
87
+ async #decodeGroup(group) {
88
+ const messages = []; let pending = null;
89
+ for (const index of group) {
90
+ const value = parseNativeJson(await this.#raw(index)), kind = stringField(value, 'type');
91
+ if (kind === 'assistant') {
92
+ if (pending == null) pending = value; else mergeClaudeAssistantChunk(pending, value);
93
+ } else {
94
+ const before = messages.length;
95
+ if (kind === 'user') pushClaudeUser(value, messages);
96
+ else if (kind === 'attachment') pushClaudeAttachment(value, messages);
97
+ else if (kind === 'system') pushClaudeSystem(value, messages);
98
+ captureClaudeProvenance(value, messages, before);
99
+ }
100
+ }
101
+ flushClaudeAssistant(pending, messages);
102
+ return messages;
103
+ }
104
+
105
+ async #addGroup(group) {
106
+ if (!group.length) return;
107
+ const groupId = this.#groups.length, decoded = await this.#decodeGroup(group);
108
+ this.#groups.push(group);
109
+ decoded.forEach((message, ordinal) => {
110
+ if (message.content != null) message.content = nativeTrim(message.content) === '' ? '' : 'x';
111
+ if (message.content_parts != null) message.content_parts = message.content_parts.length ? [null] : [];
112
+ for (const call of message.tool_calls ?? []) call.function.arguments = '';
113
+ message.metadata = Object.create(null); message.metadata[REFERENCE] = `${groupId}:${ordinal}`;
114
+ this.#skeleton.push(message);
115
+ });
116
+ }
117
+
118
+ itemCount(from, end) {
119
+ if (!Number.isSafeInteger(from) || !Number.isSafeInteger(end) || from < 0 || end < from || end > this.length) throw new RangeError('invalid Claude message range');
120
+ let count = 0;
121
+ for (let index = from; index < end; index++) {
122
+ const message = this.#skeleton[index];
123
+ count += (['user', 'assistant', 'tool'].includes(message.role) && messageHasContent(message) ? 1 : 0) + (message.tool_calls?.length ?? 0);
124
+ }
125
+ return count;
126
+ }
127
+
128
+ async readMessages(indices) {
129
+ const session = createSession('claude_code'); session.meta = cloneNativeJson(this.#meta);
130
+ let cachedId = null, cached = null;
131
+ for (const index of indices) {
132
+ if (!Number.isSafeInteger(index) || index < 0 || index >= this.length) throw new RangeError('invalid Claude message position');
133
+ const message = this.#skeleton[index], reference = message.metadata[REFERENCE];
134
+ if (reference === undefined) session.messages.push(cloneNativeJson(message));
135
+ else {
136
+ const [group, ordinal] = reference.split(':').map(Number);
137
+ if (cachedId !== group) { cached = await this.#decodeGroup(this.#groups[group]); cachedId = group; }
138
+ session.messages.push(cloneNativeJson(cached[ordinal]));
139
+ }
140
+ }
141
+ session.imported_message_count = this.length; session.raw_trailing_newline = false;
142
+ session.parse_error_lines = this.#parseErrors; session.load_residue = [...this.#residue];
143
+ return session;
144
+ }
145
+
146
+ async readSummary() {
147
+ const indices = new Set();
148
+ for (const assistantOnly of [false, true]) {
149
+ let first = null, last = null;
150
+ this.#skeleton.forEach((message, index) => {
151
+ if ((message.role === 'assistant' || (!assistantOnly && message.role === 'user')) && messageHasContent(message)) {
152
+ first ??= index; last = index;
153
+ }
154
+ });
155
+ if (first != null) indices.add(first);
156
+ if (last != null) indices.add(last);
157
+ }
158
+ const latest = this.#skeleton.findLastIndex((message) => message.role !== 'system');
159
+ if (latest !== -1) indices.add(latest);
160
+ return this.readMessages([...indices].sort((a, b) => a - b));
161
+ }
162
+ }
163
+
164
+ /** The index and its file handle live only inside this callback. Payloads
165
+ * are decoded one assistant group at a time. Handle and path fingerprints
166
+ * are verified before the result leaves the scope; any change refuses it. */
167
+ export async function withClaudeReadIndex(path, work, { fidelity = 'byte_lossless', metrics } = {}) {
168
+ if (!['byte_lossless', 'value_lossless', 'semantic'].includes(fidelity)) throw new TypeError(`unknown session fidelity: ${fidelity}`);
169
+ return withSessionFile(path, async (reader) => {
170
+ const index = new ClaudeReadIndex(reader); await index.open(fidelity);
171
+ return work(index);
172
+ }, metrics);
173
+ }
@@ -0,0 +1,73 @@
1
+ import { parseNativeJson } from './native-json.mjs';
2
+ import { createSession } from './session-model.mjs';
3
+ import { splitLinesVerbatim } from './session-lines.mjs';
4
+ import { nativeTrim } from './text.mjs';
5
+ import { field, stringField, ensureToolResultsPaired } from './session-codec-utils.mjs';
6
+ import { restoreTopLevelResidue, captureNativeResidue, claudeResidueKind } from './session-native-residue.mjs';
7
+ import { ClaudeReplayIndex } from './session-claude-graph.mjs';
8
+ import { captureClaudeProvenance, pushClaudeUser, pushClaudeAssistant, pushClaudeAttachment, pushClaudeSystem, mergeClaudeAssistantChunk, flushClaudeAssistant, reorderClaudeToolResults } from './session-claude-content.mjs';
9
+
10
+ export function captureClaudeMeta(value, meta, rawLine) {
11
+ restoreTopLevelResidue(value, meta);
12
+ meta.session_id ??= stringField(value, 'sessionId');
13
+ meta.cwd ??= stringField(value, 'cwd');
14
+ meta.model ??= stringField(field(value, 'message'), 'model');
15
+ if (stringField(value, 'type') === 'fork-context-ref' && !Object.hasOwn(meta.lineage, 'claude_fork_context_ref_raw')) meta.lineage.claude_fork_context_ref_raw = rawLine;
16
+ }
17
+
18
+ /** Full native transcript is retained verbatim. Only the graph-selected
19
+ * replay branch becomes messages; semantic degradation is named separately. */
20
+ export function fromClaudeCodeStr(text, { fidelity = 'byte_lossless' } = {}) {
21
+ if (!['byte_lossless', 'value_lossless', 'semantic'].includes(fidelity)) throw new TypeError(`unknown session fidelity: ${fidelity}`);
22
+ const session = createSession('claude_code'), { meta, messages } = session, capture = splitLinesVerbatim(text);
23
+ session.raw = capture.lines; session.raw_trailing_newline = capture.trailingNewline; session.raw_is_verbatim = true;
24
+ const graph = new ClaudeReplayIndex(), residueLines = [];
25
+ capture.lines.forEach((line, index) => {
26
+ if (nativeTrim(line) === '') return;
27
+ let value;
28
+ try { value = parseNativeJson(line); } catch { session.parse_error_lines++; return; }
29
+ captureClaudeMeta(value, meta, line);
30
+ graph.observe(index, value);
31
+ if (claudeResidueKind(value) != null) residueLines.push(index);
32
+ });
33
+ const selection = graph.selectLines(fidelity);
34
+ session.load_residue = selection.residue;
35
+ for (const index of residueLines) {
36
+ const value = parseNativeJson(capture.lines[index]);
37
+ captureNativeResidue(meta, 'claude_code', index, capture.lines[index], value, claudeResidueKind(value));
38
+ }
39
+ let pending = null;
40
+ for (const index of selection.lines) {
41
+ const value = parseNativeJson(capture.lines[index]), kind = stringField(value, 'type');
42
+ if (kind === 'assistant') {
43
+ if (field(value, 'isApiErrorMessage') === true) { flushClaudeAssistant(pending, messages); pending = null; continue; }
44
+ if (pending != null) {
45
+ const messageId = stringField(field(pending, 'message'), 'id');
46
+ if (messageId != null && stringField(field(value, 'message'), 'id') === messageId) { mergeClaudeAssistantChunk(pending, value); continue; }
47
+ flushClaudeAssistant(pending, messages);
48
+ }
49
+ pending = value; continue;
50
+ }
51
+ flushClaudeAssistant(pending, messages); pending = null;
52
+ const before = messages.length;
53
+ if (kind === 'user') pushClaudeUser(value, messages);
54
+ else if (kind === 'assistant') pushClaudeAssistant(value, messages);
55
+ else if (kind === 'attachment') pushClaudeAttachment(value, messages);
56
+ else if (kind === 'system') pushClaudeSystem(value, messages);
57
+ captureClaudeProvenance(value, messages, before);
58
+ }
59
+ flushClaudeAssistant(pending, messages);
60
+ reorderClaudeToolResults(messages); ensureToolResultsPaired(messages);
61
+ // Keep the native creating-record lookup: compact literal UUID spelling,
62
+ // last raw occurrence wins, and synthetic results have no source record.
63
+ const records = new Map();
64
+ capture.lines.forEach((line, index) => {
65
+ const marker = line.indexOf('"uuid":"');
66
+ if (marker === -1) return;
67
+ const from = marker + 8, end = line.indexOf('"', from);
68
+ if (end !== -1) records.set(line.slice(from, end), index);
69
+ });
70
+ meta.message_records = messages.map((message) => records.get(message.metadata.claude_uuid) ?? null);
71
+ session.imported_message_count = messages.length;
72
+ return session;
73
+ }
@@ -0,0 +1,115 @@
1
+ import { createMessage, deserializeMessage, markToolError, ROLES } from './message.mjs';
2
+ import { cloneNativeJson, nativeInteger, nativeObjectKeys, stringifyNativeJsonMember } from './native-json.mjs';
3
+
4
+ export const isObject = (value) => value != null && typeof value === 'object' && !Array.isArray(value);
5
+ export const field = (value, key) => isObject(value) && Object.hasOwn(value, key) ? value[key] : undefined;
6
+ export const stringField = (value, key) => typeof field(value, key) === 'string' ? value[key] : null;
7
+ export function valueString(value, key) { return typeof value[key] === 'string' ? value[key] : stringifyNativeJsonMember(value, key); }
8
+ export const functionCall = (id, name, args) => ({ id, type: 'function', function: { name, arguments: args } });
9
+ export function toolMessage(id, content) { const result = createMessage('tool', content); result.tool_call_id = id; return result; }
10
+
11
+ export function extractTextContent(value, key) {
12
+ const content = field(value, key);
13
+ if (content === undefined) return '';
14
+ if (typeof content === 'string') return content;
15
+ if (Array.isArray(content)) return content.map((part) => typeof part === 'string' ? part : stringField(part, 'text')).filter((part) => part != null).join('\n');
16
+ return stringifyNativeJsonMember(value, key);
17
+ }
18
+
19
+ /** Native loaders retain unanswered calls and stamp the existing structured
20
+ * interrupted outcome; they never manufacture a successful tool receipt. */
21
+ export function ensureToolResultsPaired(messages) {
22
+ const answered = new Set(messages.filter((message) => message.role === 'tool' && message.tool_call_id != null).map((message) => message.tool_call_id));
23
+ if (!messages.some((message) => (message.tool_calls ?? []).some((call) => call.id !== '' && !answered.has(call.id)))) return;
24
+ const result = [];
25
+ for (const message of messages) {
26
+ result.push(message);
27
+ for (const call of message.tool_calls ?? []) {
28
+ if (call.id === '' || answered.has(call.id)) continue;
29
+ const missing = toolMessage(call.id, '[no tool result recorded — turn interrupted]');
30
+ missing.name = call.function.name;
31
+ markToolError(missing);
32
+ result.push(missing);
33
+ }
34
+ }
35
+ messages.length = 0;
36
+ for (const message of result) messages.push(message);
37
+ }
38
+
39
+ const recordKey = '__supercode_record';
40
+ export function stampMessageRecords(messages, record) {
41
+ for (let index = messages.length - 1; index >= 0; index--) {
42
+ if (Object.hasOwn(messages[index].metadata, recordKey)) break;
43
+ messages[index].metadata[recordKey] = String(record);
44
+ }
45
+ }
46
+ export function takeMessageRecords(messages) {
47
+ return messages.map((message) => {
48
+ const value = message.metadata[recordKey];
49
+ delete message.metadata[recordKey];
50
+ if (typeof value !== 'string' || !/^\+?\d+$/.test(value)) return null;
51
+ const record = BigInt(value);
52
+ if (record > 18446744073709551615n) return null;
53
+ return record <= BigInt(Number.MAX_SAFE_INTEGER) ? Number(record) : record;
54
+ });
55
+ }
56
+
57
+ /** A display tail keeps up to two human boundaries from before the numeric
58
+ * window. This is a bounded view, never the input to a continuation writer. */
59
+ export function truncateMessagesWithAnchor(messages, messageLimit, precedingUsers = []) {
60
+ const limit = Math.max(1, messageLimit), rawTail = Math.max(0, messages.length - limit);
61
+ const anchorLimit = messages.length < limit ? Math.min(limit - messages.length, 2) : Math.min(limit, 2);
62
+ const candidates = [...precedingUsers];
63
+ for (let index = 0; index < rawTail; index++) if (messages[index].role === 'user') candidates.push(messages[index]);
64
+ const anchors = candidates.slice(Math.max(0, candidates.length - anchorLimit)).map(cloneNativeJson);
65
+ if (messages.length <= limit && !anchors.length) return;
66
+ const tailCount = Math.min(Math.max(0, limit - anchors.length), messages.length), tail = messages.slice(messages.length - tailCount);
67
+ messages.length = 0;
68
+ for (const message of anchors) messages.push(message);
69
+ for (const message of tail) messages.push(message);
70
+ }
71
+
72
+ function restoreFields(extension, message, { calls }) {
73
+ const role = field(extension, 'role');
74
+ if (ROLES.includes(role)) message.role = role;
75
+ message.content = stringField(extension, 'content');
76
+ const parts = field(extension, 'content_parts');
77
+ message.content_parts = Array.isArray(parts) ? cloneNativeJson(parts) : null;
78
+ if (calls) {
79
+ try { message.tool_calls = deserializeMessage({ role: message.role, tool_calls: field(extension, 'tool_calls') }).tool_calls; }
80
+ catch { message.tool_calls = null; }
81
+ }
82
+ message.tool_call_id = stringField(extension, 'tool_call_id');
83
+ message.name = stringField(extension, 'name');
84
+ message.metadata = Object.create(null);
85
+ }
86
+ function restoreMetadata(extension, message) {
87
+ const metadata = field(extension, 'metadata');
88
+ if (!isObject(metadata)) return;
89
+ for (const key of nativeObjectKeys(metadata)) {
90
+ if (typeof metadata[key] === 'string') message.metadata[key] = metadata[key];
91
+ }
92
+ }
93
+
94
+ export function restoreGeminiMessageExtension(value, message) {
95
+ const extension = field(value, '_supercode_gemini_message');
96
+ if (nativeInteger(extension, 'schema') !== 1n) return;
97
+ restoreFields(extension, message, { calls: true });
98
+ restoreMetadata(extension, message);
99
+ }
100
+
101
+ export function restoreGrokMessageExtension(value, message) {
102
+ const extension = field(value, '_supercode_grok_message');
103
+ if (extension === undefined) return;
104
+ const openTurn = message.metadata.__codex_open_turn, turnId = message.metadata.turn_id;
105
+ const extensionHasTurn = isObject(field(extension, 'metadata')) && Object.hasOwn(extension.metadata, 'turn_id');
106
+ if (nativeInteger(extension, 'schema') === 2n) restoreFields(extension, message, { calls: false });
107
+ restoreMetadata(extension, message);
108
+ const name = stringField(extension, 'name');
109
+ if (name != null) message.name = name;
110
+ if (openTurn !== undefined) message.metadata.__codex_open_turn = openTurn;
111
+ if (turnId !== undefined) {
112
+ message.metadata.turn_id = turnId;
113
+ if (!extensionHasTurn) message.metadata.__grok_remove_synthetic_turn_id = 'true';
114
+ }
115
+ }
@@ -0,0 +1,64 @@
1
+ import type { Session, ChatMessage, SessionSource } from './session-model.js';
2
+ import type { IndexMetrics, StoreRead } from './index.js';
3
+ export interface ReadLocator { harness: string; session_id: string; storage: { kind: 'file'; path: string } | { kind: 'sqlite'; path: string; selector: string } }
4
+ export interface SessionReadOptions { fidelity?: 'byte_lossless' | 'value_lossless' | 'semantic'; includeSubagents?: boolean; metrics?: IndexMetrics; env?: Record<string, string | undefined> }
5
+ export interface SessionFollowOptions extends SessionReadOptions { messageLimit?: number | null; maxMessageChars?: number | null; displayHistory?: boolean; selector?: string | null; store?: 'opencode' | 'goose' | 'hermes' }
6
+ /** Passive polling only: no timer, process control, or inferred activity. */
7
+ export class SessionFollower {
8
+ static open(path: string, options?: SessionFollowOptions): Promise<SessionFollower>;
9
+ static openLocator(locator: ReadLocator, options?: SessionFollowOptions): Promise<SessionFollower>;
10
+ poll(): Promise<Record<string, unknown> | null>;
11
+ close(): void;
12
+ }
13
+ export interface SessionFileRead<T> {
14
+ result: T;
15
+ fingerprint: { device: string; inode: string; size: string; mtime_ns: string };
16
+ size: number;
17
+ modified_ms: number | null;
18
+ }
19
+ /** Pure document readers. They retain raw source text and native extensions;
20
+ * these are not service entrypoints or format-auto-detection fallbacks. */
21
+ export function fromGeminiStr(text: string): Session;
22
+ export function fromGooseStr(text: string): Session;
23
+ /** Grok's document view; directory/summary overlays belong to the path reader. */
24
+ export function fromGrokStr(text: string): Session;
25
+ /** Strict graph reconstruction by default; semantic views name degradation. */
26
+ export function fromClaudeCodeStr(text: string, options?: { fidelity?: 'byte_lossless' | 'value_lossless' | 'semantic' }): Session;
27
+ /** Attaches native subagent files, preserving strict versus semantic refusal. */
28
+ export function readClaudeCodeFile(path: string, options?: { fidelity?: 'byte_lossless' | 'value_lossless' | 'semantic'; metrics?: IndexMetrics }): Promise<SessionFileRead<Session>>;
29
+ export interface ClaudeReadIndex {
30
+ readonly length: number;
31
+ readonly rawRecordCount: number;
32
+ itemCount(from: number, end: number): number;
33
+ readMessages(indices: Iterable<number>): Promise<Session>;
34
+ readSummary(): Promise<Session>;
35
+ }
36
+ export function supportsClaudeReadIndex(path: string, options?: { metrics?: IndexMetrics }): Promise<boolean>;
37
+ /** The index is valid only during the callback; no source file handle escapes. */
38
+ export function withClaudeReadIndex<T>(path: string, work: (index: ClaudeReadIndex) => T | Promise<T>, options?: { fidelity?: 'byte_lossless' | 'value_lossless' | 'semantic'; metrics?: IndexMetrics }): Promise<SessionFileRead<T>>;
39
+ /** Resumable model context, including compaction replacement by default. */
40
+ export function fromCodexStr(text: string, options?: { applyCompaction?: boolean }): Session;
41
+ /** Typed human-visible history; bounded display values cannot be exported. */
42
+ export function fromCodexDisplayStr(text: string, messageLimit: number): Session;
43
+ export function fromPiStr(text: string): Session;
44
+ /** Read-only dialect: leaf redirects, side appends and inert vendor metadata. */
45
+ export function fromOpenClawStr(text: string): Session;
46
+ /** Envelope JSONL or the native export document; synthesized raw is named. */
47
+ export function fromOpenCodeStr(text: string): Session;
48
+ export function resolveOpenCodeParentToolUseIds(sessions: Session[]): void;
49
+ export function readOpenCodeSqlite(path: string, sessionId?: string | null, options?: { metrics?: IndexMetrics; onDiagnostic?: (detail: { message: string; column?: string; context?: string; path?: string; error: string }) => void }): Promise<StoreRead<Session>>;
50
+ export function detectSessionSource(text: string): SessionSource | null;
51
+ export function fromNativeStr(text: string): Session;
52
+ export function fromSidecarStr(text: string): Session;
53
+ export function fromNativeMessages(messages: ChatMessage[]): Session;
54
+ export function loadNativeStoreFamily(path: string, options?: SessionReadOptions): Promise<Session | null>;
55
+ export function detectOpenCodeStorageSurface(root: string, options?: Pick<SessionReadOptions, 'env'>): Promise<{ kind: 'sqlite' | 'json_tree_a' | 'json_tree_b'; path: string } | null>;
56
+ export function loadSessionPath(path: string, options?: SessionReadOptions): Promise<Session>;
57
+ export function loadSessionLocator(locator: ReadLocator, options?: SessionReadOptions): Promise<Session>;
58
+ export function loadSessionDisplayPath(path: string, messageLimit: number, options?: SessionReadOptions): Promise<Session>;
59
+ export function loadSessionDisplayLocator(locator: ReadLocator, messageLimit: number, options?: SessionReadOptions): Promise<Session>;
60
+ export function loadSessionDisplayFrom(locator: ReadLocator, fromByte: number, options?: SessionReadOptions): Promise<Session>;
61
+ /** Verified read-only DB/WAL snapshot; a changed source refuses the read. */
62
+ export function readGooseSqlite(path: string, sessionId: string, options?: { messageLimit?: number | null; metrics?: IndexMetrics }): Promise<StoreRead<Session>>;
63
+ /** Active native rows form the replay view; all rows remain in raw. No write tier. */
64
+ export function readHermesSqlite(path: string, sessionId?: string | null, options?: { metrics?: IndexMetrics }): Promise<StoreRead<Session>>;
@@ -0,0 +1,19 @@
1
+ // Importable document/store readers and passive following, shared by the
2
+ // caller-owned read service. There is no native parser fallback.
3
+ export { fromGeminiStr } from './session-gemini.mjs';
4
+ export { fromGooseStr, readGooseSqlite } from './session-goose.mjs';
5
+ export { readHermesSqlite } from './session-hermes.mjs';
6
+ export { fromGrokStr } from './session-grok.mjs';
7
+ export { fromClaudeCodeStr } from './session-claude.mjs';
8
+ export { readClaudeCodeFile } from './session-claude-files.mjs';
9
+ export { withClaudeReadIndex, supportsClaudeReadIndex } from './session-claude-index.mjs';
10
+ export { fromCodexStr } from './session-codex.mjs';
11
+ export { fromCodexDisplayStr } from './session-codex-display.mjs';
12
+ export { fromPiStr, fromOpenClawStr } from './session-pi.mjs';
13
+ export { fromOpenCodeStr, resolveOpenCodeParentToolUseIds } from './session-opencode.mjs';
14
+ export { readOpenCodeSqlite } from './session-opencode-store.mjs';
15
+ export { fromNativeStr, fromSidecarStr, fromNativeMessages } from './session-native.mjs';
16
+ export { detectSessionSource } from './session-source.mjs';
17
+ export { loadSessionPath, loadSessionLocator, loadNativeStoreFamily, detectOpenCodeStorageSurface } from './session-path.mjs';
18
+ export { loadSessionDisplayPath, loadSessionDisplayLocator, loadSessionDisplayFrom } from './session-display.mjs';
19
+ export { SessionFollower } from './session-follower.mjs';
@@ -0,0 +1,68 @@
1
+ import { SaxesParser } from 'saxes';
2
+ import { field, stringField } from './session-codec-utils.mjs';
3
+ import { nativeTrim } from './text.mjs';
4
+
5
+ const asciiLower = (value) => value.replace(/[A-Z]/g, (letter) => letter.toLowerCase());
6
+ const markers = [
7
+ ['# AGENTS.md instructions', '</INSTRUCTIONS>'], ['<environment_context>', '</environment_context>'],
8
+ ['<agent_message_board_notification>', '</agent_message_board_notification>'], ['<skill>', '</skill>'],
9
+ ['<user_shell_command>', '</user_shell_command>'], ['<turn_aborted>', '</turn_aborted>'],
10
+ ['<subagent_notification>', '</subagent_notification>'], ['<recommended_plugins>', '</recommended_plugins>'],
11
+ ];
12
+
13
+ function standardContext(raw) {
14
+ const text = nativeTrim(raw), folded = asciiLower(text);
15
+ if (markers.some(([first, last]) => folded.startsWith(asciiLower(first)) && folded.endsWith(asciiLower(last)))) return true;
16
+ if (text.startsWith('<external_')) {
17
+ const end = text.indexOf('>', 10);
18
+ if (end !== -1 && text.slice(end + 1).endsWith(`</external_${text.slice(10, end)}>`)) return true;
19
+ }
20
+ if (text.startsWith('<goal_context>') && text.endsWith('</goal_context>')) return true;
21
+ const internal = /^<codex_internal_context source="([a-z][a-z0-9_]*)">/.exec(text);
22
+ if (internal && text.endsWith('</codex_internal_context>')) return true;
23
+ return text.startsWith('Warning: The maximum number of unified exec processes you can keep open is')
24
+ || (text.startsWith('Warning: apply_patch was requested via ') && text.endsWith('Use the apply_patch tool instead of exec_command.'))
25
+ || text.startsWith('Warning: Your account was flagged for potentially high-risk cyber activity');
26
+ }
27
+
28
+ // quick_xml's struct deserializer consumes the first element, ignores its
29
+ // name/unknown fields, and requires both @hook_run_id and $text. XML parsing
30
+ // handles quoting, entities and CDATA; a prefix regex cannot establish this.
31
+ function parseHook(raw) {
32
+ const parser = new SaxesParser(), complete = {}, segments = [];
33
+ let depth = 0, id = null, text = '', hasText = false;
34
+ const flush = () => { if (hasText) segments.push(text); text = ''; hasText = false; };
35
+ parser.on('opentag', (tag) => {
36
+ if (depth === 0) id = typeof tag.attributes.hook_run_id === 'string' ? tag.attributes.hook_run_id : null;
37
+ else if (depth === 1) flush();
38
+ depth++;
39
+ });
40
+ const take = (part) => { if (depth === 1) { text += part; hasText = true; } };
41
+ parser.on('text', take); parser.on('cdata', take);
42
+ parser.on('closetag', () => {
43
+ depth--;
44
+ if (depth === 0) { flush(); throw complete; }
45
+ });
46
+ try { parser.write(nativeTrim(raw)).close(); }
47
+ catch (error) { if (error !== complete) return null; }
48
+ return id != null && nativeTrim(id) !== '' && segments.length === 1 ? { text: segments[0], hook_run_id: id } : null;
49
+ }
50
+
51
+ /** The same classification serves full model metadata and display history.
52
+ * Typed native user events/items have already crossed this boundary. */
53
+ export function codexUserContext(item) {
54
+ const content = field(item, 'content');
55
+ if (!Array.isArray(content)) return { kind: 'ordinary' };
56
+ let contextual = false, hooksOnly = true;
57
+ const hooks = [];
58
+ for (const part of content) {
59
+ const text = stringField(part, 'type') === 'input_text' ? stringField(part, 'text') : null;
60
+ if (text == null) { hooksOnly = false; continue; }
61
+ const hook = parseHook(text);
62
+ if (hook != null) { contextual = true; hooks.push(hook); }
63
+ else if (standardContext(text)) contextual = true;
64
+ else hooksOnly = false;
65
+ }
66
+ return hooksOnly && hooks.length ? { kind: 'hook_prompt', item: { type: 'HookPrompt', id: field(item, 'id') ?? null, fragments: hooks } }
67
+ : { kind: contextual ? 'hidden' : 'ordinary' };
68
+ }
@@ -0,0 +1,89 @@
1
+ import { createMessage, markToolError } from './message.mjs';
2
+ import { nativeInteger, parseNativeJson, stringifyNativeJson, stringifyNativeJsonMember } from './native-json.mjs';
3
+ import { field, stringField, toolMessage, functionCall } from './session-codec-utils.mjs';
4
+ import { pushCodexItem, pushCodexAssistant } from './session-codex-items.mjs';
5
+
6
+ const asciiLower = (value) => value.replace(/[A-Z]/g, (letter) => letter.toLowerCase());
7
+ const render = (value, key) => field(value, key) == null ? '' : typeof field(value, key) === 'string' ? value[key] : stringifyNativeJsonMember(value, key);
8
+ const selectedJson = (value, keys) => `{${keys.map((key) => `${JSON.stringify(key)}:${field(value, key) === undefined ? 'null' : stringifyNativeJsonMember(value, key)}`).join(',')}}`;
9
+
10
+ export function codexItemKind(item) {
11
+ const kind = asciiLower(stringField(item, 'type') ?? '');
12
+ if (kind !== 'extension') return kind;
13
+ const known = { 'image_gen.generation': 'imagegeneration', 'clock.sleep': 'sleep', 'web.search': 'websearch' };
14
+ const extension = stringField(item, 'kind');
15
+ return extension != null && Object.hasOwn(known, extension) ? known[extension] : 'extension';
16
+ }
17
+
18
+ function contentText(value, key) {
19
+ return contentValue(field(value, key), value, key);
20
+ }
21
+
22
+ function contentValue(content, parent, key) {
23
+ if (Array.isArray(content)) return content.map((part, index) => contentValue(part, content, String(index))).filter((text) => text !== '').join('\n');
24
+ if (content != null && typeof content === 'object') {
25
+ if (field(content, 'text') !== undefined) return contentText(content, 'text');
26
+ if (field(content, 'content') !== undefined) return contentText(content, 'content');
27
+ }
28
+ return content == null ? '' : typeof content === 'string' ? content : stringifyNativeJsonMember(parent, key);
29
+ }
30
+
31
+ export function codexContentItem(item) {
32
+ const kind = codexItemKind(item); let type = 'text_delta', text;
33
+ if (kind === 'agentmessage') text = field(item, 'text') === undefined ? contentText(item, 'content') : render(item, 'text');
34
+ else if (kind === 'reasoning') { type = 'reasoning'; text = contentText(item, field(item, 'summary') === undefined ? 'summary_text' : 'summary'); }
35
+ else if (kind === 'plan') { const body = render(item, 'text'); if (body === '') return null; text = `\n\n**Plan**\n\n${body}\n\n`; }
36
+ else if (kind === 'hookprompt') text = `\n\n**Hook context**\n\n${contentText(item, 'fragments')}\n\n`;
37
+ else if (kind === 'subagentactivity') text = `\n\n**Subagent activity** · ${render(item, field(item, 'agentPath') === undefined ? 'agent_path' : 'agentPath')} · ${render(item, 'kind')}\n\n`;
38
+ else if (kind === 'contextcompaction') text = '\n\n_Context compacted._\n\n';
39
+ else return null;
40
+ return text === '' ? null : { type, id: field(item, 'id') ?? null, text };
41
+ }
42
+
43
+ function commandOutput(item) {
44
+ const key = field(item, 'aggregatedOutput') === undefined ? 'aggregated_output' : 'aggregatedOutput';
45
+ const aggregate = field(item, key) != null;
46
+ let output = render(item, aggregate ? key : 'stdout');
47
+ const stderr = render(item, 'stderr');
48
+ if (stderr !== '' && (!aggregate || !output.includes(stderr))) output += stderr;
49
+ return output;
50
+ }
51
+
52
+ function toolItem(item) {
53
+ const id = stringField(item, 'id'); if (id == null) return null;
54
+ const kind = codexItemKind(item); let name, args, output;
55
+ if (kind === 'commandexecution') { name = 'Shell'; args = selectedJson(item, ['command', 'cwd']); output = commandOutput(item); }
56
+ else if (kind === 'filechange') { name = 'apply_patch'; args = selectedJson(item, ['changes']); output = field(item, 'stdout') !== undefined || field(item, 'stderr') !== undefined ? render(item, 'stdout') + render(item, 'stderr') : render(item, 'changes'); }
57
+ else if (kind === 'mcptoolcall') { name = `${render(item, 'server')} · ${render(item, 'tool')}`; args = field(item, 'arguments') === undefined ? 'null' : stringifyNativeJsonMember(item, 'arguments'); output = render(item, field(item, 'error') != null ? 'error' : 'result'); }
58
+ else if (kind === 'dynamictoolcall') { name = render(item, 'tool'); args = field(item, 'arguments') === undefined ? 'null' : stringifyNativeJsonMember(item, 'arguments'); output = render(item, field(item, 'contentItems') === undefined ? 'content_items' : 'contentItems'); }
59
+ else if (kind === 'collabagenttoolcall') { name = render(item, 'tool'); args = stringifyNativeJson(item); output = render(item, field(item, 'agentsStates') === undefined ? 'agents_states' : 'agentsStates'); }
60
+ else if (kind === 'websearch') { name = 'Web search'; args = selectedJson(item, ['query', 'action']); output = render(item, 'results'); }
61
+ else if (kind === 'imageview') { name = 'View image'; args = selectedJson(item, ['path']); output = ''; }
62
+ else if (kind === 'imagegeneration') { name = 'Generate image'; args = stringifyNativeJson(item); output = render(item, field(item, 'failure') != null ? 'failure' : 'result'); }
63
+ else return null;
64
+ const exitKey = field(item, 'exitCode') === undefined ? 'exit_code' : 'exitCode';
65
+ const exit = nativeInteger(item, exitKey, { signed: true });
66
+ const error = ['failed', 'declined', 'denied', 'error'].includes(asciiLower(render(item, 'status'))) || (exit != null && exit !== 0n) || field(item, 'error') != null || field(item, 'success') === false;
67
+ return { id, name, args, output, error };
68
+ }
69
+
70
+ export function pushCodexDisplayItem(item, out) {
71
+ if (codexItemKind(item) === 'usermessage') {
72
+ // Preserve parsed numeric facts in any source parts.
73
+ const content = field(item, 'content') === undefined ? 'null' : stringifyNativeJsonMember(item, 'content');
74
+ pushCodexItem(parseNativeJson(`{"type":"message","role":"user","content":${content}}`), out);
75
+ return;
76
+ }
77
+ const content = codexContentItem(item);
78
+ if (content != null) {
79
+ const message = createMessage('assistant', content.text);
80
+ if (content.type === 'reasoning') { message.content = null; message.content_parts = [{ type: 'reasoning', text: content.text }]; }
81
+ const phase = stringField(item, 'phase'); if (phase != null) message.metadata.phase = phase;
82
+ out.push(message); return;
83
+ }
84
+ const tool = toolItem(item); if (tool == null) return;
85
+ pushCodexAssistant(out, '', [functionCall(tool.id, tool.name, tool.args)]);
86
+ const message = toolMessage(tool.id, tool.output); message.name = tool.name;
87
+ if (tool.error) { markToolError(message); message.content = null; message.content_parts = [{ type: 'text', text: tool.output, is_error: true }]; }
88
+ out.push(message);
89
+ }