@volter/supercode-interchange 0.1.1 → 0.1.2

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,81 @@
1
+ import { createMessage } from './message.mjs';
2
+ import { parseNativeJson } from './native-json.mjs';
3
+ import { createSession } from './session-model.mjs';
4
+ import { nonEmptyLines } from './session-lines.mjs';
5
+ import { field, stringField, extractTextContent, restoreGrokMessageExtension, truncateMessagesWithAnchor } from './session-codec-utils.mjs';
6
+ import { captureCodexMeta } from './session-codex.mjs';
7
+ import { codexUserWithImages, pushCodexItem, pushCodexAssistant, stampCodexMessages, clearCodexBookkeeping } from './session-codex-items.mjs';
8
+ import { codexUserContext } from './session-codex-context.mjs';
9
+ import { pushCodexDisplayItem } from './session-codex-display-items.mjs';
10
+ import { nativeTrim } from './text.mjs';
11
+
12
+ /** Bounded human-visible history, never a continuation or export artifact.
13
+ * Durable TurnItems and legacy typed-user events retain their distinct paths. */
14
+ export function fromCodexDisplayStr(text, messageLimit) {
15
+ if (!Number.isSafeInteger(messageLimit) || messageLimit < 0) throw new RangeError('invalid Codex display message limit');
16
+ const session = createSession('codex'), { meta, messages } = session, precedingUsers = [], canonicalTexts = new Set();
17
+ const retain = Math.min(Number.MAX_SAFE_INTEGER, Math.max(messageLimit, 1) + 64);
18
+ let paginated = false, recordCount = 0, total = 0;
19
+ for (const line of nonEmptyLines(text)) {
20
+ recordCount++;
21
+ let value; try { value = parseNativeJson(line); } catch { session.parse_error_lines++; continue; }
22
+ const kind = stringField(value, 'type'), payload = field(value, 'payload'), type = stringField(payload, 'type'), timestamp = stringField(value, 'timestamp');
23
+ if (kind === 'session_meta') { paginated = stringField(payload, 'history_mode') === 'paginated'; captureCodexMeta(payload, meta); }
24
+ else if (kind === 'event_msg' && paginated && type === 'item_completed') {
25
+ const item = field(payload, 'item'), id = stringField(item, 'id');
26
+ if (id != null) {
27
+ const position = messages.findIndex((message) => message.metadata.codex_display_item === id), projected = [];
28
+ pushCodexDisplayItem(item, projected);
29
+ for (const message of projected) message.metadata.codex_display_item = id;
30
+ stampCodexMessages(projected, 0, timestamp);
31
+ let removed = 0;
32
+ for (let index = messages.length - 1; index >= 0; index--) if (messages[index].metadata.codex_display_item === id) { messages.splice(index, 1); removed++; }
33
+ total = Math.max(0, total - removed) + projected.length;
34
+ const at = Math.min(position === -1 ? messages.length : position, messages.length);
35
+ for (let index = 0; index < projected.length; index++) messages.splice(at + index, 0, projected[index]);
36
+ }
37
+ } else if (['response_item', 'event_msg'].includes(kind) && paginated) { /* Model stream is separate from display history. */ }
38
+ else if (kind === 'turn_context' && meta.model == null) meta.model = stringField(payload, 'model');
39
+ else if (kind === 'event_msg' && type === 'user_message') {
40
+ const urls = [];
41
+ for (const key of ['images', 'local_images']) {
42
+ const images = field(payload, key); if (Array.isArray(images)) for (const image of images) if (typeof image === 'string') urls.push(image);
43
+ }
44
+ const text = stringField(payload, 'message') ?? '';
45
+ messages.push(urls.length ? codexUserWithImages(text, urls) : createMessage('user', text)); total++;
46
+ } else if (kind === 'response_item' && type !== 'reasoning') {
47
+ const isAssistant = type === 'message' && stringField(payload, 'role') === 'assistant';
48
+ const assistantText = isAssistant ? extractTextContent(payload, 'content') : null;
49
+ if (assistantText != null && nativeTrim(assistantText) !== '') {
50
+ const duplicate = messages.findLastIndex((message) => Object.hasOwn(message.metadata, 'codex_event_message') && message.content === assistantText);
51
+ if (duplicate !== -1) { messages.splice(duplicate, 1); total = Math.max(0, total - 1); }
52
+ canonicalTexts.add(nativeTrim(assistantText));
53
+ }
54
+ const before = messages.length;
55
+ if (type === 'message' && stringField(payload, 'role') === 'user') {
56
+ const context = codexUserContext(payload);
57
+ if (context.kind === 'hook_prompt') pushCodexDisplayItem(context.item, messages);
58
+ } else pushCodexItem(payload, messages);
59
+ total += Math.max(0, messages.length - before); stampCodexMessages(messages, before, timestamp);
60
+ if (messages.length === before + 1) restoreGrokMessageExtension(payload, messages[before]);
61
+ } else if (kind === 'event_msg' && type === 'agent_message') {
62
+ const text = extractTextContent(payload, 'message');
63
+ if (nativeTrim(text) !== '' && !canonicalTexts.has(nativeTrim(text))) {
64
+ const before = messages.length; pushCodexAssistant(messages, text); total++;
65
+ messages.at(-1).metadata.codex_event_message = 'true';
66
+ const phase = stringField(payload, 'phase'); if (phase != null) messages.at(-1).metadata.phase = phase;
67
+ stampCodexMessages(messages, before, timestamp);
68
+ }
69
+ }
70
+ if (messages.length > retain) {
71
+ for (const message of messages.splice(0, messages.length - retain)) {
72
+ if (message.role !== 'user') continue;
73
+ precedingUsers.push(message); if (precedingUsers.length > 2) precedingUsers.shift();
74
+ }
75
+ }
76
+ }
77
+ clearCodexBookkeeping(messages, { display: true }); truncateMessagesWithAnchor(messages, messageLimit, precedingUsers);
78
+ session.raw = Array(recordCount).fill(''); session.raw_trailing_newline = text.endsWith('\n'); session.imported_message_count = total;
79
+ session.load_residue = ['display history is a bounded native-record projection, not resumable model context'];
80
+ return session;
81
+ }
@@ -0,0 +1,117 @@
1
+ import { createMessage, markToolOutcomeUnknown } from './message.mjs';
2
+ import { cloneNativeJson, stringifyNativeJson, stringifyNativeJsonMember } from './native-json.mjs';
3
+ import { field, stringField, extractTextContent, functionCall, toolMessage, valueString } from './session-codec-utils.mjs';
4
+ import { codexUserContext } from './session-codex-context.mjs';
5
+ import { nativeTrim } from './text.mjs';
6
+
7
+ export const codexTurnId = (payload) => stringField(field(payload, 'metadata'), 'turn_id');
8
+ export function codexImages(content) {
9
+ if (!Array.isArray(content)) return [];
10
+ const images = [];
11
+ for (const item of content) {
12
+ const url = stringField(item, 'image_url');
13
+ if (stringField(item, 'type') === 'input_image' && url != null && url !== '') images.push({ type: 'image_url', image_url: { url } });
14
+ }
15
+ return images;
16
+ }
17
+
18
+ export function codexUserWithImages(text, urls) {
19
+ const message = createMessage('user');
20
+ message.content_parts = [{ type: 'text', text }];
21
+ for (const url of urls) message.content_parts.push({ type: 'image_url', image_url: { url } });
22
+ return message;
23
+ }
24
+
25
+ export function pushCodexAssistant(out, text, calls = []) {
26
+ const hasText = nativeTrim(text) !== '';
27
+ if (!hasText && !calls.length) return;
28
+ const message = createMessage('assistant', hasText ? text : null); message.tool_calls = calls.length ? calls : null; out.push(message);
29
+ }
30
+
31
+ function mergeCall(payload, call, out) {
32
+ const last = out.at(-1), turn = codexTurnId(payload);
33
+ if (last?.role === 'assistant' && Object.hasOwn(last.metadata, '__codex_open_turn') && (turn == null || last.metadata.turn_id === turn)) {
34
+ last.tool_calls ??= []; last.tool_calls.push(call);
35
+ } else {
36
+ pushCodexAssistant(out, '', [call]);
37
+ if (turn != null) { out.at(-1).metadata.__codex_open_turn = 'true'; out.at(-1).metadata.turn_id = turn; }
38
+ }
39
+ }
40
+
41
+ export function pushCodexItem(payload, out) {
42
+ const kind = stringField(payload, 'type');
43
+ if (kind === 'local_shell_call') {
44
+ const id = field(payload, 'call_id') === undefined ? stringField(payload, 'id') : stringField(payload, 'call_id');
45
+ const action = cloneNativeJson(field(payload, 'action') ?? null);
46
+ if (action != null && typeof action === 'object' && !Array.isArray(action)) delete action.type;
47
+ pushCodexItem({ type: 'function_call', call_id: id ?? '', name: 'local_shell', arguments: stringifyNativeJson(action) }, out);
48
+ } else if (kind === 'message') {
49
+ const nativeRole = stringField(payload, 'role'), role = ['user', 'assistant'].includes(nativeRole) ? nativeRole : 'system';
50
+ const text = extractTextContent(payload, 'content'), images = codexImages(field(payload, 'content'));
51
+ const emptyAssistant = role === 'assistant' && nativeTrim(text) === '' && !images.length;
52
+ if (nativeTrim(text) === '' && !images.length && !emptyAssistant) return;
53
+ const message = createMessage(role, images.length || text === '' ? null : text);
54
+ if (images.length) {
55
+ message.content_parts = nativeTrim(text) === '' ? [] : [{ type: 'text', text }];
56
+ for (const image of images) message.content_parts.push(image);
57
+ }
58
+ if (role === 'assistant') {
59
+ const phase = stringField(payload, 'phase'); if (phase != null) message.metadata.phase = phase;
60
+ message.metadata.__codex_open_turn = 'true';
61
+ }
62
+ const turn = codexTurnId(payload); if (turn != null) message.metadata.turn_id = turn;
63
+ if (role === 'system') {
64
+ const subtype = stringField(field(payload, 'metadata'), 'claude_system_subtype'); if (subtype != null) message.metadata.systemSubtype = subtype;
65
+ }
66
+ if (emptyAssistant) message.metadata.empty_assistant_record = 'true';
67
+ out.push(message);
68
+ } else if (kind === 'function_call' || kind === 'tool_search_call') {
69
+ const id = stringField(payload, 'call_id') ?? '', rawName = stringField(payload, 'name') ?? '', namespace = stringField(payload, 'namespace');
70
+ const name = kind === 'tool_search_call' ? 'tool_search' : namespace != null && namespace !== '' && !rawName.startsWith(namespace) ? `${namespace}__${rawName}` : rawName;
71
+ mergeCall(payload, functionCall(id, name, field(payload, 'arguments') === undefined ? '{}' : valueString(payload, 'arguments')), out);
72
+ } else if (kind === 'function_call_output' || kind === 'custom_tool_call_output') {
73
+ const message = toolMessage(stringField(payload, 'call_id') ?? '', extractTextContent(payload, 'output'));
74
+ const images = codexImages(field(payload, 'output'));
75
+ if (images.length) {
76
+ message.content_parts = message.content === '' ? [] : [{ type: 'text', text: message.content }];
77
+ for (const image of images) message.content_parts.push(image);
78
+ }
79
+ markToolOutcomeUnknown(message); out.push(message);
80
+ } else if (kind === 'custom_tool_call') {
81
+ const id = stringField(payload, 'call_id') ?? '';
82
+ pushCodexAssistant(out, '', [functionCall(id, stringField(payload, 'name') ?? '', field(payload, 'input') === undefined ? '{}' : stringifyNativeJsonMember(payload, 'input'))]);
83
+ out.at(-1).metadata.codex_custom_tool_call_ids = stringifyNativeJson([id]);
84
+ } else if (kind === 'tool_search_output') out.push(toolMessage(stringField(payload, 'call_id') ?? '', field(payload, 'tools') === undefined ? '' : valueString(payload, 'tools')));
85
+ else if (kind === 'web_search_call') pushCodexAssistant(out, '[web_search]');
86
+ else if (kind === 'image_generation_call') pushCodexAssistant(out, nativeTrim(`[image_generation] ${stringField(payload, 'revised_prompt') ?? ''}`));
87
+ }
88
+
89
+ export function pushCodexModelItem(payload, out, contentItem) {
90
+ const before = out.length; pushCodexItem(payload, out);
91
+ if (stringField(payload, 'type') !== 'message' || stringField(payload, 'role') !== 'user') return;
92
+ const context = codexUserContext(payload); if (context.kind === 'ordinary') return;
93
+ const hookText = context.kind === 'hook_prompt' ? stringField(contentItem(context.item), 'text') : null;
94
+ for (let index = before; index < out.length; index++) {
95
+ out[index].metadata.codex_contextual_kind = context.kind;
96
+ if (hookText != null) out[index].metadata.codex_hook_prompt_text = hookText;
97
+ }
98
+ }
99
+
100
+ export function stampCodexMessages(messages, from, timestamp) {
101
+ if (timestamp == null) return;
102
+ for (let index = from; index < messages.length; index++) if (!Object.hasOwn(messages[index].metadata, 'timestamp')) messages[index].metadata.timestamp = timestamp;
103
+ }
104
+
105
+ export function removeLastCodexTurn(messages) {
106
+ const index = messages.findLastIndex((message) => message.role === 'user');
107
+ messages.length = index === -1 ? 0 : index;
108
+ if (messages.length) delete messages.at(-1).metadata.__codex_open_turn;
109
+ }
110
+
111
+ export function clearCodexBookkeeping(messages, { display = false } = {}) {
112
+ for (const message of messages) {
113
+ delete message.metadata.__codex_open_turn;
114
+ if (display) delete message.metadata.codex_event_message;
115
+ if (Object.hasOwn(message.metadata, '__grok_remove_synthetic_turn_id')) { delete message.metadata.__grok_remove_synthetic_turn_id; delete message.metadata.turn_id; }
116
+ }
117
+ }
@@ -0,0 +1,62 @@
1
+ import { messageWire } from './message.mjs';
2
+ import { nativeInteger, stringifyNativeJson } from './native-json.mjs';
3
+ import { field, stringField } from './session-codec-utils.mjs';
4
+ import { codexTurnId, codexUserWithImages } from './session-codex-items.mjs';
5
+ import { codexItemKind, pushCodexDisplayItem } from './session-codex-display-items.mjs';
6
+
7
+ export class CodexUserHistory {
8
+ turn = null;
9
+ pending = [];
10
+ legacyPending = null;
11
+ typed = new Map();
12
+
13
+ observeBoundary(record) {
14
+ const kind = stringField(record, 'type'), payload = field(record, 'payload'), type = stringField(payload, 'type');
15
+ if (!(kind === 'event_msg' && type === 'user_message')) this.legacyPending = null;
16
+ if (kind === 'compacted' || (kind === 'event_msg' && type === 'thread_rolled_back')) {
17
+ this.pending = []; this.typed.clear(); this.turn = null;
18
+ } else if (kind === 'event_msg' && type === 'task_started') { this.pending = []; this.turn = stringField(payload, 'turn_id'); }
19
+ else if (kind === 'event_msg' && type === 'task_complete') { this.pending = []; this.turn = null; }
20
+ else if (!((kind === 'response_item' && type === 'message' && stringField(payload, 'role') === 'user')
21
+ || (kind === 'event_msg' && (type === 'user_message' || (['item_started', 'item_completed'].includes(type) && codexItemKind(field(payload, 'item')) === 'usermessage'))))) this.pending = [];
22
+ }
23
+
24
+ recordModelInput(record, before, messages) {
25
+ const payload = field(record, 'payload');
26
+ if (stringField(payload, 'type') !== 'message' || stringField(payload, 'role') !== 'user') return;
27
+ const annotations = field(payload, 'internal_chat_message_metadata_passthrough'), kinds = field(annotations, 'content_item_kinds'), content = field(payload, 'content');
28
+ const order = field(field(record, 'metadata'), 'user_input_order');
29
+ const userOrigin = nativeInteger(field(record, 'metadata'), 'user_input_order') != null || (Array.isArray(kinds) && kinds.length > 0 && Array.isArray(content) && kinds.length === content.length && kinds.some((kind) => typeof kind === 'string' && kind.startsWith('user.')));
30
+ const turn = stringField(annotations, 'turn_id') ?? codexTurnId(payload);
31
+ if (order === undefined && field(annotations, 'content_item_kinds') === undefined && messages.length === before + 1 && messages[before].role === 'user') this.legacyPending = [before, turn ?? this.turn];
32
+ if (turn != null && userOrigin) {
33
+ for (let index = before; index < messages.length; index++) if (messages[index].role === 'user') this.pending.push([index, turn]);
34
+ } else this.pending = [];
35
+ }
36
+
37
+ recordTypedInput(payload, messages) {
38
+ const durable = stringField(payload, 'type') === 'item_completed', item = durable ? field(payload, 'item') : payload, projected = [];
39
+ if (durable) pushCodexDisplayItem(item, projected);
40
+ else {
41
+ const images = field(item, 'images'), urls = Array.isArray(images) ? images.filter((image) => typeof image === 'string') : [];
42
+ projected.push(codexUserWithImages(stringField(item, 'message') ?? '', urls));
43
+ }
44
+ const message = projected[0]; if (message == null) return;
45
+ const id = durable ? stringField(item, 'id') : null, client = stringField(item, 'client_id');
46
+ const keys = [id != null ? `item:${id}` : null, client != null ? `client:${client}` : null].filter((key) => key != null);
47
+ const existing = keys.map((key) => this.typed.get(key)).find((index) => index !== undefined);
48
+ const turn = stringField(payload, 'turn_id') ?? this.turn;
49
+ const candidates = this.pending.filter(([, id]) => id === turn).map(([index]) => index);
50
+ const legacy = this.legacyPending; this.legacyPending = null;
51
+ const pair = candidates.length === 1 ? candidates[0] : !durable && legacy?.[1] === turn ? legacy[0] : undefined;
52
+ const display = stringifyNativeJson(messageWire(message));
53
+ let index = existing ?? pair;
54
+ if (index === undefined) { messages.push(message); index = messages.length - 1; }
55
+ const target = messages[index];
56
+ delete target.metadata.codex_contextual_kind; delete target.metadata.codex_hook_prompt_text;
57
+ target.metadata.codex_typed_user = display; target.metadata.codex_typed_user_source = durable ? 'item_completed' : 'user_message';
58
+ if (keys.length) target.metadata.codex_typed_user_id = keys[0];
59
+ for (const key of keys) this.typed.set(key, index);
60
+ if (existing === undefined) this.pending = this.pending.filter(([, id]) => id !== turn);
61
+ }
62
+ }
@@ -0,0 +1,135 @@
1
+ import { createMessage } from './message.mjs';
2
+ import { cloneNativeJson, nativeInteger, parseNativeJson, stringifyNativeJsonMember } from './native-json.mjs';
3
+ import { createSession, codexStartTrigger } from './session-model.mjs';
4
+ import { splitLinesVerbatim, nonEmptyLines } from './session-lines.mjs';
5
+ import { field, stringField, extractTextContent, restoreGrokMessageExtension, ensureToolResultsPaired, stampMessageRecords, takeMessageRecords } from './session-codec-utils.mjs';
6
+ import { restoreTopLevelResidue, captureCodexProvenance } from './session-native-residue.mjs';
7
+ import { pushCodexModelItem, pushCodexAssistant, stampCodexMessages, removeLastCodexTurn, clearCodexBookkeeping } from './session-codex-items.mjs';
8
+ import { codexContentItem, codexItemKind } from './session-codex-display-items.mjs';
9
+ import { CodexUserHistory } from './session-codex-user-history.mjs';
10
+ import { nativeTrim } from './text.mjs';
11
+
12
+ const append = (current, part) => current + (current === '' ? '' : '\n') + part;
13
+ const restoreSingle = (value, messages, before) => { if (messages.length === before + 1) restoreGrokMessageExtension(value, messages[before]); };
14
+
15
+ export function captureCodexMeta(payload, meta) {
16
+ meta.session_id ??= stringField(payload, 'id'); meta.cwd ??= stringField(payload, 'cwd'); meta.model ??= stringField(payload, 'model');
17
+ const base = field(payload, 'base_instructions');
18
+ meta.system_prompt ??= typeof base === 'string' ? base : stringField(base, 'text');
19
+ for (const key of ['parent_thread_id', 'forked_from_id', 'thread_source']) {
20
+ const value = stringField(payload, key); if (value != null) meta.lineage[key] = value;
21
+ }
22
+ meta.trigger ??= codexStartTrigger(payload);
23
+ if (!Object.hasOwn(meta.lineage, 'claude_fork_context_ref_raw') && field(payload, 'claude_fork_context_ref') !== undefined) meta.lineage.claude_fork_context_ref_raw = stringifyNativeJsonMember(payload, 'claude_fork_context_ref');
24
+ const spawn = field(field(field(payload, 'source'), 'subagent'), 'thread_spawn');
25
+ for (const key of ['parent_thread_id', 'agent_role', 'agent_nickname']) {
26
+ const value = stringField(spawn, key); if (value != null) meta.lineage[key] = value;
27
+ }
28
+ const depth = nativeInteger(spawn, 'depth', { signed: true }); if (depth != null) meta.lineage.depth = String(depth);
29
+ }
30
+
31
+ function assistantTexts(text) {
32
+ const result = new Set();
33
+ for (const line of nonEmptyLines(text)) {
34
+ let value; try { value = parseNativeJson(line); } catch { continue; }
35
+ const payload = field(value, 'payload');
36
+ if (stringField(value, 'type') !== 'response_item' || stringField(payload, 'type') !== 'message' || stringField(payload, 'role') !== 'assistant') continue;
37
+ const text = nativeTrim(extractTextContent(payload, 'content')); if (text !== '') result.add(text);
38
+ }
39
+ return result;
40
+ }
41
+
42
+ function floatDisplay(value) {
43
+ const text = String(value); if (!/[eE]/.test(text)) return Object.is(value, -0) ? '-0' : text;
44
+ const [coefficient, exponent] = text.split(/[eE]/), negative = coefficient.startsWith('-');
45
+ const unsigned = negative ? coefficient.slice(1) : coefficient, point = unsigned.indexOf('.');
46
+ const digits = unsigned.replace('.', ''), position = (point === -1 ? unsigned.length : point) + Number(exponent);
47
+ const body = position <= 0 ? `0.${'0'.repeat(-position)}${digits}` : position >= digits.length ? digits + '0'.repeat(position - digits.length) : digits.slice(0, position) + '.' + digits.slice(position);
48
+ return (negative ? '-' : '') + body;
49
+ }
50
+
51
+ /** Resumable model history. Display TurnItems have a separate bounded reader;
52
+ * compaction replacement applies here and never rewrites retained raw bytes. */
53
+ export function fromCodexStr(text, { applyCompaction = true } = {}) {
54
+ const session = createSession('codex'), { meta, messages } = session, capture = splitLinesVerbatim(text);
55
+ session.raw = capture.lines; session.raw_trailing_newline = capture.trailingNewline; session.raw_is_verbatim = true;
56
+ const canonicalTexts = assistantTexts(text), users = new CodexUserHistory();
57
+ let reasoning = '', reasoningContent = '', encrypted = false, restored = false;
58
+ const orphan = () => {
59
+ const message = createMessage('system', '[reasoning] (turn ended without a reply)');
60
+ if (reasoning !== '') message.metadata.reasoning = reasoning;
61
+ if (reasoningContent !== '') message.metadata.reasoning_content = reasoningContent;
62
+ if (encrypted) message.metadata.reasoning_encrypted = 'true';
63
+ reasoning = ''; reasoningContent = ''; encrypted = false; return message;
64
+ };
65
+ capture.lines.forEach((raw, index) => {
66
+ if (index > 0) stampMessageRecords(messages, index - 1);
67
+ const line = nativeTrim(raw); if (line === '') return;
68
+ let value; try { value = parseNativeJson(line); } catch { session.parse_error_lines++; return; }
69
+ const kind = stringField(value, 'type'), payload = field(value, 'payload'), type = stringField(payload, 'type'), timestamp = stringField(value, 'timestamp');
70
+ if (!restored && kind === 'session_meta' && restoreTopLevelResidue(payload, meta)) restored = true;
71
+ if (!restored) captureCodexProvenance(meta, index, raw, value);
72
+ users.observeBoundary(value);
73
+ if (kind === 'session_meta') { captureCodexMeta(payload, meta); if (!restored) meta.codex_headers.push(cloneNativeJson(value)); }
74
+ else if (kind === 'turn_context') { meta.model ??= stringField(payload, 'model'); if (!restored) meta.codex_headers.push(cloneNativeJson(value)); }
75
+ else if (kind === 'response_item' && type === 'reasoning') {
76
+ const summary = extractTextContent(payload, 'summary'); if (nativeTrim(summary) !== '') reasoning = append(reasoning, summary);
77
+ if (field(payload, 'content') != null) { const text = extractTextContent(payload, 'content'); if (nativeTrim(text) !== '') reasoningContent = append(reasoningContent, text); }
78
+ if (field(payload, 'encrypted_content') != null) encrypted = true;
79
+ } else if (kind === 'response_item') {
80
+ const before = messages.length; pushCodexModelItem(payload, messages, codexContentItem);
81
+ if (messages.length > before && (reasoning !== '' || reasoningContent !== '' || encrypted)) {
82
+ if (messages.at(-1).role === 'assistant') {
83
+ const last = messages.at(-1);
84
+ if (reasoning !== '') last.metadata.reasoning = reasoning;
85
+ if (reasoningContent !== '') last.metadata.reasoning_content = reasoningContent;
86
+ if (encrypted) last.metadata.reasoning_encrypted = 'true';
87
+ reasoning = ''; reasoningContent = ''; encrypted = false;
88
+ } else messages.splice(before, 0, orphan());
89
+ }
90
+ stampCodexMessages(messages, before, timestamp); restoreSingle(payload, messages, before);
91
+ users.recordModelInput(value, before, messages);
92
+ } else if (kind === 'compacted' && applyCompaction) {
93
+ messages.length = 0;
94
+ const history = field(payload, 'replacement_history');
95
+ if (Array.isArray(history)) for (const item of history) pushCodexModelItem(item, messages, codexContentItem);
96
+ stampCodexMessages(messages, 0, timestamp);
97
+ if (messages.length) delete messages.at(-1).metadata.__codex_open_turn;
98
+ } else if (kind === 'event_msg' && type === 'agent_message') {
99
+ const before = messages.length, text = extractTextContent(payload, 'message');
100
+ if (nativeTrim(text) !== '' && !canonicalTexts.has(nativeTrim(text))) {
101
+ pushCodexAssistant(messages, text); const phase = stringField(payload, 'phase'); if (phase != null) messages.at(-1).metadata.phase = phase;
102
+ }
103
+ stampCodexMessages(messages, before, timestamp);
104
+ } else if (kind === 'event_msg' && type === 'thread_rolled_back') {
105
+ const turns = nativeInteger(payload, 'num_turns') ?? 1n;
106
+ for (let left = turns; left > 0n && messages.length; left--) removeLastCodexTurn(messages);
107
+ } else if (kind === 'event_msg' && type === 'thread_goal_updated') {
108
+ const before = messages.length, goal = field(payload, 'goal'), objective = stringField(goal, 'objective');
109
+ if (objective != null && nativeTrim(objective) !== '') {
110
+ const message = createMessage('system', `[thread goal] ${objective}`), status = stringField(goal, 'status'), budget = nativeInteger(goal, 'tokenBudget', { signed: true });
111
+ if (status != null) message.metadata.goal_status = status;
112
+ if (budget != null) message.metadata.goal_token_budget = String(budget);
113
+ messages.push(message);
114
+ }
115
+ stampCodexMessages(messages, before, timestamp);
116
+ } else if (kind === 'event_msg' && type === 'exited_review_mode') {
117
+ const before = messages.length, review = field(payload, 'review_output');
118
+ if (review !== undefined) {
119
+ pushCodexAssistant(messages, `[code review] ${stringField(review, 'overall_explanation') ?? stringifyNativeJsonMember(payload, 'review_output')}`);
120
+ const last = messages.at(-1), findings = field(review, 'findings');
121
+ if (Array.isArray(findings) && findings.length) last.metadata.review_findings = stringifyNativeJsonMember(review, 'findings');
122
+ const correctness = stringField(review, 'overall_correctness'), score = field(review, 'overall_confidence_score');
123
+ if (correctness != null) last.metadata.review_overall_correctness = correctness;
124
+ if (typeof score === 'number') last.metadata.review_overall_confidence_score = floatDisplay(score);
125
+ }
126
+ stampCodexMessages(messages, before, timestamp);
127
+ } else if (kind === 'event_msg' && (type === 'user_message' || (type === 'item_completed' && codexItemKind(field(payload, 'item')) === 'usermessage'))) {
128
+ const before = messages.length; users.recordTypedInput(payload, messages); stampCodexMessages(messages, before, timestamp);
129
+ }
130
+ });
131
+ if (reasoning !== '' || reasoningContent !== '' || encrypted) messages.push(orphan());
132
+ stampMessageRecords(messages, Math.max(0, capture.lines.length - 1)); ensureToolResultsPaired(messages);
133
+ meta.message_records = takeMessageRecords(messages); clearCodexBookkeeping(messages); session.imported_message_count = messages.length;
134
+ return session;
135
+ }
@@ -0,0 +1,135 @@
1
+ import { withSessionFile } from './file-io.mjs';
2
+ import { sourceStat } from './store-registry.mjs';
3
+ import { verifySources } from './source-files.mjs';
4
+ import { sessionFileLines, decodeSessionUtf8 } from './session-file.mjs';
5
+ import { field, stringField, truncateMessagesWithAnchor } from './session-codec-utils.mjs';
6
+ import { parseNativeJson } from './native-json.mjs';
7
+ import { nativeTrim } from './text.mjs';
8
+ import { detectSessionSource } from './session-source.mjs';
9
+ import { isCompressedSessionPath, sessionReaderText, looksLikeSessionSqlite, loadSessionPath, loadLocatorDisplayStore, captureGrokPathMetadata } from './session-path.mjs';
10
+ import { fromCodexDisplayStr } from './session-codex-display.mjs';
11
+ import { fromGeminiStr } from './session-gemini.mjs';
12
+ import { fromPiStr } from './session-pi.mjs';
13
+ import { fromGrokStr } from './session-grok.mjs';
14
+ import { fromOpenCodeStr } from './session-opencode.mjs';
15
+ import { fromClaudeCodeStr } from './session-claude.mjs';
16
+
17
+ const MAX_BYTES = 64 * 1024 * 1024, MIN_BYTES = 4 * 1024 * 1024;
18
+ function humanLine(line, source, paginated) {
19
+ if (!['"user"', '"UserMessage"', '"user_message"'].some((literal) => line.includes(literal))) return false;
20
+ let value; try { value = parseNativeJson(line); } catch { return false; }
21
+ const kind = stringField(value, 'type'), payload = field(value, 'payload');
22
+ if (source === 'codex') {
23
+ if (kind === 'event_msg' && stringField(payload, 'type') === 'item_completed') return stringField(field(payload, 'item'), 'type') === 'UserMessage';
24
+ if (paginated) return false;
25
+ if (kind === 'event_msg' && stringField(payload, 'type') === 'user_message') return true;
26
+ return kind === 'response_item' && stringField(payload, 'type') === 'message' && stringField(payload, 'role') === 'user';
27
+ }
28
+ if (kind !== 'user') return false;
29
+ const content = source === 'claude_code' ? field(field(value, 'message'), 'content') : source === 'gemini' ? field(value, 'content') : null;
30
+ return typeof content === 'string' ? nativeTrim(content) !== '' : Array.isArray(content) && content.some((part) => (source !== 'claude_code' || stringField(part, 'type') === 'text') && typeof field(part, 'text') === 'string' && nativeTrim(part.text) !== '');
31
+ }
32
+ async function firstLine(reader) {
33
+ for await (const line of sessionFileLines(reader)) return line.text;
34
+ return '';
35
+ }
36
+
37
+ async function displayJsonl(reader, readLimit) {
38
+ if (isCompressedSessionPath(reader.path)) {
39
+ const text = await sessionReaderText(reader); return { source: detectSessionSource(text), text, omitted: false };
40
+ }
41
+ const first = await firstLine(reader), source = detectSessionSource(first);
42
+ let header; try { header = parseNativeJson(first); } catch { header = null; }
43
+ const paginated = source === 'codex' && stringField(field(header, 'payload'), 'history_mode') === 'paginated';
44
+ const requested = Math.min(MAX_BYTES, Math.max(MIN_BYTES, Math.max(1, readLimit) * 16 * 1024));
45
+ if (!['claude_code', 'codex', 'gemini'].includes(source) || reader.size <= requested) {
46
+ const text = await sessionReaderText(reader);
47
+ return { source: ['claude_code', 'codex', 'gemini'].includes(source) ? source : detectSessionSource(text), text, omitted: false };
48
+ }
49
+ const start = reader.size - requested;
50
+ let bytes = await reader.read(start, requested);
51
+ const newline = bytes.indexOf(10); if (newline !== -1) bytes = bytes.subarray(newline + 1);
52
+ let tail;
53
+ try { tail = decodeSessionUtf8(bytes); } catch (error) { throw new Error(`${reader.path} contains non-UTF-8 data in its display window`, { cause: error }); }
54
+ const maximum = Math.min(start, MAX_BYTES); let searchSize = Math.min(requested, maximum), anchors = [];
55
+ while (searchSize > 0) {
56
+ const searchStart = start - searchSize; let search = await reader.read(searchStart, searchSize);
57
+ if (searchStart > 0) { const newline = search.indexOf(10); search = newline === -1 ? Buffer.alloc(0) : search.subarray(newline + 1); }
58
+ const end = search.lastIndexOf(10); search = end === -1 ? Buffer.alloc(0) : search.subarray(0, end + 1);
59
+ let text; try { text = decodeSessionUtf8(search); } catch { text = ''; }
60
+ anchors = [];
61
+ const lines = text.split('\n');
62
+ for (let at = lines.length - 1; at >= 0 && anchors.length < 2; at--) {
63
+ const line = lines[at].endsWith('\r') ? lines[at].slice(0, -1) : lines[at];
64
+ if (humanLine(line, source, paginated)) anchors.push(line);
65
+ }
66
+ anchors.reverse();
67
+ if (anchors.length >= 2 || searchStart === 0 || searchSize === maximum) break;
68
+ searchSize = Math.min(searchSize * 2, maximum);
69
+ }
70
+ if (anchors.length) tail = `${anchors.join('\n')}\n${tail}`;
71
+ return { source, text: source === 'codex' || source === 'gemini' ? first + tail : tail, omitted: true };
72
+ }
73
+
74
+ async function decodeDisplay(source, text, path, fidelity, limit, metrics, sources) {
75
+ if (source === 'codex') return fromCodexDisplayStr(text, limit);
76
+ if (source === 'gemini') {
77
+ const session = fromGeminiStr(text); session.raw_is_verbatim = false;
78
+ session.load_residue.push('display history is a bounded native-record projection, not a complete Gemini artifact'); return session;
79
+ }
80
+ if (source === 'pi') return fromPiStr(text);
81
+ if (source === 'grok') {
82
+ const session = fromGrokStr(text); sources.push(await captureGrokPathMetadata(session, path, metrics)); return session;
83
+ }
84
+ if (source === 'opencode') return fromOpenCodeStr(text);
85
+ // Native display admission uses this fallback; adding a new dialect to
86
+ // display mode is separate from moving the existing complete-read contract.
87
+ return fromClaudeCodeStr(text, { fidelity });
88
+ }
89
+
90
+ export async function loadSessionDisplayPath(path, messageLimit, { fidelity = 'byte_lossless', metrics, env } = {}) {
91
+ if (!Number.isSafeInteger(messageLimit) || messageLimit < 0) throw new TypeError('display message limit must be a nonnegative exact integer');
92
+ if ((await sourceStat(path))?.isDirectory() || await looksLikeSessionSqlite(path, metrics)) {
93
+ const session = await loadSessionPath(path, { fidelity, includeSubagents: false, metrics, env });
94
+ truncateMessagesWithAnchor(session.messages, messageLimit); return session;
95
+ }
96
+ return (await withSessionFile(path, async (reader) => {
97
+ let readLimit = Math.max(1, messageLimit), previousBytes = 0, session, omitted;
98
+ const sources = [];
99
+ while (true) {
100
+ const read = await displayJsonl(reader, readLimit);
101
+ session = await decodeDisplay(read.source, read.text, path, fidelity, messageLimit, metrics, sources); omitted = read.omitted;
102
+ const count = Math.max(session.imported_message_count ?? session.messages.length, session.messages.length), humans = session.messages.filter((message) => message.role === 'user').length;
103
+ const bytes = Buffer.byteLength(read.text), sufficient = !omitted || (count > Math.max(1, messageLimit) && humans >= 2);
104
+ if (sufficient || (bytes <= previousBytes && readLimit >= 4096)) {
105
+ if (omitted) session.imported_message_count = Math.max(count, Math.max(1, messageLimit) + 1); break;
106
+ }
107
+ previousBytes = bytes; readLimit = Math.min(Number.MAX_SAFE_INTEGER, readLimit * 2);
108
+ }
109
+ if (omitted) session.load_residue.push('older native records remain outside this bounded display window');
110
+ await verifySources(sources);
111
+ truncateMessagesWithAnchor(session.messages, messageLimit); return session;
112
+ }, metrics)).result;
113
+ }
114
+
115
+ export async function loadSessionDisplayLocator(locator, messageLimit, options = {}) {
116
+ return await loadLocatorDisplayStore(locator, messageLimit, options) ?? loadSessionDisplayPath(locator.storage.path, messageLimit, options);
117
+ }
118
+
119
+ export async function loadSessionDisplayFrom(locator, fromByte, options = {}) {
120
+ const { path, kind } = locator.storage, unbounded = 1_000_000;
121
+ if (!Number.isSafeInteger(fromByte) || fromByte < 0) throw new TypeError('display anchor must be a nonnegative exact byte offset');
122
+ if (kind !== 'file' || locator.harness === 'hermes') return loadSessionDisplayLocator(locator, unbounded, options);
123
+ if ((await sourceStat(path))?.isDirectory() || await looksLikeSessionSqlite(path, options.metrics) || isCompressedSessionPath(path) || fromByte === 0) return loadSessionDisplayPath(path, unbounded, options);
124
+ // Keep the fixed anchor and detection under one open file identity.
125
+ const captured = await withSessionFile(path, async (reader) => {
126
+ const first = await firstLine(reader); if (detectSessionSource(first) !== 'codex') return null;
127
+ if (fromByte >= reader.size) throw new Error(`${path} is shorter than its display anchor (${fromByte})`);
128
+ let bytes = await reader.read(fromByte, reader.size - fromByte);
129
+ const newline = bytes.indexOf(10); bytes = newline === -1 ? Buffer.alloc(0) : bytes.subarray(newline + 1);
130
+ let tail; try { tail = decodeSessionUtf8(bytes); } catch (error) { throw new Error(`${path} contains non-UTF-8 data after its display anchor`, { cause: error }); }
131
+ const session = fromCodexDisplayStr(first + tail, unbounded);
132
+ session.load_residue.push(`older native records remain before this display anchor (${fromByte})`); return session;
133
+ }, options.metrics);
134
+ return captured.result ?? loadSessionDisplayPath(path, unbounded, options);
135
+ }
@@ -0,0 +1,19 @@
1
+ import { fromClaudeCodeStr } from './session-claude.mjs';
2
+ import { fromCodexStr } from './session-codex.mjs';
3
+ import { fromGeminiStr } from './session-gemini.mjs';
4
+ import { fromGooseStr } from './session-goose.mjs';
5
+ import { fromGrokStr } from './session-grok.mjs';
6
+ import { fromOpenCodeStr } from './session-opencode.mjs';
7
+ import { fromPiStr, fromOpenClawStr } from './session-pi.mjs';
8
+ import { detectSessionSource } from './session-source.mjs';
9
+
10
+ export function readSessionDocument(text, { source = detectSessionSource(text), fidelity = 'byte_lossless' } = {}) {
11
+ if (source === 'codex') return fromCodexStr(text);
12
+ if (source === 'gemini') return fromGeminiStr(text);
13
+ if (source === 'goose') return fromGooseStr(text);
14
+ if (source === 'grok') return fromGrokStr(text);
15
+ if (source === 'opencode') return fromOpenCodeStr(text);
16
+ if (source === 'pi') return fromPiStr(text);
17
+ if (source === 'openclaw') return fromOpenClawStr(text);
18
+ return fromClaudeCodeStr(text, { fidelity });
19
+ }
@@ -0,0 +1,27 @@
1
+ import { withSessionFile } from './file-io.mjs';
2
+
3
+ export const decodeSessionUtf8 = (bytes) => new TextDecoder('utf-8', { fatal: true, ignoreBOM: true }).decode(bytes);
4
+
5
+ /** One physical line at a time, including an unterminated final line. LF and
6
+ * CR remain in the bytes; offsets always address the verified open file. */
7
+ export async function* sessionFileLines(reader) {
8
+ let pieces = [], pending = 0, start = 0;
9
+ for (let offset = 0; offset < reader.size;) {
10
+ const chunk = await reader.read(offset, Math.min(64 * 1024, reader.size - offset));
11
+ const chunkStart = offset; offset += chunk.length;
12
+ let from = 0, end;
13
+ while ((end = chunk.indexOf(10, from)) !== -1) {
14
+ pieces.push(chunk.subarray(from, end + 1)); pending += end + 1 - from;
15
+ const bytes = pieces.length === 1 ? pieces[0] : Buffer.concat(pieces, pending);
16
+ yield { offset: start, length: pending, text: decodeSessionUtf8(bytes) };
17
+ start = chunkStart + end + 1; from = end + 1; pieces = []; pending = 0;
18
+ }
19
+ if (from < chunk.length) { pieces.push(chunk.subarray(from)); pending += chunk.length - from; }
20
+ }
21
+ if (pending) yield { offset: start, length: pending, text: decodeSessionUtf8(Buffer.concat(pieces, pending)) };
22
+ }
23
+
24
+ export async function readSessionText(path, metrics) {
25
+ const capture = await withSessionFile(path, async (reader) => decodeSessionUtf8(await reader.read(0, reader.size)), metrics);
26
+ return capture.result;
27
+ }