@volter/supercode-interchange 0.0.0-stage → 0.1.0
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-APACHE +202 -0
- package/LICENSE-MIT +21 -0
- package/README.md +31 -2
- package/compressed.mjs +58 -0
- package/discovery.mjs +110 -0
- package/file-io.mjs +151 -0
- package/foreign-store.mjs +50 -0
- package/hermes-mirror.mjs +9 -0
- package/hermes-store.mjs +107 -0
- package/homes.mjs +37 -0
- package/index.d.ts +125 -0
- package/index.mjs +20 -0
- package/jsonl.mjs +124 -0
- package/metadata.mjs +168 -0
- package/native-json.mjs +160 -0
- package/native-ledgers.mjs +91 -0
- package/openclaw.mjs +97 -0
- package/orchestrator-store.mjs +79 -0
- package/package.json +14 -4
- package/previews.mjs +183 -0
- package/query.mjs +150 -0
- package/retained-projection.mjs +128 -0
- package/source-files.mjs +190 -0
- package/source-layout.mjs +92 -0
- package/sqlite-reader.mjs +75 -0
- package/store-registry.mjs +62 -0
- package/text.mjs +17 -0
- package/workspace.mjs +104 -0
package/native-json.mjs
ADDED
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
// Catalog JSON follows the shipped CLI's serde_json 1.0.150 representation:
|
|
2
|
+
// insertion-ordered objects, i64/u64 integers, and separately typed floats.
|
|
3
|
+
// Facts about ordering/number tokens stay private; callers receive ordinary
|
|
4
|
+
// JS values and never serialize this metadata into a session descriptor.
|
|
5
|
+
const facts = new WeakMap();
|
|
6
|
+
const uintMax = (1n << 64n) - 1n, intMin = -(1n << 63n), intMax = (1n << 63n) - 1n;
|
|
7
|
+
const invalid = () => { throw new SyntaxError('not native catalog JSON'); };
|
|
8
|
+
|
|
9
|
+
function validString(text) {
|
|
10
|
+
for (let at = 0; at < text.length; at++) {
|
|
11
|
+
const code = text.charCodeAt(at);
|
|
12
|
+
if (code >= 0xd800 && code <= 0xdbff) {
|
|
13
|
+
const next = text.charCodeAt(++at);
|
|
14
|
+
if (!(next >= 0xdc00 && next <= 0xdfff)) invalid();
|
|
15
|
+
} else if (code >= 0xdc00 && code <= 0xdfff) invalid();
|
|
16
|
+
}
|
|
17
|
+
return text;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/** serde_json's default float parser keeps a u64 significand, discards
|
|
21
|
+
* overflowing decimal digits and scales with its literal powers of ten.
|
|
22
|
+
* It does not use the optional float_roundtrip feature. */
|
|
23
|
+
function decimalValue(token) {
|
|
24
|
+
const match = /^(-?)(\d+)(?:\.(\d+))?(?:[eE]([+-]?\d+))?$/.exec(token);
|
|
25
|
+
const negative = match[1] === '-';
|
|
26
|
+
let significand = 0n, exponent = 0, overflow = false;
|
|
27
|
+
for (const digit of match[2]) {
|
|
28
|
+
const next = significand * 10n + BigInt(digit);
|
|
29
|
+
if (overflow || next > uintMax) { overflow = true; exponent++; }
|
|
30
|
+
else significand = next;
|
|
31
|
+
}
|
|
32
|
+
overflow = false;
|
|
33
|
+
for (const digit of match[3] ?? '') {
|
|
34
|
+
const next = significand * 10n + BigInt(digit);
|
|
35
|
+
if (overflow || next > uintMax) overflow = true;
|
|
36
|
+
else { significand = next; exponent--; }
|
|
37
|
+
}
|
|
38
|
+
if (match[4]) {
|
|
39
|
+
const supplied = Number(match[4]);
|
|
40
|
+
if (!Number.isFinite(supplied) || Math.abs(supplied) > 2_147_483_647) {
|
|
41
|
+
if (supplied > 0 && significand !== 0n) invalid();
|
|
42
|
+
return negative ? -0 : 0;
|
|
43
|
+
}
|
|
44
|
+
exponent = Math.max(-2_147_483_648, Math.min(2_147_483_647, exponent + supplied));
|
|
45
|
+
}
|
|
46
|
+
let number = Number(significand);
|
|
47
|
+
while (Math.abs(exponent) > 308) {
|
|
48
|
+
if (number === 0) break;
|
|
49
|
+
if (exponent > 0) invalid();
|
|
50
|
+
number /= 1e308; exponent += 308;
|
|
51
|
+
}
|
|
52
|
+
if (number !== 0 || Math.abs(exponent) <= 308) {
|
|
53
|
+
const power = Number(`1e${Math.abs(exponent)}`);
|
|
54
|
+
number = exponent < 0 ? number / power : number * power;
|
|
55
|
+
}
|
|
56
|
+
if (!Number.isFinite(number)) invalid();
|
|
57
|
+
return negative ? -number : number;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function numberFact(token) {
|
|
61
|
+
if (!/[.eE]/.test(token)) {
|
|
62
|
+
const whole = BigInt(token);
|
|
63
|
+
if (token !== '-0' && whole >= intMin && whole <= uintMax) return { value: Number(whole), kind: 'integer', integer: whole };
|
|
64
|
+
}
|
|
65
|
+
return { value: decimalValue(token), kind: 'float' };
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export function parseNativeJson(text) {
|
|
69
|
+
let at = 0;
|
|
70
|
+
const white = () => { while (/[\x20\t\r\n]/.test(text[at] ?? '') && at < text.length) at++; };
|
|
71
|
+
const takeString = () => {
|
|
72
|
+
const from = at++;
|
|
73
|
+
while (at < text.length) {
|
|
74
|
+
const char = text[at++];
|
|
75
|
+
if (char === '"') return validString(JSON.parse(text.slice(from, at)));
|
|
76
|
+
if (char === '\\') at++;
|
|
77
|
+
}
|
|
78
|
+
invalid();
|
|
79
|
+
};
|
|
80
|
+
const value = (depth) => {
|
|
81
|
+
white();
|
|
82
|
+
const char = text[at];
|
|
83
|
+
if (char === '"') return { value: takeString() };
|
|
84
|
+
if (char === '[' || char === '{') {
|
|
85
|
+
if (depth >= 127) invalid();
|
|
86
|
+
const array = char === '[', result = array ? [] : Object.create(null);
|
|
87
|
+
const meta = { keys: [], numbers: new Map() };
|
|
88
|
+
facts.set(result, meta);
|
|
89
|
+
const end = array ? ']' : '}';
|
|
90
|
+
at++; white();
|
|
91
|
+
if (text[at] === end) { at++; return { value: result }; }
|
|
92
|
+
for (;;) {
|
|
93
|
+
let key;
|
|
94
|
+
if (array) key = String(result.length);
|
|
95
|
+
else {
|
|
96
|
+
if (text[at] !== '"') invalid();
|
|
97
|
+
key = takeString(); white();
|
|
98
|
+
if (text[at++] !== ':') invalid();
|
|
99
|
+
}
|
|
100
|
+
const child = value(depth + 1);
|
|
101
|
+
if (!Object.hasOwn(result, key)) meta.keys.push(key);
|
|
102
|
+
result[key] = child.value;
|
|
103
|
+
if (child.kind) meta.numbers.set(key, child); else meta.numbers.delete(key);
|
|
104
|
+
white();
|
|
105
|
+
if (text[at] === end) { at++; break; }
|
|
106
|
+
if (text[at++] !== ',') invalid();
|
|
107
|
+
white();
|
|
108
|
+
}
|
|
109
|
+
return { value: result };
|
|
110
|
+
}
|
|
111
|
+
for (const [literal, result] of [['true', true], ['false', false], ['null', null]]) {
|
|
112
|
+
if (text.startsWith(literal, at)) { at += literal.length; return { value: result }; }
|
|
113
|
+
}
|
|
114
|
+
const number = /-?(?:0|[1-9][0-9]*)(?:\.[0-9]+)?(?:[eE][+-]?[0-9]+)?/y;
|
|
115
|
+
number.lastIndex = at;
|
|
116
|
+
const token = number.exec(text)?.[0];
|
|
117
|
+
if (token == null) invalid();
|
|
118
|
+
at += token.length;
|
|
119
|
+
return numberFact(token);
|
|
120
|
+
};
|
|
121
|
+
const result = value(0);
|
|
122
|
+
white(); if (at !== text.length) invalid();
|
|
123
|
+
return result.value;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
export function nativeObjectKeys(value) { return facts.get(value)?.keys ?? Object.keys(value); }
|
|
127
|
+
|
|
128
|
+
export function nativeInteger(value, key, { signed = false } = {}) {
|
|
129
|
+
if (value == null || typeof value !== 'object') return null;
|
|
130
|
+
const fact = facts.get(value)?.numbers.get(key);
|
|
131
|
+
if (fact?.kind !== 'integer') return null;
|
|
132
|
+
const whole = fact.integer;
|
|
133
|
+
return whole >= (signed ? intMin : 0n) && whole <= (signed ? intMax : uintMax) ? whole : null;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function formatFloat(number) {
|
|
137
|
+
if (number === 0) return Object.is(number, -0) ? '-0.0' : '0.0';
|
|
138
|
+
// V8 supplies the shortest correctly rounded decimal significand. Apply
|
|
139
|
+
// pinned zmij 1.0.21's f64 fixed/exponent spelling, including signed exponent.
|
|
140
|
+
const sign = number < 0 ? '-' : '';
|
|
141
|
+
const [coefficient, scale] = Math.abs(number).toExponential().split('e');
|
|
142
|
+
const digits = coefficient.replace('.', ''), exponent = Number(scale);
|
|
143
|
+
if (exponent >= -5 && exponent <= 15) {
|
|
144
|
+
if (exponent >= digits.length - 1) return `${sign}${digits}${'0'.repeat(exponent + 1 - digits.length)}.0`;
|
|
145
|
+
if (exponent >= 0) return `${sign}${digits.slice(0, exponent + 1)}.${digits.slice(exponent + 1)}`;
|
|
146
|
+
return `${sign}0.${'0'.repeat(-exponent - 1)}${digits}`;
|
|
147
|
+
}
|
|
148
|
+
return `${sign}${coefficient}e${exponent >= 0 ? '+' : ''}${exponent}`;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
export function stringifyNativeJson(value) {
|
|
152
|
+
if (value == null || typeof value !== 'object') return JSON.stringify(value);
|
|
153
|
+
const meta = facts.get(value);
|
|
154
|
+
const child = (key) => {
|
|
155
|
+
const fact = meta?.numbers.get(key);
|
|
156
|
+
return fact ? fact.kind === 'integer' ? String(fact.integer) : formatFloat(fact.value) : stringifyNativeJson(value[key]);
|
|
157
|
+
};
|
|
158
|
+
if (Array.isArray(value)) return `[${value.map((_, at) => child(String(at))).join(',')}]`;
|
|
159
|
+
return `{${nativeObjectKeys(value).map((key) => `${JSON.stringify(key)}:${child(key)}`).join(',')}}`;
|
|
160
|
+
}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { dirname, basename, join } from 'node:path';
|
|
2
|
+
import { withSessionFile, sameFingerprint, appendableFingerprint, visitAppendedLines, parseJsonLine } from './file-io.mjs';
|
|
3
|
+
import { optionalFile } from './source-files.mjs';
|
|
4
|
+
import { nativeTrim, displayText, truncateScalars } from './text.mjs';
|
|
5
|
+
|
|
6
|
+
export function codexNamesPath(rollout) {
|
|
7
|
+
let parent = rollout;
|
|
8
|
+
for (;;) {
|
|
9
|
+
if (basename(parent) === 'sessions') return join(dirname(parent), 'session_index.jsonl');
|
|
10
|
+
const next = dirname(parent);
|
|
11
|
+
if (next === parent) return null;
|
|
12
|
+
parent = next;
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/** Name/history ledgers are watched sources, not query-time whole-file reads.
|
|
17
|
+
* On an append, apply only its new lines and unfinished EOF; the owner commits
|
|
18
|
+
* map changes and this verified offset under one source identity. */
|
|
19
|
+
async function readLedger(path, kind, previous, metrics) {
|
|
20
|
+
return optionalFile(path, async (reader) => {
|
|
21
|
+
if (sameFingerprint(previous?.fingerprint, reader.fingerprint)) return previous.result;
|
|
22
|
+
const old = previous?.result;
|
|
23
|
+
// An EOF record can change before its newline arrives. Rebuild in that
|
|
24
|
+
// unusual case rather than leaving a rename/topic from now-invalid bytes.
|
|
25
|
+
const append = old && !old.trailing && appendableFingerprint(previous?.fingerprint, reader.fingerprint);
|
|
26
|
+
const values = new Map(append ? old.values : []);
|
|
27
|
+
const apply = (line) => {
|
|
28
|
+
const record = parseJsonLine(line, { strictUtf8: true });
|
|
29
|
+
if (record == null) return;
|
|
30
|
+
if (kind === 'names') {
|
|
31
|
+
if (typeof record.id === 'string' && typeof record.thread_name === 'string') {
|
|
32
|
+
const name = nativeTrim(record.thread_name);
|
|
33
|
+
if (name) values.set(record.id, name);
|
|
34
|
+
}
|
|
35
|
+
} else if (typeof record.session_id === 'string' && !values.has(record.session_id)) {
|
|
36
|
+
const content = displayText(record.text);
|
|
37
|
+
if (content != null && nativeTrim(content)) values.set(record.session_id, { role: 'user', content: truncateScalars(content, 4096), metadata: {} });
|
|
38
|
+
}
|
|
39
|
+
};
|
|
40
|
+
const state = await visitAppendedLines(reader, append ? old : null, apply);
|
|
41
|
+
if (state.trailing) apply(Buffer.from(state.trailing, 'base64'));
|
|
42
|
+
return { ...state, values };
|
|
43
|
+
}, metrics);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export const readCodexNames = (path, options = {}) => readLedger(path, 'names', options.previous, options.metrics);
|
|
47
|
+
export const readCodexHistory = (path, options = {}) => readLedger(path, 'history', options.previous, options.metrics);
|
|
48
|
+
|
|
49
|
+
const titleKey = Buffer.from('"customTitle":');
|
|
50
|
+
|
|
51
|
+
/** Native name matching searches this exact byte key anywhere in a Claude
|
|
52
|
+
* transcript, including late rename records. It does not require the enclosing
|
|
53
|
+
* record to parse. Keep the key boundary or an unfinished title between
|
|
54
|
+
* reads; names have no native length cap, so a long unfinished title is
|
|
55
|
+
* retained until its closing quote arrives rather than silently truncated. */
|
|
56
|
+
export async function readClaudeTitles(path, { previous, metrics } = {}) {
|
|
57
|
+
return withSessionFile(path, async (reader) => {
|
|
58
|
+
if (sameFingerprint(previous?.fingerprint, reader.fingerprint)) return previous.result;
|
|
59
|
+
const append = previous && typeof previous.result.pending === 'string' && appendableFingerprint(previous.fingerprint, reader.fingerprint);
|
|
60
|
+
const titles = new Set(append ? previous.result.titles : []);
|
|
61
|
+
let offset = append ? previous.size : 0;
|
|
62
|
+
let pending = append ? Buffer.from(previous.result.pending, 'base64') : Buffer.alloc(0);
|
|
63
|
+
while (offset < reader.size) {
|
|
64
|
+
const chunk = await reader.read(offset, Math.min(64 * 1024, reader.size - offset), { appended: append && offset >= previous.size });
|
|
65
|
+
offset += chunk.length;
|
|
66
|
+
const bytes = pending.length ? Buffer.concat([pending, chunk]) : chunk;
|
|
67
|
+
let from = 0, at;
|
|
68
|
+
pending = Buffer.alloc(0);
|
|
69
|
+
while ((at = bytes.indexOf(titleKey, from)) >= 0) {
|
|
70
|
+
const start = at + titleKey.length;
|
|
71
|
+
let quote = start;
|
|
72
|
+
while (quote < bytes.length && [9, 10, 13, 32].includes(bytes[quote])) quote++;
|
|
73
|
+
if (quote >= bytes.length) { pending = Buffer.from(bytes.subarray(at)); break; }
|
|
74
|
+
if (bytes[quote] !== 34) { from = start; continue; }
|
|
75
|
+
let end = quote + 1, closed = false;
|
|
76
|
+
while (end < bytes.length) {
|
|
77
|
+
if (bytes[end] === 92) { end += 2; continue; }
|
|
78
|
+
if (bytes[end++] === 34) { closed = true; break; }
|
|
79
|
+
}
|
|
80
|
+
if (!closed) {
|
|
81
|
+
pending = Buffer.from(bytes.subarray(at)); break;
|
|
82
|
+
}
|
|
83
|
+
const title = parseJsonLine(bytes.subarray(quote, end));
|
|
84
|
+
if (typeof title === 'string') titles.add(title);
|
|
85
|
+
from = start;
|
|
86
|
+
}
|
|
87
|
+
if (!pending.length) pending = Buffer.from(bytes.subarray(Math.max(from, bytes.length - titleKey.length + 1)));
|
|
88
|
+
}
|
|
89
|
+
return { titles: [...titles], offset: reader.size, pending: pending.toString('base64') };
|
|
90
|
+
}, metrics);
|
|
91
|
+
}
|
package/openclaw.mjs
ADDED
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import { basename, sep } from 'node:path';
|
|
2
|
+
import { withSessionFile, parseJsonLine, sameFingerprint, appendableFingerprint } from './file-io.mjs';
|
|
3
|
+
import { finalizeNouns } from './metadata.mjs';
|
|
4
|
+
import { readColdMetadataTail, advanceMetadataTail } from './jsonl.mjs';
|
|
5
|
+
import { nativeTrim } from './text.mjs';
|
|
6
|
+
|
|
7
|
+
const needle = Buffer.from('"type":"message"');
|
|
8
|
+
|
|
9
|
+
export function openclawNouns(header, path) {
|
|
10
|
+
const components = path.split(sep), at = components.indexOf('agents');
|
|
11
|
+
let profile = at >= 0 && at + 1 < components.length ? components[at + 1] : null;
|
|
12
|
+
const candidate = Object.hasOwn(header, 'sessionKey') ? header.sessionKey : header.__openclaw?.sessionKey;
|
|
13
|
+
if (typeof candidate !== 'string') return profile == null ? {} : { profile };
|
|
14
|
+
const parts = candidate.split(':');
|
|
15
|
+
let surface = { key: candidate }, trigger = null, recurrence = null;
|
|
16
|
+
if (parts[0] === 'agent' && parts.length >= 3) {
|
|
17
|
+
if (parts[2] === 'main') {
|
|
18
|
+
profile = parts[1]; surface.kind = 'main'; trigger = 'unknown';
|
|
19
|
+
} else if (parts.length >= 5) {
|
|
20
|
+
profile = parts[1]; trigger = 'channel';
|
|
21
|
+
surface = { ...surface, platform: parts[2], kind: parts[3], chat_id: parts[4] };
|
|
22
|
+
if ((parts[5] === 'thread' || parts[5] === 'topic') && parts.length >= 7) surface.thread_id = parts[6];
|
|
23
|
+
}
|
|
24
|
+
} else if (parts[0] === 'cron' && parts.length >= 2) {
|
|
25
|
+
trigger = 'cron'; recurrence = { job_id: parts.slice(1).join(':'), kind: 'cron' };
|
|
26
|
+
} else if (parts[0] === 'hook' && parts.length >= 2) trigger = 'webhook';
|
|
27
|
+
else if (parts[0] === 'acp-bridge') { trigger = 'api'; surface.platform = 'acp'; }
|
|
28
|
+
return { ...(profile != null ? { profile } : {}), ...(trigger != null ? { surface, trigger } : {}), ...(recurrence ? { recurrence } : {}) };
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** Count exactly the native per-line literal, once per line, without keeping
|
|
32
|
+
* body lines in memory. Validate all newly read UTF-8 as the native full-text
|
|
33
|
+
* reader does. The partial line's hit and overlap survive an append/restart. */
|
|
34
|
+
async function scanCounts(reader, previous) {
|
|
35
|
+
const count = previous ? { ...previous.count } : { complete: 0, pending_hit: false, match_tail: '' };
|
|
36
|
+
let overlap = Buffer.from(count.match_tail, 'base64');
|
|
37
|
+
let offset = previous?.offset ?? 0, header = previous?.header ?? null;
|
|
38
|
+
let headerTerminated = previous?.header_terminated ?? false;
|
|
39
|
+
let headerPieces = [], headerSize = 0;
|
|
40
|
+
const utf8 = new TextDecoder('utf-8', { fatal: true, ignoreBOM: true });
|
|
41
|
+
const acceptHeader = (terminated) => {
|
|
42
|
+
const bytes = Buffer.concat(headerPieces, headerSize);
|
|
43
|
+
const text = new TextDecoder('utf-8', { fatal: true, ignoreBOM: true }).decode(bytes);
|
|
44
|
+
headerPieces = []; headerSize = 0;
|
|
45
|
+
if (!nativeTrim(text)) return;
|
|
46
|
+
const value = parseJsonLine(bytes, { strictUtf8: true });
|
|
47
|
+
if (value?.type !== 'session') throw new Error(`${reader.path} has no OpenClaw session header`);
|
|
48
|
+
header = value; headerTerminated = terminated;
|
|
49
|
+
};
|
|
50
|
+
while (offset < reader.size) {
|
|
51
|
+
const chunk = await reader.read(offset, Math.min(64 * 1024, reader.size - offset), { appended: previous != null });
|
|
52
|
+
offset += chunk.length;
|
|
53
|
+
utf8.decode(chunk, { stream: true });
|
|
54
|
+
let from = 0;
|
|
55
|
+
while (from < chunk.length) {
|
|
56
|
+
const end = chunk.indexOf(10, from), terminated = end >= 0;
|
|
57
|
+
const part = chunk.subarray(from, terminated ? end : chunk.length);
|
|
58
|
+
if (!count.pending_hit) {
|
|
59
|
+
const joined = overlap.length ? Buffer.concat([overlap, part]) : part;
|
|
60
|
+
count.pending_hit = joined.indexOf(needle) >= 0;
|
|
61
|
+
overlap = count.pending_hit ? Buffer.alloc(0) : Buffer.from(joined.subarray(Math.max(0, joined.length - needle.length + 1)));
|
|
62
|
+
}
|
|
63
|
+
if (header == null) {
|
|
64
|
+
headerPieces.push(part); headerSize += part.length;
|
|
65
|
+
if (terminated) acceptHeader(true);
|
|
66
|
+
}
|
|
67
|
+
if (!terminated) break;
|
|
68
|
+
if (count.pending_hit) count.complete++;
|
|
69
|
+
count.pending_hit = false; overlap = Buffer.alloc(0);
|
|
70
|
+
from = end + 1;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
utf8.decode(); // A truncated multibyte sequence is a failed part, not none.
|
|
74
|
+
if (header == null && headerSize) acceptHeader(false);
|
|
75
|
+
if (header == null) throw new Error(`${reader.path} has no OpenClaw session header`);
|
|
76
|
+
count.match_tail = overlap.toString('base64');
|
|
77
|
+
return { count, header, header_terminated: headerTerminated };
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export async function readOpenClaw(path, { previous, metrics } = {}) {
|
|
81
|
+
if (!path.endsWith('.jsonl') || path.endsWith('.trajectory.jsonl')) return null;
|
|
82
|
+
return withSessionFile(path, async (reader) => {
|
|
83
|
+
if (sameFingerprint(previous?.fingerprint, reader.fingerprint)) return previous.result;
|
|
84
|
+
const old = previous?.result?.state;
|
|
85
|
+
const append = old?.header_terminated && appendableFingerprint(previous?.fingerprint, reader.fingerprint);
|
|
86
|
+
const counted = await scanCounts(reader, append ? old : null);
|
|
87
|
+
const tail = append && old.appendable ? await advanceMetadataTail(reader, 'openclaw', old) : await readColdMetadataTail(reader, 'openclaw');
|
|
88
|
+
const header = counted.header;
|
|
89
|
+
return {
|
|
90
|
+
descriptor: finalizeNouns({ locator: { harness: 'openclaw', session_id: typeof header.id === 'string' ? header.id : basename(path, '.jsonl'), storage: { kind: 'file', path } },
|
|
91
|
+
cwd: typeof header.cwd === 'string' ? header.cwd : null, title: null, model: null,
|
|
92
|
+
updated_at_ms: tail.facts.last_turn_ms ?? reader.modified_ms, message_count: counted.count.complete + Number(counted.count.pending_hit),
|
|
93
|
+
...openclawNouns(header, path) }),
|
|
94
|
+
state: { ...tail, ...counted },
|
|
95
|
+
};
|
|
96
|
+
}, metrics);
|
|
97
|
+
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { realpathSync } from 'node:fs';
|
|
2
|
+
import { dirname, join } from 'node:path';
|
|
3
|
+
import { readStore, statement, text, integer, unsigned, jsonColumn } from './sqlite-reader.mjs';
|
|
4
|
+
import { finalizeNouns } from './metadata.mjs';
|
|
5
|
+
|
|
6
|
+
const triggerWords = new Set(['human', 'channel', 'cron', 'heartbeat', 'webhook', 'parent', 'api', 'task', 'unknown']);
|
|
7
|
+
const endWords = new Set(['idle', 'daily', 'reset', 'new', 'handoff', 'transfer', 'left', 'error']);
|
|
8
|
+
const optionalText = (value) => value == null || typeof value === 'string';
|
|
9
|
+
const object = (value) => value != null && typeof value === 'object' && !Array.isArray(value);
|
|
10
|
+
const nonempty = (value) => typeof value === 'string' && value !== '' ? value : null;
|
|
11
|
+
const columns = "json_extract(g.entry_json, '$.metadata.supercode.binding') AS binding, CAST(strftime('%s', json_extract(g.entry_json, '$.metadata.supercode.binding.last_activity_at')) AS INTEGER) AS epoch";
|
|
12
|
+
|
|
13
|
+
function routingScope(path) {
|
|
14
|
+
const sessions = join(dirname(path), 'sessions');
|
|
15
|
+
try { return realpathSync.native(sessions); } catch { return sessions; }
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/** Binding's required records and native enum fields are validated before
|
|
19
|
+
* projection. A malformed routing binding is omitted, as by the native codec;
|
|
20
|
+
* its routing key/change position still belongs to the source snapshot. */
|
|
21
|
+
function bindingDescriptor(path, profile, raw, epoch) {
|
|
22
|
+
const binding = jsonColumn(raw);
|
|
23
|
+
if (!object(binding) || !object(binding.key) || !object(binding.worker) || typeof binding.worker.harness !== 'string') return null;
|
|
24
|
+
if (['key', 'platform', 'kind', 'chat_type', 'chat_id', 'thread_id', 'participant_id'].some((field) => !optionalText(binding.key[field]))) return null;
|
|
25
|
+
if (['session_id', 'locator'].some((field) => !optionalText(binding.worker[field]))) return null;
|
|
26
|
+
if (['started_at', 'last_activity_at', 'ended_at', 'profile', 'conversation', 'agent'].some((field) => !optionalText(binding[field]))) return null;
|
|
27
|
+
if (Object.hasOwn(binding.key, 'kind') && Object.hasOwn(binding.key, 'chat_type')) return null;
|
|
28
|
+
if (binding.residue !== undefined && !object(binding.residue)) return null;
|
|
29
|
+
if (binding.trigger !== undefined && !triggerWords.has(binding.trigger)) return null;
|
|
30
|
+
if (binding.end_reason != null && !endWords.has(binding.end_reason)) return null;
|
|
31
|
+
if (binding.recurrence != null && (!object(binding.recurrence) || typeof binding.recurrence.job_id !== 'string'
|
|
32
|
+
|| (binding.recurrence.kind !== undefined && typeof binding.recurrence.kind !== 'string'))) return null;
|
|
33
|
+
if (binding.handoff != null && (!object(binding.handoff) || typeof binding.handoff.state !== 'string'
|
|
34
|
+
|| !optionalText(binding.handoff.to) || !optionalText(binding.handoff.error))) return null;
|
|
35
|
+
// The native bridge decodes its Binding, then constructs a new binding from
|
|
36
|
+
// the orchestrator row: profile and trigger follow that row's projection.
|
|
37
|
+
const platform = binding.key.platform ?? '', kind = binding.key.kind ?? binding.key.chat_type ?? '';
|
|
38
|
+
const chat = nonempty(binding.key.chat_id), thread = nonempty(binding.key.thread_id), participant = nonempty(binding.key.participant_id);
|
|
39
|
+
const parts = ['agent', profile === '' ? 'main' : profile, platform, kind, ...[chat, thread, participant].filter((part) => part != null)];
|
|
40
|
+
const surface = { key: parts.join(':'), platform, kind,
|
|
41
|
+
...(chat != null ? { chat_id: chat } : {}), ...(thread != null ? { thread_id: thread } : {}), ...(participant != null ? { participant_id: participant } : {}) };
|
|
42
|
+
const recurrence = binding.recurrence ? { job_id: binding.recurrence.job_id, kind: 'cron' } : null;
|
|
43
|
+
const trigger = recurrence ? 'cron' : platform === 'webhook' ? 'webhook' : 'channel';
|
|
44
|
+
const id = nonempty(binding.worker.session_id), locator = nonempty(binding.worker.locator);
|
|
45
|
+
let title = `${binding.worker.harness} ${id ?? '(no worker session yet)'}`;
|
|
46
|
+
if (binding.end_reason != null && binding.ended_at != null) title += ` (ended: ${binding.end_reason})`;
|
|
47
|
+
const state = binding.handoff?.state, destination = binding.handoff?.to, error = binding.handoff?.error;
|
|
48
|
+
const at = epoch == null ? null : unsigned(integer(epoch) < 0n ? 0n : epoch);
|
|
49
|
+
if (at != null && !Number.isSafeInteger(at * 1000)) throw new Error(`${path} has an activity epoch outside the exact Node wire range`);
|
|
50
|
+
return finalizeNouns({ locator: { harness: 'orchestrator', session_id: id ?? '', storage: { kind: 'file', path: locator ?? path } },
|
|
51
|
+
cwd: null, title, updated_at_ms: at == null ? null : at * 1000, model: null, message_count: null,
|
|
52
|
+
surface, profile, trigger, ...(recurrence ? { recurrence } : {}),
|
|
53
|
+
...(state != null ? { cross_surface: { state, ...(destination != null ? { platform: destination } : {}), ...(error != null ? { error } : {}) } } : {}) });
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** Routing keys are the source-row keys; several bindings may address the
|
|
57
|
+
* same worker id. Do not discard them by deduplicating native session ids. */
|
|
58
|
+
export async function readOrchestratorStore(path, profile, { after, metrics = {} } = {}) {
|
|
59
|
+
return readStore(path, (db) => {
|
|
60
|
+
const hasChanges = db.prepare("SELECT 1 FROM sqlite_master WHERE type='table' AND name='supercode_routing_changes'").get() != null;
|
|
61
|
+
const sequence = hasChanges ? integer(statement(db, 'SELECT COALESCE(MAX(seq),0) AS seq FROM supercode_routing_changes').get().seq) : 0n;
|
|
62
|
+
const scope = routingScope(path);
|
|
63
|
+
let values, incremental = after != null && hasChanges;
|
|
64
|
+
if (incremental) {
|
|
65
|
+
const cursor = BigInt(after);
|
|
66
|
+
if (cursor < 0n || cursor > sequence) throw Object.assign(new Error('orchestrator routing cursor is stale; its source requires a full snapshot'), { code: 'INDEX_SOURCE_RESET' });
|
|
67
|
+
values = statement(db, `SELECT c.session_key, c.deleted, ${columns} FROM supercode_routing_changes c
|
|
68
|
+
LEFT JOIN gateway_routing g ON g.scope=c.scope AND g.session_key=c.session_key
|
|
69
|
+
WHERE c.scope=? AND c.seq>? ORDER BY c.seq`).all(scope, cursor);
|
|
70
|
+
} else {
|
|
71
|
+
values = statement(db, `SELECT g.session_key, 0 AS deleted, ${columns} FROM gateway_routing g
|
|
72
|
+
WHERE g.scope=? AND json_extract(g.entry_json, '$.metadata.supercode') IS NOT NULL`).all(scope);
|
|
73
|
+
}
|
|
74
|
+
const rows = values.map((value) => ({ key: text(value.session_key),
|
|
75
|
+
descriptor: integer(value.deleted) !== 0n ? null : bindingDescriptor(path, profile, value.binding, value.epoch) }));
|
|
76
|
+
metrics.store_sessions_read = (metrics.store_sessions_read ?? 0) + values.length;
|
|
77
|
+
return { rows, cursor: String(sequence), incremental, has_changes: hasChanges };
|
|
78
|
+
}, metrics);
|
|
79
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,16 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@volter/supercode-interchange",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"description": "
|
|
6
|
-
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"description": "Read-only harness metadata and discovery projection for Supercode's daemon-owned session index",
|
|
6
|
+
"license": "MIT OR Apache-2.0",
|
|
7
|
+
"exports": {
|
|
8
|
+
".": {
|
|
9
|
+
"types": "./index.d.ts",
|
|
10
|
+
"import": "./index.mjs"
|
|
11
|
+
}
|
|
12
|
+
},
|
|
13
|
+
"files": ["*.mjs", "*.d.ts", "README.md", "LICENSE-MIT", "LICENSE-APACHE"],
|
|
14
|
+
"engines": { "node": ">=22.15.0" },
|
|
15
|
+
"dependencies": { "@noble/hashes": "2.4.0" }
|
|
16
|
+
}
|
package/previews.mjs
ADDED
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
import { blake3 } from '@noble/hashes/blake3.js';
|
|
2
|
+
import { withSessionFile } from './file-io.mjs';
|
|
3
|
+
import { parseNativeJson, stringifyNativeJson, nativeInteger } from './native-json.mjs';
|
|
4
|
+
import { readStore, statement, jsonColumn } from './sqlite-reader.mjs';
|
|
5
|
+
import { nativeTrim, displayText, truncateScalars } from './text.mjs';
|
|
6
|
+
|
|
7
|
+
const maxCandidates = 8, headBytes = 512 * 1024, tailBytes = 512 * 1024, expandedTailBytes = 4 * 1024 * 1024;
|
|
8
|
+
const conversationRoles = new Set(['user', 'assistant']);
|
|
9
|
+
const fileFormats = new Set(['claude-code', 'codex', 'pi', 'gemini', 'grok', 'supercode']);
|
|
10
|
+
|
|
11
|
+
function provenance(record) {
|
|
12
|
+
const result = {};
|
|
13
|
+
for (const key of ['promptSource', 'interruptedMessageId', 'sourceToolUseID']) if (typeof record[key] === 'string') result[key] = record[key];
|
|
14
|
+
for (const key of ['isMeta', 'isCompactSummary', 'isVisibleInTranscriptOnly']) if (record[key] === true) result[key] = 'true';
|
|
15
|
+
const priority = nativeInteger(record, 'queuePriority', { signed: true });
|
|
16
|
+
if (priority != null) result.queuePriority = String(priority);
|
|
17
|
+
if (typeof record.origin?.kind === 'string') result.origin = record.origin.kind;
|
|
18
|
+
return result;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function nativeIdentity(record) {
|
|
22
|
+
for (const [object, key] of [[record, 'uuid'], [record, 'id'], [record.message, 'id'], [record.payload, 'id']]) {
|
|
23
|
+
if (object != null && typeof object === 'object' && Object.hasOwn(object, key)) {
|
|
24
|
+
// A present non-string wins the or_else chain; only timestamp remains
|
|
25
|
+
// eligible afterwards. Do not silently advance to a later id field.
|
|
26
|
+
return typeof object[key] === 'string' ? object[key] : typeof record.timestamp === 'string' ? record.timestamp : null;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
return typeof record.timestamp === 'string' ? record.timestamp : null;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function messageCandidateCursor(harness, record) {
|
|
33
|
+
const identity = nativeIdentity(record);
|
|
34
|
+
const input = Buffer.concat([Buffer.from('supercode.session-preview-cursor.v1\0'), Buffer.from(harness), Buffer.from([0]),
|
|
35
|
+
Buffer.from(identity ?? stringifyNativeJson(record))]);
|
|
36
|
+
return `v1:${Buffer.from(blake3(input)).toString('hex').slice(0, 24)}`;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function push(candidates, role, content, metadata = {}, cursor) {
|
|
40
|
+
if (candidates.length >= maxCandidates) return;
|
|
41
|
+
const text = displayText(content);
|
|
42
|
+
if (text == null || !nativeTrim(text)) return;
|
|
43
|
+
candidates.push({ ...(cursor != null ? { cursor } : {}), role, content: truncateScalars(text, 4096), metadata });
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function genericMessage(record, harness) {
|
|
47
|
+
if (harness === 'claude-code' && conversationRoles.has(record?.type)) return { role: record.type, content: record.message?.content, metadata: record.type === 'user' ? provenance(record) : {} };
|
|
48
|
+
if (harness === 'pi' && record?.type === 'message' && conversationRoles.has(record.message?.role)) return { role: record.message.role, content: record.message.content };
|
|
49
|
+
if (harness === 'gemini' && (record?.type === 'user' || record?.type === 'gemini')) return { role: record.type === 'user' ? 'user' : 'assistant', content: record.content };
|
|
50
|
+
if (harness === 'grok' && conversationRoles.has(record?.type)) return { role: record.type, content: record.content };
|
|
51
|
+
if (harness === 'supercode' && conversationRoles.has(record?.role)) return { role: record.role, content: record.content };
|
|
52
|
+
return null;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function codexMessage(native) {
|
|
56
|
+
const payload = native?.payload;
|
|
57
|
+
let role, content;
|
|
58
|
+
if (native?.type === 'response_item' && payload?.type === 'message' && conversationRoles.has(payload.role)) {
|
|
59
|
+
role = payload.role; content = payload.content;
|
|
60
|
+
} else if (native?.type === 'event_msg' && payload?.type === 'user_message') { role = 'user'; content = payload.message; }
|
|
61
|
+
else if (native?.type === 'event_msg' && payload?.type === 'agent_message') { role = 'assistant'; content = payload.message; }
|
|
62
|
+
else return null;
|
|
63
|
+
const text = displayText(content);
|
|
64
|
+
return text == null || !nativeTrim(text) ? null : { native, role, text };
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function parseLine(text) { try { return parseNativeJson(text); } catch { return null; } }
|
|
68
|
+
|
|
69
|
+
function codexCandidates(lines, latest) {
|
|
70
|
+
const candidates = [];
|
|
71
|
+
let pending = null;
|
|
72
|
+
const emit = (record) => {
|
|
73
|
+
const metadata = latest && typeof record.native.timestamp === 'string' ? { timestamp: record.native.timestamp } : {};
|
|
74
|
+
push(candidates, record.role, record.text, metadata, latest ? messageCandidateCursor('codex', record.native) : null);
|
|
75
|
+
};
|
|
76
|
+
for (const line of lines) {
|
|
77
|
+
const current = codexMessage(parseLine(line));
|
|
78
|
+
if (current == null) continue;
|
|
79
|
+
if (pending == null) pending = current;
|
|
80
|
+
else if (pending.role === current.role && pending.text === current.text && pending.native.type !== current.native.type) {
|
|
81
|
+
emit(pending.native.type === 'response_item' ? pending : current); pending = null;
|
|
82
|
+
} else { emit(pending); pending = current; }
|
|
83
|
+
if (candidates.length >= maxCandidates) break;
|
|
84
|
+
}
|
|
85
|
+
if (pending != null) emit(pending);
|
|
86
|
+
return candidates;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function candidatesIn(bytes, harness, latest) {
|
|
90
|
+
// Validate the entire window before selecting eight records, matching the
|
|
91
|
+
// native String::from_utf8 refusal even for unused later preview bytes.
|
|
92
|
+
const text = new TextDecoder('utf-8', { fatal: true, ignoreBOM: true }).decode(bytes);
|
|
93
|
+
const lines = text.split('\n');
|
|
94
|
+
if (lines.at(-1) === '') lines.pop();
|
|
95
|
+
if (latest) lines.reverse();
|
|
96
|
+
if (harness === 'codex') return codexCandidates(lines, latest);
|
|
97
|
+
const candidates = [];
|
|
98
|
+
for (const line of lines) {
|
|
99
|
+
const record = parseLine(line), message = genericMessage(record, harness);
|
|
100
|
+
if (message == null) continue;
|
|
101
|
+
const metadata = { ...message.metadata };
|
|
102
|
+
if (latest && harness === 'claude-code' && typeof record.timestamp === 'string') metadata.timestamp = record.timestamp;
|
|
103
|
+
push(candidates, message.role, message.content, metadata, latest ? messageCandidateCursor(harness, record) : null);
|
|
104
|
+
if (candidates.length >= maxCandidates) break;
|
|
105
|
+
}
|
|
106
|
+
return candidates;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/** Topic reads raw native files, including packed Codex rollouts. The existing
|
|
110
|
+
* catalog does not decompress previews; its separate history source supplies
|
|
111
|
+
* archived Codex topic fallback. Only headers require the streaming decoder. */
|
|
112
|
+
export async function readFileTopics(path, harness, metrics) {
|
|
113
|
+
if (harness !== 'claude-code' && harness !== 'codex') return [];
|
|
114
|
+
return (await withSessionFile(path, async (reader) => {
|
|
115
|
+
let bytes = await reader.read(0, Math.min(reader.size, headBytes));
|
|
116
|
+
if (reader.size > headBytes) {
|
|
117
|
+
const end = bytes.lastIndexOf(10);
|
|
118
|
+
if (end >= 0) bytes = bytes.subarray(0, end);
|
|
119
|
+
}
|
|
120
|
+
return candidatesIn(bytes, harness, false);
|
|
121
|
+
}, metrics)).result;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
export async function readFileLatest(path, harness, metrics) {
|
|
125
|
+
// OpenClaw and orchestrator deliberately have no generic native projection.
|
|
126
|
+
if (!fileFormats.has(harness)) return [];
|
|
127
|
+
return (await withSessionFile(path, async (reader) => {
|
|
128
|
+
const take = async (limit) => {
|
|
129
|
+
const from = Math.max(0, reader.size - limit);
|
|
130
|
+
let bytes = await reader.read(from, reader.size - from);
|
|
131
|
+
if (from > 0) {
|
|
132
|
+
const end = bytes.indexOf(10);
|
|
133
|
+
if (end < 0) return [];
|
|
134
|
+
bytes = bytes.subarray(end + 1);
|
|
135
|
+
}
|
|
136
|
+
return candidatesIn(bytes, harness, true);
|
|
137
|
+
};
|
|
138
|
+
const initial = await take(tailBytes);
|
|
139
|
+
return initial.length ? initial : take(expandedTailBytes);
|
|
140
|
+
}, metrics)).result;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function sqlText(value, bytes) {
|
|
144
|
+
if (typeof value !== 'string') return null;
|
|
145
|
+
try { return new TextDecoder('utf-8', { fatal: true, ignoreBOM: true }).decode(bytes); } catch { return null; }
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
async function storeLatest(locator, metrics) {
|
|
149
|
+
const { harness, storage, session_id: id } = locator;
|
|
150
|
+
return (await readStore(storage.path, (db) => {
|
|
151
|
+
const candidates = [];
|
|
152
|
+
if (harness === 'opencode') {
|
|
153
|
+
const rows = statement(db, `SELECT m.data AS message, p.data AS part, CAST(m.data AS BLOB) AS message_bytes, CAST(p.data AS BLOB) AS part_bytes
|
|
154
|
+
FROM message m JOIN part p ON p.message_id=m.id WHERE m.session_id=?
|
|
155
|
+
ORDER BY m.time_created DESC, p.time_created DESC LIMIT 32`).all(storage.selector);
|
|
156
|
+
for (const row of rows) {
|
|
157
|
+
const message = jsonColumn(sqlText(row.message, row.message_bytes)), part = jsonColumn(sqlText(row.part, row.part_bytes));
|
|
158
|
+
if (conversationRoles.has(message?.role) && part?.type === 'text') push(candidates, message.role, part.text);
|
|
159
|
+
if (candidates.length >= maxCandidates) break;
|
|
160
|
+
}
|
|
161
|
+
} else {
|
|
162
|
+
const hermes = harness === 'hermes';
|
|
163
|
+
const sql = hermes
|
|
164
|
+
? `SELECT role, content, CAST(role AS BLOB) AS role_bytes, CAST(content AS BLOB) AS content_bytes FROM messages
|
|
165
|
+
WHERE session_id=? AND active=1 AND role IN ('user','assistant') AND content IS NOT NULL AND content!=''
|
|
166
|
+
ORDER BY timestamp DESC,id DESC LIMIT 32`
|
|
167
|
+
: `SELECT role, content_json AS content, CAST(role AS BLOB) AS role_bytes, CAST(content_json AS BLOB) AS content_bytes FROM messages
|
|
168
|
+
WHERE session_id=? ORDER BY created_timestamp DESC,id DESC LIMIT 16`;
|
|
169
|
+
for (const row of statement(db, sql).all(hermes ? id : storage.selector)) {
|
|
170
|
+
const role = sqlText(row.role, row.role_bytes), raw = sqlText(row.content, row.content_bytes);
|
|
171
|
+
if (!conversationRoles.has(role) || raw == null) continue;
|
|
172
|
+
push(candidates, role, hermes ? raw : jsonColumn(raw));
|
|
173
|
+
if (candidates.length >= maxCandidates) break;
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
return candidates;
|
|
177
|
+
}, metrics)).result;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
export function readLatestCandidates(locator, metrics) {
|
|
181
|
+
if (locator.harness === 'hermes' || (locator.storage.kind === 'sqlite' && ['opencode', 'goose'].includes(locator.harness))) return storeLatest(locator, metrics);
|
|
182
|
+
return locator.storage.kind === 'file' ? readFileLatest(locator.storage.path, locator.harness, metrics) : Promise.resolve([]);
|
|
183
|
+
}
|