@volter/supercode-interchange 0.1.22 → 0.1.24

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 (129) hide show
  1. package/LICENSE-diffy-MIT +23 -0
  2. package/README.md +422 -2
  3. package/channel-records.mjs +71 -0
  4. package/channel-session-counts.mjs +81 -0
  5. package/channels-read.mjs +90 -0
  6. package/corpus-audit-records.mjs +192 -0
  7. package/corpus-audit-render.mjs +47 -0
  8. package/corpus-audit-schema.mjs +89 -0
  9. package/corpus-audit.d.ts +16 -0
  10. package/corpus-audit.mjs +132 -0
  11. package/file-io.mjs +21 -2
  12. package/harness-settings.mjs +19 -0
  13. package/hermes-fire-links.mjs +135 -0
  14. package/hermes-fires.mjs +106 -0
  15. package/hermes-read-home.d.ts +3 -0
  16. package/hermes-read-home.mjs +235 -0
  17. package/hermes-read-store.mjs +49 -0
  18. package/hermes-read-validation.mjs +170 -0
  19. package/hermes-read-workflow.mjs +164 -0
  20. package/hermes-read-yaml.mjs +68 -0
  21. package/hermes-store.mjs +1 -1
  22. package/homes.mjs +2 -2
  23. package/index.d.ts +14 -1
  24. package/index.mjs +1 -1
  25. package/inspection-builtin-tools.json +261 -0
  26. package/inspection-config-fields.json +765 -0
  27. package/inspection-schemas.d.ts +4 -0
  28. package/inspection-schemas.mjs +117 -0
  29. package/job-notepad-read.mjs +97 -0
  30. package/jobs-control.mjs +250 -0
  31. package/jobs-read.mjs +198 -0
  32. package/jsonl.mjs +4 -3
  33. package/memory-show.mjs +187 -0
  34. package/metadata.mjs +45 -3
  35. package/native-json.mjs +46 -3
  36. package/ontology-contracts.d.ts +5 -0
  37. package/ontology-contracts.mjs +29 -0
  38. package/ontology-operations.json +198 -0
  39. package/ontology-schemas.json +7946 -0
  40. package/openclaw-jobs-read.mjs +117 -0
  41. package/openclaw-profile-home.mjs +142 -0
  42. package/orchestrator-read-records.mjs +112 -0
  43. package/orchestrator-store.mjs +21 -9
  44. package/package.json +59 -4
  45. package/parity-ledger.json +6984 -0
  46. package/parity-orchestration.json +5538 -0
  47. package/parity-report.d.ts +7 -0
  48. package/parity-report.mjs +46 -0
  49. package/preset-profiles.mjs +27 -0
  50. package/profile-command.mjs +32 -0
  51. package/profile-orchestrator-door.mjs +104 -0
  52. package/profile-paths.mjs +25 -0
  53. package/profiles-control.mjs +98 -0
  54. package/profiles-read.mjs +119 -0
  55. package/reduction-attribution.mjs +54 -0
  56. package/reduction-diff.mjs +165 -0
  57. package/reduction-log.mjs +177 -0
  58. package/reduction-view.mjs +145 -0
  59. package/retained-projection.mjs +9 -0
  60. package/routes-read.mjs +103 -0
  61. package/runs-read.mjs +224 -0
  62. package/session-activity.d.ts +23 -0
  63. package/session-activity.mjs +124 -0
  64. package/session-append-scan.mjs +27 -17
  65. package/session-branch-handoff.mjs +115 -0
  66. package/session-claude-graph.mjs +29 -7
  67. package/session-claude-index.mjs +732 -114
  68. package/session-claude-runtime.d.ts +28 -0
  69. package/session-claude-runtime.mjs +171 -6
  70. package/session-claude-scan-store.mjs +164 -0
  71. package/session-claude.mjs +9 -0
  72. package/session-codex-activity.d.ts +6 -0
  73. package/session-codex-activity.mjs +120 -0
  74. package/session-codex-window.mjs +24 -4
  75. package/session-config-scan.mjs +141 -0
  76. package/session-convert.d.ts +33 -0
  77. package/session-convert.mjs +179 -0
  78. package/session-emission-version.json +1 -0
  79. package/session-file.mjs +11 -1
  80. package/session-follower.mjs +17 -5
  81. package/session-fresh-id.mjs +15 -0
  82. package/session-hermes-import.mjs +61 -0
  83. package/session-hermes.mjs +1 -1
  84. package/session-import.mjs +37 -0
  85. package/session-inspect.d.ts +18 -0
  86. package/session-inspect.mjs +122 -0
  87. package/session-machine-tree.d.ts +33 -0
  88. package/session-machine-tree.mjs +274 -0
  89. package/session-materialize-files.mjs +78 -0
  90. package/session-materialize-opencode.mjs +137 -0
  91. package/session-materialize.mjs +186 -0
  92. package/session-message.mjs +85 -0
  93. package/session-model.d.ts +9 -0
  94. package/session-model.mjs +1 -0
  95. package/session-opencode-store.mjs +39 -3
  96. package/session-path.mjs +6 -3
  97. package/session-pi-content.mjs +7 -2
  98. package/session-read.d.ts +134 -2
  99. package/session-read.mjs +86 -9
  100. package/session-recorded-config.mjs +3 -2
  101. package/session-records.mjs +12 -6
  102. package/session-residue-store.mjs +90 -0
  103. package/session-residue.mjs +150 -0
  104. package/session-resume-instructions.mjs +1 -1
  105. package/session-saved-read.mjs +105 -0
  106. package/session-transfer.mjs +141 -0
  107. package/session-views.mjs +1 -0
  108. package/session-write-claude.mjs +135 -0
  109. package/session-write-codex.mjs +146 -0
  110. package/session-write-gemini.mjs +77 -0
  111. package/session-write-goose.mjs +99 -0
  112. package/session-write-grok.mjs +70 -0
  113. package/session-write-native.mjs +41 -0
  114. package/session-write-opencode.mjs +197 -0
  115. package/session-write-pi.mjs +130 -0
  116. package/session-write-residue.mjs +19 -0
  117. package/session-write-utils.mjs +77 -0
  118. package/session-write.d.ts +10 -0
  119. package/session-write.mjs +37 -0
  120. package/skill-toml.mjs +80 -0
  121. package/skills-control.mjs +193 -0
  122. package/skills-list.mjs +237 -0
  123. package/source-files.mjs +12 -2
  124. package/sqlite-reader.mjs +134 -18
  125. package/support-registry.json +402 -0
  126. package/support-report.mjs +69 -0
  127. package/trigger-records.mjs +77 -0
  128. package/triggers-read.mjs +67 -0
  129. package/workflow-read.mjs +165 -0
@@ -0,0 +1,70 @@
1
+ import { nativeInteger } from './native-json.mjs';
2
+ import { NATIVE_RESIDUE_KEY, NATIVE_RESIDUE_SUMMARY_KEY } from './session-native-residue.mjs';
3
+ import { nativeResidueEnvelope, nativeResidueSummary } from './session-write-residue.mjs';
4
+ import { replayExcluded, parsedJsonField, targetMessageExtension, splicedPrefixLengths, jsonLine, injectFirstJsonLine } from './session-write-utils.mjs';
5
+
6
+ function grokRecords(session, messages) {
7
+ let text = '';
8
+ for (const message of messages) {
9
+ if (replayExcluded(message)) continue;
10
+ let value;
11
+ switch (message.role) {
12
+ case 'system': value = { type: 'user', content: [{ type: 'text', text: message.content ?? '' }],
13
+ synthetic_reason: 'supercode_system_event' }; break;
14
+ case 'user':
15
+ value = { type: 'user', content: [{ type: 'text', text: message.content ?? '' }] };
16
+ for (const [metadata, field] of [['grok_prompt_index', 'prompt_index'], ['grok_prior_turn_interrupt', 'prior_turn_interrupt'],
17
+ ['grok_synthetic_reason', 'synthetic_reason']]) {
18
+ const raw = message.metadata[metadata];
19
+ if (raw != null) parsedJsonField(value, field, raw);
20
+ }
21
+ break;
22
+ case 'assistant':
23
+ value = { type: 'assistant', content: message.content ?? '', tool_calls: (message.tool_calls ?? []).map(call => ({
24
+ id: call.id, name: call.function.name, arguments: call.function.arguments,
25
+ })), model_id: message.metadata.grok_model_id ?? session.meta.model ?? 'unknown' };
26
+ for (const [metadata, field] of [['grok_model_fingerprint', 'model_fingerprint'], ['grok_reasoning_effort', 'reasoning_effort']]) {
27
+ if (message.metadata[metadata] != null) value[field] = message.metadata[metadata];
28
+ }
29
+ break;
30
+ case 'tool': {
31
+ value = { type: 'tool_result', tool_call_id: message.tool_call_id ?? '', content: message.content ?? '' };
32
+ const images = (message.content_parts ?? []).filter(part => part?.type === 'image_url')
33
+ .map(part => part?.image_url?.url).filter(url => typeof url === 'string').map(url => ({ type: 'image', url }));
34
+ if (images.length) value.images = images;
35
+ break;
36
+ }
37
+ default: throw new TypeError(`unknown message role: ${message.role}`);
38
+ }
39
+ targetMessageExtension(value, message); text += jsonLine(value);
40
+ }
41
+ return text;
42
+ }
43
+
44
+ export function toGrokJsonl(session) {
45
+ let text = session.meta.system_prompt ? jsonLine({ type: 'system', content: session.meta.system_prompt }) : '';
46
+ text += grokRecords(session, session.messages);
47
+ if (session.meta.native_residue_source === 'grok') {
48
+ const recordIndex = record => nativeInteger(record, 'record_index') ?? ((1n << 64n) - 1n);
49
+ const records = [...session.meta.native_residue].sort((left, right) => {
50
+ const a = recordIndex(left), b = recordIndex(right); return a < b ? -1 : a > b ? 1 : 0;
51
+ });
52
+ for (const entry of records) if (typeof entry?.raw === 'string') text += entry.raw + '\n';
53
+ } else {
54
+ const envelope = nativeResidueEnvelope(session.meta);
55
+ if (envelope != null) {
56
+ if (!text) text = jsonLine({ type: 'system', content: '' });
57
+ text = injectFirstJsonLine(text, NATIVE_RESIDUE_SUMMARY_KEY, nativeResidueSummary(envelope));
58
+ text = injectFirstJsonLine(text, NATIVE_RESIDUE_KEY, envelope);
59
+ }
60
+ }
61
+ return text;
62
+ }
63
+
64
+ /** Grok keeps its identity in the directory; its raw records have no session id to rewrite. */
65
+ export function toGrokJsonlSpliced(session) {
66
+ const prefix = splicedPrefixLengths(session);
67
+ if (!prefix.raw) return toGrokJsonl(session);
68
+ return session.raw.slice(0, prefix.raw).map(line => line + '\n').join('')
69
+ + grokRecords(session, session.messages.slice(prefix.messages));
70
+ }
@@ -0,0 +1,41 @@
1
+ import { blake3 } from '@noble/hashes/blake3.js';
2
+ import { cloneNativeJson, stringifyNativeJson } from './native-json.mjs';
3
+ import { jsonLine, orderedMessageMetadata } from './session-write-utils.mjs';
4
+
5
+ export function toNativeJsonl(session) {
6
+ return jsonLine({ supercode_native: 1, source: session.meta.source, raw_trailing_newline: session.raw_trailing_newline })
7
+ + session.raw.map(line => line + '\n').join('');
8
+ }
9
+
10
+ function nativeTurnUuid(message, timestamp, index) {
11
+ const identity = { role: message.role, content: message.content, content_parts: message.content_parts,
12
+ tool_calls: message.tool_calls, tool_call_id: message.tool_call_id, name: message.name };
13
+ const ordinal = Buffer.alloc(8); ordinal.writeBigUInt64LE(BigInt(index));
14
+ const digest = Buffer.from(blake3(Buffer.concat([Buffer.from('supercode-native-turn-uuid-v1\0'), Buffer.from(timestamp),
15
+ ordinal, Buffer.from(stringifyNativeJson(identity))])));
16
+ const bytes = digest.subarray(0, 16); bytes[6] = (bytes[6] & 0x0f) | 0x40; bytes[8] = (bytes[8] & 0x3f) | 0x80;
17
+ const hex = bytes.toString('hex'); return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`;
18
+ }
19
+
20
+ /** Pure container construction; this does not open or append a sidecar file. */
21
+ export function toNativeJsonlV2(session, appended, fixedTimestamp = null) {
22
+ const header = { supercode_native: 2, source: session.meta.source, session_id: session.meta.session_id,
23
+ created: fixedTimestamp ?? new Date().toISOString(), raw_trailing_newline: session.raw_trailing_newline };
24
+ if (session.meta.agent_id != null) header.agent_id = session.meta.agent_id;
25
+ if (session.meta.parent_tool_use_id != null) header.parent_tool_use_id = session.meta.parent_tool_use_id;
26
+ if (Object.keys(session.meta.lineage).length) header.lineage = orderedMessageMetadata(session.meta.lineage);
27
+ let text = jsonLine(header) + session.raw.map(line => line + '\n').join('');
28
+ for (const [index, message] of appended.entries()) {
29
+ const timestamp = fixedTimestamp ?? new Date().toISOString();
30
+ const metadata = cloneNativeJson(message.metadata);
31
+ if (!Object.hasOwn(metadata, 'timestamp')) metadata.timestamp = timestamp;
32
+ if (!Object.hasOwn(metadata, 'supercode_native_uuid')) metadata.supercode_native_uuid = nativeTurnUuid(message, timestamp, index);
33
+ const turn = { supercode_turn: 1, ts: timestamp, role: message.role };
34
+ for (const key of ['content', 'content_parts', 'tool_calls', 'tool_call_id', 'name']) {
35
+ if (message[key] != null) turn[key] = cloneNativeJson(message[key]);
36
+ }
37
+ if (Object.keys(metadata).length) turn.metadata = orderedMessageMetadata(metadata);
38
+ text += jsonLine(turn);
39
+ }
40
+ return text;
41
+ }
@@ -0,0 +1,197 @@
1
+ import { cloneNativeJson, nativeInteger, nativeObjectKeys, setNativeInteger, setNativeJsonText, stringifyNativeJsonPretty } from './native-json.mjs';
2
+ import emitterVersion from './session-emission-version.json' with { type: 'json' };
3
+ import { isToolError, toolOutcome } from './message.mjs';
4
+ import { nativeTrim } from './text.mjs';
5
+ import { usageTimestamp } from './session-usage-time.mjs';
6
+ import { NATIVE_RESIDUE_KEY, NATIVE_RESIDUE_SUMMARY_KEY } from './session-native-residue.mjs';
7
+ import { nativeResidueEnvelope, nativeResidueSummary } from './session-write-residue.mjs';
8
+ import { SYNTH_TS_MS, replayExcluded, sessionCwd, tryNativeJson, sourceMessageExtension, splicedPrefixLengths,
9
+ parsedJsonField } from './session-write-utils.mjs';
10
+
11
+ const utf8Order = (left, right) => Buffer.compare(Buffer.from(left), Buffer.from(right));
12
+ const object = value => value != null && typeof value === 'object' && !Array.isArray(value);
13
+ const clock = fields => {
14
+ const result = {};
15
+ for (const [key, value] of Object.entries(fields)) setNativeInteger(result, key, value);
16
+ return result;
17
+ };
18
+ const largest = values => values.reduce((max, next) => next == null ? max : max == null || next > max ? next : max, null);
19
+
20
+ function maxTimestamp(value) {
21
+ const maxNumber = (parent, key) => {
22
+ const child = parent[key];
23
+ if (typeof child === 'number') return nativeInteger(parent, String(key), { signed: true });
24
+ return child == null || typeof child !== 'object' ? null : largest(nativeObjectKeys(child).map(key => maxNumber(child, key)));
25
+ };
26
+ if (value == null || typeof value !== 'object') return null;
27
+ return largest(nativeObjectKeys(value).map(key => key === 'time' ? maxNumber(value, key) : maxTimestamp(value[key])));
28
+ }
29
+
30
+ function synthesizedInfo(session) {
31
+ const info = { id: session.meta.session_id ?? 'ses_supercode00000000000001', projectID: session.meta.lineage.projectID ?? 'global',
32
+ slug: session.meta.lineage.slug ?? 'supercode-export', directory: sessionCwd(session), title: 'supercode export',
33
+ version: emitterVersion, time: clock({ created: SYNTH_TS_MS, updated: SYNTH_TS_MS }) };
34
+ if (session.meta.agent_id != null) info.agent = session.meta.agent_id;
35
+ const model = session.meta.model, slash = model?.indexOf('/') ?? -1;
36
+ if (slash >= 0) info.model = { providerID: model.slice(0, slash), id: model.slice(slash + 1) };
37
+ if (session.meta.lineage.parent_session_id != null) info.parentID = session.meta.lineage.parent_session_id;
38
+ if (session.meta.lineage.claude_fork_context_ref_raw != null) parsedJsonField(info, 'claude_fork_context_ref', session.meta.lineage.claude_fork_context_ref_raw);
39
+ const envelope = nativeResidueEnvelope(session.meta);
40
+ if (envelope != null) { info[NATIVE_RESIDUE_SUMMARY_KEY] = nativeResidueSummary(envelope); info[NATIVE_RESIDUE_KEY] = envelope; }
41
+ return info;
42
+ }
43
+
44
+ function recordsFromRaw(session) {
45
+ let info; const values = new Map(), parts = new Map();
46
+ for (const line of session.raw) {
47
+ const envelope = tryNativeJson(line); if (!Array.isArray(envelope?.key)) continue;
48
+ const value = envelope.value ?? null;
49
+ switch (envelope.key[0]) {
50
+ case 'session': info = value; break;
51
+ case 'message': if (typeof value?.id === 'string') values.set(value.id, value); break;
52
+ case 'part': if (typeof value?.messageID === 'string') {
53
+ const list = parts.get(value.messageID) ?? []; list.push(value); parts.set(value.messageID, list);
54
+ } break;
55
+ }
56
+ }
57
+ const created = value => nativeInteger(value?.time, 'created', { signed: true }) ?? 0n;
58
+ const records = [...values].sort(([aId, a], [bId, b]) => created(a) < created(b) ? -1 : created(a) > created(b) ? 1 : utf8Order(aId, bId))
59
+ .map(([id, value]) => ({ info: value, parts: (parts.get(id) ?? []).sort((a, b) => utf8Order(typeof a?.id === 'string' ? a.id : '', typeof b?.id === 'string' ? b.id : '')) }));
60
+ return { info, records };
61
+ }
62
+
63
+ function restoreFields(info, message, assistant) {
64
+ const metadata = message.metadata;
65
+ if (metadata.agent != null) info.agent = metadata.agent;
66
+ if (metadata.model != null) {
67
+ if (assistant) {
68
+ const slash = metadata.model.indexOf('/');
69
+ if (slash >= 0) { info.providerID = metadata.model.slice(0, slash); info.modelID = metadata.model.slice(slash + 1); }
70
+ } else try { setNativeJsonText(info, 'model', metadata.model); } catch { /* Unparseable source model is omitted. */ }
71
+ }
72
+ if (!assistant) return;
73
+ if (metadata.is_summary === 'true') info.summary = true;
74
+ if (metadata.finish != null) info.finish = metadata.finish;
75
+ for (const key of ['cost', 'tokens']) if (metadata[key] != null) {
76
+ try { setNativeJsonText(info, key, metadata[key]); } catch { /* The native writer likewise omits invalid JSON. */ }
77
+ }
78
+ }
79
+
80
+ function imageAttachments(parts) {
81
+ return (parts ?? []).filter(part => part?.type === 'image_url' && typeof part?.image_url?.url === 'string').map(part => {
82
+ const url = part.image_url.url, comma = url.indexOf(',', 5);
83
+ const mime = url.startsWith('data:') && comma >= 0 ? url.slice(5, comma).replace(/(?:;base64)+$/u, '') : 'application/octet-stream';
84
+ return { mime, url };
85
+ });
86
+ }
87
+
88
+ function appendMessages(session, messages, sessionId, cursor) {
89
+ const calls = new Map(), results = new Map(), paired = new Map();
90
+ for (const [index, message] of messages.entries()) {
91
+ if (message.role === 'assistant') for (const [toolIndex, call] of (message.tool_calls ?? []).entries()) {
92
+ const occurrences = calls.get(call.id) ?? []; occurrences.push([index, toolIndex]); calls.set(call.id, occurrences);
93
+ }
94
+ else if (message.role === 'tool' && message.tool_call_id != null) {
95
+ const occurrences = results.get(message.tool_call_id) ?? []; occurrences.push([index, message]); results.set(message.tool_call_id, occurrences);
96
+ }
97
+ }
98
+ for (const [id, occurrences] of calls) {
99
+ const outputs = results.get(id) ?? [];
100
+ for (let index = 0; index < Math.min(occurrences.length, outputs.length); index++) paired.set(occurrences[index].join(':'), outputs[index]);
101
+ }
102
+ let counter = 0n, position = cursor; const out = [];
103
+ const freshId = prefix => `${prefix}_synth${String(++counter).padStart(6, '0')}`;
104
+ const timestamp = message => {
105
+ const real = usageTimestamp(message.metadata.timestamp);
106
+ if (real != null && !(Object.hasOwn(message.metadata, 'supercode_native_uuid') && real <= position)) {
107
+ if (real > position) position = real; return real;
108
+ }
109
+ if (position === (1n << 63n) - 1n) throw new Error('cannot synthesize an OpenCode continuation timestamp after i64::MAX');
110
+ position++;
111
+ if (real == null && position < SYNTH_TS_MS) position = SYNTH_TS_MS;
112
+ return position;
113
+ };
114
+ const partBase = (messageID, type) => ({ id: freshId('prt'), sessionID: sessionId, messageID, type });
115
+ for (const [index, message] of messages.entries()) {
116
+ if (replayExcluded(message) || message.role === 'tool') continue;
117
+ if (message.role === 'system' && !nativeTrim(message.content ?? '')) continue;
118
+ const id = freshId('msg'), parts = []; let created;
119
+ if (message.role === 'system') {
120
+ const part = { ...partBase(id, 'text'), text: message.content, synthetic: true,
121
+ metadata: { supercode_claude_system_subtype: message.metadata.systemSubtype ?? 'local_command' } };
122
+ parts.push(part); created = timestamp(message);
123
+ } else if (message.role === 'user') {
124
+ if (message.content_parts != null) for (const part of message.content_parts) {
125
+ if (part?.type === 'text' && typeof part.text === 'string') parts.push({ ...partBase(id, 'text'), text: part.text });
126
+ else if (part?.type === 'image_url' && typeof part?.image_url?.url === 'string') {
127
+ const attachment = imageAttachments([part])[0]; parts.push({ ...partBase(id, 'file'), ...attachment });
128
+ }
129
+ }
130
+ else if (message.content) parts.push({ ...partBase(id, 'text'), text: message.content });
131
+ created = timestamp(message);
132
+ } else if (message.role === 'assistant') {
133
+ created = timestamp(message);
134
+ if (message.metadata.thinking != null) {
135
+ const part = { ...partBase(id, 'reasoning'), text: message.metadata.thinking, time: clock({ start: created, end: created }) };
136
+ if (message.metadata.thinking_signature != null) part.metadata = { anthropic: { signature: message.metadata.thinking_signature } };
137
+ parts.push(part);
138
+ }
139
+ if (message.content) parts.push({ ...partBase(id, 'text'), text: message.content });
140
+ for (const [toolIndex, call] of (message.tool_calls ?? []).entries()) {
141
+ const output = paired.get(`${index}:${toolIndex}`); let state;
142
+ if (output != null) {
143
+ const result = output[1], end = timestamp(result);
144
+ state = isToolError(result) ? { status: 'error', input: null, error: result.content ?? '', time: clock({ end }) }
145
+ : { status: 'completed', input: null, output: result.content ?? '', title: call.function.name, time: clock({ end }) };
146
+ if (!isToolError(result)) { const attachments = imageAttachments(result.content_parts); if (attachments.length) state.attachments = attachments; }
147
+ } else state = { status: 'pending', input: null };
148
+ const args = nativeTrim(call.function.arguments);
149
+ if (!args) state.input = {}; else parsedJsonField(state, 'input', args, {});
150
+ const part = { ...partBase(id, 'tool'), callID: call.id, tool: call.function.name, state };
151
+ if (output != null) {
152
+ part._supercode_result_position = output[0];
153
+ if (toolOutcome(output[1]) === 'unknown') part._supercode_tool_outcome = 'unknown';
154
+ sourceMessageExtension(part, session.meta.source, output[1]);
155
+ }
156
+ parts.push(part);
157
+ }
158
+ } else throw new TypeError(`unknown message role: ${message.role}`);
159
+ const info = { id, sessionID: sessionId, role: message.role === 'assistant' ? 'assistant' : 'user',
160
+ time: clock({ created }), _supercode_message_position: index };
161
+ if (message.role !== 'system') { restoreFields(info, message, message.role === 'assistant'); sourceMessageExtension(info, session.meta.source, message); }
162
+ out.push({ info, parts });
163
+ }
164
+ return { messages: out, cursor: position };
165
+ }
166
+
167
+ function touchUpdated(info, cursor) {
168
+ if (!object(info.time)) info.time = {};
169
+ setNativeInteger(info.time, 'updated', cursor);
170
+ }
171
+
172
+ export function toOpenCodeJson(session) {
173
+ const info = synthesizedInfo(session), cursor = maxTimestamp(info) ?? (SYNTH_TS_MS - 1n);
174
+ const result = appendMessages(session, session.messages, typeof info.id === 'string' ? info.id : 'ses_new', cursor);
175
+ if (result.messages.length) touchUpdated(info, result.cursor);
176
+ return stringifyNativeJsonPretty({ info, messages: result.messages });
177
+ }
178
+
179
+ export function toOpenCodeJsonSpliced(session, sessionId = null) {
180
+ if (!session.raw.length) return toOpenCodeJson(session);
181
+ const retained = recordsFromRaw(session);
182
+ let info = retained.info === undefined ? synthesizedInfo(session) : cloneNativeJson(retained.info);
183
+ if (sessionId != null) {
184
+ if (info == null) info = {};
185
+ if (!object(info)) throw new Error('OpenCode session info is not an object');
186
+ info.id = sessionId;
187
+ }
188
+ const cursor = largest([maxTimestamp(info), ...retained.records.flatMap(record => [maxTimestamp(record.info), ...record.parts.map(maxTimestamp)])])
189
+ ?? (SYNTH_TS_MS - 1n);
190
+ const prefix = splicedPrefixLengths(session);
191
+ const result = appendMessages(session, session.messages.slice(prefix.messages), typeof info?.id === 'string' ? info.id : 'ses_new', cursor);
192
+ if (result.messages.length) {
193
+ if (info == null) info = {};
194
+ touchUpdated(info, result.cursor);
195
+ }
196
+ return stringifyNativeJsonPretty({ info, messages: [...retained.records, ...result.messages] });
197
+ }
@@ -0,0 +1,130 @@
1
+ import { nativeInteger, setNativeInteger, parseNativeJson } from './native-json.mjs';
2
+ import { isToolError, toolOutcome } from './message.mjs';
3
+ import { nativeTrim } from './text.mjs';
4
+ import { NATIVE_RESIDUE_KEY, NATIVE_RESIDUE_SUMMARY_KEY } from './session-native-residue.mjs';
5
+ import { nativeResidueEnvelope, nativeResidueSummary } from './session-write-residue.mjs';
6
+ import { SYNTH_TS, SYNTH_TS_MS, replayExcluded, sessionCwd, tryNativeJson, metadataI64, sourceMessageExtension,
7
+ splicedPrefixLengths, jsonLine, injectFirstJsonLine, synthUuid, messageTimestamp, parseDataUri, parsedJsonField } from './session-write-utils.mjs';
8
+
9
+ function piHeader(session) {
10
+ const header = { type: 'session', version: 3, id: session.meta.session_id ?? synthUuid(0),
11
+ timestamp: session.meta.lineage.created_at ?? SYNTH_TS, cwd: sessionCwd(session) };
12
+ const parent = session.meta.lineage.parent_session_path;
13
+ if (parent != null) header.parentSession = parent;
14
+ const fork = session.meta.lineage.claude_fork_context_ref_raw;
15
+ if (fork != null) parsedJsonField(header, 'claude_fork_context_ref', fork);
16
+ return jsonLine(header);
17
+ }
18
+
19
+ function piContent(message) {
20
+ if (message.content_parts == null) return message.content ?? '';
21
+ const parts = [];
22
+ for (const part of message.content_parts) {
23
+ if (part?.type === 'text' && typeof part.text === 'string') parts.push({ type: 'text', text: part.text });
24
+ else if (part?.type === 'image_url') {
25
+ const image = parseDataUri(part?.image_url?.url);
26
+ if (image != null) parts.push({ type: 'image', mimeType: image.mimeType, data: image.data });
27
+ }
28
+ }
29
+ return parts;
30
+ }
31
+
32
+ function assistantContent(message) {
33
+ const content = [], metadata = message.metadata;
34
+ if (metadata.thinking != null) {
35
+ const block = { type: 'thinking', thinking: metadata.thinking };
36
+ if (metadata.thinking_signature != null) block.thinkingSignature = metadata.thinking_signature;
37
+ if (metadata.pi_thinking_redacted === 'true') block.redacted = true;
38
+ content.push(block);
39
+ }
40
+ if (message.content) {
41
+ const block = { type: 'text', text: message.content };
42
+ if (metadata.pi_text_signature != null) block.textSignature = metadata.pi_text_signature;
43
+ content.push(block);
44
+ }
45
+ for (const call of message.tool_calls ?? []) {
46
+ const block = { type: 'toolCall', id: call.id, name: call.function.name };
47
+ const args = nativeTrim(call.function.arguments);
48
+ if (args === '') block.arguments = {}; else parsedJsonField(block, 'arguments', args, {});
49
+ if (metadata.pi_thought_signature != null) block.thoughtSignature = metadata.pi_thought_signature;
50
+ content.push(block);
51
+ }
52
+ return content;
53
+ }
54
+
55
+ function defaultUsage() {
56
+ return parseNativeJson('{"input":0,"output":0,"cacheRead":0,"cacheWrite":0,"totalTokens":0,"cost":{"input":0.0,"output":0.0,"cacheRead":0.0,"cacheWrite":0.0,"total":0.0}}');
57
+ }
58
+
59
+ function piEntries(session, messages, parent = null, used = new Set()) {
60
+ let text = '', counter = 0n; const names = new Map();
61
+ const freshId = () => {
62
+ for (;;) {
63
+ counter = BigInt.asUintN(64, counter + 1n);
64
+ const id = (BigInt.asUintN(64, counter * 0x9e3779b97f4a7c15n) >> 32n).toString(16).padStart(8, '0');
65
+ if (!used.has(id)) { used.add(id); return id; }
66
+ }
67
+ };
68
+ for (const message of messages) {
69
+ if (replayExcluded(message)) continue;
70
+ for (const call of message.tool_calls ?? []) names.set(call.id, call.function.name);
71
+ const id = freshId(), metadata = message.metadata;
72
+ let native;
73
+ switch (message.role) {
74
+ case 'system': {
75
+ const content = message.content ?? ''; if (!nativeTrim(content)) continue;
76
+ native = { role: 'custom', customType: 'supercode_claude_system', content, display: true,
77
+ details: { claude_system_subtype: metadata.systemSubtype ?? 'local_command' } };
78
+ break;
79
+ }
80
+ case 'user': native = { role: 'user', content: piContent(message) }; break;
81
+ case 'assistant':
82
+ native = { role: 'assistant', content: assistantContent(message), api: metadata.pi_api ?? 'anthropic-messages',
83
+ provider: metadata.pi_provider ?? 'anthropic', model: session.meta.model ?? 'unknown' };
84
+ parsedJsonField(native, 'usage', metadata.pi_usage, defaultUsage());
85
+ native.stopReason = metadata.pi_stop_reason ?? 'stop';
86
+ break;
87
+ case 'tool': {
88
+ let content = piContent(message);
89
+ if (typeof content === 'string') content = content ? [{ type: 'text', text: content }] : [];
90
+ native = { role: 'toolResult', toolCallId: message.tool_call_id ?? '',
91
+ toolName: message.name ?? names.get(message.tool_call_id) ?? '', content,
92
+ isError: metadata.pi_is_error === 'true' || isToolError(message) };
93
+ break;
94
+ }
95
+ default: throw new TypeError(`unknown message role: ${message.role}`);
96
+ }
97
+ setNativeInteger(native, 'timestamp', metadataI64(metadata.pi_msg_timestamp) ?? SYNTH_TS_MS);
98
+ const entry = { type: 'message', id, parentId: parent, timestamp: messageTimestamp(message), message: native };
99
+ if (message.role === 'tool' && toolOutcome(message) === 'unknown') entry._supercode_tool_outcome = 'unknown';
100
+ sourceMessageExtension(entry, session.meta.source, message); text += jsonLine(entry); parent = id;
101
+ if (message.role === 'tool' && message.tool_call_id != null) names.delete(message.tool_call_id);
102
+ }
103
+ return text;
104
+ }
105
+
106
+ export function toPiJsonl(session) {
107
+ let text = piHeader(session) + piEntries(session, session.messages);
108
+ const envelope = nativeResidueEnvelope(session.meta);
109
+ if (envelope != null) {
110
+ text = injectFirstJsonLine(text, NATIVE_RESIDUE_SUMMARY_KEY, nativeResidueSummary(envelope));
111
+ text = injectFirstJsonLine(text, NATIVE_RESIDUE_KEY, envelope);
112
+ }
113
+ return text;
114
+ }
115
+
116
+ export function toPiJsonlSpliced(session, sessionId = null) {
117
+ const prefix = splicedPrefixLengths(session);
118
+ if (!prefix.raw) return toPiJsonl(session);
119
+ let text = '', leaf = null; const used = new Set();
120
+ for (const [index, line] of session.raw.slice(0, prefix.raw).entries()) {
121
+ const value = tryNativeJson(line);
122
+ if (!index && value?.type === 'session' && (nativeInteger(value, 'version') !== 3n || sessionId != null)) {
123
+ setNativeInteger(value, 'version', 3n); if (sessionId != null) value.id = sessionId;
124
+ text += jsonLine(value); continue;
125
+ }
126
+ text += line + '\n';
127
+ if (typeof value?.id === 'string') { used.add(value.id); leaf = value.id; }
128
+ }
129
+ return text + piEntries(session, session.messages.slice(prefix.messages), leaf, used);
130
+ }
@@ -0,0 +1,19 @@
1
+ import { cloneNativeJson } from './native-json.mjs';
2
+ import { residueRecordsDigest } from './session-native-residue.mjs';
3
+
4
+ /** The existing verified envelope remains independent of the portable segment cache. */
5
+ export function nativeResidueEnvelope(meta) {
6
+ const records = meta.codex_provenance.length ? meta.codex_provenance : meta.native_residue;
7
+ const source = meta.codex_provenance.length ? 'codex' : meta.native_residue_source;
8
+ return source == null || !records.length ? null : {
9
+ version: 2, source, records: cloneNativeJson(records), records_sha256: residueRecordsDigest(records),
10
+ };
11
+ }
12
+
13
+ export function nativeResidueSummary(envelope) {
14
+ const records = Array.isArray(envelope.records) ? envelope.records : [];
15
+ const kinds = [...new Set(records.map(record => record?.kind).filter(kind => typeof kind === 'string'))]
16
+ .sort((left, right) => Buffer.compare(Buffer.from(left), Buffer.from(right)));
17
+ return { version: 2, source: envelope.source ?? null, kinds, records: records.length,
18
+ records_sha256: envelope.records_sha256 ?? null };
19
+ }
@@ -0,0 +1,77 @@
1
+ // Shared emission helpers. Source bytes and native numeric facts stay separate from synthesis.
2
+ import { cloneNativeJson, parseNativeJson, setNativeJsonText, stringifyNativeJson, stringifyNativeJsonMember } from './native-json.mjs';
3
+ import { usageTimestamp } from './session-usage-time.mjs';
4
+
5
+ export const SYNTH_TS = '2026-01-01T00:00:00.000Z';
6
+ export const SYNTH_TS_MS = 1_767_225_600_000n;
7
+ export const replayExcluded = message => message.metadata.compacted_out === 'true'
8
+ || message.metadata.pi_exclude_from_context === 'true';
9
+ export const sessionCwd = session => session.meta.cwd ?? '.';
10
+ export const synthUuid = counter => `00000000-0000-4000-8000-${BigInt(counter).toString(16).padStart(12, '0')}`;
11
+ export const messageTimestamp = message => usageTimestamp(message.metadata.timestamp) == null ? SYNTH_TS : message.metadata.timestamp;
12
+ export const tryNativeJson = text => { try { return parseNativeJson(text); } catch { return undefined; } };
13
+ export function parsedJsonField(destination, key, text, fallback = text) {
14
+ try { setNativeJsonText(destination, key, text); }
15
+ catch { Object.defineProperty(destination, key, { value: fallback, enumerable: true, writable: true, configurable: true }); }
16
+ }
17
+ export function metadataI64(value) {
18
+ if (typeof value !== 'string' || !/^[+-]?[0-9]+$/u.test(value)) return null;
19
+ const integer = BigInt(value);
20
+ return integer >= -(1n << 63n) && integer <= (1n << 63n) - 1n ? integer : null;
21
+ }
22
+
23
+ /** ChatMessage metadata is a native BTreeMap, including numeric-looking keys. */
24
+ export function orderedMessageMetadata(metadata) {
25
+ const keys = Object.keys(metadata).sort((a, b) => Buffer.compare(Buffer.from(a), Buffer.from(b)));
26
+ return parseNativeJson(`{${keys.map(key => `${JSON.stringify(key)}:${stringifyNativeJsonMember(metadata, key)}`).join(',')}}`);
27
+ }
28
+
29
+ /** Full canonical envelope tolerated by foreign writers, separate from stock fields. */
30
+ export function targetMessageExtension(value, message) {
31
+ value._supercode_grok_message = messageEnvelope(message, 2);
32
+ }
33
+
34
+ export function messageEnvelope(message, schema) {
35
+ return {
36
+ schema, role: message.role, content: message.content,
37
+ content_parts: cloneNativeJson(message.content_parts), tool_calls: cloneNativeJson(message.tool_calls),
38
+ tool_call_id: message.tool_call_id, name: message.name, metadata: orderedMessageMetadata(message.metadata),
39
+ };
40
+ }
41
+
42
+ export const jsonLine = value => `${stringifyNativeJson(value)}\n`;
43
+ export function injectFirstJsonLine(text, key, extension) {
44
+ const end = text.indexOf('\n');
45
+ if (end < 0) return text;
46
+ const value = tryNativeJson(text.slice(0, end));
47
+ if (value == null || typeof value !== 'object' || Array.isArray(value)) return text;
48
+ Object.defineProperty(value, key, { value: extension, writable: true, enumerable: true, configurable: true });
49
+ return stringifyNativeJson(value) + text.slice(end);
50
+ }
51
+
52
+ export function inlineDataUrl(part) {
53
+ const url = part?.image_url?.url;
54
+ if (typeof url !== 'string' || !url.startsWith('data:')) return null;
55
+ const separator = url.indexOf(';base64,', 5);
56
+ return separator < 0 ? null : { mimeType: url.slice(5, separator), data: url.slice(separator + 8) };
57
+ }
58
+
59
+ /** The native Pi/Claude helper also accepts non-base64 data URIs. */
60
+ export function parseDataUri(url) {
61
+ if (typeof url !== 'string' || !url.startsWith('data:')) return null;
62
+ const comma = url.indexOf(',', 5); if (comma < 0) return null;
63
+ const meta = url.slice(5, comma);
64
+ return { mimeType: meta.endsWith(';base64') ? meta.slice(0, -7) : meta, data: url.slice(comma + 1) };
65
+ }
66
+
67
+ /** Carry portable source fields even after a second foreign-container hop. */
68
+ export function sourceMessageExtension(value, source, message) {
69
+ const portable = Object.keys(message.metadata).some(key => /^(?:gemini|grok|goose)_/u.test(key))
70
+ || Object.hasOwn(message.metadata, 'codex_custom_tool_call_ids');
71
+ if (['gemini', 'grok', 'goose'].includes(source) || portable || message.content_parts != null) targetMessageExtension(value, message);
72
+ }
73
+
74
+ export function splicedPrefixLengths(session) {
75
+ const messages = Math.min(session.imported_message_count ?? session.messages.length, session.messages.length);
76
+ return { messages, raw: Math.max(0, session.raw.length - (session.messages.length - messages)) };
77
+ }
@@ -0,0 +1,10 @@
1
+ import type { Session, ChatMessage, SessionFormat } from './session-model.js';
2
+
3
+ /** Full target synthesis. This does not execute or install a vendor session. */
4
+ export function toSessionJsonl(session: Session, format: SessionFormat): string;
5
+ /** Same-format raw prefix plus appended turns; parse loss refuses. */
6
+ export function toSessionJsonlSpliced(session: Session, format: SessionFormat, sessionId?: string | null): string;
7
+ /** Header and original source records, retaining the original trailing-newline flag. */
8
+ export function toNativeJsonl(session: Session): string;
9
+ /** Pure recovery container; caller supplies appended messages, no file is opened. */
10
+ export function toNativeJsonlV2(session: Session, appended: ChatMessage[], fixedTimestamp?: string | null): string;
@@ -0,0 +1,37 @@
1
+ import { cloneNativeJson } from './native-json.mjs';
2
+ import { SESSION_FORMATS } from './session-model.mjs';
3
+ import { toClaudeCodeJsonl, toClaudeCodeJsonlSpliced } from './session-write-claude.mjs';
4
+ import { toCodexJsonl, toCodexJsonlSpliced } from './session-write-codex.mjs';
5
+ import { toPiJsonl, toPiJsonlSpliced } from './session-write-pi.mjs';
6
+ import { toOpenCodeJson, toOpenCodeJsonSpliced } from './session-write-opencode.mjs';
7
+ import { toGrokJsonl, toGrokJsonlSpliced } from './session-write-grok.mjs';
8
+ import { toGeminiJsonl, toGeminiJsonlSpliced } from './session-write-gemini.mjs';
9
+ import { toGooseJson, toGooseJsonSpliced } from './session-write-goose.mjs';
10
+
11
+ export { toNativeJsonl, toNativeJsonlV2 } from './session-write-native.mjs';
12
+ const writers = Object.freeze({ claude_code: toClaudeCodeJsonl, codex: toCodexJsonl, pi: toPiJsonl,
13
+ opencode: toOpenCodeJson, grok: toGrokJsonl, gemini: toGeminiJsonl, goose: toGooseJson });
14
+ const spliceWriters = Object.freeze({ claude_code: toClaudeCodeJsonlSpliced, codex: toCodexJsonlSpliced, pi: toPiJsonlSpliced,
15
+ opencode: toOpenCodeJsonSpliced, grok: toGrokJsonlSpliced, gemini: toGeminiJsonlSpliced, goose: toGooseJsonSpliced });
16
+ const invalid = message => { throw Object.assign(new Error(message), { code: 'invalid_session' }); };
17
+ function formatWriter(table, format) {
18
+ if (!SESSION_FORMATS.includes(format)) throw new TypeError(`unknown session format: ${format}`);
19
+ return table[format];
20
+ }
21
+
22
+ /** Full synthesis follows each native target's schema; raw recovery is a separate container. */
23
+ export function toSessionJsonl(session, format) { return formatWriter(writers, format)(session); }
24
+
25
+ /** Refuse parse loss, preserve a same-format source prefix, and synthesize only appended turns. */
26
+ export function toSessionJsonlSpliced(session, format, sessionId = null) {
27
+ const writer = formatWriter(spliceWriters, format);
28
+ if (session.parse_error_lines > 0 || session.subagents.some(child => child.parse_error_lines > 0)) {
29
+ invalid('refusing spliced export because the loaded session contains parse loss');
30
+ }
31
+ if (!session.raw.length || format !== session.meta.source) {
32
+ if (sessionId == null) return toSessionJsonl(session, format);
33
+ const rewritten = cloneNativeJson(session); rewritten.meta.session_id = sessionId;
34
+ return toSessionJsonl(rewritten, format);
35
+ }
36
+ return writer(session, sessionId);
37
+ }