@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
package/jobs-read.mjs
ADDED
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
// Observed job rows, shared by public reads and harness-owned mutation readback.
|
|
2
|
+
// This reader neither starts a scheduler nor writes.
|
|
3
|
+
import { existsSync, readFileSync, readdirSync, statSync, realpathSync } from 'node:fs';
|
|
4
|
+
import { isAbsolute } from 'node:path';
|
|
5
|
+
import { resolveHarnessHomes } from './homes.mjs';
|
|
6
|
+
import { hermesJobRecords, hermesJobValues } from './hermes-read-validation.mjs';
|
|
7
|
+
import { readOpenclawRunHome } from './openclaw-profile-home.mjs';
|
|
8
|
+
import { loadSessionPath } from './session-path.mjs';
|
|
9
|
+
import { readUncompressedJsonl } from './jsonl.mjs';
|
|
10
|
+
import { claudeRuntimeManifest } from './session-claude-runtime.mjs';
|
|
11
|
+
import { childPath, directoryEntries, isDirectory, jsonl } from './session-spend-files.mjs';
|
|
12
|
+
import { profilePathJoin as join, profilePathParent as parent } from './profile-paths.mjs';
|
|
13
|
+
import { descriptorOrder } from './query.mjs';
|
|
14
|
+
import { nativeDuplicateObjectKeys, nativeInteger, parseNativeJson, setNativeFloat, stringifyNativeJsonMember } from './native-json.mjs';
|
|
15
|
+
import { openclawJobs } from './openclaw-jobs-read.mjs';
|
|
16
|
+
|
|
17
|
+
export const JOB_HARNESSES = Object.freeze(['claude-code', 'hermes', 'openclaw', 'orchestrator']);
|
|
18
|
+
const scanLimit = 200;
|
|
19
|
+
const utf8 = new TextDecoder('utf-8', { fatal: true, ignoreBOM: true });
|
|
20
|
+
const object = value => value != null && typeof value === 'object' && !Array.isArray(value);
|
|
21
|
+
const string = value => typeof value === 'string' ? value : null;
|
|
22
|
+
const display = path => Buffer.isBuffer(path) ? path.toString('utf8') : path;
|
|
23
|
+
const compare = (a, b) => Buffer.compare(Buffer.from(a), Buffer.from(b));
|
|
24
|
+
const filesystem = (path, cwd) => !display(path).length || isAbsolute(display(path)) || process.platform === 'win32' && /^[A-Za-z]:/u.test(display(path)) ? path : childPath(cwd, path);
|
|
25
|
+
const refuse = (message, rpcCode = -32602) => { throw Object.assign(new Error(message), { rpcCode }); };
|
|
26
|
+
function struct(value, fields, name) {
|
|
27
|
+
if (Array.isArray(value)) {
|
|
28
|
+
if (value.length > fields.length) refuse(`${name} has too many fields`);
|
|
29
|
+
return parseNativeJson(`{${value.map((_, at) => `${JSON.stringify(fields[at])}:${stringifyNativeJsonMember(value, String(at))}`).join(',')}}`);
|
|
30
|
+
}
|
|
31
|
+
if (!object(value)) refuse(`${name} must be a struct`);
|
|
32
|
+
if (nativeDuplicateObjectKeys(value).some(key => fields.includes(key))) refuse(`${name} has a duplicate field`);
|
|
33
|
+
return value;
|
|
34
|
+
}
|
|
35
|
+
function query(params, context, get) {
|
|
36
|
+
const selected = struct(params, get ? ['harness', 'id', 'homes'] : ['harness', 'session', 'profile', 'homes'], 'jobs params');
|
|
37
|
+
for (const key of get ? ['harness', 'id'] : ['harness', 'session', 'profile']) if (get ? typeof selected[key] !== 'string' : selected[key] != null && typeof selected[key] !== 'string') refuse(`${key} must be a string`);
|
|
38
|
+
const homes = resolveHarnessHomes(context.env);
|
|
39
|
+
if (Object.hasOwn(selected, 'homes')) {
|
|
40
|
+
const supplied = struct(selected.homes, Object.keys(homes), 'homes');
|
|
41
|
+
for (const key of Object.keys(homes)) if (Object.hasOwn(supplied, key)) {
|
|
42
|
+
if (typeof supplied[key] !== 'string') refuse(`homes.${key} must be a string`);
|
|
43
|
+
homes[key] = supplied[key];
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
if (selected.harness != null && !JOB_HARNESSES.includes(selected.harness)) refuse(`\`${selected.harness}\` has no scheduled jobs; \`jobs.${get ? 'get' : 'list'}\` is supported for: ${JOB_HARNESSES.join(', ')}`, -32020);
|
|
47
|
+
return { ...selected, harness: selected.harness ?? null, session: get ? null : selected.session ?? null, profile: get ? null : selected.profile ?? null, homes };
|
|
48
|
+
}
|
|
49
|
+
const source = (harness, path, state, profile = null, detail = null) => ({ harness, path: display(path), state, profile, sessions_scanned: null, scan_limit: null, detail });
|
|
50
|
+
export function jobFloat(value, trim = false) {
|
|
51
|
+
if (trim && Math.abs(value % 1) < Number.EPSILON) {
|
|
52
|
+
const low = -(1n << 63n), high = (1n << 63n) - 1n;
|
|
53
|
+
return String(value <= Number(low) ? low : value >= Number(high) ? high : BigInt(Math.trunc(value)));
|
|
54
|
+
}
|
|
55
|
+
if (Object.is(value, -0)) return '-0';
|
|
56
|
+
const text = String(value), match = /^(-?)(\d+)(?:\.(\d+))?e([+-]?\d+)$/u.exec(text);
|
|
57
|
+
if (!match) return text;
|
|
58
|
+
const digits = match[2] + (match[3] ?? ''), at = match[2].length + Number(match[4]);
|
|
59
|
+
return match[1] + (at <= 0 ? `0.${'0'.repeat(-at)}${digits}` : at >= digits.length ? digits + '0'.repeat(at - digits.length) : `${digits.slice(0, at)}.${digits.slice(at)}`);
|
|
60
|
+
}
|
|
61
|
+
export function jobPayload(record) {
|
|
62
|
+
const payload = record?.payload, text = keys => keys.map(key => string(payload?.[key])).find(value => value != null);
|
|
63
|
+
const native = text(['kind', 'type']) ?? ['payloadKind', 'payload_kind'].map(key => string(record?.[key])).find(value => value != null);
|
|
64
|
+
const names = { systemEvent: 'system_event', system_event: 'system_event', message: 'prompt', prompt: 'prompt', agentTurn: 'prompt', agent_turn: 'prompt', command: 'command', script: 'script' };
|
|
65
|
+
const has = key => payload != null && Object.hasOwn(Object(payload), key) || record != null && Object.hasOwn(Object(record), key);
|
|
66
|
+
return { kind: names[native] ?? (has('systemEvent') ? 'system_event' : has('command') ? 'command' : has('script') ? 'script' : 'prompt'), text: text(['text', 'message', 'command', 'script']) ?? ['message', 'command', 'script'].map(key => string(record?.[key])).find(value => value != null) ?? null };
|
|
67
|
+
}
|
|
68
|
+
export function projectJob(harness, profile, job) {
|
|
69
|
+
const raw = job.raw, kind = job.schedule.kind;
|
|
70
|
+
const schedule = { kind, expr: kind === 'cron' ? job.schedule.expr : null, minutes: null, run_at: kind === 'once' ? job.schedule.run_at : null,
|
|
71
|
+
display: kind === 'cron' ? job.schedule.expr : kind === 'once' ? `once @${job.schedule.run_at}` : `every ${jobFloat(job.schedule.minutes, true)} min` };
|
|
72
|
+
if (kind === 'interval') setNativeFloat(schedule, 'minutes', job.schedule.minutes);
|
|
73
|
+
const delivery = object(raw.__delivery) ? raw.__delivery : null, d = key => string(delivery?.[key]);
|
|
74
|
+
const target = job.deliver, explicit = target.kind === 'explicit';
|
|
75
|
+
const rendered = explicit ? target.platform + (target.chat_id == null ? '' : `:${target.chat_id}${target.thread_id == null ? '' : `:${target.thread_id}`}`) : target.kind;
|
|
76
|
+
return { id: job.id, harness, scope: 'install', profile: harness === 'openclaw' ? string(Object.hasOwn(raw, 'agentId') ? raw.agentId : raw.agent_id) ?? profile : profile, session_id: null, schedule,
|
|
77
|
+
payload: Object.hasOwn(raw, '__payload') ? jobPayload({ payload: raw.__payload }) : typeof raw.script === 'string' ? { kind: 'script', text: raw.script } : { kind: 'prompt', text: job.prompt },
|
|
78
|
+
session_target: job.session_target, deliver: { target: harness === 'openclaw' ? d('channel') : rendered, chat_id: d('to') ?? job.origin?.chat_id ?? (explicit ? target.chat_id : null), thread_id: d('threadId') ?? job.origin?.thread_id ?? (explicit ? target.thread_id : null), account: d('accountId'), mode: d('mode') },
|
|
79
|
+
enabled: job.enabled, state: job.enabled ? 'active' : 'paused', next_run_at: job.next_run_at, last_run_at: job.last_run_at, last_status: job.last_status, last_delivery_error: string(raw.last_delivery_error), created_at: job.created_at, recurring: kind !== 'once' };
|
|
80
|
+
}
|
|
81
|
+
function stores(harness, homes, cwd) {
|
|
82
|
+
if (harness === 'openclaw') return [{ path: join(homes.openclaw, 'state/openclaw.sqlite'), profile: null }, { path: join(homes.openclaw, 'cron/jobs.json'), profile: null }];
|
|
83
|
+
const root = harness === 'hermes' ? parent(homes.hermes) ?? '.' : homes.orchestrator;
|
|
84
|
+
if (harness === 'orchestrator' && !isDirectory(filesystem(root, cwd))) return [];
|
|
85
|
+
const result = [{ path: join(root, 'cron/jobs.json'), profile: null }], profiles = join(root, 'profiles');
|
|
86
|
+
try {
|
|
87
|
+
const named = readdirSync(filesystem(profiles, cwd), { encoding: 'buffer' }).flatMap(name => {
|
|
88
|
+
const text = harness === 'hermes' ? name.toString('utf8') : (() => { try { return utf8.decode(name); } catch { return null; } })();
|
|
89
|
+
const path = childPath(profiles, name);
|
|
90
|
+
if (harness === 'orchestrator') { if (!statSync(filesystem(path, cwd)).isDirectory()) return []; }
|
|
91
|
+
else if (!isDirectory(filesystem(path, cwd))) return [];
|
|
92
|
+
if (text == null || text.startsWith('.') || harness === 'hermes' && text === 'node_modules') return [];
|
|
93
|
+
return [{ path: childPath(path, 'cron/jobs.json'), profile: harness === 'orchestrator' && text === 'default' ? null : text, sort: text }];
|
|
94
|
+
}).sort((a, b) => compare(a.sort, b.sort)); result.push(...named);
|
|
95
|
+
} catch (error) { if (harness === 'orchestrator' && !['ENOENT', 'ENOTDIR'].includes(error.code)) return []; }
|
|
96
|
+
return result;
|
|
97
|
+
}
|
|
98
|
+
function folderJobs(harness, selected, context) {
|
|
99
|
+
const jobs = [], sources = [];
|
|
100
|
+
for (const store of stores(harness, selected.homes, context.cwd)) {
|
|
101
|
+
const path = filesystem(store.path, context.cwd);
|
|
102
|
+
if (!existsSync(path)) { sources.push(source(harness, store.path, 'absent_store', store.profile)); continue; }
|
|
103
|
+
try {
|
|
104
|
+
const value = parseNativeJson(utf8.decode(readFileSync(path))), decoded = hermesJobRecords(display(store.path), value), rows = hermesJobValues(display(store.path), value);
|
|
105
|
+
const byId = new Map(rows.map(row => [row.id, row]));
|
|
106
|
+
for (const id of [...decoded.keys()].sort(compare)) {
|
|
107
|
+
const raw = byId.get(id), job = decoded.get(id);
|
|
108
|
+
jobs.push(projectJob(harness, store.profile, { ...job, raw, schedule: raw.schedule, prompt: raw.prompt ?? null, session_target: raw.session_target ?? null, enabled: raw.enabled ?? true, next_run_at: raw.next_run_at ?? null, last_run_at: raw.last_run_at ?? null, last_status: raw.last_status ?? null, created_at: raw.created_at ?? null }));
|
|
109
|
+
}
|
|
110
|
+
sources.push(source(harness, store.path, 'read', store.profile));
|
|
111
|
+
} catch (error) { sources.push(source(harness, store.path, 'unreadable', store.profile, error.message)); }
|
|
112
|
+
}
|
|
113
|
+
return { jobs, sources };
|
|
114
|
+
}
|
|
115
|
+
async function claudeLocators(selected, context) {
|
|
116
|
+
const root = filesystem(selected.homes.claude_code, context.cwd), files = [], walked = new Set();
|
|
117
|
+
function walk(dir) {
|
|
118
|
+
let real; try { real = realpathSync(dir); } catch { real = display(dir); }
|
|
119
|
+
if (walked.has(real)) return; walked.add(real);
|
|
120
|
+
for (const entry of directoryEntries(dir)) {
|
|
121
|
+
if (isDirectory(entry.path)) { if (!entry.name.equals(Buffer.from('subagents'))) walk(entry.path); }
|
|
122
|
+
else { try { if (statSync(entry.path).isFile() && jsonl(entry.name)) files.push(entry.path); } catch { /* refused native directory entry */ } }
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
walk(root);
|
|
126
|
+
const found = [];
|
|
127
|
+
for (const file of files) { try { const read = await readUncompressedJsonl(display(file), 'claude-code'); if (read.result?.descriptor && (read.result.state.header.session_id != null || read.result.state.header.cwd != null)) found.push(read.result.descriptor); } catch { /* native metadata skips unreadable headers */ } }
|
|
128
|
+
// The native scan excludes child directories, then projects header parents.
|
|
129
|
+
const rows = found.filter(row => row.parent_session_id == null).sort(descriptorOrder);
|
|
130
|
+
return (selected.session == null ? rows.slice(0, scanLimit) : rows.filter(row => row.locator.session_id === selected.session)).map(row => row.locator);
|
|
131
|
+
}
|
|
132
|
+
function marker(path) {
|
|
133
|
+
let bytes; try { bytes = readFileSync(path); } catch { return false; }
|
|
134
|
+
let from = 0;
|
|
135
|
+
for (let at = 0; at <= bytes.length; at++) if (at === bytes.length || bytes[at] === 10) {
|
|
136
|
+
let line; try { line = utf8.decode(bytes.subarray(from, at)); } catch { return false; } from = at + 1;
|
|
137
|
+
if (line.includes('CronCreate') || line.includes('ScheduleWakeup')) return true;
|
|
138
|
+
}
|
|
139
|
+
return false;
|
|
140
|
+
}
|
|
141
|
+
function claudeJob(session, record, wake) {
|
|
142
|
+
return { id: wake ? record.tool_use_id : record.id, harness: 'claude-code', scope: 'session', profile: null, session_id: session,
|
|
143
|
+
schedule: { kind: wake ? 'once' : 'cron', expr: wake ? null : record.schedule, minutes: null, run_at: wake ? record.scheduled_for : null, display: wake ? `once, +${nativeInteger(record, 'delay_seconds') ?? record.delay_seconds}s` : record.schedule },
|
|
144
|
+
payload: { kind: wake ? 'wakeup' : 'prompt', text: wake ? record.prompt ?? record.reason : record.prompt }, session_target: null, deliver: { target: 'session', chat_id: null, thread_id: null, account: null, mode: null }, enabled: true, state: wake ? 'pending' : 'active', next_run_at: wake ? record.scheduled_for : null, last_run_at: null, last_status: null, last_delivery_error: null, created_at: record.created_at, recurring: wake ? false : record.recurring };
|
|
145
|
+
}
|
|
146
|
+
async function claudeJobs(selected, context) {
|
|
147
|
+
const locators = await claudeLocators(selected, context), jobs = [], sources = [];
|
|
148
|
+
for (const locator of locators) {
|
|
149
|
+
if (!marker(locator.storage.path)) continue;
|
|
150
|
+
let loaded; try { loaded = await loadSessionPath(locator.storage.path, { ...context, fidelity: 'byte_lossless', includeSubagents: true }); }
|
|
151
|
+
catch { sources.push(source('claude-code', locator.storage.path, 'unreadable', null, 'session could not be loaded')); continue; }
|
|
152
|
+
const manifest = claudeRuntimeManifest(loaded);
|
|
153
|
+
jobs.push(...manifest.active_crons.map(record => claudeJob(locator.session_id, record, false)), ...manifest.pending_wakeups.map(record => claudeJob(locator.session_id, record, true)));
|
|
154
|
+
}
|
|
155
|
+
sources.push({ ...source('claude-code', selected.homes.claude_code, 'scanned'), sessions_scanned: locators.length, scan_limit: selected.session == null ? scanLimit : null });
|
|
156
|
+
return { jobs, sources };
|
|
157
|
+
}
|
|
158
|
+
function openJobs(selected, context) {
|
|
159
|
+
const path = join(selected.homes.openclaw, 'state/openclaw.sqlite'), legacy = join(selected.homes.openclaw, 'cron/jobs.json'), jobs = [], sources = [];
|
|
160
|
+
if (!existsSync(filesystem(path, context.cwd))) sources.push(source('openclaw', path, 'absent_store'));
|
|
161
|
+
else try {
|
|
162
|
+
const profiles = openclawJobs(readOpenclawRunHome(filesystem(selected.homes.openclaw, context.cwd)));
|
|
163
|
+
for (const [name, records] of [...profiles].sort(([a], [b]) => (a !== 'default') - (b !== 'default') || compare(a, b))) {
|
|
164
|
+
for (const id of [...records.keys()].sort(compare)) jobs.push(projectJob('openclaw', name === 'default' ? null : name, records.get(id)));
|
|
165
|
+
}
|
|
166
|
+
sources.push(source('openclaw', path, 'read'));
|
|
167
|
+
} catch (error) { sources.push(source('openclaw', path, 'unreadable', null, error.message)); }
|
|
168
|
+
sources.push(source('openclaw', legacy, existsSync(filesystem(legacy, context.cwd)) ? 'read' : 'absent_store'));
|
|
169
|
+
return { jobs, sources };
|
|
170
|
+
}
|
|
171
|
+
async function collect(selected, context) {
|
|
172
|
+
const jobs = [], sources = [];
|
|
173
|
+
for (const harness of JOB_HARNESSES) if (selected.harness == null || selected.harness === harness) {
|
|
174
|
+
const read = harness === 'claude-code' ? await claudeJobs(selected, context) : harness === 'openclaw' ? openJobs(selected, context) : folderJobs(harness, selected, context);
|
|
175
|
+
jobs.push(...read.jobs); sources.push(...read.sources);
|
|
176
|
+
}
|
|
177
|
+
return { jobs: jobs.filter(job => (selected.session == null || job.session_id === selected.session) && (selected.profile == null || job.profile === selected.profile)), sources };
|
|
178
|
+
}
|
|
179
|
+
function rawRecords(path) { try { const value = parseNativeJson(utf8.decode(readFileSync(path))); return Array.isArray(value) ? value : Array.isArray(value?.jobs) ? value.jobs : []; } catch { return []; } }
|
|
180
|
+
export async function listJobs(params, { env = process.env, cwd = process.cwd() } = {}) {
|
|
181
|
+
const context = { env, cwd }; return collect(query(params, context, false), context);
|
|
182
|
+
}
|
|
183
|
+
export async function getJob(params, { env = process.env, cwd = process.cwd() } = {}) {
|
|
184
|
+
const context = { env, cwd }, selected = query(params, context, true), job = (await collect(selected, context)).jobs.find(job => job.id === selected.id);
|
|
185
|
+
if (!job) refuse(`\`${selected.harness}\` has no scheduled job \`${selected.id}\``, -32000);
|
|
186
|
+
let raw = null;
|
|
187
|
+
if (job.harness === 'claude-code') {
|
|
188
|
+
for (const locator of await claudeLocators({ ...selected, session: job.session_id }, context)) try {
|
|
189
|
+
const manifest = claudeRuntimeManifest(await loadSessionPath(locator.storage.path, { ...context, fidelity: 'byte_lossless', includeSubagents: true }));
|
|
190
|
+
raw = manifest.active_crons.find(record => record.id === job.id) ?? manifest.pending_wakeups.find(record => record.tool_use_id === job.id) ?? null;
|
|
191
|
+
if (raw != null) break;
|
|
192
|
+
} catch { /* native raw read keeps trying selected locators */ }
|
|
193
|
+
} else for (const store of stores(job.harness, selected.homes, cwd)) {
|
|
194
|
+
raw = rawRecords(filesystem(store.path, cwd)).find(record => ['id', 'job_id', 'jobId'].map(key => string(record?.[key])).find(id => id != null) === job.id) ?? null;
|
|
195
|
+
if (raw != null) break;
|
|
196
|
+
}
|
|
197
|
+
return { job, source: raw };
|
|
198
|
+
}
|
package/jsonl.mjs
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { basename, dirname, extname } from 'node:path';
|
|
2
2
|
import { withSessionFile, readHeaderRecords, parseJsonLine, visitAppendedLines, sameFingerprint, appendableFingerprint } from './file-io.mjs';
|
|
3
|
-
import { finalizeNouns, headerFacts, headerComplete, readTailFacts, foldTailRecord, finishAppendedFacts } from './metadata.mjs';
|
|
3
|
+
import { finalizeNouns, headerFacts, headerComplete, readTailFacts, foldTailRecord, finishAppendedFacts, claudeSessionName, carryNames } from './metadata.mjs';
|
|
4
4
|
import { readCompressedCodexHeader } from './compressed.mjs';
|
|
5
5
|
|
|
6
6
|
/** A cold suffix is kept only when the final line is unfinished. A partial
|
|
@@ -35,7 +35,8 @@ async function descriptor(path, harness, reader, header, facts, nativeTopic) {
|
|
|
35
35
|
// the parent from discovery. It is named by its own file, under its parent.
|
|
36
36
|
const subagentOf = harness === 'claude-code' && basename(dirname(path)) === 'subagents' ? basename(dirname(dirname(path))) : null;
|
|
37
37
|
const sessionId = subagentOf ? basename(path, extname(path)) : header.session_id ?? basename(path, extname(path));
|
|
38
|
-
|
|
38
|
+
// A rename or a newer topic is written after the header froze: the tail's names stand before the header's.
|
|
39
|
+
let title = harness === 'claude-code' ? claudeSessionName(facts, header) : header.title ?? header.topic;
|
|
39
40
|
if (title == null && harness === 'codex') {
|
|
40
41
|
if (typeof nativeTopic !== 'function') throw new Error('Codex metadata requires its native thread-name source');
|
|
41
42
|
title = await nativeTopic(path, sessionId);
|
|
@@ -123,6 +124,6 @@ export async function advanceMetadataTail(reader, harness, old) {
|
|
|
123
124
|
}
|
|
124
125
|
// The old EOF record may have been valid before the appended bytes changed
|
|
125
126
|
// it. Its predecessor is not implied by the cached newest-record facts.
|
|
126
|
-
const nextFacts = old.trailing ? await readTailFacts(reader, harness) : await finishAppendedFacts(reader, harness, old.facts, facts);
|
|
127
|
+
const nextFacts = carryNames(old.trailing ? await readTailFacts(reader, harness) : await finishAppendedFacts(reader, harness, old.facts, facts), facts);
|
|
127
128
|
return { facts: nextFacts, ...suffix };
|
|
128
129
|
}
|
package/memory-show.mjs
ADDED
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
// Recorded prose only. The search engine and every harness mutation have
|
|
2
|
+
// separate owners; listing reads the same selected stores as the native noun.
|
|
3
|
+
import { openSync, readSync, closeSync, statSync, readdirSync, existsSync, readFileSync } from 'node:fs';
|
|
4
|
+
import { isAbsolute } from 'node:path';
|
|
5
|
+
import { userInfo } from 'node:os';
|
|
6
|
+
import { resolveHarnessHomes } from './homes.mjs';
|
|
7
|
+
import { profilePathJoin as join, profilePathParent as parent } from './profile-paths.mjs';
|
|
8
|
+
import { childPath } from './session-spend-files.mjs';
|
|
9
|
+
import { listProfiles } from './profiles-read.mjs';
|
|
10
|
+
import { parseNativeJson, setNativeInteger } from './native-json.mjs';
|
|
11
|
+
import { nativeTrim, truncateScalars } from './text.mjs';
|
|
12
|
+
import { usageMoment } from './session-usage-time.mjs';
|
|
13
|
+
|
|
14
|
+
export const MEMORY_HARNESSES = Object.freeze(['claude-code', 'hermes', 'openclaw']);
|
|
15
|
+
const bytes = 1024 * 1024, documents = 512, depthLimit = 4;
|
|
16
|
+
const utf8 = new TextDecoder('utf-8', { fatal: true, ignoreBOM: true });
|
|
17
|
+
const display = path => Buffer.isBuffer(path) ? path.toString('utf8') : path;
|
|
18
|
+
const compare = (a, b) => Buffer.compare(Buffer.from(a), Buffer.from(b));
|
|
19
|
+
const directory = path => { try { return statSync(path).isDirectory(); } catch { return false; } };
|
|
20
|
+
const file = path => { try { return statSync(path).isFile(); } catch { return false; } };
|
|
21
|
+
const invalid = message => { throw Object.assign(new Error(message), { rpcCode: -32602 }); };
|
|
22
|
+
const unsupported = message => { throw Object.assign(new Error(message), { rpcCode: -32020 }); };
|
|
23
|
+
const object = value => value != null && typeof value === 'object' && !Array.isArray(value);
|
|
24
|
+
function struct(value, keys, name) {
|
|
25
|
+
if (Array.isArray(value)) {
|
|
26
|
+
if (value.length > keys.length) invalid(`${name} has too many fields`);
|
|
27
|
+
return Object.fromEntries(value.map((entry, at) => [keys[at], entry]));
|
|
28
|
+
}
|
|
29
|
+
if (!object(value)) invalid(`${name} must be a struct`);
|
|
30
|
+
return value;
|
|
31
|
+
}
|
|
32
|
+
function query(params, context) {
|
|
33
|
+
const value = struct(params, ['harness', 'query', 'profile', 'session', 'full', 'regex', 'cwd', 'homes'], 'memory params');
|
|
34
|
+
for (const key of ['harness', 'query', 'profile', 'session', 'cwd']) if (value[key] != null && typeof value[key] !== 'string') invalid(`${key} must be a string`);
|
|
35
|
+
for (const key of ['full', 'regex']) if (Object.hasOwn(value, key) && typeof value[key] !== 'boolean') invalid(`${key} must be a boolean`);
|
|
36
|
+
const homes = resolveHarnessHomes(context.env);
|
|
37
|
+
if (Object.hasOwn(value, 'homes')) {
|
|
38
|
+
const selected = struct(value.homes, Object.keys(homes), 'homes');
|
|
39
|
+
for (const key of Object.keys(homes)) if (Object.hasOwn(selected, key)) {
|
|
40
|
+
if (typeof selected[key] !== 'string') invalid(`homes.${key} must be a string`);
|
|
41
|
+
homes[key] = selected[key];
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
if (value.harness == null) invalid('`harness` is required');
|
|
45
|
+
if (!MEMORY_HARNESSES.includes(value.harness)) unsupported(`harness \`${value.harness}\` has no memory store (memory exists for: ${MEMORY_HARNESSES.join(', ')})`);
|
|
46
|
+
if (value.session != null && value.harness !== 'claude-code') unsupported(`\`${value.harness}\` scopes memory by profile, not by session — drop \`session\` or use \`profile\``);
|
|
47
|
+
return { ...value, homes, full: value.full ?? false };
|
|
48
|
+
}
|
|
49
|
+
function selectedFile(path, cwd) {
|
|
50
|
+
const text = display(path);
|
|
51
|
+
if (!text.length || isAbsolute(text) || process.platform === 'win32' && /^[A-Za-z]:/u.test(text)) return path;
|
|
52
|
+
return childPath(cwd, path);
|
|
53
|
+
}
|
|
54
|
+
function entries(path, cwd) {
|
|
55
|
+
try { return readdirSync(selectedFile(path, cwd), { encoding: 'buffer' }).map(name => childPath(path, name)).sort(compare); }
|
|
56
|
+
catch { return []; }
|
|
57
|
+
}
|
|
58
|
+
function leaf(path) {
|
|
59
|
+
const trailing = process.platform === 'win32' ? /[\\/]+$/u : /\/+$/u;
|
|
60
|
+
let text = display(path).replace(trailing, '');
|
|
61
|
+
while (text.endsWith('/.') || process.platform === 'win32' && text.endsWith('\\.')) text = text.slice(0, -2).replace(trailing, '');
|
|
62
|
+
const name = text.slice(Math.max(text.lastIndexOf('/'), process.platform === 'win32' ? text.lastIndexOf('\\') : -1) + 1);
|
|
63
|
+
return !name || name === '.' || name === '..' || process.platform === 'win32' && /^[A-Za-z]:$/u.test(name) ? null : name;
|
|
64
|
+
}
|
|
65
|
+
function store(harness, scope, profile, root, files, directories) { return { harness, scope, profile, root, files: files.map(name => join(root, name)), directories: directories.map(name => name ? join(root, name) : root) }; }
|
|
66
|
+
function unknown(harness, profile) { invalid(`\`${harness}\` has no memory store for \`${profile}\``); }
|
|
67
|
+
function claude(query, context) {
|
|
68
|
+
let project;
|
|
69
|
+
if (query.profile != null) {
|
|
70
|
+
project = isAbsolute(query.profile) ? query.profile : join(query.homes.claude_code, query.profile);
|
|
71
|
+
if (!directory(selectedFile(project, context.cwd))) unknown(query.harness, query.profile);
|
|
72
|
+
} else if (query.session != null) {
|
|
73
|
+
project = entries(query.homes.claude_code, context.cwd).find(path => directory(selectedFile(path, context.cwd)) && file(selectedFile(join(path, `${query.session}.jsonl`), context.cwd)));
|
|
74
|
+
if (project == null) invalid(`no Claude Code project directory holds session \`${query.session}\``);
|
|
75
|
+
project = display(project);
|
|
76
|
+
} else {
|
|
77
|
+
const cwd = query.cwd ?? context.cwd; let enclosing = cwd;
|
|
78
|
+
for (let ancestor = cwd; ancestor != null; ancestor = parent(ancestor)) if (existsSync(selectedFile(join(ancestor, '.git'), context.cwd))) { enclosing = ancestor; break; }
|
|
79
|
+
project = join(query.homes.claude_code, [...enclosing].map(char => /^[A-Za-z0-9]$/u.test(char) ? char : '-').join(''));
|
|
80
|
+
}
|
|
81
|
+
const nested = join(project, 'memory'), root = directory(selectedFile(nested, context.cwd)) ? nested : project;
|
|
82
|
+
return directory(selectedFile(root, context.cwd)) ? [store('claude-code', 'project', leaf(project), root, ['MEMORY.md'], [''])] : [];
|
|
83
|
+
}
|
|
84
|
+
function hermes(query, context) {
|
|
85
|
+
const home = parent(query.homes.hermes); if (home == null) return [];
|
|
86
|
+
let wanted = [['default', home], ...entries(join(home, 'profiles'), context.cwd).filter(path => directory(selectedFile(path, context.cwd))).map(path => [leaf(path), display(path)]).sort((a, b) => compare(a[0], b[0]) || compare(a[1], b[1]))];
|
|
87
|
+
if (query.profile != null) { wanted = wanted.filter(([name]) => name === query.profile); if (!wanted.length) unknown('hermes', query.profile); }
|
|
88
|
+
return wanted.filter(([, path]) => directory(selectedFile(path, context.cwd))).map(([name, path]) => store('hermes', name === 'default' ? 'user' : 'profile', name, path, ['MEMORY.md', 'USER.md'], ['memories']));
|
|
89
|
+
}
|
|
90
|
+
// memory's native profiles::read_json5 removes comments/trailing commas only.
|
|
91
|
+
// Its workspace projection does not accept the codec's broader key/quote forms.
|
|
92
|
+
function stripJson5(text) {
|
|
93
|
+
const chars = [...text]; let out = '', quoted = false, escaped = false;
|
|
94
|
+
for (let at = 0; at < chars.length; at++) {
|
|
95
|
+
const ch = chars[at];
|
|
96
|
+
if (quoted) { out += ch; if (escaped) escaped = false; else if (ch === '\\') escaped = true; else if (ch === '"') quoted = false; continue; }
|
|
97
|
+
if (ch === '"') { quoted = true; out += ch; }
|
|
98
|
+
else if (ch === '/' && chars[at + 1] === '/') { while (++at < chars.length) if (chars[at] === '\n') { out += '\n'; break; } }
|
|
99
|
+
else if (ch === '/' && chars[at + 1] === '*') { at++; let previous = '\0'; while (++at < chars.length) { if (previous === '*' && chars[at] === '/') break; previous = chars[at]; } out += ' '; }
|
|
100
|
+
else out += ch;
|
|
101
|
+
}
|
|
102
|
+
const scanned = [...out]; let cleaned = ''; quoted = false; escaped = false;
|
|
103
|
+
for (let at = 0; at < scanned.length; at++) {
|
|
104
|
+
const ch = scanned[at];
|
|
105
|
+
if (quoted) { cleaned += ch; if (escaped) escaped = false; else if (ch === '\\') escaped = true; else if (ch === '"') quoted = false; continue; }
|
|
106
|
+
if (ch === '"') quoted = true;
|
|
107
|
+
if (ch === ',') { let next = at + 1; while (scanned[next] != null && nativeTrim(scanned[next]) === '') next++; if (['}', ']'].includes(scanned[next])) continue; }
|
|
108
|
+
cleaned += ch;
|
|
109
|
+
}
|
|
110
|
+
return cleaned;
|
|
111
|
+
}
|
|
112
|
+
function configuredWorkspace(home, agent, context) {
|
|
113
|
+
let doc; try { doc = parseNativeJson(stripJson5(utf8.decode(readFileSync(selectedFile(join(home, 'openclaw.json'), context.cwd))))); } catch { return null; }
|
|
114
|
+
const entry = (Array.isArray(doc?.agents?.list) ? doc.agents.list.find(item => item?.id === agent) : null) ?? doc?.agents?.entries?.[agent];
|
|
115
|
+
if (typeof entry?.workspace !== 'string') return null;
|
|
116
|
+
const workspace = nativeTrim(entry.workspace); if (!workspace) return null;
|
|
117
|
+
if (workspace.startsWith('~/')) {
|
|
118
|
+
let home = context.env.HOME || (process.platform === 'win32' && context.env.USERPROFILE);
|
|
119
|
+
if (!home) { try { home = userInfo().homedir; } catch { /* no home fallback */ } }
|
|
120
|
+
if (home) return join(home, workspace.slice(2));
|
|
121
|
+
}
|
|
122
|
+
return workspace;
|
|
123
|
+
}
|
|
124
|
+
function openclaw(query, context) {
|
|
125
|
+
const home = query.homes.openclaw;
|
|
126
|
+
let wanted; try { wanted = listProfiles({ homes: query.homes, harness: 'openclaw' }, context).profiles.map(row => [row.name, row.default]); } catch { wanted = []; }
|
|
127
|
+
if (!wanted.length) wanted = [['main', true]];
|
|
128
|
+
else if (!wanted.some(([, defaults]) => defaults)) { const main = wanted.findIndex(([name]) => name === 'main'); wanted[main < 0 ? 0 : main][1] = true; }
|
|
129
|
+
if (query.profile != null) { wanted = wanted.filter(([name]) => name === query.profile); if (!wanted.length) unknown('openclaw', query.profile); }
|
|
130
|
+
return wanted.flatMap(([name, defaults]) => {
|
|
131
|
+
let root = configuredWorkspace(home, name, context);
|
|
132
|
+
if (root == null && !defaults) root = join(home, `workspace-${name}`);
|
|
133
|
+
if (root == null && context.env.OPENCLAW_WORKSPACE_DIR) root = context.env.OPENCLAW_WORKSPACE_DIR;
|
|
134
|
+
const profile = nativeTrim(context.env.OPENCLAW_PROFILE ?? '');
|
|
135
|
+
root ??= join(home, profile && profile !== 'default' ? `workspace-${profile}` : 'workspace');
|
|
136
|
+
return directory(selectedFile(root, context.cwd)) ? [store('openclaw', 'agent', name, root, ['MEMORY.md', 'DREAMS.md', 'dreams.md'], ['memory'])] : [];
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
function walk(path, depth, out, cwd) {
|
|
140
|
+
if (depth >= depthLimit || out.length >= documents) return;
|
|
141
|
+
for (const child of entries(path, cwd)) {
|
|
142
|
+
if (out.length >= documents) return;
|
|
143
|
+
const name = leaf(child); if (name?.startsWith('.')) continue;
|
|
144
|
+
if (directory(selectedFile(child, cwd))) walk(child, depth + 1, out, cwd);
|
|
145
|
+
else if (/\.(md|markdown|txt)$/i.test(name ?? '')) out.push(child);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
function paths(store, cwd) {
|
|
149
|
+
const out = store.files.filter(path => file(selectedFile(path, cwd)));
|
|
150
|
+
for (const path of store.directories) {
|
|
151
|
+
const found = []; walk(path, 0, found, cwd); found.sort(compare);
|
|
152
|
+
for (const path of found) if (!out.some(existing => Buffer.from(existing).equals(Buffer.from(path)))) out.push(path);
|
|
153
|
+
}
|
|
154
|
+
return out;
|
|
155
|
+
}
|
|
156
|
+
function memoryLines(text) {
|
|
157
|
+
if (!text) return [];
|
|
158
|
+
const lines = text.split('\n'); if (lines.at(-1) === '') lines.pop();
|
|
159
|
+
return lines.map((line, at) => (at < lines.length - 1 || text.endsWith('\n')) && line.endsWith('\r') ? line.slice(0, -1) : line);
|
|
160
|
+
}
|
|
161
|
+
const clip = line => { const trimmed = line.replace(/[\u0009-\u000d\u0020\u0085\u00a0\u1680\u2000-\u200a\u2028\u2029\u202f\u205f\u3000]+$/u, ''); return [...trimmed].length <= 200 ? trimmed : truncateScalars(trimmed, 200) + '…'; };
|
|
162
|
+
function read(path) {
|
|
163
|
+
let fd;
|
|
164
|
+
try {
|
|
165
|
+
fd = openSync(path, 'r'); const buffer = Buffer.alloc(bytes); let used = 0;
|
|
166
|
+
while (used < bytes) { const count = readSync(fd, buffer, used, bytes - used, null); if (!count) break; used += count; }
|
|
167
|
+
return utf8.decode(buffer.subarray(0, used));
|
|
168
|
+
} catch { return null; }
|
|
169
|
+
finally { if (fd != null) closeSync(fd); }
|
|
170
|
+
}
|
|
171
|
+
function document(store, path, full, cwd) {
|
|
172
|
+
const actual = selectedFile(path, cwd); let info; try { info = statSync(actual, { bigint: true }); } catch { /* unknown metadata */ }
|
|
173
|
+
const text = read(actual), lines = text == null ? [] : memoryLines(text), preview = lines.slice(0, 5).map(clip);
|
|
174
|
+
const root = Buffer.from(store.root), source = Buffer.from(path), relative = source.subarray(root.length + Number(root.length && source[root.length] === (process.platform === 'win32' ? 92 : 47)));
|
|
175
|
+
const row = { name: relative.toString('utf8').replaceAll('\\', '/'), harness: store.harness, scope: store.scope, ...(store.profile == null ? {} : { profile: store.profile }), path: display(path) };
|
|
176
|
+
setNativeInteger(row, 'size', info?.size ?? 0n);
|
|
177
|
+
if (info?.mtimeNs >= 0n) row.updated_at = usageMoment(info.mtimeNs / 1000000n > (1n << 63n) - 1n ? (1n << 63n) - 1n : info.mtimeNs / 1000000n);
|
|
178
|
+
row.preview = preview; row.truncated = lines.length > preview.length || (info?.size ?? 0n) > BigInt(bytes);
|
|
179
|
+
if (full && text != null) row.content = text;
|
|
180
|
+
return row;
|
|
181
|
+
}
|
|
182
|
+
export function showMemory(params, { env = process.env, cwd = process.cwd() } = {}) {
|
|
183
|
+
const context = { env, cwd }, selected = query(params, context), stores = selected.harness === 'claude-code' ? claude(selected, context) : selected.harness === 'hermes' ? hermes(selected, context) : openclaw(selected, context);
|
|
184
|
+
const out = [];
|
|
185
|
+
for (const store of stores) for (const path of paths(store, cwd)) { if (out.length >= documents) return { schema: 'supercode.memory.v1', documents: out }; out.push(document(store, path, selected.full, cwd)); }
|
|
186
|
+
return { schema: 'supercode.memory.v1', documents: out };
|
|
187
|
+
}
|
package/metadata.mjs
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { basename, dirname } from 'node:path';
|
|
1
2
|
import { tailRecords } from './file-io.mjs';
|
|
2
3
|
import { nativeTrim } from './text.mjs';
|
|
3
4
|
|
|
@@ -53,11 +54,16 @@ export function recordTimestamp(record, harness) {
|
|
|
53
54
|
* append-aware owner. Newest means the last valid stamped record in the file,
|
|
54
55
|
* rather than the maximum clock across records or the file's mtime. */
|
|
55
56
|
export async function readTailFacts(reader, harness) {
|
|
56
|
-
const facts = { last_turn_ms: null, model: null, clock_offset: null, model_offset: null };
|
|
57
|
+
const facts = { last_turn_ms: null, model: null, clock_offset: null, model_offset: null, ...(harness === 'claude-code' ? { ...UNNAMED } : {}) };
|
|
58
|
+
// A Claude session's names are looked for in at most the last 128 KiB, never in a subagent's transcript (it has none),
|
|
59
|
+
// and only once a whole batch of them is passed: one cut at the window's edge would name it by the wrong one.
|
|
60
|
+
const naming = harness === 'claude-code' && basename(dirname(reader.path ?? '')) !== 'subagents';
|
|
61
|
+
let named = !naming, batch = false;
|
|
57
62
|
let window = Math.min(16 * 1024, reader.size);
|
|
58
63
|
for (;;) {
|
|
59
64
|
const from = reader.size - window;
|
|
60
65
|
const bytes = await reader.read(from, window);
|
|
66
|
+
batch = false;
|
|
61
67
|
for (const { value, start } of tailRecords(bytes, window < reader.size)) {
|
|
62
68
|
if (facts.last_turn_ms == null) {
|
|
63
69
|
facts.last_turn_ms = recordTimestamp(value, harness);
|
|
@@ -67,17 +73,52 @@ export async function readTailFacts(reader, harness) {
|
|
|
67
73
|
facts.model = recordModel(value, harness);
|
|
68
74
|
if (facts.model != null) facts.model_offset = from + start;
|
|
69
75
|
}
|
|
70
|
-
if (
|
|
76
|
+
if (naming && !named) {
|
|
77
|
+
const name = recordName(value);
|
|
78
|
+
if (name != null) { facts[name.key] ??= name.value; batch = true; }
|
|
79
|
+
else if (batch) named = true;
|
|
80
|
+
}
|
|
81
|
+
if (facts.last_turn_ms != null && facts.model != null && named) break;
|
|
71
82
|
}
|
|
72
|
-
|
|
83
|
+
const searching = !named && window < NAMING_BYTES;
|
|
84
|
+
if ((facts.last_turn_ms != null && !searching) || window >= reader.size || window >= 1024 * 1024) return { ...facts, scan_from: from };
|
|
73
85
|
window = Math.min(window * 2, reader.size, 1024 * 1024);
|
|
74
86
|
}
|
|
75
87
|
}
|
|
76
88
|
|
|
89
|
+
const NAMING_BYTES = 128 * 1024;
|
|
90
|
+
const UNNAMED = Object.freeze({ agent_name: null, custom_title: null, ai_title: null });
|
|
91
|
+
const NAMES = Object.freeze({ 'agent-name': ['agent_name', 'agentName'], 'custom-title': ['custom_title', 'customTitle'], 'ai-title': ['ai_title', 'aiTitle'] });
|
|
92
|
+
|
|
93
|
+
/** A Claude session's names, as Claude Code writes them again in one batch as the session goes on (custom-title,
|
|
94
|
+
* ai-title, agent-name), so the newest of each stands near the end. An empty one is a name cleared, and stands. */
|
|
95
|
+
function recordName(record) {
|
|
96
|
+
const name = NAMES[record?.type];
|
|
97
|
+
if (!name || typeof record[name[1]] !== 'string') return null;
|
|
98
|
+
return { key: name[0], value: nativeTrim(record[name[1]]) };
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/** A Claude session's name as Claude Code lists it: its agent name, else its custom title, else its generated topic;
|
|
102
|
+
* each the newest the tail holds, else the first its header holds. */
|
|
103
|
+
export function claudeSessionName(facts, header) {
|
|
104
|
+
const newest = (tail, head) => (tail ?? head ?? '') || null;
|
|
105
|
+
return newest(facts?.agent_name, header.agent_name) ?? newest(facts?.custom_title, header.custom_title) ?? newest(facts?.ai_title, header.topic);
|
|
106
|
+
}
|
|
107
|
+
|
|
77
108
|
export function foldTailRecord(facts, record, harness, start) {
|
|
78
109
|
const clock = recordTimestamp(record, harness), model = recordModel(record, harness);
|
|
79
110
|
if (clock != null) { facts.last_turn_ms = clock; facts.clock_offset = start; }
|
|
80
111
|
if (model != null) { facts.model = model; facts.model_offset = start; }
|
|
112
|
+
if (harness === 'claude-code') {
|
|
113
|
+
const name = recordName(record);
|
|
114
|
+
if (name != null) facts[name.key] = name.value;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/** A full rescan of the tail that found no batch of names keeps those the appended lines carried. */
|
|
119
|
+
export function carryNames(next, previous) {
|
|
120
|
+
for (const key of Object.keys(UNNAMED)) if (next[key] == null && previous?.[key] != null) next[key] = previous[key];
|
|
121
|
+
return next;
|
|
81
122
|
}
|
|
82
123
|
|
|
83
124
|
/** Reuse known positions only while the new native tail window contains them.
|
|
@@ -127,6 +168,7 @@ export function applyHeaderRecord(result, value, harness) {
|
|
|
127
168
|
if (harness === 'claude-code') {
|
|
128
169
|
fill(result, 'session_id', value?.sessionId); fill(result, 'cwd', value?.cwd);
|
|
129
170
|
fill(result, 'title', value?.customTitle); fill(result, 'title', value?.agentName);
|
|
171
|
+
fill(result, 'custom_title', value?.customTitle); fill(result, 'agent_name', value?.agentName);
|
|
130
172
|
if (value?.type === 'ai-title') fill(result, 'topic', value.aiTitle);
|
|
131
173
|
fill(result, 'model', value?.message?.model);
|
|
132
174
|
} else if (harness === 'codex') {
|
package/native-json.mjs
CHANGED
|
@@ -65,7 +65,7 @@ function numberFact(token) {
|
|
|
65
65
|
return { value: decimalValue(token), kind: 'float' };
|
|
66
66
|
}
|
|
67
67
|
|
|
68
|
-
|
|
68
|
+
function readNativeJson(text) {
|
|
69
69
|
let at = 0;
|
|
70
70
|
const white = () => { while (/[\x20\t\r\n]/.test(text[at] ?? '') && at < text.length) at++; };
|
|
71
71
|
const takeString = () => {
|
|
@@ -84,7 +84,7 @@ export function parseNativeJson(text) {
|
|
|
84
84
|
if (char === '[' || char === '{') {
|
|
85
85
|
if (depth >= 127) invalid();
|
|
86
86
|
const array = char === '[', result = array ? [] : Object.create(null);
|
|
87
|
-
const meta = { keys: [], numbers: new Map() };
|
|
87
|
+
const meta = { keys: [], numbers: new Map(), duplicates: [] };
|
|
88
88
|
facts.set(result, meta);
|
|
89
89
|
const end = array ? ']' : '}';
|
|
90
90
|
at++; white();
|
|
@@ -99,6 +99,7 @@ export function parseNativeJson(text) {
|
|
|
99
99
|
}
|
|
100
100
|
const child = value(depth + 1);
|
|
101
101
|
if (!Object.hasOwn(result, key)) meta.keys.push(key);
|
|
102
|
+
else meta.duplicates.push(key);
|
|
102
103
|
result[key] = child.value;
|
|
103
104
|
if (child.kind) meta.numbers.set(key, child); else meta.numbers.delete(key);
|
|
104
105
|
white();
|
|
@@ -120,7 +121,33 @@ export function parseNativeJson(text) {
|
|
|
120
121
|
};
|
|
121
122
|
const result = value(0);
|
|
122
123
|
white(); if (at !== text.length) invalid();
|
|
123
|
-
return result
|
|
124
|
+
return result;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
export function parseNativeJson(text) { return readNativeJson(text).value; }
|
|
128
|
+
|
|
129
|
+
/** Preserve a scalar JSON number's native type when installing parsed metadata. */
|
|
130
|
+
export function setNativeJsonText(destination, key, text) {
|
|
131
|
+
const parsed = readNativeJson(text);
|
|
132
|
+
if (parsed.kind === 'integer') setNativeInteger(destination, key, parsed.integer);
|
|
133
|
+
else if (parsed.kind === 'float') setNativeFloat(destination, key, parsed.value);
|
|
134
|
+
else Object.defineProperty(destination, key, { value: parsed.value, enumerable: true, writable: true, configurable: true });
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/** Copy a member without rounding its native integer or erasing its float
|
|
138
|
+
* type. Nested values keep their existing facts; no payload clone is needed. */
|
|
139
|
+
export function copyNativeJsonMember(destination, key, source, sourceKey = key) {
|
|
140
|
+
key = String(key); sourceKey = String(sourceKey);
|
|
141
|
+
const value = source[sourceKey], fact = facts.get(source)?.numbers.get(sourceKey);
|
|
142
|
+
if (fact && Object.is(value, fact.value)) {
|
|
143
|
+
if (fact.kind === 'integer') setNativeInteger(destination, key, fact.integer);
|
|
144
|
+
else setNativeFloat(destination, key, fact.value);
|
|
145
|
+
return;
|
|
146
|
+
}
|
|
147
|
+
const meta = facts.get(destination);
|
|
148
|
+
if (meta && !Object.hasOwn(destination, key)) meta.keys.push(key);
|
|
149
|
+
meta?.numbers.delete(key);
|
|
150
|
+
Object.defineProperty(destination, key, { value, enumerable: true, writable: true, configurable: true });
|
|
124
151
|
}
|
|
125
152
|
|
|
126
153
|
export function nativeObjectKeys(value) {
|
|
@@ -130,6 +157,22 @@ export function nativeObjectKeys(value) {
|
|
|
130
157
|
return [...keys.filter((key) => Object.hasOwn(value, key)), ...Object.keys(value).filter((key) => !known.has(key))];
|
|
131
158
|
}
|
|
132
159
|
|
|
160
|
+
/** serde_json's insertion-ordered Map::remove swaps its last entry into the
|
|
161
|
+
* removed slot. Preserve that order and the remaining scalar number facts. */
|
|
162
|
+
export function swapRemoveNativeJsonMember(value, key) {
|
|
163
|
+
const keys = nativeObjectKeys(value), index = keys.indexOf(key);
|
|
164
|
+
if (index < 0) return;
|
|
165
|
+
const last = keys.pop(); if (index < keys.length) keys[index] = last;
|
|
166
|
+
const meta = facts.get(value) ?? { numbers: new Map() };
|
|
167
|
+
meta.keys = keys; meta.numbers.delete(key); facts.set(value, meta);
|
|
168
|
+
delete value[key];
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
// Direct serde struct reads reject a repeated field; Value reads keep its
|
|
173
|
+
// last value. The private parser fact lets typed readers preserve that split.
|
|
174
|
+
export function nativeDuplicateObjectKeys(value) { return facts.get(value)?.duplicates ?? []; }
|
|
175
|
+
|
|
133
176
|
// Transcript projections clone JSON before bounding strings or eliding media.
|
|
134
177
|
// Keep the source's numeric tokens and insertion order with those values.
|
|
135
178
|
export function cloneNativeJson(value) {
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export interface OntologyOperation { action: string; method: string }
|
|
2
|
+
export const ONTOLOGY_RECORDS: readonly string[];
|
|
3
|
+
export function ontologyOperations(): OntologyOperation[];
|
|
4
|
+
export function ontologySchema(record?: string): Record<string, unknown> | undefined;
|
|
5
|
+
export function formatOntologyContract(value: unknown): string;
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
// The committed ontology contracts own schema output and SDK generation.
|
|
2
|
+
// Native execution types remain with the native capabilities still using them.
|
|
3
|
+
import { readFileSync } from 'node:fs';
|
|
4
|
+
import { parseNativeJson, cloneNativeJson, stringifyNativeJson, stringifyNativeJsonMember, stringifyNativeJsonPretty } from './native-json.mjs';
|
|
5
|
+
|
|
6
|
+
const schemas = parseNativeJson(readFileSync(new URL('./ontology-schemas.json', import.meta.url), 'utf8'));
|
|
7
|
+
const operations = parseNativeJson(readFileSync(new URL('./ontology-operations.json', import.meta.url), 'utf8'));
|
|
8
|
+
export const ONTOLOGY_RECORDS = Object.freeze([...schemas.records]);
|
|
9
|
+
export function ontologyOperations() { return cloneNativeJson(operations); }
|
|
10
|
+
|
|
11
|
+
/** Return a fresh schema, retaining its record-specific definitions and numeric facts. */
|
|
12
|
+
export function ontologySchema(record = 'Orchestration') {
|
|
13
|
+
if (!Object.hasOwn(schemas.roots, record)) return undefined;
|
|
14
|
+
const root = schemas.roots[record], members = new Map();
|
|
15
|
+
for (const key of Object.keys(root.schema)) members.set(key, stringifyNativeJsonMember(root.schema, key));
|
|
16
|
+
if (root.definitions.length) members.set('$defs', `{${root.definitions.map(name => `${JSON.stringify(name)}:${stringifyNativeJson(schemas.definitions[name])}`).join(',')}}`);
|
|
17
|
+
const schema = parseNativeJson(`{${root.members.map(key => `${JSON.stringify(key)}:${members.get(key)}`).join(',')}}`);
|
|
18
|
+
// The shipped Windows native schemas embed the checked-out role prompt
|
|
19
|
+
// templates with CRLF. Keep those default values in this metadata contract;
|
|
20
|
+
// no provider prompt is executed or rewritten here.
|
|
21
|
+
const roles = schema.properties?.workflow?.default?.run?.roles;
|
|
22
|
+
if (process.platform === 'win32' && roles) for (const name of ['implementer', 'reviewer', 'triager']) {
|
|
23
|
+
if (typeof roles[name]?.prompt === 'string') roles[name].prompt = roles[name].prompt.replace(/\r?\n/gu, '\r\n');
|
|
24
|
+
}
|
|
25
|
+
return schema;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** Native JSON formatting, including integer/float distinction and map ordering. */
|
|
29
|
+
export function formatOntologyContract(value) { return stringifyNativeJsonPretty(value); }
|