@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,181 @@
|
|
|
1
|
+
// Passive persisted-history following. The owner chooses when to poll; this
|
|
2
|
+
// object starts no timer, runtime, process or filesystem watcher.
|
|
3
|
+
import { stat, lstat, readdir } from 'node:fs/promises';
|
|
4
|
+
import { basename, dirname, extname, join, resolve } from 'node:path';
|
|
5
|
+
import { nativeJsonEqual, setNativeInteger } from './native-json.mjs';
|
|
6
|
+
import { normalizedSessionJson } from './session-model.mjs';
|
|
7
|
+
import { messageJson } from './message.mjs';
|
|
8
|
+
import { boundSessionView } from './session-views.mjs';
|
|
9
|
+
import { looksLikeSessionSqlite, loadNativeStoreFamily, loadSessionPath } from './session-path.mjs';
|
|
10
|
+
import { loadSessionDisplayPath } from './session-display.mjs';
|
|
11
|
+
import { readGooseSqlite } from './session-goose.mjs';
|
|
12
|
+
import { readHermesSqlite } from './session-hermes.mjs';
|
|
13
|
+
import { readOpenCodeSqlite } from './session-opencode-store.mjs';
|
|
14
|
+
import { ClaudeAppendState } from './session-claude-append.mjs';
|
|
15
|
+
|
|
16
|
+
const pathOrder = (a, b) => Buffer.compare(Buffer.from(a), Buffer.from(b));
|
|
17
|
+
const identityKeys = ['source', 'session_id', 'model', 'cwd', 'system_prompt', 'agent_id', 'parent_tool_use_id', 'lineage'];
|
|
18
|
+
const sameIdentity = (left, right) => identityKeys.every((key) => nativeJsonEqual(left.meta[key], right.meta[key]));
|
|
19
|
+
const sameChildren = (left, right) => left.subagents.length === right.subagents.length
|
|
20
|
+
&& left.subagents.every((child, at) => normalizedEqual(child, right.subagents[at]));
|
|
21
|
+
function normalizedEqual(left, right) {
|
|
22
|
+
return sameIdentity(left, right) && nativeJsonEqual(left.messages, right.messages) && sameChildren(left, right)
|
|
23
|
+
&& left.parse_error_lines === right.parse_error_lines && nativeJsonEqual(left.load_residue, right.load_residue);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** Longest suffix of the retained window which is a prefix of the successor.
|
|
27
|
+
* KMP keeps repetitive histories linear in message comparisons. */
|
|
28
|
+
function overlap(current, next) {
|
|
29
|
+
if (!current.length || !next.length) return 0;
|
|
30
|
+
const prefix = new Uint32Array(next.length);
|
|
31
|
+
for (let at = 1, length = 0; at < next.length; at++) {
|
|
32
|
+
while (length && !nativeJsonEqual(next[at], next[length])) length = prefix[length - 1];
|
|
33
|
+
if (nativeJsonEqual(next[at], next[length])) length++;
|
|
34
|
+
prefix[at] = length;
|
|
35
|
+
}
|
|
36
|
+
let length = 0;
|
|
37
|
+
for (const message of current) {
|
|
38
|
+
while (length && (length === next.length || !nativeJsonEqual(message, next[length]))) length = prefix[length - 1];
|
|
39
|
+
if (nativeJsonEqual(message, next[length])) length++;
|
|
40
|
+
}
|
|
41
|
+
return length;
|
|
42
|
+
}
|
|
43
|
+
function appendPrefix(current, next) {
|
|
44
|
+
const plain = overlap(current, next);
|
|
45
|
+
const anchored = next.length > 1 && current.length > 1 && nativeJsonEqual(current[0], next[0])
|
|
46
|
+
? overlap(current.slice(1), next.slice(1)) : 0;
|
|
47
|
+
return Math.max(plain, anchored ? anchored + 1 : 0);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
async function pathStamp(path, follow = true) {
|
|
51
|
+
let info;
|
|
52
|
+
try { info = await (follow ? stat : lstat)(path, { bigint: true }); }
|
|
53
|
+
catch (error) { if (error.code !== 'ENOENT') throw error; return { path, kind: 'missing' }; }
|
|
54
|
+
return { path, kind: info.isFile() ? 'file' : info.isDirectory() ? 'directory' : 'other',
|
|
55
|
+
device: String(info.dev), inode: String(info.ino), size: String(info.size),
|
|
56
|
+
modified_ns: info.mtimeNs < 0n ? null : String(info.mtimeNs) };
|
|
57
|
+
}
|
|
58
|
+
async function collectTree(path, out) {
|
|
59
|
+
const pending = [{ path, follow: true }];
|
|
60
|
+
while (pending.length) {
|
|
61
|
+
const entry = pending.pop(), stamp = await pathStamp(entry.path, entry.follow); out.push(stamp);
|
|
62
|
+
// Follow the named root, never a nested symlink (or its cycle).
|
|
63
|
+
if (stamp.kind !== 'directory') continue;
|
|
64
|
+
const children = (await readdir(entry.path)).sort(pathOrder);
|
|
65
|
+
for (let at = children.length - 1; at >= 0; at--) pending.push({ path: join(entry.path, children[at]), follow: false });
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
async function sourceFingerprint(path, session, selected, includeSubagents) {
|
|
69
|
+
const stamps = [await pathStamp(path)], source = session.meta.source;
|
|
70
|
+
const directory = dirname(path), name = basename(path, extname(path));
|
|
71
|
+
if (source === 'claude_code' && includeSubagents) await collectTree(join(directory, name, 'subagents'), stamps);
|
|
72
|
+
if (['opencode', 'hermes', 'goose'].includes(source) && await looksLikeSessionSqlite(path)) {
|
|
73
|
+
// All supported SQLite stores can commit to their WAL before checkpointing.
|
|
74
|
+
stamps.push(await pathStamp(`${path}-wal`), await pathStamp(`${path}-shm`));
|
|
75
|
+
if (source === 'opencode' && selected != null) stamps.push(await pathStamp(join(directory, 'storage', 'session_diff', `${selected}.json`)));
|
|
76
|
+
}
|
|
77
|
+
if (source === 'grok') stamps.push(await pathStamp(join(directory, 'updates.jsonl')), await pathStamp(join(directory, 'summary.json')));
|
|
78
|
+
if (source === 'native') {
|
|
79
|
+
stamps.push(await pathStamp(join(directory, `${name}.sidecar.jsonl`)), await pathStamp(join(directory, `${name}.meta.json`)));
|
|
80
|
+
await collectTree(join(directory, `${name}.subagents`), stamps);
|
|
81
|
+
}
|
|
82
|
+
return stamps.sort((a, b) => pathOrder(a.path, b.path));
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export class SessionFollower {
|
|
86
|
+
static async open(path, options = {}) { return this.#open(path, options.selector ?? null, options.store ?? 'opencode', options); }
|
|
87
|
+
static async openLocator(locator, options = {}) {
|
|
88
|
+
const hermes = locator.harness === 'hermes', sqlite = locator.storage.kind === 'sqlite';
|
|
89
|
+
return this.#open(locator.storage.path, hermes ? locator.session_id : sqlite ? locator.storage.selector : null,
|
|
90
|
+
hermes ? 'hermes' : sqlite && locator.harness === 'goose' ? 'goose' : 'opencode', options);
|
|
91
|
+
}
|
|
92
|
+
static async #open(path, selected, store, options) {
|
|
93
|
+
if (typeof path !== 'string') throw new TypeError('a session follower requires a path');
|
|
94
|
+
for (const key of ['messageLimit', 'maxMessageChars']) if (options[key] != null && (!Number.isSafeInteger(options[key]) || options[key] < 0)) throw new TypeError(`${key} must be a nonnegative exact integer`);
|
|
95
|
+
if (!['opencode', 'hermes', 'goose'].includes(store)) throw new TypeError(`unknown session store: ${store}`);
|
|
96
|
+
const follower = new SessionFollower();
|
|
97
|
+
follower.path = resolve(path); follower.selected = selected; follower.store = store;
|
|
98
|
+
follower.options = { ...options, fidelity: options.fidelity ?? 'byte_lossless', includeSubagents: options.includeSubagents ?? true };
|
|
99
|
+
const sqlite = await looksLikeSessionSqlite(follower.path);
|
|
100
|
+
if (selected != null && !sqlite) throw new Error(`an OpenCode session selector requires a SQLite store; ${follower.path} is not one`);
|
|
101
|
+
follower.current = await follower.#load();
|
|
102
|
+
if (sqlite && selected == null) follower.selected = follower.current.meta.session_id;
|
|
103
|
+
// Discover the dialect/default selector, then bracket the opening read's
|
|
104
|
+
// actual companions. Unrelated neighboring files are never admission
|
|
105
|
+
// dependencies, and one OpenCode follow never walks every diff sidecar.
|
|
106
|
+
const before = await sourceFingerprint(follower.path, follower.current, follower.selected, follower.options.includeSubagents);
|
|
107
|
+
const opening = await follower.#load();
|
|
108
|
+
if (opening.meta.source !== follower.current.meta.source
|
|
109
|
+
|| !nativeJsonEqual(before, await sourceFingerprint(follower.path, follower.current, follower.selected, follower.options.includeSubagents))) throw new Error(`${follower.path} changed while opening its follower; retry the read`);
|
|
110
|
+
follower.current = opening;
|
|
111
|
+
follower.appendState = follower.#appendState(opening);
|
|
112
|
+
boundSessionView(follower.current, options.messageLimit, options.maxMessageChars);
|
|
113
|
+
follower.fingerprint = before;
|
|
114
|
+
follower.initialPending = true; follower.nextSequence = 1n; follower.closed = false;
|
|
115
|
+
return follower;
|
|
116
|
+
}
|
|
117
|
+
async #load() {
|
|
118
|
+
const { path, selected, store, options } = this;
|
|
119
|
+
if (await looksLikeSessionSqlite(path)) {
|
|
120
|
+
if (store === 'goose') {
|
|
121
|
+
if (selected == null) throw new Error('a Goose SQLite locator requires a session selector');
|
|
122
|
+
return (await readGooseSqlite(path, selected, options)).result;
|
|
123
|
+
}
|
|
124
|
+
if (store === 'hermes') {
|
|
125
|
+
if (selected == null) throw new Error('a Hermes SQLite locator requires a session selector');
|
|
126
|
+
return (await readHermesSqlite(path, selected, options)).result;
|
|
127
|
+
}
|
|
128
|
+
return (await readOpenCodeSqlite(path, selected, options)).result;
|
|
129
|
+
}
|
|
130
|
+
const native = await loadNativeStoreFamily(path, options); if (native != null) return native;
|
|
131
|
+
return options.displayHistory ? loadSessionDisplayPath(path, options.messageLimit ?? 500, options) : loadSessionPath(path, options);
|
|
132
|
+
}
|
|
133
|
+
#sequence(event) { setNativeInteger(event, 'sequence', this.nextSequence++); return event; }
|
|
134
|
+
#appendState(session) {
|
|
135
|
+
if (this.options.displayHistory) return null;
|
|
136
|
+
try { return ClaudeAppendState.fromSession(session, this.options.fidelity); } catch { return null; }
|
|
137
|
+
}
|
|
138
|
+
#snapshot(reason) { return this.#sequence({ type: 'session_snapshot', reason, session: normalizedSessionJson(this.current) }); }
|
|
139
|
+
#error(message) { return this.#sequence({ type: 'watch_error', recoverable: true, message }); }
|
|
140
|
+
/** Serial polls only; a service serializes them per follower. */
|
|
141
|
+
async poll() {
|
|
142
|
+
if (this.closed) throw new Error('the session follower is closed');
|
|
143
|
+
if (this.initialPending) { this.initialPending = false; return this.#snapshot('initial'); }
|
|
144
|
+
const observed = await sourceFingerprint(this.path, this.current, this.selected, this.options.includeSubagents);
|
|
145
|
+
if (nativeJsonEqual(observed, this.fingerprint)) return null;
|
|
146
|
+
const companionsSame = nativeJsonEqual(observed.filter((stamp) => stamp.path !== this.path), this.fingerprint.filter((stamp) => stamp.path !== this.path));
|
|
147
|
+
this.fingerprint = observed;
|
|
148
|
+
let next;
|
|
149
|
+
try {
|
|
150
|
+
const appended = companionsSame && this.appendState
|
|
151
|
+
? await this.appendState.append(this.path, this.current, this.options.fidelity, this.options).catch(() => null) : null;
|
|
152
|
+
next = appended?.session ?? await this.#load();
|
|
153
|
+
if (next.parse_error_lines > 0) return this.#error(`${this.path} contains ${next.parse_error_lines} malformed or truncated JSON line(s); retaining the last good snapshot`);
|
|
154
|
+
const after = await sourceFingerprint(this.path, this.current, this.selected, this.options.includeSubagents);
|
|
155
|
+
const stale = () => Object.assign(new Error('the source or its companions changed during the follow read'), { code: 'ESTALE' });
|
|
156
|
+
if (!nativeJsonEqual(observed, after)) throw stale();
|
|
157
|
+
if (next.meta.source !== this.current.meta.source) {
|
|
158
|
+
// A changed dialect has a different companion set. Bracket a read
|
|
159
|
+
// under that set before publishing it, rather than stamping new
|
|
160
|
+
// companions beside a snapshot which preceded their last mutation.
|
|
161
|
+
const source = next.meta.source, before = await sourceFingerprint(this.path, next, this.selected, this.options.includeSubagents);
|
|
162
|
+
next = await this.#load();
|
|
163
|
+
if (next.meta.source !== source || !nativeJsonEqual(before, await sourceFingerprint(this.path, next, this.selected, this.options.includeSubagents))) throw stale();
|
|
164
|
+
this.fingerprint = before;
|
|
165
|
+
}
|
|
166
|
+
this.appendState = appended?.state ?? this.#appendState(next);
|
|
167
|
+
boundSessionView(next, this.options.messageLimit, this.options.maxMessageChars);
|
|
168
|
+
} catch (error) {
|
|
169
|
+
if (error.code === 'ESTALE') this.fingerprint = []; // retry a raced read even if the last writer then stopped
|
|
170
|
+
return this.#error(`could not reload ${this.path}: ${error.message}; retaining the last good snapshot`);
|
|
171
|
+
}
|
|
172
|
+
if (normalizedEqual(this.current, next)) { this.current = next; return null; }
|
|
173
|
+
const identitySame = sameIdentity(this.current, next);
|
|
174
|
+
const prefix = identitySame && sameChildren(this.current, next) ? appendPrefix(this.current.messages, next.messages) : 0;
|
|
175
|
+
this.current = next;
|
|
176
|
+
if (prefix > 0 && next.messages.length > prefix) return this.#sequence({ type: 'messages_appended', session_id: next.meta.session_id,
|
|
177
|
+
messages: next.messages.slice(prefix).map(messageJson), total_message_count: Math.max(next.imported_message_count ?? next.messages.length, next.messages.length) });
|
|
178
|
+
return this.#snapshot(identitySame ? 'history_rewritten' : 'source_changed');
|
|
179
|
+
}
|
|
180
|
+
close() { this.closed = true; this.current = null; this.fingerprint = []; this.appendState = null; }
|
|
181
|
+
}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import { createMessage } from './message.mjs';
|
|
2
|
+
import { parseNativeJson, stringifyNativeJsonMember } from './native-json.mjs';
|
|
3
|
+
import { createSession } from './session-model.mjs';
|
|
4
|
+
import { splitLinesVerbatim } from './session-lines.mjs';
|
|
5
|
+
import { nativeTrim } from './text.mjs';
|
|
6
|
+
import {
|
|
7
|
+
field, stringField, valueString, functionCall, toolMessage, ensureToolResultsPaired,
|
|
8
|
+
stampMessageRecords, takeMessageRecords, restoreGeminiMessageExtension,
|
|
9
|
+
} from './session-codec-utils.mjs';
|
|
10
|
+
|
|
11
|
+
function pushUserParts(messages, parts, timestamp, source) {
|
|
12
|
+
if (!parts.length) return;
|
|
13
|
+
const message = createMessage('user');
|
|
14
|
+
message.content_parts = parts;
|
|
15
|
+
if (timestamp != null) message.metadata.timestamp = timestamp;
|
|
16
|
+
restoreGeminiMessageExtension(source, message);
|
|
17
|
+
messages.push(message);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/** Gemini's document reader. Unknown native records stay in the exact raw
|
|
21
|
+
* capture; this decoder does not open a store or a service. */
|
|
22
|
+
export function fromGeminiStr(text) {
|
|
23
|
+
const session = createSession('gemini'), { meta, messages } = session;
|
|
24
|
+
const capture = splitLinesVerbatim(text);
|
|
25
|
+
session.raw = capture.lines; session.raw_trailing_newline = capture.trailingNewline; session.raw_is_verbatim = true;
|
|
26
|
+
const records = capture.lines.map((line, index) => ({ line: nativeTrim(line), index })).filter((record) => record.line !== '');
|
|
27
|
+
const pending = new Map();
|
|
28
|
+
for (let lineIndex = 0; lineIndex < records.length; lineIndex++) {
|
|
29
|
+
if (lineIndex > 0) stampMessageRecords(messages, records[lineIndex - 1].index);
|
|
30
|
+
let value;
|
|
31
|
+
try { value = parseNativeJson(records[lineIndex].line); } catch { session.parse_error_lines++; continue; }
|
|
32
|
+
const kind = stringField(value, 'type');
|
|
33
|
+
if (kind == null) {
|
|
34
|
+
meta.session_id ??= stringField(value, 'sessionId');
|
|
35
|
+
for (const [key, target] of [['projectHash', 'gemini_project_hash'], ['startTime', 'created_at'], ['lastUpdated', 'updated_at'], ['kind', 'gemini_session_kind']]) {
|
|
36
|
+
if (field(value, key) !== undefined) meta.lineage[target] = valueString(value, key);
|
|
37
|
+
}
|
|
38
|
+
continue;
|
|
39
|
+
}
|
|
40
|
+
if (kind !== 'user' && kind !== 'gemini') continue;
|
|
41
|
+
const timestamp = stringField(value, 'timestamp'), model = stringField(value, 'model');
|
|
42
|
+
if (model != null) meta.model = model;
|
|
43
|
+
const content = field(value, 'content'), parts = Array.isArray(content) ? content : null;
|
|
44
|
+
const contentText = typeof content === 'string' ? content : parts ? nativeTrim(parts.map((part) => stringField(part, 'text')).filter((part) => part != null).join(' ')) : '';
|
|
45
|
+
if (kind === 'gemini') {
|
|
46
|
+
const nativeCalls = field(value, 'toolCalls'), calls = [];
|
|
47
|
+
const candidates = [...(Array.isArray(nativeCalls) ? nativeCalls : []), ...(parts ?? []).filter((part) => field(part, 'functionCall') !== undefined).map((part) => part.functionCall)];
|
|
48
|
+
for (let callIndex = 0; callIndex < candidates.length; callIndex++) {
|
|
49
|
+
const call = candidates[callIndex], name = stringField(call, 'name');
|
|
50
|
+
if (name == null) continue;
|
|
51
|
+
const id = stringField(call, 'id') ?? `gemini-${lineIndex}-${callIndex}`;
|
|
52
|
+
if (!pending.has(name)) pending.set(name, []);
|
|
53
|
+
pending.get(name).push(id);
|
|
54
|
+
calls.push(functionCall(id, name, field(call, 'args') === undefined ? '{}' : valueString(call, 'args')));
|
|
55
|
+
}
|
|
56
|
+
const message = createMessage('assistant', contentText === '' ? null : contentText);
|
|
57
|
+
message.tool_calls = calls.length ? calls : null;
|
|
58
|
+
if (timestamp != null) message.metadata.timestamp = timestamp;
|
|
59
|
+
if (model != null) message.metadata.gemini_model = model;
|
|
60
|
+
if (field(value, 'thoughts') != null) message.metadata.gemini_thoughts = stringifyNativeJsonMember(value, 'thoughts');
|
|
61
|
+
restoreGeminiMessageExtension(value, message);
|
|
62
|
+
if (message.content != null || message.tool_calls != null) messages.push(message);
|
|
63
|
+
continue;
|
|
64
|
+
}
|
|
65
|
+
let userParts = [];
|
|
66
|
+
if (parts) {
|
|
67
|
+
for (const part of parts) {
|
|
68
|
+
const response = field(part, 'functionResponse');
|
|
69
|
+
if (response !== undefined) {
|
|
70
|
+
pushUserParts(messages, userParts, timestamp, value); userParts = [];
|
|
71
|
+
const name = stringField(response, 'name') ?? 'tool', explicitId = stringField(response, 'id'), ids = pending.get(name);
|
|
72
|
+
if (explicitId != null && ids) { const at = ids.indexOf(explicitId); if (at >= 0) ids.splice(at, 1); }
|
|
73
|
+
const id = explicitId ?? (ids?.length ? ids.shift() : `gemini-${lineIndex}-response`);
|
|
74
|
+
const payload = field(response, 'response'), output = field(payload, 'output');
|
|
75
|
+
const body = output !== undefined ? valueString(payload, 'output') : payload !== undefined ? stringifyNativeJsonMember(response, 'response') : '';
|
|
76
|
+
const message = toolMessage(id, body); message.name = name;
|
|
77
|
+
if (timestamp != null) message.metadata.timestamp = timestamp;
|
|
78
|
+
restoreGeminiMessageExtension(value, message); messages.push(message);
|
|
79
|
+
continue;
|
|
80
|
+
}
|
|
81
|
+
const partText = stringField(part, 'text');
|
|
82
|
+
if (partText != null) { userParts.push({ type: 'text', text: partText }); continue; }
|
|
83
|
+
const inline = field(part, 'inlineData'), data = stringField(inline, 'data');
|
|
84
|
+
if (data != null) userParts.push({ type: 'image_url', image_url: { url: `data:${stringField(inline, 'mimeType') ?? 'application/octet-stream'};base64,${data}` } });
|
|
85
|
+
}
|
|
86
|
+
} else if (contentText !== '') userParts.push({ type: 'text', text: contentText });
|
|
87
|
+
pushUserParts(messages, userParts, timestamp, value);
|
|
88
|
+
}
|
|
89
|
+
if (records.length) stampMessageRecords(messages, records.at(-1).index);
|
|
90
|
+
ensureToolResultsPaired(messages);
|
|
91
|
+
meta.message_records = takeMessageRecords(messages); session.imported_message_count = messages.length;
|
|
92
|
+
return session;
|
|
93
|
+
}
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
import { createMessage, markToolError } from './message.mjs';
|
|
2
|
+
import { cloneNativeJson, nativeInteger, parseNativeJson, setNativeInteger, stringifyNativeJson, stringifyNativeJsonMember, stringifyNativeJsonPretty } from './native-json.mjs';
|
|
3
|
+
import { createSession } from './session-model.mjs';
|
|
4
|
+
import { splitLinesVerbatim } from './session-lines.mjs';
|
|
5
|
+
import { readStore, statement, text as columnText, integer as columnInteger } from './sqlite-reader.mjs';
|
|
6
|
+
import { isObject, field, stringField, valueString, functionCall, toolMessage, ensureToolResultsPaired, restoreGrokMessageExtension } from './session-codec-utils.mjs';
|
|
7
|
+
|
|
8
|
+
function captureMetadata(native, message) {
|
|
9
|
+
const created = nativeInteger(native, 'created', { signed: true }), id = stringField(native, 'id');
|
|
10
|
+
if (created != null) message.metadata.goose_created = String(created);
|
|
11
|
+
if (id != null) message.metadata.goose_message_id = id;
|
|
12
|
+
if (field(native, 'metadata') !== undefined) message.metadata.goose_metadata = stringifyNativeJsonMember(native, 'metadata');
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function normalizeMessage(native, nativeIndex, out) {
|
|
16
|
+
const role = stringField(native, 'role') === 'assistant' ? 'assistant' : 'user';
|
|
17
|
+
const content = field(native, 'content'), parts = [], text = [], calls = [], results = [];
|
|
18
|
+
const blocks = Array.isArray(content) ? content : [];
|
|
19
|
+
for (let blockIndex = 0; blockIndex < blocks.length; blockIndex++) {
|
|
20
|
+
const block = blocks[blockIndex], kind = stringField(block, 'type');
|
|
21
|
+
if (kind === 'text') {
|
|
22
|
+
const value = stringField(block, 'text');
|
|
23
|
+
if (value != null) { text.push(value); parts.push({ type: 'text', text: value }); }
|
|
24
|
+
} else if (kind === 'image') {
|
|
25
|
+
const data = stringField(block, 'data') ?? '';
|
|
26
|
+
const media = field(block, 'mimeType') !== undefined ? stringField(block, 'mimeType') : stringField(block, 'mime_type');
|
|
27
|
+
parts.push({ type: 'image_url', image_url: { url: `data:${media ?? 'application/octet-stream'};base64,${data}` } });
|
|
28
|
+
} else if (kind === 'toolRequest' || kind === 'frontendToolRequest') {
|
|
29
|
+
const id = stringField(block, 'id') ?? `goose-${nativeIndex}-${blockIndex}`, nativeCall = field(block, 'toolCall');
|
|
30
|
+
const call = stringField(nativeCall, 'status') === 'success' && field(nativeCall, 'value') !== undefined ? nativeCall.value : nativeCall;
|
|
31
|
+
if (call === undefined) continue;
|
|
32
|
+
calls.push(functionCall(id, stringField(call, 'name') ?? 'tool', field(call, 'arguments') === undefined ? '{}' : valueString(call, 'arguments')));
|
|
33
|
+
} else if (kind === 'toolResponse') results.push(block);
|
|
34
|
+
}
|
|
35
|
+
if (text.length || parts.length || calls.length) {
|
|
36
|
+
const message = createMessage(role, text.length ? text.join('\n') : null);
|
|
37
|
+
message.content_parts = parts.some((part) => part.type !== 'text') ? parts : null;
|
|
38
|
+
message.tool_calls = calls.length ? calls : null;
|
|
39
|
+
captureMetadata(native, message); out.push(message);
|
|
40
|
+
}
|
|
41
|
+
for (let index = 0; index < results.length; index++) {
|
|
42
|
+
const block = results[index], id = stringField(block, 'id') ?? `goose-${nativeIndex}-result-${index}`;
|
|
43
|
+
const result = field(block, 'toolResult') ?? null, statusError = stringField(result, 'status') === 'error';
|
|
44
|
+
const value = field(result, 'value') !== undefined ? result.value : result;
|
|
45
|
+
const isError = statusError || field(value, 'isError') === true;
|
|
46
|
+
let output;
|
|
47
|
+
if (statusError) output = stringField(result, 'error') ?? 'Goose tool call failed';
|
|
48
|
+
else {
|
|
49
|
+
const content = field(value, 'content');
|
|
50
|
+
output = Array.isArray(content) ? content.map((part, at) => stringField(part, 'text') ?? stringifyNativeJsonMember(content, at)).join('\n')
|
|
51
|
+
: field(result, 'value') !== undefined ? stringifyNativeJsonMember(result, 'value')
|
|
52
|
+
: field(block, 'toolResult') !== undefined ? stringifyNativeJsonMember(block, 'toolResult') : 'null';
|
|
53
|
+
}
|
|
54
|
+
const message = toolMessage(id, output);
|
|
55
|
+
if (isError) markToolError(message);
|
|
56
|
+
captureMetadata(native, message); out.push(message);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** Goose's official export document is the normalization boundary for its
|
|
61
|
+
* files and SQLite store. Unknown blocks remain in native-message metadata. */
|
|
62
|
+
export function fromGooseStr(json) {
|
|
63
|
+
const document = parseNativeJson(json);
|
|
64
|
+
if (!isObject(document)) throw Object.assign(new Error('Goose session export must be a JSON object'), { code: 'invalid_session' });
|
|
65
|
+
if (!Array.isArray(document.conversation)) throw Object.assign(new Error('Goose session export must contain a conversation array'), { code: 'invalid_session' });
|
|
66
|
+
const session = createSession('goose'), { meta, messages } = session;
|
|
67
|
+
meta.session_id = stringField(document, 'id');
|
|
68
|
+
meta.cwd = field(document, 'working_dir') !== undefined ? stringField(document, 'working_dir') : stringField(document, 'workingDir');
|
|
69
|
+
const config = field(document, 'model_config') !== undefined ? document.model_config : document.modelConfig;
|
|
70
|
+
meta.model = field(config, 'model_name') !== undefined ? stringField(config, 'model_name') : stringField(config, 'modelName');
|
|
71
|
+
for (const [key, target] of [['name', 'session_name'], ['created_at', 'created_at'], ['updated_at', 'updated_at'], ['session_type', 'goose_session_type'], ['goose_mode', 'goose_mode'], ['provider_name', 'goose_provider_name'], ['parent_session_id', 'parent_session_id']]) {
|
|
72
|
+
if (field(document, key) !== undefined) meta.lineage[target] = valueString(document, key);
|
|
73
|
+
}
|
|
74
|
+
const header = cloneNativeJson(document); delete header.conversation;
|
|
75
|
+
meta.goose_header = header;
|
|
76
|
+
for (let nativeIndex = 0; nativeIndex < document.conversation.length; nativeIndex++) {
|
|
77
|
+
const native = document.conversation[nativeIndex], before = messages.length;
|
|
78
|
+
normalizeMessage(native, nativeIndex, messages);
|
|
79
|
+
if (messages[before]) {
|
|
80
|
+
const first = messages[before];
|
|
81
|
+
first.metadata.goose_native_message = stringifyNativeJsonMember(document.conversation, nativeIndex);
|
|
82
|
+
first.metadata.goose_native_index = String(nativeIndex);
|
|
83
|
+
if (nativeIndex === 0) first.metadata.goose_session_header = stringifyNativeJson(header);
|
|
84
|
+
restoreGrokMessageExtension(native, first);
|
|
85
|
+
}
|
|
86
|
+
for (let index = before + 1; index < messages.length; index++) messages[index].metadata.goose_native_index = String(nativeIndex);
|
|
87
|
+
}
|
|
88
|
+
ensureToolResultsPaired(messages);
|
|
89
|
+
const capture = splitLinesVerbatim(json);
|
|
90
|
+
session.raw = capture.lines; session.raw_trailing_newline = capture.trailingNewline;
|
|
91
|
+
session.raw_is_verbatim = true; session.imported_message_count = messages.length;
|
|
92
|
+
return session;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/** One read-only, fingerprint-verified SQLite snapshot. Display callers may
|
|
96
|
+
* select a newest native-row window; a full artifact never silently uses it. */
|
|
97
|
+
export async function readGooseSqlite(path, sessionId, { messageLimit = null, metrics = {} } = {}) {
|
|
98
|
+
if (typeof sessionId !== 'string') throw new TypeError('Goose session selector must be a string');
|
|
99
|
+
if (messageLimit != null && (!Number.isSafeInteger(messageLimit) || messageLimit < 0)) throw new TypeError('Goose display limit must be a nonnegative exact integer');
|
|
100
|
+
const limit = messageLimit == null ? null : Math.max(1, messageLimit);
|
|
101
|
+
return readStore(path, (db) => {
|
|
102
|
+
const row = statement(db, `SELECT id, name, working_dir, created_at, updated_at, session_type,
|
|
103
|
+
extension_data, goose_mode, provider_name, model_config_json FROM sessions WHERE id = ?`).get(sessionId);
|
|
104
|
+
if (!row) throw Object.assign(new Error('failed to load Goose session: query returned no rows'), { code: 'invalid_session' });
|
|
105
|
+
const document = {
|
|
106
|
+
id: columnText(row.id), working_dir: columnText(row.working_dir), name: columnText(row.name), user_set_name: false,
|
|
107
|
+
session_type: columnText(row.session_type), created_at: columnText(row.created_at), updated_at: columnText(row.updated_at),
|
|
108
|
+
extension_data: parseColumn(columnText(row.extension_data, true), {}), usage: {}, accumulated_usage: {}, accumulated_cost: null,
|
|
109
|
+
schedule_id: null, recipe: null, user_recipe_values: null, conversation: [], message_count: 0, last_message_at: null,
|
|
110
|
+
provider_name: columnText(row.provider_name, true), model_config: parseColumn(columnText(row.model_config_json, true), null),
|
|
111
|
+
goose_mode: columnText(row.goose_mode), archived_at: null, project_id: null, parent_session_id: null, last_message_snippet: null,
|
|
112
|
+
};
|
|
113
|
+
const sql = limit == null
|
|
114
|
+
? 'SELECT message_id, role, content_json, created_timestamp, metadata_json FROM messages WHERE session_id = ? ORDER BY created_timestamp, id'
|
|
115
|
+
: `SELECT message_id, role, content_json, created_timestamp, metadata_json FROM
|
|
116
|
+
(SELECT id AS native_row_id, message_id, role, content_json, created_timestamp, metadata_json
|
|
117
|
+
FROM messages WHERE session_id = ? ORDER BY created_timestamp DESC, id DESC LIMIT ?)
|
|
118
|
+
ORDER BY created_timestamp, native_row_id`;
|
|
119
|
+
const rows = statement(db, sql).all(...(limit == null ? [sessionId] : [sessionId, limit]));
|
|
120
|
+
for (const row of rows) {
|
|
121
|
+
const native = {
|
|
122
|
+
id: columnText(row.message_id, true), role: columnText(row.role), created: 0,
|
|
123
|
+
content: parseColumn(columnText(row.content_json), []),
|
|
124
|
+
metadata: parseColumn(columnText(row.metadata_json, true), { userVisible: true, agentVisible: true }),
|
|
125
|
+
};
|
|
126
|
+
setNativeInteger(native, 'created', columnInteger(row.created_timestamp));
|
|
127
|
+
document.conversation.push(native);
|
|
128
|
+
}
|
|
129
|
+
document.message_count = document.conversation.length;
|
|
130
|
+
const session = fromGooseStr(stringifyNativeJsonPretty(document));
|
|
131
|
+
session.raw_is_verbatim = false;
|
|
132
|
+
return session;
|
|
133
|
+
}, metrics);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function parseColumn(text, fallback) {
|
|
137
|
+
if (text == null) return fallback;
|
|
138
|
+
try { return parseNativeJson(text); } catch { return fallback; }
|
|
139
|
+
}
|
package/session-grok.mjs
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { createMessage } from './message.mjs';
|
|
2
|
+
import { parseNativeJson } from './native-json.mjs';
|
|
3
|
+
import { createSession } from './session-model.mjs';
|
|
4
|
+
import { splitLinesVerbatim } from './session-lines.mjs';
|
|
5
|
+
import { nativeTrim } from './text.mjs';
|
|
6
|
+
import { field, stringField, valueString, extractTextContent, functionCall, toolMessage, restoreGrokMessageExtension, ensureToolResultsPaired, stampMessageRecords, takeMessageRecords } from './session-codec-utils.mjs';
|
|
7
|
+
import { restoreTopLevelResidue, captureNativeResidue, grokResidueKind } from './session-native-residue.mjs';
|
|
8
|
+
|
|
9
|
+
function humanText(raw) {
|
|
10
|
+
const text = nativeTrim(raw);
|
|
11
|
+
if (text === '' || text.startsWith('<user_info>') || text.startsWith('<system-reminder>')) return null;
|
|
12
|
+
const unwrapped = text.startsWith('<user_query>') && text.endsWith('</user_query>') ? nativeTrim(text.slice(12, -13)) : text;
|
|
13
|
+
return unwrapped === '' ? null : unwrapped;
|
|
14
|
+
}
|
|
15
|
+
function captureScalars(value, message, keys) {
|
|
16
|
+
for (const key of keys) if (field(value, key) !== undefined) message.metadata[`grok_${key}`] = valueString(value, key);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/** Conversation turns and source-native residue are separate; reasoning and
|
|
20
|
+
* backend records survive raw capture without becoming replay messages. */
|
|
21
|
+
export function fromGrokStr(text) {
|
|
22
|
+
const session = createSession('grok'), { meta, messages } = session, capture = splitLinesVerbatim(text);
|
|
23
|
+
session.raw = capture.lines; session.raw_trailing_newline = capture.trailingNewline; session.raw_is_verbatim = true;
|
|
24
|
+
const records = capture.lines.map((line, index) => ({ line: nativeTrim(line), index })).filter((record) => record.line !== '');
|
|
25
|
+
const toolNames = new Map();
|
|
26
|
+
for (let index = 0; index < records.length; index++) {
|
|
27
|
+
if (index > 0) stampMessageRecords(messages, records[index - 1].index);
|
|
28
|
+
let value;
|
|
29
|
+
try { value = parseNativeJson(records[index].line); } catch { session.parse_error_lines++; continue; }
|
|
30
|
+
restoreTopLevelResidue(value, meta);
|
|
31
|
+
const residue = grokResidueKind(value);
|
|
32
|
+
if (residue != null) captureNativeResidue(meta, 'grok', index, records[index].line, value, residue);
|
|
33
|
+
const kind = stringField(value, 'type');
|
|
34
|
+
if (kind === 'system') { meta.system_prompt ??= stringField(value, 'content'); continue; }
|
|
35
|
+
let message;
|
|
36
|
+
if (kind === 'user') {
|
|
37
|
+
const role = stringField(value, 'synthetic_reason') === 'supercode_system_event' ? 'system' : 'user';
|
|
38
|
+
let content = extractTextContent(value, 'content');
|
|
39
|
+
if (role === 'user') {
|
|
40
|
+
content = humanText(content);
|
|
41
|
+
if (content == null && field(value, '_supercode_grok_message') === undefined) continue;
|
|
42
|
+
content ??= '';
|
|
43
|
+
}
|
|
44
|
+
message = createMessage(role, content);
|
|
45
|
+
captureScalars(value, message, ['prompt_index', 'prior_turn_interrupt', 'synthetic_reason']);
|
|
46
|
+
} else if (kind === 'assistant') {
|
|
47
|
+
const nativeCalls = field(value, 'tool_calls'), calls = [];
|
|
48
|
+
if (Array.isArray(nativeCalls)) for (const call of nativeCalls) {
|
|
49
|
+
const id = stringField(call, 'id'), name = stringField(call, 'name');
|
|
50
|
+
if (id == null || name == null) continue;
|
|
51
|
+
calls.push(functionCall(id, name, field(call, 'arguments') === undefined ? '{}' : valueString(call, 'arguments')));
|
|
52
|
+
toolNames.set(id, name);
|
|
53
|
+
}
|
|
54
|
+
const content = stringField(value, 'content'); message = createMessage('assistant', content === '' ? null : content);
|
|
55
|
+
message.tool_calls = calls.length ? calls : null;
|
|
56
|
+
captureScalars(value, message, ['model_id', 'model_fingerprint', 'reasoning_effort']);
|
|
57
|
+
const model = stringField(value, 'model_id'); if (model != null) meta.model = model;
|
|
58
|
+
} else if (kind === 'tool_result') {
|
|
59
|
+
const id = stringField(value, 'tool_call_id') ?? '';
|
|
60
|
+
message = toolMessage(id, extractTextContent(value, 'content')); message.name = toolNames.get(id) ?? null;
|
|
61
|
+
const nativeImages = field(value, 'images'), images = [];
|
|
62
|
+
if (Array.isArray(nativeImages)) for (const image of nativeImages) {
|
|
63
|
+
const url = stringField(image, 'url'); if (url != null && url !== '') images.push({ type: 'image_url', image_url: { url } });
|
|
64
|
+
}
|
|
65
|
+
if (images.length) message.content_parts = [...(message.content !== '' ? [{ type: 'text', text: message.content }] : []), ...images];
|
|
66
|
+
} else continue;
|
|
67
|
+
restoreGrokMessageExtension(value, message); messages.push(message);
|
|
68
|
+
}
|
|
69
|
+
if (records.length) stampMessageRecords(messages, records.at(-1).index);
|
|
70
|
+
ensureToolResultsPaired(messages); meta.message_records = takeMessageRecords(messages); session.imported_message_count = messages.length;
|
|
71
|
+
return session;
|
|
72
|
+
}
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import { createMessage } from './message.mjs';
|
|
2
|
+
import { createSession } from './session-model.mjs';
|
|
3
|
+
import { field, stringField, ensureToolResultsPaired } from './session-codec-utils.mjs';
|
|
4
|
+
import { parseNativeJson, setNativeInteger, setNativeFloat, stringifyNativeJson } from './native-json.mjs';
|
|
5
|
+
import { readStore, statement, tableColumns, text, integer } from './sqlite-reader.mjs';
|
|
6
|
+
import { sourceStat } from './store-registry.mjs';
|
|
7
|
+
import { hermesLineageKind, hermesSessionNouns } from './hermes-store.mjs';
|
|
8
|
+
import { bindingEpoch, messageEpoch } from './session-time.mjs';
|
|
9
|
+
|
|
10
|
+
const extendedColumns = ['session_key', 'chat_id', 'chat_type', 'thread_id', 'user_id', 'profile_name', 'handoff_state', 'handoff_platform', 'handoff_error'];
|
|
11
|
+
function epoch(value) {
|
|
12
|
+
if (value == null) return null;
|
|
13
|
+
if (typeof value !== 'number' && typeof value !== 'bigint') throw new Error('Hermes returned a non-numeric native epoch');
|
|
14
|
+
return Number(value);
|
|
15
|
+
}
|
|
16
|
+
function configJson(value) { try { return value == null ? null : parseNativeJson(value); } catch { return null; } }
|
|
17
|
+
|
|
18
|
+
function captureNouns(db, row, relationship, config, meta) {
|
|
19
|
+
let extended = false, ended = null, reason = null;
|
|
20
|
+
try {
|
|
21
|
+
const values = statement(db, `SELECT ${extendedColumns.join(',')} FROM sessions WHERE id=?`).get(row.id);
|
|
22
|
+
if (values) {
|
|
23
|
+
// A wrong column type refuses this optional extended reading whole.
|
|
24
|
+
const selected = Object.fromEntries(extendedColumns.map((column) => [column, text(values[column], true)]));
|
|
25
|
+
Object.assign(row, selected); extended = true;
|
|
26
|
+
}
|
|
27
|
+
} catch { /* Older schemas have no gateway columns; source still names trigger. */ }
|
|
28
|
+
try {
|
|
29
|
+
const values = statement(db, 'SELECT ended_at, end_reason FROM sessions WHERE id=?').get(row.id);
|
|
30
|
+
if (values) { const time = epoch(values.ended_at), word = text(values.end_reason, true); ended = time; reason = word; }
|
|
31
|
+
} catch { /* No recorded lifecycle reading remains unknown. */ }
|
|
32
|
+
const nouns = hermesSessionNouns(row, relationship, config, extended);
|
|
33
|
+
meta.ended_at = ended == null ? null : bindingEpoch(ended); meta.end_reason = reason;
|
|
34
|
+
meta.trigger = nouns.trigger; meta.recurrence = nouns.recurrence ?? null;
|
|
35
|
+
if (extended) {
|
|
36
|
+
meta.surface = nouns.surface ?? null; meta.profile = nouns.profile ?? null; meta.cross_surface = nouns.cross_surface ?? null;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** Hermes rescue remains read-only. Inactive rows are retained in raw but
|
|
41
|
+
* only active=1 rows, ordered by native id, enter its replay conversation. */
|
|
42
|
+
export async function readHermesSqlite(path, sessionId = null, { metrics = {} } = {}) {
|
|
43
|
+
if (sessionId != null && typeof sessionId !== 'string') throw new TypeError('Hermes session selector must be a string');
|
|
44
|
+
const info = await sourceStat(path);
|
|
45
|
+
if (!info?.isFile()) throw new Error(`Hermes SQLite store not found at ${path} — expected a \`state.db\` file`);
|
|
46
|
+
return readStore(path, (db) => {
|
|
47
|
+
const tables = new Set(db.prepare("SELECT name FROM sqlite_master WHERE type='table'").all().map((row) => row.name));
|
|
48
|
+
if (!['sessions', 'messages', 'schema_version'].every((table) => tables.has(table)) || tables.has('schema_meta')) {
|
|
49
|
+
throw new Error(`${path} is SQLite but not a Hermes state.db (missing sessions/messages/schema_version, or it carries OpenClaw's schema_meta)`);
|
|
50
|
+
}
|
|
51
|
+
const id = sessionId ?? (() => {
|
|
52
|
+
const newest = statement(db, 'SELECT id FROM sessions ORDER BY started_at DESC LIMIT 1').get();
|
|
53
|
+
if (!newest) throw new Error(`${path} contains no Hermes sessions`);
|
|
54
|
+
return text(newest.id);
|
|
55
|
+
})();
|
|
56
|
+
const row = statement(db, `SELECT source, model, cwd, system_prompt, title, parent_session_id, model_config, started_at
|
|
57
|
+
FROM sessions WHERE id=?`).get(id);
|
|
58
|
+
if (!row) throw new Error(`Hermes session \`${id}\` not found in ${path}`);
|
|
59
|
+
const session = createSession('hermes'), { meta, messages } = session;
|
|
60
|
+
for (const key of ['source', 'model', 'cwd', 'system_prompt', 'title', 'parent_session_id', 'model_config']) row[key] = text(row[key], true);
|
|
61
|
+
row.started_at = epoch(row.started_at); row.id = id;
|
|
62
|
+
const config = configJson(row.model_config);
|
|
63
|
+
meta.session_id = id; meta.model = row.model; meta.system_prompt = row.system_prompt;
|
|
64
|
+
meta.cwd = row.cwd !== '' && row.cwd != null ? row.cwd : stringField(config, 'cwd');
|
|
65
|
+
if (meta.cwd === '') meta.cwd = null;
|
|
66
|
+
if (row.title != null && row.title !== '') meta.lineage.session_name = row.title;
|
|
67
|
+
if (row.source != null && row.source !== '') meta.lineage.hermes_source = row.source;
|
|
68
|
+
let relationship = null;
|
|
69
|
+
if (row.parent_session_id != null) {
|
|
70
|
+
meta.lineage.hermes_parent_session_id = row.parent_session_id;
|
|
71
|
+
try { relationship = hermesLineageKind(db, tableColumns(db, 'sessions'), row, config); } catch { relationship = 'unknown'; }
|
|
72
|
+
meta.lineage.hermes_lineage_kind = relationship;
|
|
73
|
+
}
|
|
74
|
+
captureNouns(db, row, relationship, config, meta);
|
|
75
|
+
const header = { id, cwd: meta.cwd, parent_session_id: row.parent_session_id, started_at: row.started_at };
|
|
76
|
+
if (row.started_at != null) setNativeFloat(header, 'started_at', row.started_at);
|
|
77
|
+
session.raw.push(stringifyNativeJson({ hermes_session: header }));
|
|
78
|
+
const rows = statement(db, `SELECT id, role, content, tool_call_id, tool_calls, tool_name, timestamp, reasoning_content, active, compacted
|
|
79
|
+
FROM messages WHERE session_id=? ORDER BY id`).all(id);
|
|
80
|
+
for (const row of rows) {
|
|
81
|
+
for (const key of ['role', 'content', 'tool_call_id', 'tool_calls', 'tool_name', 'reasoning_content']) row[key] = text(row[key], true);
|
|
82
|
+
const rowId = integer(row.id), timestamp = epoch(row.timestamp), active = integer(row.active, true), compacted = integer(row.compacted, true);
|
|
83
|
+
const native = {
|
|
84
|
+
id: 0, role: row.role, content: row.content, tool_call_id: row.tool_call_id, tool_calls: row.tool_calls,
|
|
85
|
+
tool_name: row.tool_name, timestamp, active: null, compacted: null,
|
|
86
|
+
};
|
|
87
|
+
setNativeInteger(native, 'id', rowId);
|
|
88
|
+
if (active != null) setNativeInteger(native, 'active', active);
|
|
89
|
+
if (compacted != null) setNativeInteger(native, 'compacted', compacted);
|
|
90
|
+
if (timestamp != null) setNativeFloat(native, 'timestamp', timestamp);
|
|
91
|
+
session.raw.push(stringifyNativeJson({ hermes_message: native }));
|
|
92
|
+
if (active !== 1n) continue;
|
|
93
|
+
let message;
|
|
94
|
+
if (row.role === 'user' || row.role === 'assistant') message = createMessage(row.role, row.content ?? '');
|
|
95
|
+
else if (row.role === 'tool') {
|
|
96
|
+
message = createMessage('tool', row.content ?? ''); message.tool_call_id = row.tool_call_id ?? ''; message.name = row.tool_name ?? 'tool';
|
|
97
|
+
} else continue;
|
|
98
|
+
if (row.role === 'assistant') {
|
|
99
|
+
const calls = configJson(row.tool_calls), selected = [];
|
|
100
|
+
if (Array.isArray(calls)) for (const call of calls) {
|
|
101
|
+
const id = stringField(call, 'id'), fn = field(call, 'function'), name = stringField(fn, 'name');
|
|
102
|
+
if (id == null || name == null) continue;
|
|
103
|
+
selected.push({ id, type: stringField(call, 'type') ?? 'function', function: { name, arguments: stringField(fn, 'arguments') ?? '{}' } });
|
|
104
|
+
}
|
|
105
|
+
if (selected.length) message.tool_calls = selected;
|
|
106
|
+
if (row.reasoning_content != null && row.reasoning_content !== '') message.metadata.reasoning_content = row.reasoning_content;
|
|
107
|
+
}
|
|
108
|
+
message.metadata.hermes_message_id = String(rowId);
|
|
109
|
+
if (timestamp != null) message.metadata.timestamp = messageEpoch(timestamp);
|
|
110
|
+
if (compacted === 1n) message.metadata.compacted_out = 'true';
|
|
111
|
+
messages.push(message);
|
|
112
|
+
}
|
|
113
|
+
ensureToolResultsPaired(messages); session.imported_message_count = messages.length;
|
|
114
|
+
return session;
|
|
115
|
+
}, metrics);
|
|
116
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { nativeTrim } from './text.mjs';
|
|
2
|
+
|
|
3
|
+
/** Split only LF, retaining CR, blanks and trailing spaces in the raw lines. */
|
|
4
|
+
export function splitLinesVerbatim(text) {
|
|
5
|
+
if (text === '') return { lines: [], trailingNewline: false };
|
|
6
|
+
const trailingNewline = text.endsWith('\n');
|
|
7
|
+
return { lines: (trailingNewline ? text.slice(0, -1) : text).split('\n'), trailingNewline };
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function joinLinesVerbatim(lines, trailingNewline) {
|
|
11
|
+
return lines.join('\n') + (trailingNewline ? '\n' : '');
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/** Parsing view only. Never use these trimmed lines for retained source bytes. */
|
|
15
|
+
export function* nonEmptyLines(text) {
|
|
16
|
+
for (const line of text.split('\n')) {
|
|
17
|
+
const trimmed = nativeTrim(line);
|
|
18
|
+
if (trimmed !== '') yield trimmed;
|
|
19
|
+
}
|
|
20
|
+
}
|