@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,165 @@
|
|
|
1
|
+
// Persisted FileReadDiffed views use diffy 0.5.0's default line algorithm:
|
|
2
|
+
// bidirectional Myers, deletion-then-insertion compaction and three context
|
|
3
|
+
// lines. Keep its choices here rather than changing old logs' token figures.
|
|
4
|
+
// Derived from diffy (MIT/Apache-2.0); see LICENSE-diffy-MIT.
|
|
5
|
+
const lines = value => value.match(/[^\n]*\n|[^\n]+$/gu) ?? [];
|
|
6
|
+
const range = (type, old, fresh, length) => ({ type, old, fresh, length });
|
|
7
|
+
function common(a, b, aStart, bStart, aLength, bLength, suffix = false) {
|
|
8
|
+
let count = 0;
|
|
9
|
+
while (count < Math.min(aLength, bLength)
|
|
10
|
+
&& a[aStart + (suffix ? aLength - count - 1 : count)] === b[bStart + (suffix ? bLength - count - 1 : count)]) count++;
|
|
11
|
+
return count;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function diff(old, fresh) {
|
|
15
|
+
const forward = new Map(), backward = new Map(), result = [], get = (map, key) => map.get(key) ?? 0;
|
|
16
|
+
function middle(a, b, n, m) {
|
|
17
|
+
const delta = n - m, odd = Math.abs(delta) % 2 === 1;
|
|
18
|
+
forward.set(1, 0); backward.set(1, 0);
|
|
19
|
+
for (let d = 0; d < Math.ceil((n + m) / 2) + 1; d++) {
|
|
20
|
+
for (let k = d; k >= -d; k -= 2) {
|
|
21
|
+
let x = k === -d || k !== d && get(forward, k - 1) < get(forward, k + 1)
|
|
22
|
+
? get(forward, k + 1) : get(forward, k - 1) + 1;
|
|
23
|
+
let y = x - k; const xStart = x, yStart = y;
|
|
24
|
+
if (x <= n && y >= 0 && y <= m) {
|
|
25
|
+
const count = common(old, fresh, a + x, b + y, n - x, m - y); x += count; y += count;
|
|
26
|
+
}
|
|
27
|
+
forward.set(k, x);
|
|
28
|
+
if (odd && Math.abs(k - delta) <= d - 1 && x + get(backward, -(k - delta)) >= n) return [xStart, yStart];
|
|
29
|
+
}
|
|
30
|
+
for (let k = d; k >= -d; k -= 2) {
|
|
31
|
+
let x = k === -d || k !== d && get(backward, k - 1) < get(backward, k + 1)
|
|
32
|
+
? get(backward, k + 1) : get(backward, k - 1) + 1;
|
|
33
|
+
let y = x - k; const xStart = x, yStart = y;
|
|
34
|
+
if (x < n && y >= 0 && y < m) {
|
|
35
|
+
const count = common(old, fresh, a, b, n - x, m - y, true); x += count; y += count;
|
|
36
|
+
}
|
|
37
|
+
backward.set(k, x);
|
|
38
|
+
if (!odd && Math.abs(k - delta) <= d && x + get(forward, -(k - delta)) >= n) return [n - x, m - y];
|
|
39
|
+
// The other end of the backward snake is not needed: conquer splits
|
|
40
|
+
// at its beginning and finds its equality in the recursive calls.
|
|
41
|
+
void xStart; void yStart;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
throw new Error('unable to find a middle diff snake');
|
|
45
|
+
}
|
|
46
|
+
// Explicit continuation stack preserves conquer's prefix/left/right/suffix
|
|
47
|
+
// order without making a large transcript consume the JavaScript call stack.
|
|
48
|
+
const stack = [{ a: 0, b: 0, n: old.length, m: fresh.length }];
|
|
49
|
+
while (stack.length) {
|
|
50
|
+
const item = stack.pop();
|
|
51
|
+
if (item.type) { result.push(item); continue; }
|
|
52
|
+
let { a, b, n, m } = item;
|
|
53
|
+
const prefix = common(old, fresh, a, b, n, m);
|
|
54
|
+
if (prefix) result.push(range('equal', a, b, prefix));
|
|
55
|
+
a += prefix; b += prefix; n -= prefix; m -= prefix;
|
|
56
|
+
const suffix = common(old, fresh, a, b, n, m, true);
|
|
57
|
+
if (suffix) stack.push(range('equal', a + n - suffix, b + m - suffix, suffix));
|
|
58
|
+
n -= suffix; m -= suffix;
|
|
59
|
+
if (!n && !m) continue;
|
|
60
|
+
if (!n) result.push(range('insert', a, b, m));
|
|
61
|
+
else if (!m) result.push(range('delete', a, b, n));
|
|
62
|
+
else {
|
|
63
|
+
const [x, y] = middle(a, b, n, m);
|
|
64
|
+
stack.push({ a: a + x, b: b + y, n: n - x, m: m - y });
|
|
65
|
+
stack.push({ a, b, n: x, m: y });
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
return result;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function compact(ranges, old, fresh) {
|
|
72
|
+
const growUp = (r, count) => { r.old -= count; r.fresh -= count; r.length += count; };
|
|
73
|
+
const growDown = (r, count) => { r.length += count; };
|
|
74
|
+
const shift = (r, count) => { if (r.type !== 'insert') r.old += count; if (r.type !== 'delete') r.fresh += count; };
|
|
75
|
+
function up(pointer) {
|
|
76
|
+
while (pointer > 0) {
|
|
77
|
+
const current = ranges[pointer], previous = ranges[pointer - 1];
|
|
78
|
+
if (previous.type === 'equal') {
|
|
79
|
+
const insert = current.type === 'insert';
|
|
80
|
+
const source = insert ? fresh : old, start = insert ? current.fresh : current.old;
|
|
81
|
+
const other = insert ? old : fresh, otherStart = insert ? previous.old : previous.fresh;
|
|
82
|
+
const count = common(source, other, start, otherStart, current.length, previous.length, true);
|
|
83
|
+
if (count) {
|
|
84
|
+
if (ranges[pointer + 1]?.type === 'equal') growUp(ranges[pointer + 1], count);
|
|
85
|
+
else ranges.splice(pointer + 1, 0, range('equal',
|
|
86
|
+
insert ? previous.old + previous.length - count : current.old + current.length - count,
|
|
87
|
+
insert ? current.fresh + current.length - count : previous.fresh + previous.length - count, count));
|
|
88
|
+
shift(current, -count); previous.length -= count;
|
|
89
|
+
if (!previous.length) { ranges.splice(pointer - 1, 1); pointer--; }
|
|
90
|
+
} else if (!previous.length) { ranges.splice(pointer - 1, 1); pointer--; }
|
|
91
|
+
else break;
|
|
92
|
+
} else if (previous.type !== current.type) {
|
|
93
|
+
ranges[pointer - 1] = current; ranges[pointer] = previous; pointer--;
|
|
94
|
+
} else { growDown(previous, current.length); ranges.splice(pointer, 1); pointer--; }
|
|
95
|
+
}
|
|
96
|
+
return pointer;
|
|
97
|
+
}
|
|
98
|
+
function down(pointer) {
|
|
99
|
+
while (pointer + 1 < ranges.length) {
|
|
100
|
+
const current = ranges[pointer], next = ranges[pointer + 1];
|
|
101
|
+
if (next.type === 'equal') {
|
|
102
|
+
const insert = current.type === 'insert';
|
|
103
|
+
const source = insert ? fresh : old, start = insert ? current.fresh : current.old;
|
|
104
|
+
const other = insert ? old : fresh, otherStart = insert ? next.old : next.fresh;
|
|
105
|
+
const count = common(source, other, start, otherStart, current.length, next.length);
|
|
106
|
+
if (count) {
|
|
107
|
+
if (ranges[pointer - 1]?.type === 'equal') growDown(ranges[pointer - 1], count);
|
|
108
|
+
else {
|
|
109
|
+
ranges.splice(pointer, 0, range('equal', insert ? next.old : current.old, insert ? current.fresh : next.fresh, count)); pointer++;
|
|
110
|
+
}
|
|
111
|
+
shift(current, count); shift(next, count); next.length -= count;
|
|
112
|
+
if (!next.length) ranges.splice(pointer + 1, 1);
|
|
113
|
+
} else if (!next.length) ranges.splice(pointer + 1, 1);
|
|
114
|
+
else break;
|
|
115
|
+
} else if (next.type !== current.type) {
|
|
116
|
+
ranges[pointer] = next; ranges[pointer + 1] = current; pointer++;
|
|
117
|
+
} else { growDown(current, next.length); ranges.splice(pointer + 1, 1); }
|
|
118
|
+
}
|
|
119
|
+
return pointer;
|
|
120
|
+
}
|
|
121
|
+
for (const type of ['delete', 'insert']) for (let pointer = 0; pointer < ranges.length; pointer++) {
|
|
122
|
+
if (ranges[pointer].type === type) pointer = down(up(pointer));
|
|
123
|
+
}
|
|
124
|
+
return ranges;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/** Byte spelling matches the native default patch, including blank context
|
|
128
|
+
* suppression, one-line ranges and the missing-newline marker. */
|
|
129
|
+
export function reductionUnifiedDiff(original, modified) {
|
|
130
|
+
const old = lines(original), fresh = lines(modified), scripts = [];
|
|
131
|
+
let a = 0, b = 0, script;
|
|
132
|
+
for (const item of compact(diff(old, fresh), old, fresh)) {
|
|
133
|
+
if (item.type === 'equal') {
|
|
134
|
+
a += item.length; b += item.length;
|
|
135
|
+
if (script) { scripts.push(script); script = null; }
|
|
136
|
+
} else {
|
|
137
|
+
script ??= { oldStart: a, oldEnd: a, newStart: b, newEnd: b };
|
|
138
|
+
if (item.type === 'delete') { a += item.length; script.oldEnd += item.length; }
|
|
139
|
+
else { b += item.length; script.newEnd += item.length; }
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
if (script) scripts.push(script);
|
|
143
|
+
let output = '--- original\n+++ modified\n';
|
|
144
|
+
const end = s => { const count = Math.min(3, Math.max(0, old.length - s.oldEnd), Math.max(0, fresh.length - s.newEnd)); return [s.oldEnd + count, s.newEnd + count]; };
|
|
145
|
+
const hunkRange = (start, length) => length === 1 ? String(start + 1) : `${start + (length > 0 ? 1 : 0)},${length}`;
|
|
146
|
+
const lineText = (sign, value) => (sign === ' ' && value === '\n' ? value : sign + value)
|
|
147
|
+
+ (value.endsWith('\n') ? '' : '\n\\n');
|
|
148
|
+
for (let index = 0; index < scripts.length; index++) {
|
|
149
|
+
let current = scripts[index];
|
|
150
|
+
const startA = Math.max(0, current.oldStart - 3), startB = Math.max(0, current.newStart - 3);
|
|
151
|
+
let [endA, endB] = end(current), content = '';
|
|
152
|
+
for (const value of fresh.slice(startB, current.newStart)) content += lineText(' ', value);
|
|
153
|
+
while (true) {
|
|
154
|
+
for (const value of old.slice(current.oldStart, current.oldEnd)) content += lineText('-', value);
|
|
155
|
+
for (const value of fresh.slice(current.newStart, current.newEnd)) content += lineText('+', value);
|
|
156
|
+
const next = scripts[index + 1];
|
|
157
|
+
if (!next || Math.max(0, Math.min(next.oldStart, old.length - 1) - 3) >= endA) break;
|
|
158
|
+
for (const value of fresh.slice(current.newEnd, next.newStart)) content += lineText(' ', value);
|
|
159
|
+
current = next; index++; [endA, endB] = end(current);
|
|
160
|
+
}
|
|
161
|
+
for (const value of fresh.slice(current.newEnd, endB)) content += lineText(' ', value);
|
|
162
|
+
output += `@@ -${hunkRange(startA, endA - startA)} +${hunkRange(startB, endB - startB)} @@\n${content}`;
|
|
163
|
+
}
|
|
164
|
+
return output;
|
|
165
|
+
}
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
// Persisted reduction records, not a compactor: this reader never creates a
|
|
2
|
+
// reduction, edits a source session or consults a model/tool/filesystem.
|
|
3
|
+
import { blake3 } from '@noble/hashes/blake3.js';
|
|
4
|
+
import { messageWire, parsedArguments, ROLES } from './message.mjs';
|
|
5
|
+
import { nativeDuplicateObjectKeys, nativeInteger, parseNativeJson, stringifyNativeJson } from './native-json.mjs';
|
|
6
|
+
|
|
7
|
+
export const REDUCTION_SENTINEL = '[sc-reduced';
|
|
8
|
+
export const REDUCTION_METADATA_KEY = 'sc.reduction';
|
|
9
|
+
export const REDUCTION_KINDS = Object.freeze(['ToolOutputTruncated', 'FileReadElided', 'ImageRedacted',
|
|
10
|
+
'TurnsCleared', 'ToolInputElided', 'OutputNormalized', 'FileReadDiffed', 'DuplicateOutput', 'Superseded']);
|
|
11
|
+
const stubKinds = new Set(['tool-output', 'file-read', 'image', 'turns-cleared', 'tool-input',
|
|
12
|
+
'output-normalized', 'file-read-diffed', 'duplicate', 'superseded']);
|
|
13
|
+
const fail = text => { throw new TypeError(`invalid reduction log: ${text}`); };
|
|
14
|
+
const isObject = value => value != null && typeof value === 'object' && !Array.isArray(value);
|
|
15
|
+
function object(value, fields, context) {
|
|
16
|
+
if (!isObject(value)) fail(`${context} must be an object`);
|
|
17
|
+
for (const key of nativeDuplicateObjectKeys(value)) if (fields.includes(key)) fail(`${context} has duplicate field ${key}`);
|
|
18
|
+
}
|
|
19
|
+
function text(value, key) { if (typeof value[key] !== 'string') fail(`${key} must be a string`); }
|
|
20
|
+
function optionalText(value, key) { if (value[key] != null) text(value, key); }
|
|
21
|
+
function unsigned(value, key, maximum = (1n << 64n) - 1n) {
|
|
22
|
+
const number = nativeInteger(value, String(key));
|
|
23
|
+
if (number == null || number < 0n || number > maximum) fail(`${key} must be an unsigned integer`);
|
|
24
|
+
return number;
|
|
25
|
+
}
|
|
26
|
+
function array(value, key) { if (!Array.isArray(value[key])) fail(`${key} must be an array`); return value[key]; }
|
|
27
|
+
function address(value) {
|
|
28
|
+
object(value, ['index', 'role'], 'message address'); unsigned(value, 'index');
|
|
29
|
+
if (!ROLES.includes(value.role)) fail('message address role must be system, user, assistant or tool');
|
|
30
|
+
}
|
|
31
|
+
function pointer(value) {
|
|
32
|
+
object(value, ['addr', 'span', 'content_hash'], 'sidecar pointer'); address(value.addr); text(value, 'content_hash');
|
|
33
|
+
if (value.span != null) {
|
|
34
|
+
if (!Array.isArray(value.span) || value.span.length !== 2) fail('pointer span must be a pair');
|
|
35
|
+
unsigned(value.span, 0); unsigned(value.span, 1);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
function readEntry(value) {
|
|
39
|
+
object(value, ['path', 'addr', 'content_hash', 'mtime'], 'read-log entry');
|
|
40
|
+
text(value, 'path'); address(value.addr); text(value, 'content_hash');
|
|
41
|
+
if (value.mtime != null && nativeInteger(value, 'mtime', { signed: true }) == null) fail('mtime must be an i64');
|
|
42
|
+
}
|
|
43
|
+
function summary(value) {
|
|
44
|
+
if (value == null) return;
|
|
45
|
+
object(value, ['model_id', 'prompt_version', 'summary_hash'], 'span summary');
|
|
46
|
+
for (const key of ['model_id', 'prompt_version', 'summary_hash']) text(value, key);
|
|
47
|
+
}
|
|
48
|
+
function kind(value) {
|
|
49
|
+
if (!isObject(value) || Object.keys(value).length !== 1 || nativeDuplicateObjectKeys(value).length) fail('kind must contain exactly one variant');
|
|
50
|
+
const name = Object.keys(value)[0], data = value[name];
|
|
51
|
+
const fields = {
|
|
52
|
+
ToolOutputTruncated: ['original_bytes', 'kept_bytes'],
|
|
53
|
+
FileReadElided: ['path', 'read_log'], ImageRedacted: ['part_index'], TurnsCleared: ['first', 'last', 'summary'],
|
|
54
|
+
ToolInputElided: ['original_bytes', 'path', 'content_hash', 'call_id', 'field'],
|
|
55
|
+
OutputNormalized: ['original_bytes', 'normalized_bytes'],
|
|
56
|
+
FileReadDiffed: ['path', 'base', 'base_hash', 'new_hash', 'original_bytes', 'diff_bytes'],
|
|
57
|
+
DuplicateOutput: ['canonical', 'original_bytes'], Superseded: ['by', 'original_bytes'],
|
|
58
|
+
}[name];
|
|
59
|
+
if (fields == null) fail(`unknown kind ${name}`);
|
|
60
|
+
object(data, fields, name);
|
|
61
|
+
for (const key of fields) {
|
|
62
|
+
if (['original_bytes', 'kept_bytes', 'part_index', 'first', 'last', 'normalized_bytes', 'diff_bytes'].includes(key)) unsigned(data, key);
|
|
63
|
+
else if (['base', 'canonical', 'by'].includes(key)) address(data[key]);
|
|
64
|
+
else if (key === 'read_log') readEntry(data[key]);
|
|
65
|
+
else if (key === 'summary') summary(data[key]);
|
|
66
|
+
else if (key === 'path' && name === 'ToolInputElided') optionalText(data, key);
|
|
67
|
+
else text(data, key);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
function reduction(value) {
|
|
71
|
+
object(value, ['id', 'kind', 'ptr', 'placeholder'], 'reduction');
|
|
72
|
+
text(value, 'id'); kind(value.kind); pointer(value.ptr); text(value, 'placeholder');
|
|
73
|
+
}
|
|
74
|
+
const passCounts = ['candidate_count', 'candidate_original_bytes', 'applied_count', 'applied_original_bytes',
|
|
75
|
+
'suppressed_by_later_pass_count', 'suppressed_by_later_pass_bytes', 'standalone_saved_bytes', 'standalone_saved_tokens',
|
|
76
|
+
'marginal_saved_bytes', 'marginal_saved_tokens', 'suppressed_bytes', 'suppressed_tokens', 'retained_bytes', 'retained_tokens'];
|
|
77
|
+
const aggregateCounts = ['full_bytes', 'view_bytes', 'full_tokens', 'view_tokens', 'aggregate_saved_bytes',
|
|
78
|
+
'aggregate_saved_tokens', 'standalone_saved_bytes', 'standalone_saved_tokens', 'overlap_suppressed_bytes', 'overlap_suppressed_tokens'];
|
|
79
|
+
function attribution(value) {
|
|
80
|
+
if (value == null) return;
|
|
81
|
+
object(value, [...aggregateCounts, 'passes'], 'attribution');
|
|
82
|
+
for (const key of aggregateCounts) unsigned(value, key);
|
|
83
|
+
for (const pass of array(value, 'passes')) {
|
|
84
|
+
object(pass, ['kind', 'enabled', ...passCounts], 'pass attribution'); text(pass, 'kind');
|
|
85
|
+
if (typeof pass.enabled !== 'boolean') fail('pass enabled must be a boolean');
|
|
86
|
+
for (const key of passCounts) unsigned(pass, key);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export const emptyReductionLog = () => ({ reductions: [], expanded: [], read_log: [], attribution: null });
|
|
91
|
+
|
|
92
|
+
/** Direct typed serde reads reject duplicate known fields and float counts;
|
|
93
|
+
* optional history/attribution fields retain their backwards-compatible defaults. */
|
|
94
|
+
export function readReductionLog(textValue) {
|
|
95
|
+
const log = parseNativeJson(textValue);
|
|
96
|
+
object(log, ['reductions', 'expanded', 'read_log', 'attribution'], 'log');
|
|
97
|
+
array(log, 'reductions').forEach(reduction); array(log, 'read_log').forEach(readEntry);
|
|
98
|
+
if (Object.hasOwn(log, 'expanded')) array(log, 'expanded').forEach(reduction);
|
|
99
|
+
else log.expanded = [];
|
|
100
|
+
attribution(log.attribution);
|
|
101
|
+
return log;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export const reductionContentHash = value => Buffer.from(blake3(typeof value === 'string' ? Buffer.from(value) : value)).toString('hex');
|
|
105
|
+
export const messageWireText = message => stringifyNativeJson(messageWire(message));
|
|
106
|
+
export const estimateViewTokens = messages => messages.reduce((sum, message) => sum + BigInt(Math.ceil(Buffer.byteLength(messageWireText(message)) / 4)), 0n);
|
|
107
|
+
|
|
108
|
+
function verifyHash(ptr, actual) {
|
|
109
|
+
if (actual !== ptr.content_hash) throw new Error(`sidecar pointer hash mismatch: expected ${ptr.content_hash}, got ${actual}`);
|
|
110
|
+
}
|
|
111
|
+
const debugRole = role => role[0].toUpperCase() + role.slice(1);
|
|
112
|
+
function resolveMessage(ptr, messages) {
|
|
113
|
+
const index = nativeInteger(ptr.addr, 'index');
|
|
114
|
+
if (index >= BigInt(messages.length)) throw new Error(`invert: sidecar has no message at index ${index} (reduction pointer unresolvable)`);
|
|
115
|
+
const message = messages[Number(index)];
|
|
116
|
+
if (message.role !== ptr.addr.role) throw new Error(`invert: role mismatch at sidecar index ${index}: pointer expects ${debugRole(ptr.addr.role)}, sidecar has ${debugRole(message.role)}`);
|
|
117
|
+
return message;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/** Validate every applied and expanded pointer before a caller can export.
|
|
121
|
+
* Auxiliary diff/dedup provenance is informational, as in the native verifier. */
|
|
122
|
+
export function verifyReductionLog(log, messages) {
|
|
123
|
+
for (const record of [...log.reductions, ...log.expanded]) {
|
|
124
|
+
const name = Object.keys(record.kind)[0], data = record.kind[name], ptr = record.ptr;
|
|
125
|
+
try {
|
|
126
|
+
let original;
|
|
127
|
+
if (name === 'TurnsCleared') {
|
|
128
|
+
const first = nativeInteger(data, 'first'), last = nativeInteger(data, 'last');
|
|
129
|
+
if (first <= last && last >= BigInt(messages.length)) {
|
|
130
|
+
const missing = first >= BigInt(messages.length) ? first : BigInt(messages.length);
|
|
131
|
+
throw new Error(`invert: sidecar has no message at index ${missing} (turns-cleared range unresolvable)`);
|
|
132
|
+
}
|
|
133
|
+
const range = first > last ? [] : messages.slice(Number(first), Number(last) + 1);
|
|
134
|
+
if (range.length && range[0].role !== ptr.addr.role) throw new Error(`invert: role mismatch at sidecar index ${first}: pointer expects ${debugRole(ptr.addr.role)}, sidecar has ${debugRole(range[0].role)}`);
|
|
135
|
+
original = Buffer.concat(range.map(message => Buffer.from(messageWireText(message))));
|
|
136
|
+
} else {
|
|
137
|
+
const message = resolveMessage(ptr, messages), index = nativeInteger(ptr.addr, 'index');
|
|
138
|
+
if (name === 'ImageRedacted') {
|
|
139
|
+
const partIndex = nativeInteger(data, 'part_index'), parts = message.content_parts;
|
|
140
|
+
if (parts == null || partIndex >= BigInt(parts.length)) throw new Error(`invert: sidecar message at index ${index} has no content part ${partIndex}`);
|
|
141
|
+
original = stringifyNativeJson(parts[Number(partIndex)]);
|
|
142
|
+
} else if (name === 'ToolInputElided') {
|
|
143
|
+
const call = message.tool_calls?.find(call => call.id === data.call_id);
|
|
144
|
+
if (call == null) throw new Error(`invert: sidecar message at index ${index} has no tool_call with id ${data.call_id}`);
|
|
145
|
+
let argumentsValue;
|
|
146
|
+
try { argumentsValue = parsedArguments(call); }
|
|
147
|
+
catch (error) { throw new Error(`invert: tool_call ${data.call_id} arguments are not valid JSON: ${error.message}`); }
|
|
148
|
+
if (!isObject(argumentsValue) || typeof argumentsValue[data.field] !== 'string') throw new Error(`invert: tool_call ${data.call_id} has no string field \`${data.field}\``);
|
|
149
|
+
original = argumentsValue[data.field];
|
|
150
|
+
} else original = message.content ?? '';
|
|
151
|
+
}
|
|
152
|
+
verifyHash(ptr, reductionContentHash(original));
|
|
153
|
+
} catch (error) { throw new Error(`reduction ${record.id} unresolvable against the sidecar: ${error.message}`, { cause: error }); }
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
export function parseReductionStub(line) {
|
|
158
|
+
const match = /^\[sc-reduced ([^ ]+) (r[0-9]{4}-[0-9a-f]{4}): ([^\]\n]+)\]$/u.exec(line);
|
|
159
|
+
return match && match[0] === line && stubKinds.has(match[1]) ? { kind: match[1], id: match[2], summary: match[3] } : null;
|
|
160
|
+
}
|
|
161
|
+
const stringHasStub = text => {
|
|
162
|
+
const lines = text.split('\n');
|
|
163
|
+
return lines.some((line, index) => parseReductionStub(index < lines.length - 1 && line.endsWith('\r') ? line.slice(0, -1) : line));
|
|
164
|
+
};
|
|
165
|
+
function jsonHasStub(value) {
|
|
166
|
+
if (typeof value === 'string') return stringHasStub(value);
|
|
167
|
+
if (Array.isArray(value)) return value.some(jsonHasStub);
|
|
168
|
+
return isObject(value) && Object.values(value).some(jsonHasStub);
|
|
169
|
+
}
|
|
170
|
+
export function rejectReducedSidecar(session) {
|
|
171
|
+
const leaked = session.messages.some(message => stringHasStub(message.content ?? '') || jsonHasStub(message.content_parts)
|
|
172
|
+
|| message.tool_calls?.some(call => {
|
|
173
|
+
try { return jsonHasStub(parseNativeJson(call.function.arguments)); }
|
|
174
|
+
catch { return stringHasStub(call.function.arguments); }
|
|
175
|
+
}));
|
|
176
|
+
if (leaked) throw new Error(`export_session_spliced: refusing to export — the sidecar contains a grammar-valid reduction stub beginning with ${JSON.stringify(REDUCTION_SENTINEL)}; a reduced view leaked into an export path that must only read the full-fidelity sidecar`);
|
|
177
|
+
}
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
// Only reapply recorded reductions. Runtime candidate selection, freshness,
|
|
2
|
+
// compaction, escalation and policy remain with their separate owner.
|
|
3
|
+
import { cloneNativeJson, nativeInteger } from './native-json.mjs';
|
|
4
|
+
import { createMessage } from './message.mjs';
|
|
5
|
+
import { REDUCTION_METADATA_KEY } from './reduction-log.mjs';
|
|
6
|
+
import { reductionUnifiedDiff } from './reduction-diff.mjs';
|
|
7
|
+
|
|
8
|
+
/** Native terminal-noise normalizer: unrecognized/truncated escapes survive. */
|
|
9
|
+
export function normalizeReductionOutput(input) {
|
|
10
|
+
const rows = [[]]; let row = 0, column = 0;
|
|
11
|
+
const write = value => { for (const char of value) { while (rows[row].length < column) rows[row].push(' '); rows[row][column++] = char; } };
|
|
12
|
+
const parameter = (text, fallback) => {
|
|
13
|
+
const digits = text.split(/[;:]/u, 1)[0].replace(/[^0-9]/gu, '');
|
|
14
|
+
const value = digits ? BigInt(digits) : null;
|
|
15
|
+
return value == null || value > 0xffffffffn ? fallback : Number(value);
|
|
16
|
+
};
|
|
17
|
+
for (let at = 0; at < input.length;) {
|
|
18
|
+
if (input[at] === '\r') { column = 0; at++; }
|
|
19
|
+
else if (input[at] === '\n') { row++; rows[row] ??= []; column = 0; at++; }
|
|
20
|
+
else if (input[at] === '\x1b' && input[at + 1] === '[') {
|
|
21
|
+
let end = at + 2;
|
|
22
|
+
while (end < input.length && input.charCodeAt(end) >= 0x30 && input.charCodeAt(end) <= 0x3f) end++;
|
|
23
|
+
if (end === input.length || input.charCodeAt(end) < 0x40 || input.charCodeAt(end) > 0x7e) { write(input.slice(at)); break; }
|
|
24
|
+
const params = input.slice(at + 2, end), final = input[end];
|
|
25
|
+
if (final === 'm' || ['h', 'l'].includes(final) && params.startsWith('?')) { /* recognized no rendered effect */ }
|
|
26
|
+
else if (final === 'K') {
|
|
27
|
+
const mode = parameter(params, 0);
|
|
28
|
+
if (mode === 0) rows[row].length = Math.min(column, rows[row].length);
|
|
29
|
+
else if (mode === 1) for (let cell = 0; cell < Math.min(column + 1, rows[row].length); cell++) rows[row][cell] = ' ';
|
|
30
|
+
else rows[row] = [];
|
|
31
|
+
} else if (final === 'A') row = Math.max(0, row - Math.max(1, parameter(params, 1)));
|
|
32
|
+
else if (final === 'B') row = Math.min(row + Math.max(1, parameter(params, 1)), rows.length - 1);
|
|
33
|
+
else if (final === 'G') column = Math.max(0, parameter(params, 1) - 1);
|
|
34
|
+
else write(input.slice(at, end + 1));
|
|
35
|
+
at = end + 1;
|
|
36
|
+
} else if (input[at] === '\x1b' && input[at + 1] === ']') {
|
|
37
|
+
let end = at + 2;
|
|
38
|
+
while (end < input.length && input[end] !== '\x07' && !(input[end] === '\x1b' && input[end + 1] === '\\')) end++;
|
|
39
|
+
end = Math.min(input.length, end + (input[end] === '\x07' ? 1 : 2)); write(input.slice(at, end)); at = end;
|
|
40
|
+
} else {
|
|
41
|
+
const start = at++;
|
|
42
|
+
while (at < input.length && !['\r', '\n', '\x1b'].includes(input[at])) at++;
|
|
43
|
+
write(input.slice(start, at));
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
return rows.map(value => value.join('')).join('\n');
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Locate the original string token at the top level. Replacing its token
|
|
50
|
+
// preserves whitespace, key order, other arguments and number spellings.
|
|
51
|
+
function replaceArgument(text, field, replacement) {
|
|
52
|
+
const whitespace = () => { while (/[\x20\t\r\n]/u.test(text[at] ?? '') && at < text.length) at++; };
|
|
53
|
+
let at = 0;
|
|
54
|
+
function stringToken() {
|
|
55
|
+
const start = at;
|
|
56
|
+
if (text[at++] !== '"') return null;
|
|
57
|
+
while (at < text.length) {
|
|
58
|
+
if (text[at] === '\\') at += 2;
|
|
59
|
+
else if (text[at++] === '"') return [start, at];
|
|
60
|
+
}
|
|
61
|
+
return null;
|
|
62
|
+
}
|
|
63
|
+
function skipValue() {
|
|
64
|
+
if (text[at] === '"') return stringToken() != null;
|
|
65
|
+
if (['[', '{'].includes(text[at])) {
|
|
66
|
+
const close = text[at++] === '[' ? ']' : '}';
|
|
67
|
+
while (at < text.length) {
|
|
68
|
+
if (text[at] === close) { at++; return true; }
|
|
69
|
+
if (text[at] === '"') { if (!stringToken()) return false; }
|
|
70
|
+
else if (['[', '{'].includes(text[at])) { if (!skipValue()) return false; }
|
|
71
|
+
else at++;
|
|
72
|
+
}
|
|
73
|
+
return false;
|
|
74
|
+
}
|
|
75
|
+
const start = at;
|
|
76
|
+
while (at < text.length && !/[\x20\t\r\n,}\]]/u.test(text[at])) at++;
|
|
77
|
+
return at !== start;
|
|
78
|
+
}
|
|
79
|
+
whitespace(); if (text[at++] !== '{') return null;
|
|
80
|
+
while (at < text.length) {
|
|
81
|
+
whitespace(); if (text[at] === '}') return null;
|
|
82
|
+
const key = stringToken(); if (!key) return null;
|
|
83
|
+
// The native byte scanner compares the literal key body, not a decoded
|
|
84
|
+
// spelling (e.g. an escaped key can verify yet remain unreplaced).
|
|
85
|
+
const name = text.slice(key[0] + 1, key[1] - 1);
|
|
86
|
+
whitespace(); if (text[at++] !== ':') return null;
|
|
87
|
+
whitespace();
|
|
88
|
+
if (name === field) {
|
|
89
|
+
const token = stringToken(); if (!token) return null;
|
|
90
|
+
return text.slice(0, token[0]) + JSON.stringify(replacement) + text.slice(token[1]);
|
|
91
|
+
}
|
|
92
|
+
if (!skipValue()) return null;
|
|
93
|
+
whitespace(); if (text[at++] !== ',') return null;
|
|
94
|
+
}
|
|
95
|
+
return null;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/** Mirrors project_messages with the CLI's reapply-only policy. Expanded
|
|
99
|
+
* records are verified elsewhere but deliberately remain unreduced here. */
|
|
100
|
+
export function reapplyReductionLog(messages, log) {
|
|
101
|
+
const view = cloneNativeJson(messages), clears = [];
|
|
102
|
+
for (const record of log.reductions) {
|
|
103
|
+
const kind = Object.keys(record.kind)[0], data = record.kind[kind];
|
|
104
|
+
if (kind === 'TurnsCleared') { clears.push(record); continue; }
|
|
105
|
+
const index = nativeInteger(record.ptr.addr, 'index');
|
|
106
|
+
if (index >= BigInt(view.length)) continue;
|
|
107
|
+
const message = view[Number(index)];
|
|
108
|
+
if (kind === 'ToolOutputTruncated' && message.content != null) {
|
|
109
|
+
const original = Buffer.from(message.content), kept = record.ptr.span == null ? 0n : nativeInteger(record.ptr.span, '0');
|
|
110
|
+
const boundary = Number(kept > BigInt(original.length) ? BigInt(original.length) : kept);
|
|
111
|
+
// Native slicing requires a UTF-8 character boundary; refuse a corrupt
|
|
112
|
+
// kept span instead of silently introducing a replacement character.
|
|
113
|
+
const prefix = new TextDecoder('utf-8', { fatal: true, ignoreBOM: true }).decode(original.subarray(0, boundary));
|
|
114
|
+
message.content = prefix + '\n\n' + record.placeholder;
|
|
115
|
+
} else if (kind === 'OutputNormalized' && message.content != null) message.content = normalizeReductionOutput(message.content) + '\n\n' + record.placeholder;
|
|
116
|
+
else if (['FileReadElided', 'DuplicateOutput', 'Superseded'].includes(kind)) message.content = record.placeholder;
|
|
117
|
+
else if (kind === 'ImageRedacted') {
|
|
118
|
+
const index = nativeInteger(data, 'part_index');
|
|
119
|
+
if (message.content_parts != null && index < BigInt(message.content_parts.length)) message.content_parts[Number(index)] = { type: 'text', text: record.placeholder };
|
|
120
|
+
} else if (kind === 'FileReadDiffed') {
|
|
121
|
+
const base = nativeInteger(data.base, 'index');
|
|
122
|
+
const old = base >= BigInt(messages.length) ? null : messages[Number(base)].content, fresh = messages[Number(index)].content;
|
|
123
|
+
if (old == null || fresh == null) continue;
|
|
124
|
+
message.content = record.placeholder + '\n' + reductionUnifiedDiff(old, fresh);
|
|
125
|
+
} else if (kind === 'ToolInputElided') {
|
|
126
|
+
const call = message.tool_calls?.find(call => call.id === data.call_id);
|
|
127
|
+
if (call == null) continue;
|
|
128
|
+
const spliced = replaceArgument(call.function.arguments, data.field, record.placeholder);
|
|
129
|
+
if (spliced == null) continue;
|
|
130
|
+
call.function.arguments = spliced;
|
|
131
|
+
}
|
|
132
|
+
message.metadata[REDUCTION_METADATA_KEY] = record.id;
|
|
133
|
+
}
|
|
134
|
+
clears.sort((a, b) => {
|
|
135
|
+
const left = nativeInteger(a.kind.TurnsCleared, 'first'), right = nativeInteger(b.kind.TurnsCleared, 'first');
|
|
136
|
+
return left > right ? -1 : left < right ? 1 : 0;
|
|
137
|
+
});
|
|
138
|
+
for (const record of clears) {
|
|
139
|
+
const first = nativeInteger(record.kind.TurnsCleared, 'first'), last = nativeInteger(record.kind.TurnsCleared, 'last');
|
|
140
|
+
if (first > last || last >= BigInt(view.length)) continue;
|
|
141
|
+
const placeholder = createMessage('system', record.placeholder); placeholder.metadata[REDUCTION_METADATA_KEY] = record.id;
|
|
142
|
+
view.splice(Number(first), Number(last - first + 1n), placeholder);
|
|
143
|
+
}
|
|
144
|
+
return view;
|
|
145
|
+
}
|
package/retained-projection.mjs
CHANGED
|
@@ -120,6 +120,15 @@ export class RetainedMetadataProjection {
|
|
|
120
120
|
}
|
|
121
121
|
}
|
|
122
122
|
|
|
123
|
+
/** The sessions one part holds rows for now: what a change to that part can have moved, without a pass over every
|
|
124
|
+
* row a window holds. A part that holds no row any longer re-projected its sessions (`updatePart`), so they read as
|
|
125
|
+
* changed rows, not through this. */
|
|
126
|
+
keysFor(source, part) {
|
|
127
|
+
const keys = new Set();
|
|
128
|
+
for (const origin of this.parts.get(partKey(source, part)) ?? []) keys.add(descriptorKey(this.origins.get(origin).descriptor));
|
|
129
|
+
return keys;
|
|
130
|
+
}
|
|
131
|
+
|
|
123
132
|
partsFor(key) {
|
|
124
133
|
return new Set([...(this.candidates.get(key) ?? [])].map((origin) => {
|
|
125
134
|
const row = this.origins.get(origin); return partKey(row.source, row.part);
|
package/routes-read.mjs
ADDED
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
// Recorded routing inventory after the owning codec's complete home read.
|
|
2
|
+
// This projects precedence; it neither resolves traffic nor changes a route.
|
|
3
|
+
import { isAbsolute } from 'node:path';
|
|
4
|
+
import { resolveHarnessHomes } from './homes.mjs';
|
|
5
|
+
import { profilePathJoin as join, profilePathParent as parent } from './profile-paths.mjs';
|
|
6
|
+
import { readRouteFolderHome } from './hermes-read-home.mjs';
|
|
7
|
+
import { readOpenclawRouteHome } from './openclaw-profile-home.mjs';
|
|
8
|
+
import { nativeDuplicateObjectKeys, nativeInteger, nativeNumberKind, parseNativeJson, stringifyNativeJsonMember } from './native-json.mjs';
|
|
9
|
+
|
|
10
|
+
export const ROUTE_HARNESSES = Object.freeze(['hermes', 'openclaw', 'orchestrator']);
|
|
11
|
+
const object = value => value != null && typeof value === 'object' && !Array.isArray(value);
|
|
12
|
+
const compare = (a, b) => Buffer.compare(Buffer.from(a), Buffer.from(b));
|
|
13
|
+
const refuse = (message, rpcCode = -32602) => { throw Object.assign(new Error(message), { rpcCode }); };
|
|
14
|
+
const filesystem = (path, cwd) => !path.length || isAbsolute(path) || process.platform === 'win32' && /^[A-Za-z]:/u.test(path) ? path : join(cwd, path);
|
|
15
|
+
function struct(value, fields, name) {
|
|
16
|
+
if (Array.isArray(value)) {
|
|
17
|
+
if (value.length > fields.length) refuse(`${name} has too many fields`);
|
|
18
|
+
return parseNativeJson(`{${value.map((_, at) => `${JSON.stringify(fields[at])}:${stringifyNativeJsonMember(value, String(at))}`).join(',')}}`);
|
|
19
|
+
}
|
|
20
|
+
if (!object(value)) refuse(`${name} must be a struct`);
|
|
21
|
+
if (nativeDuplicateObjectKeys(value).some(key => fields.includes(key))) refuse(`${name} has a duplicate field`);
|
|
22
|
+
return value;
|
|
23
|
+
}
|
|
24
|
+
function query(params, context) {
|
|
25
|
+
const selected = struct(params, ['harness', 'profile', 'homes'], 'routes params');
|
|
26
|
+
for (const key of ['harness', 'profile']) if (selected[key] != null && typeof selected[key] !== 'string') refuse(`${key} must be a string`);
|
|
27
|
+
const homes = resolveHarnessHomes(context.env);
|
|
28
|
+
if (Object.hasOwn(selected, 'homes')) {
|
|
29
|
+
const supplied = struct(selected.homes, Object.keys(homes), 'homes');
|
|
30
|
+
for (const key of Object.keys(homes)) if (Object.hasOwn(supplied, key)) {
|
|
31
|
+
if (typeof supplied[key] !== 'string') refuse(`homes.${key} must be a string`);
|
|
32
|
+
homes[key] = supplied[key];
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
if (selected.harness != null && !ROUTE_HARNESSES.includes(selected.harness)) refuse(`\`${selected.harness}\` has no routing concept; \`routes.list\` is supported for: ${ROUTE_HARNESSES.join(', ')}`, -32020);
|
|
36
|
+
return { harness: selected.harness ?? null, profile: selected.profile ?? null, homes };
|
|
37
|
+
}
|
|
38
|
+
function text(row, key, hermes = false) {
|
|
39
|
+
const value = row?.[key];
|
|
40
|
+
if (typeof value === 'string') return value;
|
|
41
|
+
if (typeof value !== 'number') return null;
|
|
42
|
+
// The folder codec trims integral REALs with its saturating i64 cast;
|
|
43
|
+
// OpenClaw keeps Number::to_string, including its float spelling.
|
|
44
|
+
if (hermes && nativeNumberKind(row, key) === 'float' && Number.isInteger(value) && Math.abs(value) < 1e21) {
|
|
45
|
+
const low = -(1n << 63n), high = (1n << 63n) - 1n;
|
|
46
|
+
return String(value <= Number(low) ? low : value >= Number(high) ? high : BigInt(value));
|
|
47
|
+
}
|
|
48
|
+
return stringifyNativeJsonMember(row, key);
|
|
49
|
+
}
|
|
50
|
+
const field = (match, key, value) => { if (value != null) match[key] = value; };
|
|
51
|
+
function folderRows(harness, supplied, cwd) {
|
|
52
|
+
const home = harness === 'hermes' ? parent(supplied) ?? '.' : supplied;
|
|
53
|
+
let profiles; try { profiles = readRouteFolderHome(filesystem(home, cwd), harness); } catch { return []; }
|
|
54
|
+
const rows = [];
|
|
55
|
+
for (const name of [...profiles.keys()].sort((a, b) => Number(a !== 'default') - Number(b !== 'default') || compare(a, b))) {
|
|
56
|
+
const source = join(name === 'default' ? home : join(join(home, 'profiles'), name), 'config.yaml');
|
|
57
|
+
for (const route of profiles.get(name)) {
|
|
58
|
+
const match = {};
|
|
59
|
+
field(match, 'platform', route.platform || null);
|
|
60
|
+
field(match, 'guild', text(route, 'guild_id', true));
|
|
61
|
+
field(match, 'chat_id', text(route, 'chat_id', true));
|
|
62
|
+
field(match, 'thread_id', text(route, 'thread_id', true));
|
|
63
|
+
const specificity = Number(match.thread_id != null) * 8 + Number(match.chat_id != null) * 4 + Number(match.guild != null) * 2;
|
|
64
|
+
rows.push({ harness, target: route.agent ?? route.profile, match, specificity, default: false, source });
|
|
65
|
+
}
|
|
66
|
+
if (name === 'default') rows.push({ harness, target: 'default', match: {}, specificity: 0, default: true, source });
|
|
67
|
+
}
|
|
68
|
+
return rows;
|
|
69
|
+
}
|
|
70
|
+
function openclawRows(home, cwd) {
|
|
71
|
+
let loaded; try { loaded = readOpenclawRouteHome(filesystem(home, cwd)); } catch { return []; }
|
|
72
|
+
const source = join(home, 'openclaw.json');
|
|
73
|
+
const bindings = loaded.bindings.flatMap((binding, at) => object(binding) && typeof binding.agentId === 'string'
|
|
74
|
+
? [{ binding, index: Object.hasOwn(binding, 'index') ? nativeInteger(binding, 'index') : BigInt(at) }] : []);
|
|
75
|
+
// Residue's explicit index replaces the codec's original binding index.
|
|
76
|
+
// None sorts first, and the sort is stable for equal keys.
|
|
77
|
+
bindings.sort((a, b) => a.index === b.index ? 0 : a.index == null ? -1 : b.index == null ? 1 : a.index < b.index ? -1 : 1);
|
|
78
|
+
const rows = bindings.map(({ binding }) => {
|
|
79
|
+
const raw = object(binding.match) ? binding.match : {}, peer = object(raw.peer) ? raw.peer : {}, match = {};
|
|
80
|
+
field(match, 'platform', typeof raw.channel === 'string' && raw.channel ? raw.channel : null);
|
|
81
|
+
field(match, 'account', text(raw, 'accountId'));
|
|
82
|
+
field(match, 'guild', text(raw, 'guildId'));
|
|
83
|
+
field(match, 'team', text(raw, 'teamId'));
|
|
84
|
+
field(match, 'chat_id', text(peer, 'id'));
|
|
85
|
+
field(match, 'peer_kind', typeof peer.kind === 'string' ? peer.kind : null);
|
|
86
|
+
const roles = Array.isArray(raw.roles) ? raw.roles.filter(value => typeof value === 'string') : [];
|
|
87
|
+
if (roles.length) match.roles = roles;
|
|
88
|
+
const specificity = match.chat_id === '*' ? 6 : match.chat_id != null ? match.peer_kind === 'parent' ? 7 : 8
|
|
89
|
+
: match.guild != null ? roles.length ? 5 : 4 : match.team != null ? 3 : match.account != null ? 2 : match.platform != null ? 1 : 0;
|
|
90
|
+
const agent = binding.agentId === loaded.defaultAgent ? 'default' : binding.agentId;
|
|
91
|
+
const target = !Object.hasOwn(binding, 'agent_id') ? binding.agentId : typeof binding.agent_id === 'string' ? binding.agent_id : loaded.profiles.get(agent)?.id ?? agent;
|
|
92
|
+
return { harness: 'openclaw', target, match, specificity, default: false, source };
|
|
93
|
+
});
|
|
94
|
+
rows.push({ harness: 'openclaw', target: loaded.defaultAgent, match: {}, specificity: 0, default: true, source });
|
|
95
|
+
return rows;
|
|
96
|
+
}
|
|
97
|
+
export function listRoutes(params, { env = process.env, cwd = process.cwd() } = {}) {
|
|
98
|
+
const selected = query(params, { env, cwd });
|
|
99
|
+
const routes = ROUTE_HARNESSES.flatMap(harness => selected.harness != null && selected.harness !== harness ? []
|
|
100
|
+
: harness === 'openclaw' ? openclawRows(selected.homes.openclaw, cwd) : folderRows(harness, selected.homes[harness], cwd));
|
|
101
|
+
return { schema: 'supercode.routes.v1', routes: routes.filter(row => selected.profile == null || row.target === selected.profile)
|
|
102
|
+
.sort((a, b) => compare(a.harness, b.harness) || b.specificity - a.specificity || compare(a.target, b.target)) };
|
|
103
|
+
}
|