@volter/supercode-interchange 0.1.22 → 0.1.24
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE-diffy-MIT +23 -0
- package/README.md +422 -2
- package/channel-records.mjs +71 -0
- package/channel-session-counts.mjs +81 -0
- package/channels-read.mjs +90 -0
- package/corpus-audit-records.mjs +192 -0
- package/corpus-audit-render.mjs +47 -0
- package/corpus-audit-schema.mjs +89 -0
- package/corpus-audit.d.ts +16 -0
- package/corpus-audit.mjs +132 -0
- package/file-io.mjs +21 -2
- package/harness-settings.mjs +19 -0
- package/hermes-fire-links.mjs +135 -0
- package/hermes-fires.mjs +106 -0
- package/hermes-read-home.d.ts +3 -0
- package/hermes-read-home.mjs +235 -0
- package/hermes-read-store.mjs +49 -0
- package/hermes-read-validation.mjs +170 -0
- package/hermes-read-workflow.mjs +164 -0
- package/hermes-read-yaml.mjs +68 -0
- package/hermes-store.mjs +1 -1
- package/homes.mjs +2 -2
- package/index.d.ts +14 -1
- package/index.mjs +1 -1
- package/inspection-builtin-tools.json +261 -0
- package/inspection-config-fields.json +765 -0
- package/inspection-schemas.d.ts +4 -0
- package/inspection-schemas.mjs +117 -0
- package/job-notepad-read.mjs +97 -0
- package/jobs-control.mjs +250 -0
- package/jobs-read.mjs +198 -0
- package/jsonl.mjs +4 -3
- package/memory-show.mjs +187 -0
- package/metadata.mjs +45 -3
- package/native-json.mjs +46 -3
- package/ontology-contracts.d.ts +5 -0
- package/ontology-contracts.mjs +29 -0
- package/ontology-operations.json +198 -0
- package/ontology-schemas.json +7946 -0
- package/openclaw-jobs-read.mjs +117 -0
- package/openclaw-profile-home.mjs +142 -0
- package/orchestrator-read-records.mjs +112 -0
- package/orchestrator-store.mjs +21 -9
- package/package.json +59 -4
- package/parity-ledger.json +6984 -0
- package/parity-orchestration.json +5538 -0
- package/parity-report.d.ts +7 -0
- package/parity-report.mjs +46 -0
- package/preset-profiles.mjs +27 -0
- package/profile-command.mjs +32 -0
- package/profile-orchestrator-door.mjs +104 -0
- package/profile-paths.mjs +25 -0
- package/profiles-control.mjs +98 -0
- package/profiles-read.mjs +119 -0
- package/reduction-attribution.mjs +54 -0
- package/reduction-diff.mjs +165 -0
- package/reduction-log.mjs +177 -0
- package/reduction-view.mjs +145 -0
- package/retained-projection.mjs +9 -0
- package/routes-read.mjs +103 -0
- package/runs-read.mjs +224 -0
- package/session-activity.d.ts +23 -0
- package/session-activity.mjs +124 -0
- package/session-append-scan.mjs +27 -17
- package/session-branch-handoff.mjs +115 -0
- package/session-claude-graph.mjs +29 -7
- package/session-claude-index.mjs +732 -114
- package/session-claude-runtime.d.ts +28 -0
- package/session-claude-runtime.mjs +171 -6
- package/session-claude-scan-store.mjs +164 -0
- package/session-claude.mjs +9 -0
- package/session-codex-activity.d.ts +6 -0
- package/session-codex-activity.mjs +120 -0
- package/session-codex-window.mjs +24 -4
- package/session-config-scan.mjs +141 -0
- package/session-convert.d.ts +33 -0
- package/session-convert.mjs +179 -0
- package/session-emission-version.json +1 -0
- package/session-file.mjs +11 -1
- package/session-follower.mjs +17 -5
- package/session-fresh-id.mjs +15 -0
- package/session-hermes-import.mjs +61 -0
- package/session-hermes.mjs +1 -1
- package/session-import.mjs +37 -0
- package/session-inspect.d.ts +18 -0
- package/session-inspect.mjs +122 -0
- package/session-machine-tree.d.ts +33 -0
- package/session-machine-tree.mjs +274 -0
- package/session-materialize-files.mjs +78 -0
- package/session-materialize-opencode.mjs +137 -0
- package/session-materialize.mjs +186 -0
- package/session-message.mjs +85 -0
- package/session-model.d.ts +9 -0
- package/session-model.mjs +1 -0
- package/session-opencode-store.mjs +39 -3
- package/session-path.mjs +6 -3
- package/session-pi-content.mjs +7 -2
- package/session-read.d.ts +134 -2
- package/session-read.mjs +86 -9
- package/session-recorded-config.mjs +3 -2
- package/session-records.mjs +12 -6
- package/session-residue-store.mjs +90 -0
- package/session-residue.mjs +150 -0
- package/session-resume-instructions.mjs +1 -1
- package/session-saved-read.mjs +105 -0
- package/session-transfer.mjs +141 -0
- package/session-views.mjs +1 -0
- package/session-write-claude.mjs +135 -0
- package/session-write-codex.mjs +146 -0
- package/session-write-gemini.mjs +77 -0
- package/session-write-goose.mjs +99 -0
- package/session-write-grok.mjs +70 -0
- package/session-write-native.mjs +41 -0
- package/session-write-opencode.mjs +197 -0
- package/session-write-pi.mjs +130 -0
- package/session-write-residue.mjs +19 -0
- package/session-write-utils.mjs +77 -0
- package/session-write.d.ts +10 -0
- package/session-write.mjs +37 -0
- package/skill-toml.mjs +80 -0
- package/skills-control.mjs +193 -0
- package/skills-list.mjs +237 -0
- package/source-files.mjs +12 -2
- package/sqlite-reader.mjs +134 -18
- package/support-registry.json +402 -0
- package/support-report.mjs +69 -0
- package/trigger-records.mjs +77 -0
- package/triggers-read.mjs +67 -0
- package/workflow-read.mjs +165 -0
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
// Recorded configuration keeps only its folds and reference ids. Message bodies
|
|
2
|
+
// are read from one verified snapshot and never retained by this cache.
|
|
3
|
+
import { AppendScan } from './session-append-scan.mjs';
|
|
4
|
+
import { withSessionRecordFile, sessionMetaFold } from './session-records.mjs';
|
|
5
|
+
import { recordedConfigurationFold } from './session-recorded-config.mjs';
|
|
6
|
+
import { claudeRuntimeFold } from './session-claude-runtime.mjs';
|
|
7
|
+
import { sessionFileLines } from './session-file.mjs';
|
|
8
|
+
import { sessionTextError } from './session-path.mjs';
|
|
9
|
+
import { parseNativeJson } from './native-json.mjs';
|
|
10
|
+
import { field, stringField } from './session-codec-utils.mjs';
|
|
11
|
+
import { nativeTrim } from './text.mjs';
|
|
12
|
+
|
|
13
|
+
const scans = new Map();
|
|
14
|
+
const MAX_ENTRIES = 400_000, MAX_FILES = 32;
|
|
15
|
+
const REBUILD = Symbol('a new runtime call refers to an earlier result');
|
|
16
|
+
const runtimeCall = name => ['CronCreate', 'CronDelete', 'CronList', 'ScheduleWakeup', 'Agent'].includes(name);
|
|
17
|
+
const content = value => field(field(value, 'message'), 'content');
|
|
18
|
+
|
|
19
|
+
function record(line, index) {
|
|
20
|
+
const raw = line.text.endsWith('\n') ? line.text.slice(0, -1) : line.text;
|
|
21
|
+
if (nativeTrim(raw) === '') return null;
|
|
22
|
+
let value;
|
|
23
|
+
try { value = parseNativeJson(raw); } catch { /* the complete manifest is unknown */ }
|
|
24
|
+
return { raw, value, index };
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
class ConfigurationScan extends AppendScan {
|
|
28
|
+
lines = 0; calls = new Set(); results = new Set(); posture = true;
|
|
29
|
+
constructor(reader, source) {
|
|
30
|
+
super(reader); this.source = source;
|
|
31
|
+
this.fold = recordedConfigurationFold(); this.meta = sessionMetaFold(source, this.fold);
|
|
32
|
+
this.runtime = source === 'claude_code' ? claudeRuntimeFold() : null;
|
|
33
|
+
}
|
|
34
|
+
get entries() { return 1 + this.calls.size + this.results.size + this.meta.meta.codex_headers.length + (this.runtime?.retainedEntries ?? 0); }
|
|
35
|
+
|
|
36
|
+
first(item, { commit = true, calls = new Set() } = {}) {
|
|
37
|
+
if (!item) return;
|
|
38
|
+
this.meta.observe(item);
|
|
39
|
+
if (!this.runtime) return;
|
|
40
|
+
if (item.value === undefined) { this.posture = false; return; }
|
|
41
|
+
const blocks = content(item.value);
|
|
42
|
+
if (stringField(item.value, 'type') === 'assistant' && Array.isArray(blocks)) {
|
|
43
|
+
for (const block of blocks) {
|
|
44
|
+
if (stringField(block, 'type') !== 'tool_use' || !runtimeCall(stringField(block, 'name'))) continue;
|
|
45
|
+
const id = stringField(block, 'id');
|
|
46
|
+
if (id == null) continue; // The runtime fold supplies the same malformed-call refusal.
|
|
47
|
+
// The complete manifest collects all calls before replaying any result.
|
|
48
|
+
// A retroactive reference changes that old replay, rather than merely
|
|
49
|
+
// extending it. Rebuild just as for a rewritten prefix.
|
|
50
|
+
if (this.results.has(id)) throw REBUILD;
|
|
51
|
+
const key = `${stringField(block, 'name') === 'Agent' ? 'child' : 'call'}:${id}`;
|
|
52
|
+
if (this.calls.has(key) || calls.has(key)) this.posture = false;
|
|
53
|
+
calls.add(key);
|
|
54
|
+
if (commit) this.calls.add(key);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
if (this.posture) try { this.runtime.call(item.value, item.index + 1, null); } catch { this.posture = false; }
|
|
58
|
+
}
|
|
59
|
+
second(item) {
|
|
60
|
+
if (item && this.runtime && this.posture) try { this.runtime.record(item.value, item.index + 1, null); } catch { this.posture = false; }
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
async extend(reader) {
|
|
64
|
+
const from = this.bytes, firstLine = this.lines, results = [];
|
|
65
|
+
// All new calls precede the new results in the runtime replay. Only the
|
|
66
|
+
// appended range is decoded; the retained prefix has already been folded.
|
|
67
|
+
for await (const line of sessionFileLines(reader, from)) {
|
|
68
|
+
if (!line.terminated) break;
|
|
69
|
+
const item = record(line, this.lines++);
|
|
70
|
+
this.first(item);
|
|
71
|
+
if (this.runtime && stringField(item?.value, 'type') === 'user' && Array.isArray(content(item.value)))
|
|
72
|
+
for (const block of content(item.value)) if (stringField(block, 'type') === 'tool_result') {
|
|
73
|
+
const id = stringField(block, 'tool_use_id'); if (id != null) results.push(id);
|
|
74
|
+
}
|
|
75
|
+
this.took(line.bytes, line.offset + line.length); this.bytes = line.offset + line.length;
|
|
76
|
+
}
|
|
77
|
+
if (this.runtime && this.posture) {
|
|
78
|
+
let index = firstLine;
|
|
79
|
+
for await (const line of sessionFileLines(reader, from)) {
|
|
80
|
+
if (!line.terminated) break;
|
|
81
|
+
this.second(record(line, index++));
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
for (const id of results) this.results.add(id);
|
|
85
|
+
await this.settle(reader, from);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
async answer(reader, harness) {
|
|
89
|
+
let snapshot = this;
|
|
90
|
+
if (reader.size > this.bytes) {
|
|
91
|
+
// An unterminated last line participates in this answer, never in the
|
|
92
|
+
// committed prefix. Its later completion cannot count it twice.
|
|
93
|
+
snapshot = Object.create(ConfigurationScan.prototype);
|
|
94
|
+
Object.assign(snapshot, this, { fold: this.fold.clone(), runtime: this.runtime?.clone() ?? null });
|
|
95
|
+
snapshot.meta = this.meta.clone(snapshot.fold);
|
|
96
|
+
for await (const line of sessionFileLines(reader, this.bytes)) {
|
|
97
|
+
const item = record(line, this.lines);
|
|
98
|
+
snapshot.first(item, { commit: false }); snapshot.second(item);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
let mode = null;
|
|
102
|
+
if (snapshot.runtime && snapshot.posture) try { mode = snapshot.runtime.permissionMode(); } catch { /* invalid complete manifest */ }
|
|
103
|
+
return snapshot.fold.finish(snapshot.meta.meta, harness, mode);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function retain(path, scan, metrics) {
|
|
108
|
+
scans.delete(path); scans.set(path, scan);
|
|
109
|
+
let entries = [...scans.values()].reduce((sum, item) => sum + item.entries, 0);
|
|
110
|
+
for (const [other, item] of scans) {
|
|
111
|
+
if (scans.size <= MAX_FILES && entries <= MAX_ENTRIES || item === scan) break;
|
|
112
|
+
scans.delete(other); entries -= item.entries;
|
|
113
|
+
}
|
|
114
|
+
if (metrics) metrics.configuration_retained_entries = entries;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/** Null retains the historical document/fallback reader for other formats. */
|
|
118
|
+
export async function loadRecordedConfigurationRecords(locator, context = {}) {
|
|
119
|
+
const path = locator.storage.path;
|
|
120
|
+
try {
|
|
121
|
+
const read = await withSessionRecordFile(locator, async (source, reader) => {
|
|
122
|
+
let scan = scans.get(path); scans.delete(path);
|
|
123
|
+
if (!scan || scan.source !== source || !await scan.holds(reader)) scan = new ConfigurationScan(reader, source);
|
|
124
|
+
try { await scan.extend(reader); }
|
|
125
|
+
catch (error) {
|
|
126
|
+
if (error !== REBUILD) throw error;
|
|
127
|
+
scan = new ConfigurationScan(reader, source); await scan.extend(reader);
|
|
128
|
+
}
|
|
129
|
+
let result;
|
|
130
|
+
try { result = await scan.answer(reader, locator.harness); }
|
|
131
|
+
catch (error) { if (error !== REBUILD) throw error; return { scan, result: null }; }
|
|
132
|
+
return { scan, result };
|
|
133
|
+
}, context);
|
|
134
|
+
if (read == null) return null;
|
|
135
|
+
retain(path, read.result.scan, context.metrics);
|
|
136
|
+
return read.result.result;
|
|
137
|
+
} catch (error) {
|
|
138
|
+
scans.delete(path);
|
|
139
|
+
throw ['ERR_ENCODING_INVALID_ENCODED_DATA', 'ERR_STRING_TOO_LONG'].includes(error?.code) ? sessionTextError(path, error) : error;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
export const CONVERSION_FORMATS: readonly ['claude-code', 'codex', 'opencode', 'pi', 'grok', 'gemini', 'goose', 'hermes'];
|
|
2
|
+
export interface ConversionOptions {
|
|
3
|
+
file: string;
|
|
4
|
+
to: typeof CONVERSION_FORMATS[number];
|
|
5
|
+
out?: string | null;
|
|
6
|
+
sessionId?: string | null;
|
|
7
|
+
selector?: string | null;
|
|
8
|
+
cwd?: string | null;
|
|
9
|
+
}
|
|
10
|
+
export interface ConversionSummary {
|
|
11
|
+
out?: string;
|
|
12
|
+
source: string;
|
|
13
|
+
target: string;
|
|
14
|
+
messages: number;
|
|
15
|
+
parity: 'verbatim' | 'cross' | 'id' | 'metadata' | 'nonverbatim' | 'view';
|
|
16
|
+
complete: boolean;
|
|
17
|
+
reduced: { stubCount: number; viewTokens: bigint; fullTokens: bigint } | null;
|
|
18
|
+
}
|
|
19
|
+
export interface ConversionContext {
|
|
20
|
+
env?: NodeJS.ProcessEnv;
|
|
21
|
+
cwd?: string;
|
|
22
|
+
signal?: AbortSignal;
|
|
23
|
+
storeRoot?: string;
|
|
24
|
+
output?: (text: string) => void | Promise<void>;
|
|
25
|
+
diagnostic?: (text: string, role?: 'ERR' | 'WARN') => void;
|
|
26
|
+
summary?: (result: ConversionSummary) => void | Promise<void>;
|
|
27
|
+
}
|
|
28
|
+
/** Complete CLI conversion. Writes an explicitly requested output or hands
|
|
29
|
+
* text to output; Hermes import executes its own vendor command only for a
|
|
30
|
+
* literal-path input, preserving the existing native command's distinction. */
|
|
31
|
+
export function convertSession(options: ConversionOptions, context?: ConversionContext): Promise<ConversionSummary | {
|
|
32
|
+
imported: { session_id: string; home: string; rollout: string; via: string };
|
|
33
|
+
}>;
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
// Complete convert behavior. Literal sources and saved working transcripts
|
|
2
|
+
// are different inputs; a reduced saved session always exports its full sidecar.
|
|
3
|
+
import { mkdir, readdir, stat, writeFile } from 'node:fs/promises';
|
|
4
|
+
import { basename, dirname, extname, join } from 'node:path';
|
|
5
|
+
import { loadSessionPath, looksLikeSessionSqlite } from './session-path.mjs';
|
|
6
|
+
import { readOpenCodeSqlite } from './session-opencode-store.mjs';
|
|
7
|
+
import { readStore, statement, text as sqliteText } from './sqlite-reader.mjs';
|
|
8
|
+
import { fromSidecarStr } from './session-native.mjs';
|
|
9
|
+
import { rawVerbatim } from './session-model.mjs';
|
|
10
|
+
import { toSessionJsonl, toSessionJsonlSpliced } from './session-write.mjs';
|
|
11
|
+
import { importIntoHermes } from './session-hermes-import.mjs';
|
|
12
|
+
import { SavedSessionReader, parseSavedMessages, savedPathExists, savedSessionRoot } from './session-saved-read.mjs';
|
|
13
|
+
import { estimateViewTokens, rejectReducedSidecar, verifyReductionLog } from './reduction-log.mjs';
|
|
14
|
+
import { reapplyReductionLog } from './reduction-view.mjs';
|
|
15
|
+
import { nativeTrim } from './text.mjs';
|
|
16
|
+
import { cloneNativeJson } from './native-json.mjs';
|
|
17
|
+
|
|
18
|
+
export const CONVERSION_FORMATS = Object.freeze(['claude-code', 'codex', 'opencode', 'pi', 'grok', 'gemini', 'goose', 'hermes']);
|
|
19
|
+
const formatName = name => name === 'claude-code' ? 'claude_code' : name === 'hermes' ? 'codex' : name;
|
|
20
|
+
export function conversionParseLoss(session) {
|
|
21
|
+
const sessions = [session, ...session.subagents];
|
|
22
|
+
return { errors: sessions.reduce((sum, value) => sum + value.parse_error_lines, 0),
|
|
23
|
+
lines: sessions.reduce((sum, value) => sum + value.raw.filter(line => nativeTrim(line) !== '').length, 0),
|
|
24
|
+
children: session.subagents.filter(value => value.parse_error_lines > 0).length,
|
|
25
|
+
childErrors: session.subagents.reduce((sum, value) => sum + value.parse_error_lines, 0) };
|
|
26
|
+
}
|
|
27
|
+
function childDirectory(path) { return join(dirname(path), basename(path, extname(path)), 'subagents'); }
|
|
28
|
+
function lossError(session, path, written, diagnostic) {
|
|
29
|
+
const { errors, lines, children, childErrors } = conversionParseLoss(session);
|
|
30
|
+
if (!errors) return;
|
|
31
|
+
const note = childErrors ? ` (${childErrors} of which came from ${children} subagent transcript(s) under ${childDirectory(path)}/)` : '';
|
|
32
|
+
const outputNote = written ? `everything above reflects only the ${Math.max(0, lines - errors)} line(s) that DID parse, not the complete session`
|
|
33
|
+
: errors < lines ? 'no provider call was made because the session was only partially loaded' : 'no output was written because no input line parsed successfully';
|
|
34
|
+
diagnostic(`error: ${path} — ${errors} of ${lines} line(s) failed to parse and were DROPPED${note} (the file may be truncated or corrupted); ${outputNote}`, 'ERR');
|
|
35
|
+
throw new Error(written
|
|
36
|
+
? `${path}: ${errors} of ${lines} line(s) failed to parse — refusing to report success on a partially-loaded session (PARITY-15)${note}`
|
|
37
|
+
: errors < lines ? `${path}: ${errors} of ${lines} line(s) failed to parse — refusing to continue a partially-loaded session; no provider call was made (PARITY-15)${note}`
|
|
38
|
+
: `${path}: all ${lines} line(s) failed to parse — no session data loaded and no output written (PARITY-15)${note}`);
|
|
39
|
+
}
|
|
40
|
+
async function alias(source, out) {
|
|
41
|
+
try {
|
|
42
|
+
const [left, right] = await Promise.all([stat(source, { bigint: true }), stat(out, { bigint: true })]);
|
|
43
|
+
return left.dev === right.dev && left.ino === right.ino;
|
|
44
|
+
} catch { return source === out; }
|
|
45
|
+
}
|
|
46
|
+
async function refusePartialOverwrite(session, input, out, diagnostic) {
|
|
47
|
+
const { errors, lines } = conversionParseLoss(session);
|
|
48
|
+
if (!errors || out == null) return;
|
|
49
|
+
let source = await alias(input, out) ? input : null;
|
|
50
|
+
if (source == null && session.meta.source === 'claude_code') {
|
|
51
|
+
let files; try { files = await readdir(childDirectory(input)); } catch { files = []; }
|
|
52
|
+
for (const file of files) if (extname(file) === '.jsonl') {
|
|
53
|
+
const child = join(childDirectory(input), file); if (await alias(child, out)) { source = child; break; }
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
if (source == null) return;
|
|
57
|
+
const primary = source === input;
|
|
58
|
+
diagnostic(`error: ${input} — ${errors} of ${lines} line(s) failed to parse; \`--out ${out}\` identifies ${primary ? 'the input file' : `source transcript \`${source}\``}, so no output was written (use a distinct path or stdout to keep the recoverable partial conversion)`, 'ERR');
|
|
59
|
+
throw new Error(`${input}: refusing to overwrite a parse-damaged source transcript with partial output (PARITY-15/${primary ? 'D41' : 'D42'})`);
|
|
60
|
+
}
|
|
61
|
+
async function warnOpenCode(path, selector, diagnostic) {
|
|
62
|
+
if (selector != null) return;
|
|
63
|
+
let ids;
|
|
64
|
+
try {
|
|
65
|
+
ids = (await readStore(path, db => statement(db, 'SELECT id FROM session ORDER BY time_created, id').all().map(row => sqliteText(row.id)))).result;
|
|
66
|
+
} catch { return; }
|
|
67
|
+
if (ids.length <= 1) return;
|
|
68
|
+
let chosen = '<unknown>';
|
|
69
|
+
try { chosen = (await readStore(path, db => sqliteText(statement(db, 'SELECT id FROM session ORDER BY (parent_id IS NULL) DESC,time_updated DESC LIMIT 1').get()?.id))).result; } catch { /* warning names unknown selection */ }
|
|
70
|
+
diagnostic(`warning: ${path} holds ${ids.length} sessions; converting ${chosen} (most-recently-updated) — pass --opencode-session <id> to pick another. All session ids: ${ids.join(', ')}`);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function childOutput(child, sessionId) {
|
|
74
|
+
if (conversionParseLoss(child).errors) throw new Error('refusing stored Claude subagent export because the loaded session contains parse loss');
|
|
75
|
+
return sessionId == null && !(child.imported_message_count != null && child.imported_message_count < child.messages.length)
|
|
76
|
+
? rawVerbatim(child) : toSessionJsonlSpliced(child, 'claude_code', sessionId);
|
|
77
|
+
}
|
|
78
|
+
async function prepareChildren(store, name, out, source, target, sessionId) {
|
|
79
|
+
if (source !== 'claude_code' || target !== 'claude_code') return [];
|
|
80
|
+
const ids = [];
|
|
81
|
+
for (const id of await store.childIds(name)) {
|
|
82
|
+
const source = await store.child(name, id);
|
|
83
|
+
if (source == null) throw new Error(`subagent \`${id}\` disappeared while exporting \`${name}\``);
|
|
84
|
+
let child;
|
|
85
|
+
try { child = fromSidecarStr(source); }
|
|
86
|
+
catch (error) { throw new Error(`parsing stored subagent \`${id}\` for \`${name}\`: ${error.message}`); }
|
|
87
|
+
if (child.meta.source !== 'claude_code') continue;
|
|
88
|
+
if (!child.raw_is_verbatim) throw new Error(`stored Claude subagent \`${id}\` for \`${name}\` has no verbatim source bytes`);
|
|
89
|
+
childOutput(child, sessionId); ids.push(id);
|
|
90
|
+
}
|
|
91
|
+
if (!ids.length) return [];
|
|
92
|
+
if (out == null) throw new Error(`Claude bundle export for \`${name}\` has ${ids.length} subagent transcript(s); use \`--out <file>\` so Volter Harness can recreate \`<file-stem>/subagents/\``);
|
|
93
|
+
if (!basename(out) || ['.', '..'].includes(basename(out))) throw new Error(`cannot derive a Claude subagent directory from output path ${out}`);
|
|
94
|
+
const names = new Set();
|
|
95
|
+
return ids.map(id => {
|
|
96
|
+
const bare = id.startsWith('agent-') ? id.slice(6) : id;
|
|
97
|
+
if (!bare) throw new Error(`stored Claude subagent id \`${id}\` has no filename-safe id body`);
|
|
98
|
+
const filename = `agent-${bare}.jsonl`;
|
|
99
|
+
if (names.has(filename)) throw new Error(`stored Claude subagent ids collide at output filename \`${filename}\` for \`${name}\``);
|
|
100
|
+
names.add(filename); return { id, path: join(childDirectory(out), filename) };
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
async function writeChildren(store, name, children, sessionId) {
|
|
104
|
+
for (const { id, path } of children) {
|
|
105
|
+
const source = await store.child(name, id);
|
|
106
|
+
if (source == null) throw new Error(`subagent \`${id}\` disappeared while writing \`${name}\``);
|
|
107
|
+
const child = fromSidecarStr(source), text = childOutput(child, sessionId);
|
|
108
|
+
await mkdir(dirname(path), { recursive: true }); await writeFile(path, text);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/** Write only after loading/validating the selected complete input. Partial
|
|
113
|
+
* literal output remains useful but ends in a failure; corrupt saved pointers
|
|
114
|
+
* fail before any primary or child output. Callbacks own CLI presentation. */
|
|
115
|
+
export async function convertSession({ file, to, out = null, sessionId = null, selector = null, cwd = null }, context = {}) {
|
|
116
|
+
if (!CONVERSION_FORMATS.includes(to)) throw new TypeError(`unknown conversion target: ${to}`);
|
|
117
|
+
const target = formatName(to), diagnostic = context.diagnostic ?? (() => {}), output = context.output ?? (() => {}), summary = context.summary ?? (() => {});
|
|
118
|
+
let session, jsonl, parity, reduced = null, store, name, children = [], complete = true, literal = false;
|
|
119
|
+
if (await savedPathExists(file)) {
|
|
120
|
+
literal = true;
|
|
121
|
+
try {
|
|
122
|
+
const sqlite = await looksLikeSessionSqlite(file);
|
|
123
|
+
if (sqlite) await warnOpenCode(file, selector, diagnostic);
|
|
124
|
+
session = sqlite && selector != null ? (await readOpenCodeSqlite(file, selector)).result
|
|
125
|
+
: await loadSessionPath(file, { env: context.env ?? process.env });
|
|
126
|
+
} catch (error) { throw new Error(`loading session ${file}: ${error.message}`, { cause: error }); }
|
|
127
|
+
const counts = conversionParseLoss(session);
|
|
128
|
+
if (counts.errors > 0 && counts.errors === counts.lines) lossError(session, file, false, diagnostic);
|
|
129
|
+
await refusePartialOverwrite(session, file, out, diagnostic);
|
|
130
|
+
if (session.raw.length && !session.messages.length) diagnostic(`warning: ${file} parsed to zero messages from a non-empty file — it may be corrupt or truncated; the output will be empty`, 'WARN');
|
|
131
|
+
if (cwd != null) session.meta.cwd = cwd;
|
|
132
|
+
const verbatim = sessionId == null && cwd == null && target === session.meta.source && session.raw_is_verbatim;
|
|
133
|
+
if (sessionId != null) session.meta.session_id = sessionId;
|
|
134
|
+
if (to === 'hermes') {
|
|
135
|
+
const imported = await importIntoHermes(session, context);
|
|
136
|
+
await output(`imported into hermes as ${imported.session_id} (${imported.home}): continue with \`hermes --resume ${imported.session_id}\`\n`);
|
|
137
|
+
return { imported };
|
|
138
|
+
}
|
|
139
|
+
jsonl = verbatim ? rawVerbatim(session) : toSessionJsonl(session, target);
|
|
140
|
+
parity = verbatim ? 'verbatim' : target !== session.meta.source ? 'cross' : sessionId != null ? 'id' : cwd != null ? 'metadata' : 'nonverbatim';
|
|
141
|
+
complete = counts.errors === 0;
|
|
142
|
+
} else {
|
|
143
|
+
store = new SavedSessionReader(context.storeRoot ?? savedSessionRoot(context.env)); name = await store.resolve(file);
|
|
144
|
+
const sidecar = await store.read(name, '.sidecar.jsonl', true);
|
|
145
|
+
if (sidecar != null) {
|
|
146
|
+
try { session = fromSidecarStr(sidecar); } catch (error) { throw new Error(`parsing sidecar for \`${name}\`: ${error.message}`); }
|
|
147
|
+
const log = await store.reductionLog(name);
|
|
148
|
+
try { verifyReductionLog(log, session.messages); }
|
|
149
|
+
catch (error) { throw new Error(`${error.message} — repair pointer: \`sessions show-reductions ${name}\``, { cause: error }); }
|
|
150
|
+
const view = reapplyReductionLog(session.messages, log);
|
|
151
|
+
reduced = { stubCount: log.reductions.length, viewTokens: estimateViewTokens(view), fullTokens: estimateViewTokens(session.messages) };
|
|
152
|
+
rejectReducedSidecar(session);
|
|
153
|
+
const original = session;
|
|
154
|
+
session = cloneNativeJson(original);
|
|
155
|
+
if (cwd != null) session.meta.cwd = cwd;
|
|
156
|
+
try { jsonl = toSessionJsonlSpliced(session, target, sessionId); }
|
|
157
|
+
catch (error) { throw new Error(`exporting reduced session: ${error.message}`, { cause: error }); }
|
|
158
|
+
if (target !== session.meta.source) parity = 'cross';
|
|
159
|
+
else if (jsonl === rawVerbatim(session)) parity = 'verbatim';
|
|
160
|
+
else {
|
|
161
|
+
let baseline; if (sessionId != null) try { baseline = toSessionJsonlSpliced(original, target); } catch { /* non-authoritative diagnostic baseline */ }
|
|
162
|
+
parity = baseline != null && baseline !== jsonl ? 'id' : 'view';
|
|
163
|
+
}
|
|
164
|
+
children = await prepareChildren(store, name, out, session.meta.source, target, sessionId);
|
|
165
|
+
} else {
|
|
166
|
+
session = parseSavedMessages(name, await store.read(name, '.jsonl'));
|
|
167
|
+
if (cwd != null) session.meta.cwd = cwd;
|
|
168
|
+
if (sessionId != null) session.meta.session_id = sessionId;
|
|
169
|
+
jsonl = toSessionJsonl(session, target); parity = 'view';
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
if (out != null) {
|
|
173
|
+
try { await writeFile(out, jsonl); } catch (error) { throw new Error(`writing ${out}: ${error.message}`, { cause: error }); }
|
|
174
|
+
await summary({ out, source: session.meta.source, target, messages: session.messages.length, parity, complete, reduced });
|
|
175
|
+
} else await output(jsonl);
|
|
176
|
+
if (literal) lossError(session, file, true, diagnostic);
|
|
177
|
+
else await writeChildren(store, name, children, sessionId);
|
|
178
|
+
return { source: session.meta.source, target, messages: session.messages.length, parity, complete, reduced };
|
|
179
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"0.5.450"
|
package/session-file.mjs
CHANGED
|
@@ -2,11 +2,16 @@ import { constants } from 'node:buffer';
|
|
|
2
2
|
import { withSessionFile } from './file-io.mjs';
|
|
3
3
|
|
|
4
4
|
const MAX_TEXT = constants.MAX_STRING_LENGTH;
|
|
5
|
+
// A document whose text fits the runtime string limit needs no more than
|
|
6
|
+
// four encoded bytes per UTF-16 unit. Compressed input gets the same finite
|
|
7
|
+
// byte admission; compressed framing is not evidence of decoded text size.
|
|
8
|
+
export const MAX_SESSION_DOCUMENT_BYTES = Math.min(constants.MAX_LENGTH, MAX_TEXT * 4);
|
|
5
9
|
/** The session-read doors' answer for a file this reader cannot read for what is in it (text that is not UTF-8,
|
|
6
10
|
* or too long to hold): the same bytes get the same answer from the same reader. Any other failure of a read (the
|
|
7
11
|
* disk, a file changed under it, a fault in the reader) answers the generic code and says nothing of the file. */
|
|
8
12
|
export const SESSION_UNREADABLE = -32050;
|
|
9
13
|
export const textTooLong = (path, cause) => Object.assign(new Error(`${path} holds more text than this runtime keeps in one string (${MAX_TEXT} UTF-16 units); it can be read record by record, never as one document`, { cause }), { code: 'EFBIG', rpcCode: SESSION_UNREADABLE });
|
|
14
|
+
export const sessionByteLimit = (path, part, cause) => Object.assign(new Error(`${path} exceeds the session document byte budget (${MAX_SESSION_DOCUMENT_BYTES} bytes) for ${part}; this reader leaves the source unchanged`, { cause }), { code: 'EFBIG', rpcCode: SESSION_UNREADABLE });
|
|
10
15
|
export const decodeSessionUtf8 = (bytes) => new TextDecoder('utf-8', { fatal: true, ignoreBOM: true }).decode(bytes);
|
|
11
16
|
|
|
12
17
|
/** The file from `from` to its end, for a reader that needs it as one text.
|
|
@@ -14,6 +19,7 @@ export const decodeSessionUtf8 = (bytes) => new TextDecoder('utf-8', { fatal: tr
|
|
|
14
19
|
* held: their UTF-16 length is counted a bounded block at a time. A compressed
|
|
15
20
|
* file's bytes are not its text, which is measured where it is decoded. */
|
|
16
21
|
export async function sessionFileBytes(reader, from = 0) {
|
|
22
|
+
if (reader.path.endsWith('.zst') && reader.size - from > MAX_SESSION_DOCUMENT_BYTES) throw sessionByteLimit(reader.path, 'compressed input');
|
|
17
23
|
if (reader.size - from > MAX_TEXT && !reader.path.endsWith('.zst')) {
|
|
18
24
|
let units = 0;
|
|
19
25
|
for (let offset = from; offset < reader.size && units <= MAX_TEXT;) {
|
|
@@ -43,11 +49,15 @@ export async function* sessionFileLines(reader, from = 0) {
|
|
|
43
49
|
const chunkStart = offset; offset += chunk.length;
|
|
44
50
|
let at = 0, end;
|
|
45
51
|
while ((end = chunk.indexOf(10, at)) !== -1) {
|
|
52
|
+
if (pending + end + 1 - at > MAX_SESSION_DOCUMENT_BYTES) throw sessionByteLimit(reader.path, 'one physical record');
|
|
46
53
|
pieces.push(chunk.subarray(at, end + 1)); pending += end + 1 - at;
|
|
47
54
|
yield fileLine(start, pieces.length === 1 ? pieces[0] : Buffer.concat(pieces, pending), true);
|
|
48
55
|
start = chunkStart + end + 1; at = end + 1; pieces = []; pending = 0;
|
|
49
56
|
}
|
|
50
|
-
if (at < chunk.length) {
|
|
57
|
+
if (at < chunk.length) {
|
|
58
|
+
if (pending + chunk.length - at > MAX_SESSION_DOCUMENT_BYTES) throw sessionByteLimit(reader.path, 'one physical record');
|
|
59
|
+
pieces.push(chunk.subarray(at)); pending += chunk.length - at;
|
|
60
|
+
}
|
|
51
61
|
}
|
|
52
62
|
if (pending) yield fileLine(start, Buffer.concat(pieces, pending), false);
|
|
53
63
|
}
|
package/session-follower.mjs
CHANGED
|
@@ -12,6 +12,7 @@ import { readGooseSqlite } from './session-goose.mjs';
|
|
|
12
12
|
import { readHermesSqlite } from './session-hermes.mjs';
|
|
13
13
|
import { readOpenCodeSqlite } from './session-opencode-store.mjs';
|
|
14
14
|
import { ClaudeAppendState } from './session-claude-append.mjs';
|
|
15
|
+
import { loadClaudeBackgroundAgents } from './session-claude-index.mjs';
|
|
15
16
|
|
|
16
17
|
const pathOrder = (a, b) => Buffer.compare(Buffer.from(a), Buffer.from(b));
|
|
17
18
|
const identityKeys = ['source', 'session_id', 'model', 'cwd', 'system_prompt', 'agent_id', 'parent_tool_use_id', 'lineage'];
|
|
@@ -135,12 +136,20 @@ export class SessionFollower {
|
|
|
135
136
|
if (this.options.displayHistory) return null;
|
|
136
137
|
try { return ClaudeAppendState.fromSession(session, this.options.fidelity); } catch { return null; }
|
|
137
138
|
}
|
|
138
|
-
#snapshot(reason) { return this.#sequence({ type: 'session_snapshot', reason, session: normalizedSessionJson(this.current) }); }
|
|
139
|
+
#snapshot(reason) { return this.#sequence({ type: 'session_snapshot', reason, session: { ...normalizedSessionJson(this.current), ...(this.agents ? { background_agents: this.agents } : {}) } }); }
|
|
140
|
+
/** A Claude Code transcript's background agents now, of the whole file (null for any other source): whether they
|
|
141
|
+
* changed since the last event said them. */
|
|
142
|
+
async #readAgents(source) {
|
|
143
|
+
if (source !== 'claude_code') { const changed = this.agents != null; this.agents = null; return changed; }
|
|
144
|
+
const agents = await loadClaudeBackgroundAgents({ harness: 'claude-code', storage: { kind: 'file', path: this.path } }, this.options).catch(() => this.agents);
|
|
145
|
+
const changed = !nativeJsonEqual(agents ?? null, this.agents ?? null);
|
|
146
|
+
this.agents = agents; return changed;
|
|
147
|
+
}
|
|
139
148
|
#error(message) { return this.#sequence({ type: 'watch_error', recoverable: true, message }); }
|
|
140
149
|
/** Serial polls only; a service serializes them per follower. */
|
|
141
150
|
async poll() {
|
|
142
151
|
if (this.closed) throw new Error('the session follower is closed');
|
|
143
|
-
if (this.initialPending) { this.initialPending = false; return this.#snapshot('initial'); }
|
|
152
|
+
if (this.initialPending) { this.initialPending = false; await this.#readAgents(this.current.meta.source); return this.#snapshot('initial'); }
|
|
144
153
|
const observed = await sourceFingerprint(this.path, this.current, this.selected, this.options.includeSubagents);
|
|
145
154
|
if (nativeJsonEqual(observed, this.fingerprint)) return null;
|
|
146
155
|
const companionsSame = nativeJsonEqual(observed.filter((stamp) => stamp.path !== this.path), this.fingerprint.filter((stamp) => stamp.path !== this.path));
|
|
@@ -169,13 +178,16 @@ export class SessionFollower {
|
|
|
169
178
|
if (error.code === 'ESTALE') this.fingerprint = []; // retry a raced read even if the last writer then stopped
|
|
170
179
|
return this.#error(`could not reload ${this.path}: ${error.message}; retaining the last good snapshot`);
|
|
171
180
|
}
|
|
172
|
-
|
|
181
|
+
// A notification can end an agent before any message shows it (queued while the session works).
|
|
182
|
+
const agentsChanged = await this.#readAgents(next.meta.source);
|
|
183
|
+
if (normalizedEqual(this.current, next)) { this.current = next; return agentsChanged ? this.#snapshot('background_agents') : null; }
|
|
173
184
|
const identitySame = sameIdentity(this.current, next);
|
|
174
185
|
const prefix = identitySame && sameChildren(this.current, next) ? appendPrefix(this.current.messages, next.messages) : 0;
|
|
175
186
|
this.current = next;
|
|
176
187
|
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)
|
|
188
|
+
messages: next.messages.slice(prefix).map(messageJson), total_message_count: Math.max(next.imported_message_count ?? next.messages.length, next.messages.length),
|
|
189
|
+
...(this.agents ? { background_agents: this.agents } : {}) });
|
|
178
190
|
return this.#snapshot(identitySame ? 'history_rewritten' : 'source_changed');
|
|
179
191
|
}
|
|
180
|
-
close() { this.closed = true; this.current = null; this.fingerprint = []; this.appendState = null; }
|
|
192
|
+
close() { this.closed = true; this.current = null; this.fingerprint = []; this.appendState = null; this.agents = null; }
|
|
181
193
|
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { randomBytes } from 'node:crypto';
|
|
2
|
+
|
|
3
|
+
/** Rust uses random UUID bytes with a clock fallback if entropy is unavailable. */
|
|
4
|
+
export function freshSessionId() {
|
|
5
|
+
let bytes;
|
|
6
|
+
try { bytes = randomBytes(16); }
|
|
7
|
+
catch {
|
|
8
|
+
let nanos = BigInt(Math.max(0, Date.now())) * 1_000_000n;
|
|
9
|
+
bytes = Buffer.alloc(16);
|
|
10
|
+
for (let at = 0; at < bytes.length; at++) { bytes[at] = Number(nanos & 255n); nanos >>= 8n; }
|
|
11
|
+
}
|
|
12
|
+
bytes[6] = (bytes[6] & 15) | 64; bytes[8] = (bytes[8] & 63) | 128;
|
|
13
|
+
const hex = bytes.toString('hex');
|
|
14
|
+
return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`;
|
|
15
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
// Hermes owns its session database and writes it through this CLI door.
|
|
2
|
+
import { mkdir, writeFile } from 'node:fs/promises';
|
|
3
|
+
import { join, resolve, sep } from 'node:path';
|
|
4
|
+
import { userInfo } from 'node:os';
|
|
5
|
+
import { spawn } from 'node:child_process';
|
|
6
|
+
import { toSessionJsonl } from './session-write.mjs';
|
|
7
|
+
|
|
8
|
+
function hermesHome(env) {
|
|
9
|
+
if (env.HERMES_HOME != null) return env.HERMES_HOME;
|
|
10
|
+
let home = env.HOME || (process.platform === 'win32' && env.USERPROFILE);
|
|
11
|
+
if (!home) try { home = userInfo().homedir; } catch { home = ''; }
|
|
12
|
+
return home ? nativeJoin(home, '.hermes') : '.hermes';
|
|
13
|
+
}
|
|
14
|
+
function nativeJoin(root, ...parts) {
|
|
15
|
+
return process.platform === 'win32' && /^[A-Za-z]:$/u.test(root) ? root + parts.join(sep) : join(root, ...parts);
|
|
16
|
+
}
|
|
17
|
+
async function importCommand(rollout, home, { env, cwd, signal }) {
|
|
18
|
+
signal?.throwIfAborted();
|
|
19
|
+
return await new Promise((resolveResult, reject) => {
|
|
20
|
+
const stdout = [], stderr = [];
|
|
21
|
+
const child = spawn('hermes', ['sessions', 'import', '--from', 'codex', rollout], { cwd,
|
|
22
|
+
env: { ...env, HERMES_HOME: home, HERMES_NO_ONBOARDING: '1', HERMES_NONINTERACTIVE: '1' }, windowsHide: true });
|
|
23
|
+
// This handle and PID belong to this command only; never select a process
|
|
24
|
+
// by name. Client closure stops the import without claiming a rollback.
|
|
25
|
+
let forceStop;
|
|
26
|
+
const abort = () => {
|
|
27
|
+
child.kill('SIGTERM');
|
|
28
|
+
forceStop = setTimeout(() => child.kill('SIGKILL'), 250); forceStop.unref();
|
|
29
|
+
};
|
|
30
|
+
signal?.addEventListener('abort', abort, { once: true });
|
|
31
|
+
if (signal?.aborted) abort();
|
|
32
|
+
child.stdout.on('data', chunk => stdout.push(chunk)); child.stderr.on('data', chunk => stderr.push(chunk));
|
|
33
|
+
let startError;
|
|
34
|
+
child.once('error', error => { startError = error; });
|
|
35
|
+
child.once('close', (code, stoppedBy) => {
|
|
36
|
+
clearTimeout(forceStop);
|
|
37
|
+
signal?.removeEventListener('abort', abort);
|
|
38
|
+
if (startError) reject(new Error(`\`hermes sessions import\` could not start: ${startError.message}`, { cause: startError }));
|
|
39
|
+
else if (signal?.aborted) reject(signal.reason ?? new Error('Hermes import cancelled'));
|
|
40
|
+
else resolveResult({ code, stoppedBy, stdout: Buffer.concat(stdout).toString('utf8'), stderr: Buffer.concat(stderr).toString('utf8') });
|
|
41
|
+
});
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export async function importIntoHermes(session, { env = process.env, cwd = process.cwd(), signal } = {}) {
|
|
46
|
+
signal?.throwIfAborted();
|
|
47
|
+
const home = hermesHome(env), rolloutDirectory = nativeJoin(home, 'imports', 'supercode');
|
|
48
|
+
await mkdir(resolve(cwd, rolloutDirectory), { recursive: true });
|
|
49
|
+
const rollout = nativeJoin(rolloutDirectory, `rollout-${Math.max(0, Date.now())}-${session.meta.session_id ?? 'session'}.jsonl`);
|
|
50
|
+
await writeFile(resolve(cwd, rollout), toSessionJsonl(session, 'codex'));
|
|
51
|
+
const output = await importCommand(rollout, home, { env, cwd, signal });
|
|
52
|
+
let sessionId;
|
|
53
|
+
for (const line of output.stdout.split(/\r?\n/u)) {
|
|
54
|
+
if (line.includes(' session as ')) { sessionId = line.split(' session as ')[1].trim(); break; }
|
|
55
|
+
}
|
|
56
|
+
if (output.code === 0 && sessionId) return { session_id: sessionId, home, rollout, via: 'hermes sessions import --from codex' };
|
|
57
|
+
if (output.code !== 0 && output.stderr.includes('database is locked')) throw new Error(
|
|
58
|
+
`Hermes's session database is locked by another Hermes process; Hermes waited, gave up, and imported nothing. The converted rollout is kept at ${rollout}: run \`hermes sessions import --from codex ${rollout}\` once Hermes is idle.`);
|
|
59
|
+
const status = output.stoppedBy ? `signal: ${output.stoppedBy}` : `exit status: ${output.code}`;
|
|
60
|
+
throw new Error(`\`hermes sessions import --from codex ${rollout}\` did not report a session id (exit ${status}): ${output.stdout.trim()}${output.stderr.trim()}`);
|
|
61
|
+
}
|
package/session-hermes.mjs
CHANGED
|
@@ -44,7 +44,7 @@ export async function readHermesSqlite(path, sessionId = null, { metrics = {} }
|
|
|
44
44
|
const info = await sourceStat(path);
|
|
45
45
|
if (!info?.isFile()) throw new Error(`Hermes SQLite store not found at ${path} — expected a \`state.db\` file`);
|
|
46
46
|
return readStore(path, (db) => {
|
|
47
|
-
const tables = new Set(db
|
|
47
|
+
const tables = new Set(statement(db, "SELECT name FROM sqlite_master WHERE type='table'").all().map((row) => row.name));
|
|
48
48
|
if (!['sessions', 'messages', 'schema_version'].every((table) => tables.has(table)) || tables.has('schema_meta')) {
|
|
49
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
50
|
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
// The public import door reads the supplied document only. Format selection is
|
|
2
|
+
// explicit; it does not discover a home, write a store, or invoke a vendor.
|
|
3
|
+
import { readSessionDocument } from './session-document.mjs';
|
|
4
|
+
import { normalizedSessionJson } from './session-model.mjs';
|
|
5
|
+
|
|
6
|
+
const formats = new Map([
|
|
7
|
+
['claude-code', 'claude_code'], ['codex', 'codex'], ['opencode', 'opencode'],
|
|
8
|
+
['open-code', 'opencode'], ['pi', 'pi'], ['grok', 'grok'],
|
|
9
|
+
['gemini', 'gemini'], ['goose', 'goose'], ['hermes', 'codex'],
|
|
10
|
+
]);
|
|
11
|
+
const invalid = message => { throw Object.assign(new Error(message), { rpcCode: -32602 }); };
|
|
12
|
+
|
|
13
|
+
function transferFormat(value) {
|
|
14
|
+
// Serde's externally tagged unit variants admit both a string and a
|
|
15
|
+
// single-key object whose value is null. No format auto-detection occurs.
|
|
16
|
+
if (value !== null && typeof value === 'object' && !Array.isArray(value)) {
|
|
17
|
+
const keys = Object.keys(value);
|
|
18
|
+
if (keys.length !== 1 || value[keys[0]] !== null) invalid('source_harness must be a transfer format');
|
|
19
|
+
value = keys[0];
|
|
20
|
+
}
|
|
21
|
+
if (typeof value !== 'string' || !formats.has(value)) invalid(`unknown source_harness: ${String(value)}`);
|
|
22
|
+
return formats.get(value);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function importSessionDocument(params) {
|
|
26
|
+
let source, content;
|
|
27
|
+
if (Array.isArray(params)) {
|
|
28
|
+
if (params.length !== 2) invalid('session import positional params require source_harness and content');
|
|
29
|
+
[source, content] = params;
|
|
30
|
+
} else {
|
|
31
|
+
if (params === null || typeof params !== 'object') invalid('session import params must be an object or positional array');
|
|
32
|
+
source = params.source_harness; content = params.content;
|
|
33
|
+
}
|
|
34
|
+
source = transferFormat(source);
|
|
35
|
+
if (typeof content !== 'string') invalid('content must be a string');
|
|
36
|
+
return { session: normalizedSessionJson(readSessionDocument(content, { source, fidelity: 'byte_lossless' })) };
|
|
37
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { Session } from './session-model.js';
|
|
2
|
+
export interface ReducedInspection {
|
|
3
|
+
stubCount: number; fullTokens: bigint; viewTokens: bigint; pct: bigint; sidecarPath: string; attribution: object;
|
|
4
|
+
}
|
|
5
|
+
export interface InspectionContext {
|
|
6
|
+
env?: Record<string, string | undefined>; storeRoot?: string;
|
|
7
|
+
output?: (text: string) => void | Promise<void>;
|
|
8
|
+
diagnostic?: (text: string, role?: string) => void;
|
|
9
|
+
render?: (value: { session: Session; reduced: ReducedInspection | null; opencodeStore: object | null }) => void | Promise<void>;
|
|
10
|
+
schemas?: () => void | Promise<void>;
|
|
11
|
+
}
|
|
12
|
+
/** Complete source reading, strict saved pointers and persisted working-view
|
|
13
|
+
* precedence. Prints partial reports before refusing literal parse loss. */
|
|
14
|
+
export function inspectSession(options: { file: string; json?: boolean }, context?: InspectionContext): Promise<{ session: object; opencode_store?: object }>;
|
|
15
|
+
export function inspectionSessionSummary(session: Session, reduced?: ReducedInspection | null): object;
|
|
16
|
+
export function inspectionStoredRecords(session: Session): number | null;
|
|
17
|
+
export function inspectionFirstLine(text: string, maximum: number): string;
|
|
18
|
+
export function inspectionPercentage(view: bigint, full: bigint): bigint;
|