@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.
- package/README.md +88 -0
- package/hermes-store.mjs +5 -2
- package/message.mjs +85 -0
- package/native-json.mjs +94 -7
- package/package.json +16 -3
- package/session-claude-append.mjs +107 -0
- package/session-claude-content.mjs +209 -0
- package/session-claude-files.mjs +78 -0
- package/session-claude-graph.mjs +285 -0
- package/session-claude-index.mjs +173 -0
- package/session-claude.mjs +73 -0
- package/session-codec-utils.mjs +115 -0
- package/session-codecs.d.ts +64 -0
- package/session-codecs.mjs +19 -0
- package/session-codex-context.mjs +68 -0
- package/session-codex-display-items.mjs +89 -0
- package/session-codex-display.mjs +81 -0
- package/session-codex-items.mjs +117 -0
- package/session-codex-user-history.mjs +62 -0
- package/session-codex.mjs +135 -0
- package/session-display.mjs +135 -0
- package/session-document.mjs +19 -0
- package/session-file.mjs +27 -0
- package/session-follower.mjs +181 -0
- package/session-gemini.mjs +93 -0
- package/session-goose.mjs +139 -0
- package/session-grok.mjs +72 -0
- package/session-hermes.mjs +116 -0
- package/session-lines.mjs +20 -0
- package/session-model.d.ts +114 -0
- package/session-model.mjs +70 -0
- package/session-native-residue.mjs +109 -0
- package/session-native.mjs +62 -0
- package/session-opencode-content.mjs +115 -0
- package/session-opencode-store.mjs +101 -0
- package/session-opencode.mjs +115 -0
- package/session-path.mjs +129 -0
- package/session-pi-content.mjs +129 -0
- package/session-pi.mjs +86 -0
- package/session-read.d.ts +12 -0
- package/session-read.mjs +104 -0
- package/session-source.mjs +30 -0
- package/session-time.mjs +46 -0
- package/session-views.mjs +171 -0
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import type { SessionSurface, SessionTrigger } from './index.js';
|
|
2
|
+
|
|
3
|
+
/** Native JSON numeric token information is retained privately by the reader.
|
|
4
|
+
* Source bytes are stored separately; a display object is not an export. */
|
|
5
|
+
export type JsonValue = null | boolean | number | string | JsonValue[] | { [key: string]: JsonValue };
|
|
6
|
+
export type SessionSource = 'claude_code' | 'codex' | 'opencode' | 'pi' | 'grok' | 'gemini' | 'goose' | 'openclaw' | 'hermes' | 'native';
|
|
7
|
+
/** Only sources with a declared read/write translation format. */
|
|
8
|
+
export type SessionFormat = 'claude_code' | 'codex' | 'opencode' | 'pi' | 'grok' | 'gemini' | 'goose';
|
|
9
|
+
export type LoadFidelity = 'byte_lossless' | 'value_lossless' | 'semantic';
|
|
10
|
+
export type MessageRole = 'system' | 'user' | 'assistant' | 'tool';
|
|
11
|
+
export interface FunctionCall { name: string; arguments: string }
|
|
12
|
+
export interface ToolCall { id: string; type: string; function: FunctionCall }
|
|
13
|
+
export interface ChatMessage {
|
|
14
|
+
role: MessageRole; content: string | null; content_parts: JsonValue[] | null;
|
|
15
|
+
tool_calls: ToolCall[] | null; tool_call_id: string | null; name: string | null;
|
|
16
|
+
metadata: Record<string, string>;
|
|
17
|
+
}
|
|
18
|
+
export interface MessageWire {
|
|
19
|
+
role: MessageRole; content?: string | JsonValue[]; tool_calls?: ToolCall[];
|
|
20
|
+
tool_call_id?: string; name?: string;
|
|
21
|
+
}
|
|
22
|
+
export interface MessageJson extends MessageWire { metadata: Record<string, string> }
|
|
23
|
+
export type ToolOutcome = 'known_success' | 'known_error' | 'unknown';
|
|
24
|
+
export interface SessionMeta {
|
|
25
|
+
source: SessionSource; session_id: string | null; ended_at: string | null; end_reason: string | null;
|
|
26
|
+
model: string | null; cwd: string | null; system_prompt: string | null;
|
|
27
|
+
codex_headers: JsonValue[]; codex_provenance: JsonValue[];
|
|
28
|
+
native_residue: JsonValue[]; native_residue_source: string | null;
|
|
29
|
+
message_records: Array<number | bigint | null>; opencode_headers: JsonValue[]; goose_header: JsonValue | null;
|
|
30
|
+
agent_id: string | null; parent_tool_use_id: string | null; lineage: Record<string, string>;
|
|
31
|
+
trigger: SessionTrigger | null; surface: SessionSurface | null; profile: string | null;
|
|
32
|
+
recurrence: { job_id: string; kind: string } | null;
|
|
33
|
+
cross_surface: { state: string; platform?: string; error?: string } | null;
|
|
34
|
+
}
|
|
35
|
+
/** Complete retained representation, distinct from its bounded read view. */
|
|
36
|
+
export interface Session {
|
|
37
|
+
meta: SessionMeta; messages: ChatMessage[]; subagents: Session[]; raw: string[];
|
|
38
|
+
raw_trailing_newline: boolean; imported_message_count: number | null;
|
|
39
|
+
raw_is_verbatim: boolean; parse_error_lines: number; load_residue: string[];
|
|
40
|
+
}
|
|
41
|
+
export interface NormalizedSession {
|
|
42
|
+
source: SessionSource; session_id: string | null; ended_at: string | null; end_reason: string | null;
|
|
43
|
+
model: string | null; cwd: string | null; system_prompt: string | null;
|
|
44
|
+
agent_id: string | null; parent_tool_use_id: string | null; lineage: Record<string, string>;
|
|
45
|
+
trigger: SessionTrigger; surface: SessionSurface | null; profile: string | null;
|
|
46
|
+
recurrence: SessionMeta['recurrence']; cross_surface: SessionMeta['cross_surface'];
|
|
47
|
+
workspace: { kind: 'repo' | 'channel' | 'none'; value?: string };
|
|
48
|
+
messages: MessageJson[]; subagents: NormalizedSession[]; raw_record_count: number;
|
|
49
|
+
total_message_count: number; parse_error_lines: number; fidelity: LoadFidelity; residue: string[];
|
|
50
|
+
}
|
|
51
|
+
/** Service parsing validates nonnegative integral sizes and rejects tail plus
|
|
52
|
+
* offset/limit. These representation helpers consume already parsed options. */
|
|
53
|
+
export interface SessionLoadOptions {
|
|
54
|
+
include_subagents?: boolean | null; inline_media?: 'full' | 'metadata'; max_inline_media_bytes?: number | null;
|
|
55
|
+
message_limit?: number | null; message_offset?: number | null; message_tail?: number | null;
|
|
56
|
+
}
|
|
57
|
+
export interface RecordedSessionProfile {
|
|
58
|
+
worker: { harness: string; model: string | null; cwd: string | null; binary: { version: string | null } };
|
|
59
|
+
permissions: { native: { approval: JsonValue; sandbox: JsonValue } }; source: 'native-session';
|
|
60
|
+
}
|
|
61
|
+
export interface RecordedSessionProfileChange { native_key: string; at: string | null; profile: RecordedSessionProfile }
|
|
62
|
+
export interface ProjectedSession {
|
|
63
|
+
source: SessionSource; session_id: string | null; ended_at: string | null; end_reason: string | null;
|
|
64
|
+
model: string | null; resolved_profile: RecordedSessionProfile; resolved_profile_history: RecordedSessionProfileChange[];
|
|
65
|
+
cwd: string | null; system_prompt: string | null; agent_id: string | null; parent_tool_use_id: string | null;
|
|
66
|
+
lineage: Record<string, string>; messages: MessageJson[]; subagents: ProjectedSession[];
|
|
67
|
+
raw_record_count: number; parse_error_lines: number;
|
|
68
|
+
}
|
|
69
|
+
export interface SessionSummary {
|
|
70
|
+
end_of_turn: boolean; first_assistant_message: MessageJson | null; first_message: MessageJson | null;
|
|
71
|
+
last_assistant_message: MessageJson | null; last_assistant_text: string; last_message: MessageJson | null;
|
|
72
|
+
}
|
|
73
|
+
export interface SessionMessageWindow {
|
|
74
|
+
has_more: boolean; has_newer: boolean; has_older: boolean; newer_items: number; offset: number;
|
|
75
|
+
older_items: number; returned: number; total_messages: number;
|
|
76
|
+
}
|
|
77
|
+
export interface ProjectedSessionResult { session: ProjectedSession; summary: SessionSummary; window: SessionMessageWindow }
|
|
78
|
+
|
|
79
|
+
export const SESSION_SOURCES: readonly SessionSource[];
|
|
80
|
+
export const SESSION_FORMATS: readonly SessionFormat[];
|
|
81
|
+
export const ROLES: readonly MessageRole[];
|
|
82
|
+
export const TOOL_ERROR_METADATA_KEY: 'sc.tool_error';
|
|
83
|
+
export const TOOL_OUTCOME_UNKNOWN_METADATA_KEY: 'sc.tool_outcome_unknown';
|
|
84
|
+
export function sourceFormat(source: SessionSource): SessionFormat | null;
|
|
85
|
+
export function createMessage(role: MessageRole, content?: string | null): ChatMessage;
|
|
86
|
+
export function deserializeMessage(value: unknown): ChatMessage;
|
|
87
|
+
export function messageWire(message: ChatMessage): MessageWire;
|
|
88
|
+
export function messageJson(message: ChatMessage): MessageJson;
|
|
89
|
+
export function parsedArguments(call: ToolCall): JsonValue;
|
|
90
|
+
export function markToolError(message: ChatMessage): void;
|
|
91
|
+
export function markToolOutcomeUnknown(message: ChatMessage): void;
|
|
92
|
+
export function isToolError(message: ChatMessage): boolean;
|
|
93
|
+
export function toolOutcome(message: ChatMessage): ToolOutcome;
|
|
94
|
+
export function splitLinesVerbatim(text: string): { lines: string[]; trailingNewline: boolean };
|
|
95
|
+
export function joinLinesVerbatim(lines: readonly string[], trailingNewline: boolean): string;
|
|
96
|
+
export function nonEmptyLines(text: string): Generator<string>;
|
|
97
|
+
export function createSession(source: SessionSource, messages?: ChatMessage[]): Session;
|
|
98
|
+
export function rawVerbatim(session: Session): string;
|
|
99
|
+
export function loadFidelity(session: Session): LoadFidelity;
|
|
100
|
+
export function sessionTrigger(meta: SessionMeta): SessionTrigger;
|
|
101
|
+
export function sessionWorkspace(meta: SessionMeta): NormalizedSession['workspace'];
|
|
102
|
+
export function codexStartTrigger(payload: JsonValue): SessionTrigger | null;
|
|
103
|
+
export function normalizedSessionJson(session: Session): NormalizedSession;
|
|
104
|
+
export function boundSessionView(session: Session, messageLimit?: number | null, maxMessageChars?: number | null): void;
|
|
105
|
+
export function messageHasContent(message: ChatMessage): boolean;
|
|
106
|
+
export function messageText(message: ChatMessage): string;
|
|
107
|
+
export function normalizedItemCount(messages: readonly ChatMessage[]): number;
|
|
108
|
+
export function projectedMessageWindow(total: number, options: SessionLoadOptions): [number, number];
|
|
109
|
+
export function projectInlineMedia(message: MessageJson, options: SessionLoadOptions): MessageJson;
|
|
110
|
+
export function projectedSessionSummary(session: Session, options: SessionLoadOptions): SessionSummary;
|
|
111
|
+
export function recordedSessionProfile(session: Session): RecordedSessionProfile;
|
|
112
|
+
export function recordedSessionProfileHistory(session: Session): RecordedSessionProfileChange[];
|
|
113
|
+
export function projectedSessionJson(session: Session, options: SessionLoadOptions): ProjectedSession;
|
|
114
|
+
export function projectedSessionResult(session: Session, options: SessionLoadOptions): ProjectedSessionResult;
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { messageJson } from './message.mjs';
|
|
2
|
+
import { cloneNativeJson } from './native-json.mjs';
|
|
3
|
+
import { joinLinesVerbatim } from './session-lines.mjs';
|
|
4
|
+
|
|
5
|
+
export const SESSION_SOURCES = Object.freeze(['claude_code', 'codex', 'opencode', 'pi', 'grok', 'gemini', 'goose', 'openclaw', 'hermes', 'native']);
|
|
6
|
+
export const SESSION_FORMATS = Object.freeze(['claude_code', 'codex', 'opencode', 'pi', 'grok', 'gemini', 'goose']);
|
|
7
|
+
export const sourceFormat = (source) => SESSION_FORMATS.includes(source) ? source : null;
|
|
8
|
+
|
|
9
|
+
export function createSession(source, messages = []) {
|
|
10
|
+
if (!SESSION_SOURCES.includes(source)) throw new TypeError(`unknown session source: ${source}`);
|
|
11
|
+
return {
|
|
12
|
+
meta: {
|
|
13
|
+
source, session_id: null, ended_at: null, end_reason: null, model: null, cwd: null, system_prompt: null,
|
|
14
|
+
codex_headers: [], codex_provenance: [], native_residue: [], native_residue_source: null,
|
|
15
|
+
message_records: [], opencode_headers: [], goose_header: null, agent_id: null, parent_tool_use_id: null,
|
|
16
|
+
lineage: Object.create(null), trigger: null, surface: null, profile: null, recurrence: null, cross_surface: null,
|
|
17
|
+
},
|
|
18
|
+
messages, subagents: [], raw: [], raw_trailing_newline: true, imported_message_count: null,
|
|
19
|
+
raw_is_verbatim: false, parse_error_lines: 0, load_residue: [],
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function rawVerbatim(session) { return joinLinesVerbatim(session.raw, session.raw_trailing_newline); }
|
|
24
|
+
|
|
25
|
+
export function loadFidelity(session) {
|
|
26
|
+
if (session.load_residue.length || session.subagents.some((child) => loadFidelity(child) === 'semantic')) return 'semantic';
|
|
27
|
+
return session.raw_is_verbatim ? 'byte_lossless' : 'value_lossless';
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function sessionTrigger(meta) {
|
|
31
|
+
if (meta.trigger != null) return meta.trigger;
|
|
32
|
+
return meta.agent_id != null || meta.parent_tool_use_id != null || meta.lineage.hermes_lineage_kind === 'delegate' ? 'parent' : 'human';
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function sessionWorkspace(meta) {
|
|
36
|
+
if (meta.cwd != null) return { kind: 'repo', value: meta.cwd };
|
|
37
|
+
if (meta.surface?.platform != null) return {
|
|
38
|
+
kind: 'channel', value: meta.surface.platform + (meta.surface.chat_id != null ? `:${meta.surface.chat_id}` : ''),
|
|
39
|
+
};
|
|
40
|
+
return { kind: 'none' };
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function codexStartTrigger(payload) {
|
|
44
|
+
if (payload?.source != null && typeof payload.source === 'object' && Object.hasOwn(payload.source, 'subagent')) return 'parent';
|
|
45
|
+
const originator = payload?.originator;
|
|
46
|
+
return typeof originator === 'string' && (originator === 'codex_exec' || originator === 'supercode' || originator.startsWith('supercode_')) ? 'api' : null;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function normalizedSessionJson(session) {
|
|
50
|
+
const { meta } = session;
|
|
51
|
+
return {
|
|
52
|
+
source: meta.source, session_id: meta.session_id, ended_at: meta.ended_at, end_reason: meta.end_reason,
|
|
53
|
+
model: meta.model, cwd: meta.cwd, system_prompt: meta.system_prompt, agent_id: meta.agent_id,
|
|
54
|
+
parent_tool_use_id: meta.parent_tool_use_id, lineage: cloneNativeJson(meta.lineage), trigger: sessionTrigger(meta),
|
|
55
|
+
surface: cloneNativeJson(meta.surface), profile: meta.profile, recurrence: cloneNativeJson(meta.recurrence),
|
|
56
|
+
cross_surface: cloneNativeJson(meta.cross_surface), workspace: sessionWorkspace(meta),
|
|
57
|
+
messages: session.messages.map(messageJson), subagents: session.subagents.map(normalizedSessionJson),
|
|
58
|
+
raw_record_count: session.raw.length,
|
|
59
|
+
total_message_count: Math.max(session.imported_message_count ?? session.messages.length, session.messages.length),
|
|
60
|
+
parse_error_lines: session.parse_error_lines, fidelity: loadFidelity(session), residue: [...session.load_residue],
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export { createMessage, deserializeMessage, messageWire, messageJson, parsedArguments, markToolError, markToolOutcomeUnknown, isToolError, toolOutcome, ROLES, TOOL_ERROR_METADATA_KEY, TOOL_OUTCOME_UNKNOWN_METADATA_KEY } from './message.mjs';
|
|
65
|
+
export { splitLinesVerbatim, joinLinesVerbatim, nonEmptyLines } from './session-lines.mjs';
|
|
66
|
+
export {
|
|
67
|
+
boundSessionView, messageHasContent, messageText, normalizedItemCount, projectedMessageWindow,
|
|
68
|
+
projectInlineMedia, projectedSessionSummary, recordedSessionProfile, recordedSessionProfileHistory,
|
|
69
|
+
projectedSessionJson, projectedSessionResult,
|
|
70
|
+
} from './session-views.mjs';
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import { blake3 } from '@noble/hashes/blake3.js';
|
|
2
|
+
import { cloneNativeJson, nativeInteger, nativeObjectKeys, parseNativeJson, setNativeInteger, stringifyNativeJson, stringifyNativeJsonMember } from './native-json.mjs';
|
|
3
|
+
import { field, isObject, stringField } from './session-codec-utils.mjs';
|
|
4
|
+
|
|
5
|
+
export const NATIVE_RESIDUE_KEY = '_supercode_native_residue';
|
|
6
|
+
export const NATIVE_RESIDUE_SUMMARY_KEY = '_supercode_native_residue_summary';
|
|
7
|
+
export const CODEX_PROVENANCE_KEY = '_supercode_codex_provenance';
|
|
8
|
+
const fail = (message) => { throw Object.assign(new Error(message), { code: 'invalid_session' }); };
|
|
9
|
+
const utf8Order = (left, right) => Buffer.compare(Buffer.from(left), Buffer.from(right));
|
|
10
|
+
|
|
11
|
+
function canonical(value, parent = null, key = null) {
|
|
12
|
+
if (Array.isArray(value)) return `[${value.map((child, at) => canonical(child, value, at)).join(',')}]`;
|
|
13
|
+
if (isObject(value)) return `{${nativeObjectKeys(value).sort(utf8Order).map((key) => `${JSON.stringify(key)}:${canonical(value[key], value, key)}`).join(',')}}`;
|
|
14
|
+
return parent == null ? stringifyNativeJson(value) : stringifyNativeJsonMember(parent, key);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/** The historical field says sha256; the shipped contract uses BLAKE3. */
|
|
18
|
+
export function residueRecordsDigest(records) { return Buffer.from(blake3(Buffer.from(canonical(records)))).toString('hex'); }
|
|
19
|
+
|
|
20
|
+
export function claudeResidueKind(record) {
|
|
21
|
+
const kind = stringField(record, 'type');
|
|
22
|
+
return ['file-history-snapshot', 'queue-operation', 'last-prompt', 'mode', 'fork-context-ref'].includes(kind) ? kind : null;
|
|
23
|
+
}
|
|
24
|
+
export function grokResidueKind(record) {
|
|
25
|
+
if (!isObject(record)) return null;
|
|
26
|
+
const kind = stringField(record, 'type');
|
|
27
|
+
return kind == null ? 'untyped' : ['system', 'user', 'assistant', 'tool_result'].includes(kind) ? null : kind;
|
|
28
|
+
}
|
|
29
|
+
export function codexProvenanceKind(record) {
|
|
30
|
+
const kind = stringField(record, 'type');
|
|
31
|
+
if (['session_meta', 'turn_context', 'compacted'].includes(kind)) return kind;
|
|
32
|
+
const event = stringField(field(record, 'payload'), 'type');
|
|
33
|
+
return kind === 'event_msg' && ['thread_rolled_back', 'thread_goal_updated', 'entered_review_mode', 'exited_review_mode'].includes(event) ? `event_msg/${event}` : null;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function captureNativeResidue(meta, source, recordIndex, raw, record, kind) {
|
|
37
|
+
if ([NATIVE_RESIDUE_KEY, NATIVE_RESIDUE_SUMMARY_KEY, CODEX_PROVENANCE_KEY].some((key) => field(record, key) !== undefined)) return;
|
|
38
|
+
if (meta.native_residue_source != null && meta.native_residue_source !== source) return;
|
|
39
|
+
const entry = { record_index: 0, kind, raw }; setNativeInteger(entry, 'record_index', BigInt(recordIndex));
|
|
40
|
+
meta.native_residue.push(entry); meta.native_residue_source = source;
|
|
41
|
+
}
|
|
42
|
+
export function captureCodexProvenance(meta, recordIndex, raw, record) {
|
|
43
|
+
const kind = codexProvenanceKind(record);
|
|
44
|
+
if (kind == null) return;
|
|
45
|
+
const entry = { record_index: 0, kind, raw }; setNativeInteger(entry, 'record_index', BigInt(recordIndex));
|
|
46
|
+
meta.codex_provenance.push(entry);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function restoreCodexProvenance(extension, meta) {
|
|
50
|
+
if (nativeInteger(extension, 'version') !== 1n) fail('invalid portable Codex provenance: expected version 1');
|
|
51
|
+
const records = field(extension, 'records');
|
|
52
|
+
if (!Array.isArray(records)) fail('invalid portable Codex provenance: `records` must be an array');
|
|
53
|
+
if (!records.length) fail('invalid portable Codex provenance: `records` must not be empty');
|
|
54
|
+
const restored = [], headers = [];
|
|
55
|
+
for (const entry of records) {
|
|
56
|
+
if (nativeInteger(entry, 'record_index') == null) fail('invalid portable Codex provenance: record_index must be an integer');
|
|
57
|
+
const kind = stringField(entry, 'kind'), raw = stringField(entry, 'raw');
|
|
58
|
+
if (kind == null) fail('invalid portable Codex provenance: kind must be a string');
|
|
59
|
+
if (raw == null) fail('invalid portable Codex provenance: raw must be a string');
|
|
60
|
+
let record;
|
|
61
|
+
try { record = parseNativeJson(raw); } catch { fail('invalid portable Codex provenance: raw is not valid JSON'); }
|
|
62
|
+
if (codexProvenanceKind(record) !== kind) fail(`invalid portable Codex provenance: kind \`${kind}\` does not match raw record`);
|
|
63
|
+
restored.push(cloneNativeJson(entry));
|
|
64
|
+
if (['session_meta', 'turn_context'].includes(stringField(record, 'type'))) headers.push(record);
|
|
65
|
+
}
|
|
66
|
+
meta.codex_provenance = restored; meta.codex_headers = headers;
|
|
67
|
+
return true;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export function restoreNativeResidue(extension, meta) {
|
|
71
|
+
const version = nativeInteger(extension, 'version');
|
|
72
|
+
if (version !== 2n) fail(`invalid portable native residue: expected version 2, found ${version == null ? 'None' : `Some(${version})`} — source-native records cannot be restored from this envelope`);
|
|
73
|
+
const source = stringField(extension, 'source');
|
|
74
|
+
if (!['codex', 'claude_code', 'grok'].includes(source)) fail(`invalid portable native residue: unsupported source ${source == null ? 'None' : `Some(${JSON.stringify(source)})`} — this build restores codex, claude_code and grok residue; the records are preserved raw but not replayed`);
|
|
75
|
+
const records = field(extension, 'records');
|
|
76
|
+
if (!Array.isArray(records)) fail('invalid portable native residue: `records` must be an array');
|
|
77
|
+
const claimed = stringField(extension, 'records_sha256');
|
|
78
|
+
if (claimed == null) fail('invalid portable native residue: `records_sha256` digest is missing — cannot verify the residue was not tampered with; refusing to restore');
|
|
79
|
+
if (claimed !== residueRecordsDigest(records)) fail('invalid portable native residue: records digest mismatch — the residue was modified or corrupted after export; refusing to restore source-native records');
|
|
80
|
+
if (source === 'codex') {
|
|
81
|
+
const v1 = { version: 0, records }; setNativeInteger(v1, 'version', 1n);
|
|
82
|
+
return restoreCodexProvenance(v1, meta);
|
|
83
|
+
}
|
|
84
|
+
const restored = [];
|
|
85
|
+
for (const entry of records) {
|
|
86
|
+
const kind = stringField(entry, 'kind'), raw = stringField(entry, 'raw');
|
|
87
|
+
if (nativeInteger(entry, 'record_index') == null || kind == null || raw == null) fail('invalid portable native residue: each record needs record_index/kind/raw');
|
|
88
|
+
let record;
|
|
89
|
+
try { record = parseNativeJson(raw); } catch { fail('invalid portable native residue: raw is not valid JSON'); }
|
|
90
|
+
const expected = source === 'claude_code' ? claudeResidueKind(record) : grokResidueKind(record);
|
|
91
|
+
if (expected !== kind) fail(`invalid portable native residue: kind \`${kind}\` does not match raw record`);
|
|
92
|
+
restored.push(cloneNativeJson(entry));
|
|
93
|
+
}
|
|
94
|
+
if (!restored.length) fail('invalid portable native residue: `records` must not be empty');
|
|
95
|
+
meta.native_residue = restored; meta.native_residue_source = source;
|
|
96
|
+
return true;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export function restoreTopLevelResidue(record, meta) {
|
|
100
|
+
if (field(record, NATIVE_RESIDUE_KEY) !== undefined) return restoreNativeResidue(record[NATIVE_RESIDUE_KEY], meta);
|
|
101
|
+
if (field(record, CODEX_PROVENANCE_KEY) !== undefined) return restoreCodexProvenance(record[CODEX_PROVENANCE_KEY], meta);
|
|
102
|
+
const summary = field(record, NATIVE_RESIDUE_SUMMARY_KEY);
|
|
103
|
+
if (summary !== undefined) {
|
|
104
|
+
const kinds = field(summary, 'kinds'), count = nativeInteger(summary, 'records') ?? 0n, source = stringField(summary, 'source') ?? 'unknown';
|
|
105
|
+
const names = Array.isArray(kinds) ? kinds.filter((kind) => typeof kind === 'string').join(', ') : '';
|
|
106
|
+
meta.lineage.residue_loss = `portable ${source} residue deleted: ${count} record(s) of kind(s) [${names}] can no longer be restored`;
|
|
107
|
+
}
|
|
108
|
+
return false;
|
|
109
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { createSession } from './session-model.mjs';
|
|
2
|
+
import { deserializeMessage } from './message.mjs';
|
|
3
|
+
import { parseNativeJson, cloneNativeJson, nativeInteger, nativeObjectKeys } from './native-json.mjs';
|
|
4
|
+
import { field, stringField, isObject } from './session-codec-utils.mjs';
|
|
5
|
+
import { splitLinesVerbatim, joinLinesVerbatim } from './session-lines.mjs';
|
|
6
|
+
import { readSessionDocument } from './session-document.mjs';
|
|
7
|
+
|
|
8
|
+
const invalid = (message) => { throw Object.assign(new Error(message), { code: 'invalid_session' }); };
|
|
9
|
+
function nativeTurn(value) {
|
|
10
|
+
const discriminant = nativeInteger(value, 'supercode_turn');
|
|
11
|
+
if (discriminant == null || discriminant > 255n || stringField(value, 'ts') == null) invalid('native turn needs an unsigned byte discriminator and a string ts');
|
|
12
|
+
const content = field(value, 'content'), parts = field(value, 'content_parts');
|
|
13
|
+
if (content != null && typeof content !== 'string') invalid('native turn content must be text');
|
|
14
|
+
if (parts != null && !Array.isArray(parts)) invalid('native turn content_parts must be an array');
|
|
15
|
+
const message = deserializeMessage(value);
|
|
16
|
+
message.content_parts = parts == null ? null : cloneNativeJson(parts);
|
|
17
|
+
const metadata = field(value, 'metadata');
|
|
18
|
+
if (metadata !== undefined) {
|
|
19
|
+
if (!isObject(metadata)) invalid('native turn metadata must be an object');
|
|
20
|
+
for (const key of nativeObjectKeys(metadata)) {
|
|
21
|
+
if (typeof metadata[key] !== 'string') invalid('native turn metadata values must be strings');
|
|
22
|
+
message.metadata[key] = metadata[key];
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
return message;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** Tolerant native wrapper reader; persisted sidecars use the strict framing
|
|
29
|
+
* entrypoint below. Rejected appended turns survive in raw and parse loss. */
|
|
30
|
+
export function fromNativeStr(text) {
|
|
31
|
+
const lines = splitLinesVerbatim(text).lines;
|
|
32
|
+
let header; try { header = parseNativeJson(lines[0] ?? ''); } catch { header = null; }
|
|
33
|
+
const trailing = typeof field(header, 'raw_trailing_newline') === 'boolean' ? header.raw_trailing_newline : true;
|
|
34
|
+
const body = [], turns = [];
|
|
35
|
+
for (const line of lines.slice(1)) {
|
|
36
|
+
let value; try { value = parseNativeJson(line); } catch { /* Broken body records are handled by the source codec. */ }
|
|
37
|
+
(field(value, 'supercode_turn') === undefined ? body : turns).push(line);
|
|
38
|
+
}
|
|
39
|
+
const source = stringField(header, 'source'), original = joinLinesVerbatim(body, trailing);
|
|
40
|
+
const known = ['claude_code', 'codex', 'pi', 'opencode', 'grok', 'gemini', 'goose', 'openclaw'];
|
|
41
|
+
const session = readSessionDocument(original, source === 'native' ? { source: 'claude_code' } : known.includes(source) ? { source } : {});
|
|
42
|
+
if (source === 'native') session.meta.source = 'native';
|
|
43
|
+
for (const line of turns) {
|
|
44
|
+
session.raw.push(line);
|
|
45
|
+
try { session.messages.push(nativeTurn(parseNativeJson(line))); } catch { session.parse_error_lines++; }
|
|
46
|
+
}
|
|
47
|
+
for (const key of ['agent_id', 'parent_tool_use_id']) { const value = stringField(header, key); if (value != null) session.meta[key] = value; }
|
|
48
|
+
const lineage = field(header, 'lineage');
|
|
49
|
+
if (isObject(lineage)) for (const key of nativeObjectKeys(lineage)) if (typeof lineage[key] === 'string') session.meta.lineage[key] = lineage[key];
|
|
50
|
+
return session;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function fromSidecarStr(text) {
|
|
54
|
+
const first = splitLinesVerbatim(text).lines[0];
|
|
55
|
+
if (first === undefined) invalid('sidecar header is missing from an empty artifact');
|
|
56
|
+
let header; try { header = parseNativeJson(first); } catch (error) { invalid(`sidecar header is not valid JSON: ${error.message}`); }
|
|
57
|
+
if (![1n, 2n].includes(nativeInteger(header, 'supercode_native'))) invalid('sidecar header must declare supported `supercode_native` version 1 or 2');
|
|
58
|
+
if (!['native', 'claude_code', 'codex', 'gemini', 'goose', 'opencode', 'pi', 'grok'].includes(stringField(header, 'source'))) invalid('sidecar header must declare a supported `source`');
|
|
59
|
+
return fromNativeStr(text);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export function fromNativeMessages(messages) { return createSession('native', messages); }
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import { createMessage, markToolError, isToolError, markToolOutcomeUnknown } from './message.mjs';
|
|
2
|
+
import { nativeInteger, stringifyNativeJsonMember } from './native-json.mjs';
|
|
3
|
+
import { field, stringField, functionCall, restoreGrokMessageExtension } from './session-codec-utils.mjs';
|
|
4
|
+
import { messageMilliseconds } from './session-time.mjs';
|
|
5
|
+
import { nativeTrim } from './text.mjs';
|
|
6
|
+
|
|
7
|
+
export const OPENCODE_ORIGINAL_POSITION = '__supercode_original_position';
|
|
8
|
+
export const OPENCODE_COMPACTED_TOOL_PLACEHOLDER = '[Old tool result content cleared]';
|
|
9
|
+
const append = (text, part) => typeof part === 'string' ? text + (text === '' ? '' : '\n') + part : text;
|
|
10
|
+
const partsOf = (value) => Array.isArray(value) ? value : [];
|
|
11
|
+
|
|
12
|
+
export function opencodeImagePart(part) {
|
|
13
|
+
const mime = stringField(part, 'mime'), url = stringField(part, 'url');
|
|
14
|
+
return mime?.startsWith('image/') && url?.startsWith('data:') ? { type: 'image_url', image_url: { url } } : null;
|
|
15
|
+
}
|
|
16
|
+
function timestamp(value, message) {
|
|
17
|
+
const ms = nativeInteger(field(value, 'time'), 'created', { signed: true });
|
|
18
|
+
if (ms != null) message.metadata.timestamp = messageMilliseconds(ms);
|
|
19
|
+
}
|
|
20
|
+
function retainJson(value, message, key) {
|
|
21
|
+
if (field(value, key) != null) message.metadata[key] = stringifyNativeJsonMember(value, key);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function pushOpenCodeUser(value, parts, session) {
|
|
25
|
+
// The namespaced single synthetic part is our portable Claude system marker.
|
|
26
|
+
const only = parts.length === 1 ? parts[0] : null;
|
|
27
|
+
const subtype = stringField(field(only, 'metadata'), 'supercode_claude_system_subtype');
|
|
28
|
+
if (stringField(only, 'type') === 'text' && field(only, 'synthetic') === true && subtype != null) {
|
|
29
|
+
const text = stringField(only, 'text');
|
|
30
|
+
if (text == null || nativeTrim(text) === '') return;
|
|
31
|
+
const message = createMessage('system', text); message.metadata.systemSubtype = subtype;
|
|
32
|
+
timestamp(value, message); session.messages.push(message); return;
|
|
33
|
+
}
|
|
34
|
+
let text = '', ignored = false;
|
|
35
|
+
const images = [];
|
|
36
|
+
for (const part of parts) {
|
|
37
|
+
if (stringField(part, 'type') === 'text') {
|
|
38
|
+
if (field(part, 'ignored') === true) ignored = true;
|
|
39
|
+
else text = append(text, stringField(part, 'text'));
|
|
40
|
+
} else if (stringField(part, 'type') === 'file') {
|
|
41
|
+
const image = opencodeImagePart(part); if (image != null) images.push(image);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
if (nativeTrim(text) === '' && !images.length) return;
|
|
45
|
+
const message = createMessage('user', images.length ? null : text);
|
|
46
|
+
if (images.length) message.content_parts = [...(nativeTrim(text) === '' ? [] : [{ type: 'text', text }]), ...images];
|
|
47
|
+
if (ignored) message.metadata.oc_has_ignored_part = 'true';
|
|
48
|
+
const id = stringField(value, 'id'), agent = stringField(value, 'agent'), system = stringField(value, 'system');
|
|
49
|
+
if (id != null) message.metadata.oc_message_id = id;
|
|
50
|
+
if (agent != null) message.metadata.agent = agent;
|
|
51
|
+
retainJson(value, message, 'model');
|
|
52
|
+
if (system != null) {
|
|
53
|
+
if (!session.firstSystemSeen) { session.meta.system_prompt = system; session.firstSystemSeen = true; }
|
|
54
|
+
message.metadata.system = system;
|
|
55
|
+
}
|
|
56
|
+
for (const part of parts) if (stringField(part, 'type') === 'compaction') {
|
|
57
|
+
message.metadata.phase = 'compaction';
|
|
58
|
+
const tail = stringField(part, 'tail_start_id'); if (tail != null) message.metadata.tail_start_id = tail;
|
|
59
|
+
}
|
|
60
|
+
timestamp(value, message); restoreGrokMessageExtension(value, message); session.messages.push(message);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export function pushOpenCodeAssistant(value, parts, session) {
|
|
64
|
+
let text = '', thinking = '', reasoningSeen = false, signature = null;
|
|
65
|
+
const calls = [], results = [];
|
|
66
|
+
for (const part of parts) {
|
|
67
|
+
const kind = stringField(part, 'type');
|
|
68
|
+
if (kind === 'text' && field(part, 'ignored') !== true) text = append(text, stringField(part, 'text'));
|
|
69
|
+
else if (kind === 'reasoning') {
|
|
70
|
+
reasoningSeen = true; thinking = append(thinking, stringField(part, 'text'));
|
|
71
|
+
signature = stringField(field(field(part, 'metadata'), 'anthropic'), 'signature') ?? signature;
|
|
72
|
+
} else if (kind === 'tool') {
|
|
73
|
+
const id = stringField(part, 'callID') ?? '', name = stringField(part, 'tool') ?? '', state = field(part, 'state'), status = stringField(state, 'status');
|
|
74
|
+
if (id === '' || !['pending', 'running', 'completed', 'error'].includes(status)) continue;
|
|
75
|
+
calls.push(functionCall(id, name, field(state, 'input') === undefined ? '{}' : stringifyNativeJsonMember(state, 'input')));
|
|
76
|
+
if (status === 'completed' || status === 'error') results.push({ id, name, part, state, status });
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
const message = createMessage('assistant', nativeTrim(text) === '' ? null : text);
|
|
80
|
+
message.tool_calls = calls.length ? calls : null;
|
|
81
|
+
if (message.content == null && !calls.length && !reasoningSeen) message.metadata.empty_assistant_record = 'true';
|
|
82
|
+
if (reasoningSeen) message.metadata.thinking = thinking;
|
|
83
|
+
if (signature != null) message.metadata.thinking_signature = signature;
|
|
84
|
+
const id = stringField(value, 'id'), agent = stringField(value, 'agent'), provider = stringField(value, 'providerID'), model = stringField(value, 'modelID');
|
|
85
|
+
if (id != null) message.metadata.oc_message_id = id;
|
|
86
|
+
if (agent != null) { message.metadata.agent = agent; session.meta.agent_id ??= agent; }
|
|
87
|
+
if (provider != null && model != null) { message.metadata.model = `${provider}/${model}`; session.meta.model ??= message.metadata.model; }
|
|
88
|
+
session.meta.cwd ??= stringField(field(value, 'path'), 'cwd');
|
|
89
|
+
if (field(value, 'summary') === true) message.metadata.is_summary = 'true';
|
|
90
|
+
for (const key of ['finish', 'variant', 'mode']) { const text = stringField(value, key); if (text != null) message.metadata[key] = text; }
|
|
91
|
+
for (const key of ['cost', 'tokens', 'error', 'structured']) retainJson(value, message, key);
|
|
92
|
+
for (const part of parts) if (stringField(part, 'type') === 'tool' && stringField(part, 'tool') === 'task') {
|
|
93
|
+
const call = stringField(part, 'callID'), child = stringField(field(part, 'metadata'), 'sessionId');
|
|
94
|
+
if (call != null && child != null) message.metadata[`oc_task_child_session_id__${call}`] = child;
|
|
95
|
+
}
|
|
96
|
+
timestamp(value, message); restoreGrokMessageExtension(value, message); session.messages.push(message);
|
|
97
|
+
for (const { id, name, part, state, status } of results) {
|
|
98
|
+
const compacted = nativeInteger(field(state, 'time'), 'compacted', { signed: true }), output = stringField(state, 'output') ?? '';
|
|
99
|
+
const content = status === 'error' ? stringField(state, 'error') ?? '' : compacted != null ? OPENCODE_COMPACTED_TOOL_PLACEHOLDER : output;
|
|
100
|
+
const tool = createMessage('tool', content); tool.tool_call_id = id; tool.name = name;
|
|
101
|
+
const position = nativeInteger(part, '_supercode_result_position');
|
|
102
|
+
if (position != null) tool.metadata[OPENCODE_ORIGINAL_POSITION] = String(position);
|
|
103
|
+
if (status === 'error') markToolError(tool);
|
|
104
|
+
if (!isToolError(tool) && stringField(part, '_supercode_tool_outcome') === 'unknown') markToolOutcomeUnknown(tool);
|
|
105
|
+
if (compacted != null) { tool.metadata.oc_tool_output_compacted = output; tool.metadata.oc_tool_time_compacted = String(compacted); }
|
|
106
|
+
if (status === 'completed') {
|
|
107
|
+
const images = partsOf(field(state, 'attachments')).map(opencodeImagePart).filter((image) => image != null);
|
|
108
|
+
if (images.length) tool.content_parts = [...(content === '' ? [] : [{ type: 'text', text: content }]), ...images];
|
|
109
|
+
}
|
|
110
|
+
const partId = stringField(part, 'id'); if (partId != null) tool.metadata.oc_part_id = partId;
|
|
111
|
+
const time = field(state, 'time'), clock = field(time, 'end') === undefined ? 'start' : 'end';
|
|
112
|
+
const ms = nativeInteger(time, clock, { signed: true }); if (ms != null) tool.metadata.timestamp = messageMilliseconds(ms);
|
|
113
|
+
restoreGrokMessageExtension(part, tool); session.messages.push(tool);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import { dirname, join } from 'node:path';
|
|
2
|
+
import { readStore, statement, tableColumns, text, integer } from './sqlite-reader.mjs';
|
|
3
|
+
import { sourceStat } from './store-registry.mjs';
|
|
4
|
+
import { optionalFile, verifySources } from './source-files.mjs';
|
|
5
|
+
import { decodeSessionUtf8 } from './session-file.mjs';
|
|
6
|
+
import { parseNativeJson, setNativeInteger, setNativeFloat } from './native-json.mjs';
|
|
7
|
+
import { isObject, stringField } from './session-codec-utils.mjs';
|
|
8
|
+
import { fromOpenCodeStr, opencodeEnvelope } from './session-opencode.mjs';
|
|
9
|
+
|
|
10
|
+
function diagnostic(options, detail) {
|
|
11
|
+
if (options.onDiagnostic) options.onDiagnostic(detail);
|
|
12
|
+
else process.emitWarning(`${detail.message}: ${detail.context ?? detail.path} (${detail.column ?? 'session_diff'}): ${detail.error}`, { code: 'SUPERCODE_OPENCODE_JSON' });
|
|
13
|
+
}
|
|
14
|
+
function jsonColumn(value, column, context, options) {
|
|
15
|
+
const source = text(value, true); if (source == null) return null;
|
|
16
|
+
try { return parseNativeJson(source); }
|
|
17
|
+
catch (error) { diagnostic(options, { message: 'opencode SQLite column failed to parse as JSON — treating as absent', column, context, error: error.message }); return null; }
|
|
18
|
+
}
|
|
19
|
+
const stamp = (value, key, number) => { if (number == null) value[key] = null; else setNativeInteger(value, key, number); };
|
|
20
|
+
|
|
21
|
+
function sessionInfo(db, id, options) {
|
|
22
|
+
const columns = tableColumns(db, 'session'), row = statement(db, 'SELECT * FROM session WHERE id=?').get(id);
|
|
23
|
+
if (!row) throw new Error(`OpenCode session \`${id}\` not found in this SQLite store`);
|
|
24
|
+
for (const key of ['id', 'project_id', 'parent_id', 'slug', 'directory', 'title', 'version', 'share_url', 'summary_additions', 'summary_deletions', 'summary_files', 'summary_diffs', 'revert', 'time_created', 'time_updated']) {
|
|
25
|
+
if (!columns.has(key)) throw new Error(`OpenCode session table has no required native column ${key}`);
|
|
26
|
+
}
|
|
27
|
+
const optional = (key) => columns.has(key) ? row[key] : null;
|
|
28
|
+
const additions = integer(row.summary_additions, true), deletions = integer(row.summary_deletions, true), files = integer(row.summary_files, true);
|
|
29
|
+
const diff = text(row.summary_diffs, true), share = text(row.share_url, true);
|
|
30
|
+
const summary = additions != null || deletions != null || files != null ? { additions: 0, deletions: 0, files: 0, diffs: jsonColumn(diff, 'summary_diffs', id, options) } : null;
|
|
31
|
+
if (summary) { stamp(summary, 'additions', additions ?? 0n); stamp(summary, 'deletions', deletions ?? 0n); stamp(summary, 'files', files ?? 0n); }
|
|
32
|
+
const tokens = { input: 0, output: 0, reasoning: 0, cache: { read: 0, write: 0 } };
|
|
33
|
+
for (const key of ['input', 'output', 'reasoning']) stamp(tokens, key, columns.has(`tokens_${key}`) ? integer(row[`tokens_${key}`]) : 0n);
|
|
34
|
+
for (const key of ['read', 'write']) stamp(tokens.cache, key, columns.has(`tokens_cache_${key}`) ? integer(row[`tokens_cache_${key}`]) : 0n);
|
|
35
|
+
const time = { created: 0, updated: 0, compacting: null, archived: null };
|
|
36
|
+
stamp(time, 'created', integer(row.time_created)); stamp(time, 'updated', integer(row.time_updated));
|
|
37
|
+
stamp(time, 'compacting', integer(optional('time_compacting'), true)); stamp(time, 'archived', integer(optional('time_archived'), true));
|
|
38
|
+
const cost = columns.has('cost') ? row.cost : 0;
|
|
39
|
+
if (typeof cost !== 'number' && typeof cost !== 'bigint') throw new Error('OpenCode returned a non-numeric cost column');
|
|
40
|
+
const info = {
|
|
41
|
+
id: text(row.id), slug: text(row.slug), projectID: text(row.project_id), workspaceID: text(optional('workspace_id'), true),
|
|
42
|
+
directory: text(row.directory), path: text(optional('path'), true), parentID: text(row.parent_id, true), summary, cost: 0, tokens,
|
|
43
|
+
share: share == null ? null : { url: share }, title: text(row.title), agent: text(optional('agent'), true),
|
|
44
|
+
model: jsonColumn(optional('model'), 'model', id, options), version: text(row.version), metadata: jsonColumn(optional('metadata'), 'metadata', id, options),
|
|
45
|
+
time, permission: jsonColumn(optional('permission'), 'permission', id, options), revert: jsonColumn(row.revert, 'revert', id, options),
|
|
46
|
+
};
|
|
47
|
+
setNativeFloat(info, 'cost', Number(cost)); return info;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function nativeRow(row, sessionId, messageId, options) {
|
|
51
|
+
const id = text(row.id), value = jsonColumn(text(row.data), messageId == null ? 'message.data' : 'part.data', id, options);
|
|
52
|
+
// A malformed/non-object data column remains diagnostically absent, as native.
|
|
53
|
+
if (isObject(value)) {
|
|
54
|
+
value.id = id; value.sessionID = sessionId;
|
|
55
|
+
if (messageId != null) value.messageID = messageId;
|
|
56
|
+
stamp(value, 'time_created', integer(row.time_created)); stamp(value, 'time_updated', integer(row.time_updated));
|
|
57
|
+
} else { integer(row.time_created); integer(row.time_updated); }
|
|
58
|
+
return value;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function envelopeLines(db, sessionId, options) {
|
|
62
|
+
const info = sessionInfo(db, sessionId, options), lines = [opencodeEnvelope(['session', stringField(info, 'projectID') ?? 'global', sessionId], info)];
|
|
63
|
+
const messages = statement(db, 'SELECT id, data, time_created, time_updated FROM message WHERE session_id=? ORDER BY time_created,id');
|
|
64
|
+
const parts = statement(db, 'SELECT id, data, time_created, time_updated FROM part WHERE message_id=? ORDER BY id');
|
|
65
|
+
for (const row of messages.iterate(sessionId)) {
|
|
66
|
+
const id = text(row.id); lines.push(opencodeEnvelope(['message', sessionId, id], nativeRow(row, sessionId, null, options)));
|
|
67
|
+
for (const part of parts.iterate(id)) lines.push(opencodeEnvelope(['part', id, text(part.id)], nativeRow(part, sessionId, id, options)));
|
|
68
|
+
}
|
|
69
|
+
for (const row of statement(db, 'SELECT content,status,priority,position,time_created,time_updated FROM todo WHERE session_id=? ORDER BY position').iterate(sessionId)) {
|
|
70
|
+
const value = { sessionID: sessionId, content: text(row.content), status: text(row.status), priority: text(row.priority), position: 0, time: { created: 0, updated: 0 } };
|
|
71
|
+
const position = integer(row.position); stamp(value, 'position', position);
|
|
72
|
+
stamp(value.time, 'created', integer(row.time_created)); stamp(value.time, 'updated', integer(row.time_updated));
|
|
73
|
+
const key = ['todo', sessionId, 0]; setNativeInteger(key, 2, position); lines.push(opencodeEnvelope(key, value));
|
|
74
|
+
}
|
|
75
|
+
return lines;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** Reconstructs every native SQL field inside a read-only DB/WAL snapshot.
|
|
79
|
+
* The external revert sidecar and the database are verified together. */
|
|
80
|
+
export async function readOpenCodeSqlite(path, sessionId = null, options = {}) {
|
|
81
|
+
if (sessionId != null && typeof sessionId !== 'string') throw new TypeError('OpenCode session selector must be a string');
|
|
82
|
+
if (!(await sourceStat(path))?.isFile()) throw new Error(`OpenCode SQLite store not found at ${path} — expected an \`opencode*.db\` file`);
|
|
83
|
+
const captured = await readStore(path, (db) => {
|
|
84
|
+
if (!statement(db, "SELECT count(*) AS count FROM sqlite_master WHERE type='table' AND name='session'").get().count) throw new Error(`${path} is a SQLite database but has no \`session\` table — not a recognized OpenCode store`);
|
|
85
|
+
const id = sessionId ?? (() => {
|
|
86
|
+
const row = statement(db, 'SELECT id FROM session ORDER BY (parent_id IS NULL) DESC,time_updated DESC LIMIT 1').get();
|
|
87
|
+
if (!row) throw new Error('OpenCode SQLite store contains no sessions'); return text(row.id);
|
|
88
|
+
})();
|
|
89
|
+
return { id, lines: envelopeLines(db, id, options) };
|
|
90
|
+
}, options.metrics);
|
|
91
|
+
const { id, lines } = captured.result, sidecarPath = join(dirname(path), 'storage', 'session_diff', `${id}.json`);
|
|
92
|
+
const sidecar = await optionalFile(sidecarPath, async (reader) => {
|
|
93
|
+
const source = decodeSessionUtf8(await reader.read(0, reader.size));
|
|
94
|
+
try { return { parsed: true, value: parseNativeJson(source) }; }
|
|
95
|
+
catch (error) { diagnostic(options, { message: 'opencode session_diff sidecar failed to parse as JSON — skipping', path: sidecarPath, error: error.message }); return { parsed: false }; }
|
|
96
|
+
}, options.metrics);
|
|
97
|
+
if (sidecar.result?.parsed) lines.push(opencodeEnvelope(['session_diff', id], sidecar.result.value));
|
|
98
|
+
const sources = [...captured.sources, sidecar]; await verifySources(sources);
|
|
99
|
+
const session = fromOpenCodeStr(`${lines.join('\n')}\n`); session.raw_is_verbatim = false;
|
|
100
|
+
return { result: session, sources };
|
|
101
|
+
}
|