@volter/supercode-interchange 0.1.3 → 0.1.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +33 -5
- package/package.json +5 -1
- package/session-claude-runtime.d.ts +35 -0
- package/session-claude-runtime.mjs +213 -0
- package/session-launch-agent.mjs +30 -0
- package/session-read.d.ts +5 -1
- package/session-read.mjs +21 -2
- package/session-recorded-config.mjs +24 -0
package/README.md
CHANGED
|
@@ -42,8 +42,8 @@ attaches native subagent transcripts and verifies the main and child file
|
|
|
42
42
|
identities. Its scoped index retains graph references and ordering/count
|
|
43
43
|
skeletons while materializing only requested assistant groups and summary
|
|
44
44
|
messages. The file handle closes before the result leaves the scope. The
|
|
45
|
-
public read/follow handoff has
|
|
46
|
-
|
|
45
|
+
public read/follow handoff has [scoped private Mac/Windows observations](../../docs/evidence/session-read-port-2026-10-08.md);
|
|
46
|
+
these do not qualify a large corpus, native children or remote admission.
|
|
47
47
|
Codex has separate model-context and bounded display-history readers. The
|
|
48
48
|
model reader retains compaction replacement, turn grouping, reasoning and
|
|
49
49
|
native typed-input pairing; the display reader projects durable TurnItems or
|
|
@@ -57,15 +57,16 @@ dialect follows recorded leaf redirects, excludes side appends from default
|
|
|
57
57
|
leaf selection and keeps vendor references inert. Unknown Pi image shapes
|
|
58
58
|
remain raw-only, following the existing reader's refusal to synthesize image
|
|
59
59
|
data it cannot establish. These are read-library additions, with no new
|
|
60
|
-
OpenClaw write tier
|
|
60
|
+
OpenClaw write tier. The observation above covers a small recorded leaf branch,
|
|
61
|
+
not every native compaction or side-append shape.
|
|
61
62
|
OpenCode's envelope and export-document readers share creation/id ordering,
|
|
62
63
|
compaction boundaries, portable message extensions and task-child provenance.
|
|
63
64
|
Completed tool parts split into calls/results, retaining masked original output
|
|
64
65
|
and attachment content. Its SQLite reader reconstructs native columns, reads
|
|
65
66
|
the external revert sidecar and verifies both identities; malformed JSON columns
|
|
66
67
|
remain diagnostic. Legacy JSON-tree directories retain their native
|
|
67
|
-
unsupported-source refusal.
|
|
68
|
-
|
|
68
|
+
unsupported-source refusal. The small envelope observation does not qualify
|
|
69
|
+
concurrent SQLite/revert changes or all compaction/tool shapes.
|
|
69
70
|
The path layer selects formats structurally, pins native store selectors and
|
|
70
71
|
reads compressed transcripts through Node's zstd support. Native sidecars
|
|
71
72
|
validate framing and retain malformed appended turns as raw parse loss.
|
|
@@ -95,6 +96,33 @@ The SDK checks the read capability before moved requests; an older service
|
|
|
95
96
|
refuses explicitly instead of supplying a native read. Its catalog and other
|
|
96
97
|
retained operations remain independently available on the same client.
|
|
97
98
|
|
|
99
|
+
`./session-claude-runtime` folds the complete recorded Claude runtime manifest:
|
|
100
|
+
posture, cron/wakeup results, queue references, async child outcomes and
|
|
101
|
+
verbatim runtime residue. It validates forward references before exposing
|
|
102
|
+
posture and neither fires schedules nor launches children. The public
|
|
103
|
+
`sessions.recorded_config` door uses that full fold, leaving permission mode
|
|
104
|
+
unknown if unrelated runtime records are malformed. Codex policies come only
|
|
105
|
+
from recorded headers, with opaque values and numeric tokens retained. The
|
|
106
|
+
capability's `recorded_config` marker distinguishes this moved method from
|
|
107
|
+
older read services; retained methods and startup remain available after a
|
|
108
|
+
refusal. [Private Mac/Windows observations](../../docs/evidence/session-recorded-config-2026-10-08.md)
|
|
109
|
+
cover recorded posture/policies through npm and operator streams, including
|
|
110
|
+
unknown posture for malformed records. The manifest's other branches,
|
|
111
|
+
corpus/scale, concurrent reads and independent source review remain open.
|
|
112
|
+
|
|
113
|
+
`sessions.launch_agent` reads `<harness home>/supercode/launches/<session id>.json`
|
|
114
|
+
in the same caller-owned load lane. Claude `projects` and Codex `sessions`
|
|
115
|
+
ancestors select the transcript's recorded home, independent of current vendor
|
|
116
|
+
defaults; no transcript must exist. Unsupported locators, unsafe ids, absent,
|
|
117
|
+
invalid UTF-8 or malformed records, and nonobject environments return null.
|
|
118
|
+
The environment is recorded metadata, never caller authority. The new
|
|
119
|
+
`launch_agent` capability marker refuses older read services explicitly.
|
|
120
|
+
Native runtime launch-record writers remain for their separate replacement.
|
|
121
|
+
[Private Mac/Windows conversations](../../docs/evidence/session-launch-metadata-2026-10-08.md)
|
|
122
|
+
record selected-home answers and null malformed metadata through npm and
|
|
123
|
+
operator streams; concurrent path/record replacement and runtime writing remain
|
|
124
|
+
unqualified.
|
|
125
|
+
|
|
98
126
|
`resolveHarnessHomes` follows native HOME-first defaults and preserves explicitly empty harness-specific
|
|
99
127
|
home values. `projectMetadata` applies workspace scope, native lineage, repository-family, profile, time and
|
|
100
128
|
metadata-search filters with stable newest-first ordering. It clones raw rows before descendant roll-up.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@volter/supercode-interchange",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.5",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Read-only harness metadata, transcript codecs and session projections for Supercode",
|
|
6
6
|
"license": "MIT OR Apache-2.0",
|
|
@@ -20,6 +20,10 @@
|
|
|
20
20
|
"./session-read": {
|
|
21
21
|
"types": "./session-read.d.ts",
|
|
22
22
|
"import": "./session-read.mjs"
|
|
23
|
+
},
|
|
24
|
+
"./session-claude-runtime": {
|
|
25
|
+
"types": "./session-claude-runtime.d.ts",
|
|
26
|
+
"import": "./session-claude-runtime.mjs"
|
|
23
27
|
}
|
|
24
28
|
},
|
|
25
29
|
"files": [
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import type { Session } from './session-model.js';
|
|
2
|
+
export const CLAUDE_RUNTIME_MANIFEST_VERSION: 1;
|
|
3
|
+
export interface ClaudeRuntimePosture {
|
|
4
|
+
permission_mode: string | null; last_prompt_leaf_uuid: string | null;
|
|
5
|
+
last_prompt: string | null; timestamp: string | null; entrypoint: string | null;
|
|
6
|
+
user_type: string | null; version: string | null; cwd: string | null;
|
|
7
|
+
}
|
|
8
|
+
export interface ClaudeCronJob {
|
|
9
|
+
id: string; tool_use_id: string; schedule: string; recurring: boolean;
|
|
10
|
+
durable_requested: boolean; prompt: string; created_at: string | null;
|
|
11
|
+
expires_after_seconds: number | null; creation_result: string;
|
|
12
|
+
}
|
|
13
|
+
export interface ClaudeWakeup {
|
|
14
|
+
tool_use_id: string; delay_seconds: number; reason: string | null;
|
|
15
|
+
prompt: string | null; created_at: string | null; scheduled_for: string | null;
|
|
16
|
+
creation_result: string;
|
|
17
|
+
}
|
|
18
|
+
export interface ClaudeQueueState { enqueued: number; dequeued: number; removed: number; pending: string[] }
|
|
19
|
+
export type ClaudeBackgroundState = 'launch_pending' | 'running' | 'completed' | 'failed' | 'killed' | 'unknown_terminal';
|
|
20
|
+
export interface ClaudeBackgroundChild {
|
|
21
|
+
tool_use_id: string; origin_observed: boolean; agent_id: string | null;
|
|
22
|
+
agent_type: string | null; description: string | null; requested_model: string | null;
|
|
23
|
+
resolved_model: string | null; prompt: string | null; output_file: string | null;
|
|
24
|
+
state: ClaudeBackgroundState; started_at: string | null; finished_at: string | null;
|
|
25
|
+
summary: string | null;
|
|
26
|
+
}
|
|
27
|
+
export interface ClaudeRuntimeResidue { line: number; kind: string; raw: string }
|
|
28
|
+
export interface ClaudeRuntimeManifest {
|
|
29
|
+
schema_version: 1; posture: ClaudeRuntimePosture; active_crons: ClaudeCronJob[];
|
|
30
|
+
pending_wakeups: ClaudeWakeup[]; queue: ClaudeQueueState;
|
|
31
|
+
background_children: ClaudeBackgroundChild[];
|
|
32
|
+
reported_pending_background_children: number | null; residue: ClaudeRuntimeResidue[];
|
|
33
|
+
}
|
|
34
|
+
/** A pure strict fold. It never executes scheduled work or launches a child. */
|
|
35
|
+
export function claudeRuntimeManifest(session: Session): ClaudeRuntimeManifest;
|
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
// Pure reconstruction of recorded Claude runtime facts. Jobs, wakeups and
|
|
2
|
+
// child states in this manifest are evidence; this module never executes them.
|
|
3
|
+
import { parseNativeJson, nativeInteger, setNativeInteger } from './native-json.mjs';
|
|
4
|
+
import { field, stringField } from './session-codec-utils.mjs';
|
|
5
|
+
import { nativeTrim } from './text.mjs';
|
|
6
|
+
|
|
7
|
+
export const CLAUDE_RUNTIME_MANIFEST_VERSION = 1;
|
|
8
|
+
const fail = message => { throw new Error(message); };
|
|
9
|
+
const order = (left, right) => Buffer.compare(Buffer.from(left), Buffer.from(right));
|
|
10
|
+
const sorted = map => [...map.keys()].sort(order).map(key => map.get(key));
|
|
11
|
+
const bool = (value, key) => field(value, key) === true;
|
|
12
|
+
const required = (value, key, line, kind) => stringField(value, key)
|
|
13
|
+
?? fail(`malformed Claude ${kind} at line ${line}: missing string ${key}`);
|
|
14
|
+
const residue = (state, line, kind, raw) => state.residue.push({ line, kind, raw });
|
|
15
|
+
const runtimeResult = text => ['Scheduled recurring job ', 'Scheduled job ', 'Scheduled one-shot task ', 'Cancelled job ', 'Next wakeup scheduled for '].some(prefix => text.startsWith(prefix));
|
|
16
|
+
const runtimeType = kind => ['queue', 'permission', 'schedule', 'cron', 'background'].some(word => kind.includes(word));
|
|
17
|
+
function between(text, start, end) {
|
|
18
|
+
const from = text.indexOf(start); if (from < 0) return null;
|
|
19
|
+
const rest = text.slice(from + start.length), until = rest.indexOf(end);
|
|
20
|
+
return until < 0 ? null : rest.slice(0, until);
|
|
21
|
+
}
|
|
22
|
+
function taskNotification(text) {
|
|
23
|
+
if (!text.includes('<task-notification>')) return null;
|
|
24
|
+
const tag = name => between(text, `<${name}>`, `</${name}>`);
|
|
25
|
+
return { task_id: tag('task-id'), tool_use_id: tag('tool-use-id'), status: tag('status'), summary: tag('summary') };
|
|
26
|
+
}
|
|
27
|
+
function resultText(content) {
|
|
28
|
+
if (typeof content === 'string') return content;
|
|
29
|
+
if (!Array.isArray(content)) return null;
|
|
30
|
+
const joined = content.filter(block => stringField(block, 'type') === 'text').map(block => stringField(block, 'text')).filter(text => text != null).join('\n');
|
|
31
|
+
return joined === '' ? null : joined;
|
|
32
|
+
}
|
|
33
|
+
function cronId(text) {
|
|
34
|
+
const prefix = ['Scheduled recurring job ', 'Scheduled job ', 'Scheduled one-shot task '].find(prefix => text.startsWith(prefix));
|
|
35
|
+
if (prefix == null) return null;
|
|
36
|
+
const rest = nativeTrim(text.slice(prefix.length));
|
|
37
|
+
return rest === '' ? null : rest.split(/[\u0009-\u000d\u0020\u0085\u00a0\u1680\u2000-\u200a\u2028\u2029\u202f\u205f\u3000]+/u)[0];
|
|
38
|
+
}
|
|
39
|
+
function emptyChild(id, timestamp) {
|
|
40
|
+
return { tool_use_id: id, origin_observed: true, agent_id: null, agent_type: null, description: null,
|
|
41
|
+
requested_model: null, resolved_model: null, prompt: null, output_file: null,
|
|
42
|
+
state: 'launch_pending', started_at: timestamp, finished_at: null, summary: null };
|
|
43
|
+
}
|
|
44
|
+
function assistantCalls(value, line, raw, state) {
|
|
45
|
+
const content = field(field(value, 'message'), 'content'); if (!Array.isArray(content)) return;
|
|
46
|
+
const timestamp = stringField(value, 'timestamp');
|
|
47
|
+
for (const block of content) {
|
|
48
|
+
if (stringField(block, 'type') !== 'tool_use') continue;
|
|
49
|
+
const name = stringField(block, 'name'); if (name == null) continue;
|
|
50
|
+
const id = stringField(block, 'id');
|
|
51
|
+
if (id == null) {
|
|
52
|
+
if (['CronCreate', 'CronDelete', 'ScheduleWakeup', 'Agent'].includes(name)) fail(`malformed Claude runtime tool call at line ${line}: ${name} has no id`);
|
|
53
|
+
continue;
|
|
54
|
+
}
|
|
55
|
+
const input = field(block, 'input'); let call = null;
|
|
56
|
+
if (name === 'CronCreate') call = { kind: name, tool_use_id: id, schedule: required(input, 'cron', line, name),
|
|
57
|
+
recurring: bool(input, 'recurring'), durable_requested: bool(input, 'durable'), prompt: required(input, 'prompt', line, name), created_at: timestamp };
|
|
58
|
+
else if (name === 'CronDelete') {
|
|
59
|
+
const target = stringField(input, 'id'); call = target == null ? { kind: 'Invalid', name } : { kind: name, id: target };
|
|
60
|
+
} else if (name === 'ScheduleWakeup') {
|
|
61
|
+
const delay = nativeInteger(input, 'delaySeconds');
|
|
62
|
+
if (delay == null) fail(`malformed ScheduleWakeup at line ${line}: missing integer delaySeconds`);
|
|
63
|
+
call = { kind: name, tool_use_id: id, delay_seconds: delay, reason: stringField(input, 'reason'), prompt: stringField(input, 'prompt'), created_at: timestamp };
|
|
64
|
+
} else if (name === 'CronList') call = { kind: name };
|
|
65
|
+
else if (name === 'Agent') {
|
|
66
|
+
const child = emptyChild(id, timestamp);
|
|
67
|
+
for (const [key, target] of [['subagent_type', 'agent_type'], ['description', 'description'], ['model', 'requested_model'], ['prompt', 'prompt']]) child[target] = stringField(input, key);
|
|
68
|
+
if (state.children.has(id)) fail(`malformed Claude Agent reference at line ${line}: duplicate tool-use id ${id}`);
|
|
69
|
+
state.children.set(id, child); residue(state, line, 'agent-call', raw);
|
|
70
|
+
} else if (name.startsWith('Cron') || name.includes('Wakeup')) residue(state, line, 'unknown-runtime-tool-call', raw);
|
|
71
|
+
if (call != null) {
|
|
72
|
+
if (state.pending.has(id)) fail(`malformed Claude runtime reference at line ${line}: duplicate tool-use id ${id}`);
|
|
73
|
+
state.pending.set(id, call); residue(state, line, 'runtime-tool-call', raw);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
function toolResults(value, line, raw, state) {
|
|
78
|
+
const content = field(field(value, 'message'), 'content'); if (!Array.isArray(content)) return;
|
|
79
|
+
const timestamp = stringField(value, 'timestamp'), nativeResult = field(value, 'toolUseResult');
|
|
80
|
+
for (const block of content) {
|
|
81
|
+
if (stringField(block, 'type') !== 'tool_result') continue;
|
|
82
|
+
const id = stringField(block, 'tool_use_id'); if (id == null) continue;
|
|
83
|
+
const text = resultText(field(block, 'content')), isError = bool(block, 'is_error'), child = state.children.get(id);
|
|
84
|
+
if (child != null) {
|
|
85
|
+
if (isError) Object.assign(child, { state: 'failed', finished_at: timestamp, summary: text });
|
|
86
|
+
else if (bool(nativeResult, 'isAsync')) Object.assign(child, { state: 'running', agent_id: stringField(nativeResult, 'agentId'),
|
|
87
|
+
resolved_model: stringField(nativeResult, 'resolvedModel'), output_file: stringField(nativeResult, 'outputFile') });
|
|
88
|
+
else Object.assign(child, { state: 'completed', finished_at: timestamp, summary: text });
|
|
89
|
+
residue(state, line, 'agent-result', raw); continue;
|
|
90
|
+
}
|
|
91
|
+
const notification = text == null ? null : taskNotification(text);
|
|
92
|
+
if (notification != null) {
|
|
93
|
+
state.notifications.push({ notification, timestamp, line }); residue(state, line, 'agent-notification', raw); continue;
|
|
94
|
+
}
|
|
95
|
+
const call = state.pending.get(id); state.pending.delete(id);
|
|
96
|
+
if (call == null) {
|
|
97
|
+
if (text != null && runtimeResult(text)) fail(`malformed Claude runtime result at line ${line}: unknown tool-use id ${id}`);
|
|
98
|
+
continue;
|
|
99
|
+
}
|
|
100
|
+
residue(state, line, 'runtime-tool-result', raw); if (isError) continue;
|
|
101
|
+
if (text == null) fail(`malformed Claude runtime result at line ${line}: non-text result for ${id}`);
|
|
102
|
+
if (call.kind === 'Invalid') fail(`malformed ${call.name} result at line ${line}: invalid input unexpectedly succeeded`);
|
|
103
|
+
if (call.kind === 'CronCreate') {
|
|
104
|
+
const assigned = cronId(text); if (assigned == null) fail(`malformed CronCreate result at line ${line}: no assigned job id`);
|
|
105
|
+
const job = { id: assigned, tool_use_id: call.tool_use_id, schedule: call.schedule,
|
|
106
|
+
recurring: text.startsWith('Scheduled recurring job ') ? true : text.startsWith('Scheduled one-shot task ') ? false : call.recurring,
|
|
107
|
+
durable_requested: call.durable_requested, prompt: call.prompt, created_at: call.created_at,
|
|
108
|
+
expires_after_seconds: text.includes('Auto-expires after 7 days') ? 604800 : null, creation_result: text };
|
|
109
|
+
if (state.crons.has(assigned)) fail(`malformed CronCreate result at line ${line}: duplicate active job id ${assigned}`);
|
|
110
|
+
state.crons.set(assigned, job);
|
|
111
|
+
} else if (call.kind === 'CronDelete') {
|
|
112
|
+
if (!text.startsWith('Cancelled job ')) fail(`malformed CronDelete result at line ${line}: unexpected success text`);
|
|
113
|
+
if (!state.crons.delete(call.id)) fail(`malformed CronDelete reference at line ${line}: unknown active job id ${call.id}`);
|
|
114
|
+
} else if (call.kind === 'ScheduleWakeup') {
|
|
115
|
+
const scheduled = between(text, 'Next wakeup scheduled for ', ' (in ');
|
|
116
|
+
if (scheduled == null) fail(`malformed ScheduleWakeup result at line ${line}: no scheduled time`);
|
|
117
|
+
state.wakeups.clear();
|
|
118
|
+
const wakeup = { tool_use_id: call.tool_use_id, delay_seconds: 0, reason: call.reason, prompt: call.prompt,
|
|
119
|
+
created_at: call.created_at, scheduled_for: scheduled, creation_result: text };
|
|
120
|
+
setNativeInteger(wakeup, 'delay_seconds', call.delay_seconds); state.wakeups.set(call.tool_use_id, wakeup);
|
|
121
|
+
} else if (call.kind === 'CronList') {
|
|
122
|
+
let parsed; try { parsed = parseNativeJson(text); } catch { /* absent intrinsic result */ }
|
|
123
|
+
// A present record-level null is authoritative too; only absence falls back.
|
|
124
|
+
const result = nativeResult === undefined ? parsed : nativeResult, jobs = field(result, 'jobs');
|
|
125
|
+
if (!Array.isArray(jobs)) fail(`malformed CronList result at line ${line}: missing jobs array`);
|
|
126
|
+
const listed = new Map();
|
|
127
|
+
for (const job of jobs) {
|
|
128
|
+
const jobId = required(job, 'id', line, 'CronList job'), schedule = required(job, 'cron', line, 'CronList job'), previous = state.crons.get(jobId);
|
|
129
|
+
listed.set(jobId, { id: jobId, tool_use_id: previous?.tool_use_id ?? id, schedule,
|
|
130
|
+
recurring: bool(job, 'recurring'), durable_requested: previous?.durable_requested ?? bool(job, 'durable'),
|
|
131
|
+
prompt: required(job, 'prompt', line, 'CronList job'), created_at: previous?.created_at ?? null,
|
|
132
|
+
expires_after_seconds: previous?.expires_after_seconds ?? null, creation_result: previous?.creation_result ?? text });
|
|
133
|
+
}
|
|
134
|
+
state.crons = listed;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
function applyNotification({ notification: note, timestamp, line }, children) {
|
|
139
|
+
const byTask = note.task_id == null ? null : [...children.keys()].sort(order).find(id => children.get(id).agent_id === note.task_id);
|
|
140
|
+
const reference = note.tool_use_id ?? byTask ?? (note.task_id == null ? null : `task:${note.task_id}`);
|
|
141
|
+
if (reference == null) fail(`malformed Claude task notification at line ${line}: missing tool-use-id and task-id`);
|
|
142
|
+
let child = children.get(reference);
|
|
143
|
+
if (child == null) {
|
|
144
|
+
child = emptyChild(reference, null); Object.assign(child, { origin_observed: false, agent_id: note.task_id,
|
|
145
|
+
agent_type: note.summary?.startsWith('Background command ') ? 'background-command' : 'unresolved-task', description: note.summary });
|
|
146
|
+
children.set(reference, child);
|
|
147
|
+
}
|
|
148
|
+
child.agent_id ??= note.task_id;
|
|
149
|
+
child.state = note.status === 'completed' ? 'completed' : ['failed', 'error'].includes(note.status) ? 'failed'
|
|
150
|
+
: note.status === 'killed' ? 'killed' : 'unknown_terminal';
|
|
151
|
+
child.finished_at = timestamp; child.summary = note.summary;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/** Reconstruct all runtime-affecting records before exposing any posture.
|
|
155
|
+
* Invalid unrelated queue/tool/notification references refuse the manifest. */
|
|
156
|
+
export function claudeRuntimeManifest(session) {
|
|
157
|
+
if (session.meta.source !== 'claude_code') fail('Claude runtime state can only be extracted from a Claude Code session');
|
|
158
|
+
if (session.parse_error_lines !== 0) fail(`cannot reconstruct Claude runtime state: ${session.parse_error_lines} malformed JSONL line(s)`);
|
|
159
|
+
const state = { crons: new Map(), pending: new Map(), children: new Map(), wakeups: new Map(), notifications: [], residue: [] };
|
|
160
|
+
const posture = { permission_mode: null, last_prompt_leaf_uuid: null, last_prompt: null, timestamp: null, entrypoint: null, user_type: null, version: null, cwd: null };
|
|
161
|
+
const queue = { enqueued: 0, dequeued: 0, removed: 0, pending: [] };
|
|
162
|
+
let incoming = [], outgoing = [];
|
|
163
|
+
let reported = null;
|
|
164
|
+
const decode = (raw, line) => {
|
|
165
|
+
if (nativeTrim(raw) === '') return null;
|
|
166
|
+
try { return { value: parseNativeJson(raw) }; }
|
|
167
|
+
catch (error) { fail(`cannot reconstruct Claude runtime state: malformed JSON at line ${line}: ${error.message}`); }
|
|
168
|
+
};
|
|
169
|
+
// Calls precede the ordered fold so physically earlier async results can
|
|
170
|
+
// resolve their origins. Each pass releases one parsed record at a time.
|
|
171
|
+
for (let offset = 0; offset < session.raw.length; offset++) {
|
|
172
|
+
const raw = session.raw[offset], line = offset + 1, decoded = decode(raw, line); if (decoded == null) continue;
|
|
173
|
+
if (stringField(decoded.value, 'type') === 'assistant') assistantCalls(decoded.value, line, raw, state);
|
|
174
|
+
}
|
|
175
|
+
for (let offset = 0; offset < session.raw.length; offset++) {
|
|
176
|
+
const raw = session.raw[offset], line = offset + 1, decoded = decode(raw, line); if (decoded == null) continue;
|
|
177
|
+
const { value } = decoded;
|
|
178
|
+
for (const [key, target] of [['timestamp', 'timestamp'], ['entrypoint', 'entrypoint'], ['userType', 'user_type'], ['version', 'version'], ['cwd', 'cwd']]) {
|
|
179
|
+
const text = stringField(value, key); if (text != null) posture[target] = text;
|
|
180
|
+
}
|
|
181
|
+
const kind = stringField(value, 'type') ?? '';
|
|
182
|
+
if (kind === 'permission-mode') { posture.permission_mode = required(value, 'permissionMode', line, 'permission-mode'); residue(state, line, kind, raw); }
|
|
183
|
+
else if (kind === 'last-prompt') {
|
|
184
|
+
posture.last_prompt_leaf_uuid = stringField(value, 'leafUuid'); posture.last_prompt = stringField(value, 'lastPrompt'); residue(state, line, kind, raw);
|
|
185
|
+
} else if (kind === 'queue-operation') {
|
|
186
|
+
const operation = required(value, 'operation', line, kind);
|
|
187
|
+
if (operation === 'enqueue') {
|
|
188
|
+
const content = required(value, 'content', line, 'queue enqueue'); queue.enqueued++; incoming.push(content);
|
|
189
|
+
const fired = [...state.wakeups.keys()].sort(order).find(id => { const held = state.wakeups.get(id); return (held.prompt ?? held.reason) === content; });
|
|
190
|
+
if (fired !== undefined) state.wakeups.delete(fired);
|
|
191
|
+
const notification = taskNotification(content); if (notification != null) state.notifications.push({ notification, timestamp: stringField(value, 'timestamp'), line });
|
|
192
|
+
} else if (operation === 'dequeue' || operation === 'remove') {
|
|
193
|
+
queue[operation === 'dequeue' ? 'dequeued' : 'removed']++;
|
|
194
|
+
if (!outgoing.length) { outgoing = incoming.reverse(); incoming = []; }
|
|
195
|
+
if (!outgoing.length) fail(`malformed Claude queue reference at line ${line}: ${operation} with an empty queue`);
|
|
196
|
+
outgoing.pop();
|
|
197
|
+
} else { residue(state, line, `unknown-queue-operation:${operation}`, raw); continue; }
|
|
198
|
+
residue(state, line, kind, raw);
|
|
199
|
+
} else if (kind === 'user') toolResults(value, line, raw, state);
|
|
200
|
+
else if (kind === 'system') {
|
|
201
|
+
const count = nativeInteger(value, 'pendingBackgroundAgentCount');
|
|
202
|
+
if (count != null) { reported = count; residue(state, line, 'background-count', raw); }
|
|
203
|
+
else if (stringField(value, 'subtype') === 'scheduled_task_fire') residue(state, line, 'scheduled-task-fire', raw);
|
|
204
|
+
} else if (kind !== 'assistant' && runtimeType(kind)) residue(state, line, 'unknown-runtime-record', raw);
|
|
205
|
+
}
|
|
206
|
+
for (const notification of state.notifications) applyNotification(notification, state.children);
|
|
207
|
+
queue.pending = outgoing.reverse().concat(incoming);
|
|
208
|
+
state.residue.sort((left, right) => left.line - right.line);
|
|
209
|
+
const manifest = { schema_version: CLAUDE_RUNTIME_MANIFEST_VERSION, posture, active_crons: sorted(state.crons), pending_wakeups: sorted(state.wakeups), queue,
|
|
210
|
+
background_children: sorted(state.children), reported_pending_background_children: null, residue: state.residue };
|
|
211
|
+
if (reported != null) setNativeInteger(manifest, 'reported_pending_background_children', reported);
|
|
212
|
+
return manifest;
|
|
213
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
// Recorded launch metadata is a reading, never caller authority. The native
|
|
2
|
+
// runtime writer remains until its own replacement boundary.
|
|
3
|
+
import { readFileSync } from 'node:fs';
|
|
4
|
+
import { basename, dirname, isAbsolute, sep } from 'node:path';
|
|
5
|
+
import { parseNativeJson } from './native-json.mjs';
|
|
6
|
+
|
|
7
|
+
export function recordedLaunchAgent(locator, { cwd = process.cwd() } = {}) {
|
|
8
|
+
if (locator.storage.kind !== 'file' || !locator.session_id.length
|
|
9
|
+
|| /[^A-Za-z0-9_-]/.test(locator.session_id)) return null;
|
|
10
|
+
const store = locator.harness === 'codex' ? 'sessions' : locator.harness === 'claude-code' ? 'projects' : null;
|
|
11
|
+
if (store == null) return null;
|
|
12
|
+
// Walk the lexical path, starting with the path itself as Path::ancestors
|
|
13
|
+
// does. Resolving '..' first could choose a different recorded harness home.
|
|
14
|
+
let at = locator.storage.path, home = null;
|
|
15
|
+
for (;;) {
|
|
16
|
+
if (basename(at) === store) { home = dirname(at); break; }
|
|
17
|
+
const parent = dirname(at);
|
|
18
|
+
if (parent === at || at === '.') break;
|
|
19
|
+
at = parent;
|
|
20
|
+
}
|
|
21
|
+
if (home == null) return null;
|
|
22
|
+
// Leave '..' for the filesystem, including its symlink semantics.
|
|
23
|
+
const path = `${isAbsolute(home) ? '' : `${cwd}${sep}`}${home}${sep}supercode${sep}launches${sep}${locator.session_id}.json`;
|
|
24
|
+
try {
|
|
25
|
+
const text = new TextDecoder('utf-8', { fatal: true, ignoreBOM: true }).decode(readFileSync(path));
|
|
26
|
+
const record = parseNativeJson(text);
|
|
27
|
+
const environment = record && !Array.isArray(record) && typeof record === 'object' ? record.environment : null;
|
|
28
|
+
return environment && !Array.isArray(environment) && typeof environment === 'object' ? environment : null;
|
|
29
|
+
} catch { return null; }
|
|
30
|
+
}
|
package/session-read.d.ts
CHANGED
|
@@ -3,10 +3,14 @@ import { SessionFollower } from './session-codecs.js';
|
|
|
3
3
|
import type { ProjectedSessionResult, NormalizedSession, RecordedSessionProfile, RecordedSessionProfileChange } from './session-model.js';
|
|
4
4
|
export const SESSION_READ_METHODS: readonly string[];
|
|
5
5
|
export const SESSION_READ_EVENT: 'harness.v1.sessions.event';
|
|
6
|
-
export const SESSION_READ_CAPABILITY: Readonly<{ version: 1; owner: 'caller'; language: 'typescript'; native_parser: false }>;
|
|
6
|
+
export const SESSION_READ_CAPABILITY: Readonly<{ version: 1; owner: 'caller'; language: 'typescript'; native_parser: false; recorded_config: true; launch_agent: true }>;
|
|
7
7
|
export interface SessionReadContext { cwd?: string; env?: Record<string, string | undefined> }
|
|
8
8
|
export function parseSessionWire(text: string): unknown;
|
|
9
9
|
export function stringifySessionWire(value: unknown): string;
|
|
10
10
|
export function parseSessionReadParams(params: unknown, context?: SessionReadContext): { locator: ReadLocator; fidelity: string; view: (SessionFollowOptions & { fromByte: number | null }) | null; options: import('./session-model.js').SessionLoadOptions | null };
|
|
11
11
|
export function loadSessionRead(params: unknown, context?: SessionReadContext): Promise<ProjectedSessionResult | { session: NormalizedSession & { resolved_profile: RecordedSessionProfile; resolved_profile_history: RecordedSessionProfileChange[] } }>;
|
|
12
12
|
export function openSessionReadFollower(params: unknown, context?: SessionReadContext): Promise<SessionFollower>;
|
|
13
|
+
/** Recorded values only; malformed complete Claude runtime state leaves mode unknown. */
|
|
14
|
+
export function loadRecordedSessionConfiguration(params: unknown, context?: SessionReadContext): Promise<{ harness: string; session_id: string | null; cwd: unknown; model: unknown; approval_policy: unknown; sandbox_policy: unknown; permission_mode: string | null }>;
|
|
15
|
+
/** Best-effort recorded environment only, independent of transcript existence or caller authority. */
|
|
16
|
+
export function loadSessionLaunchAgent(params: unknown, context?: SessionReadContext): Promise<{ environment: Record<string, unknown> | null }>;
|
package/session-read.mjs
CHANGED
|
@@ -8,10 +8,12 @@ import { withClaudeReadIndex, supportsClaudeReadIndex } from './session-claude-i
|
|
|
8
8
|
import { SessionFollower } from './session-follower.mjs';
|
|
9
9
|
import { normalizedSessionJson } from './session-model.mjs';
|
|
10
10
|
import { boundSessionView, projectedMessageWindow, projectedSessionJson, projectedSessionResult, projectedSessionSummary, recordedSessionProfile, recordedSessionProfileHistory } from './session-views.mjs';
|
|
11
|
+
import { recordedSessionConfiguration } from './session-recorded-config.mjs';
|
|
12
|
+
import { recordedLaunchAgent } from './session-launch-agent.mjs';
|
|
11
13
|
|
|
12
|
-
export const SESSION_READ_METHODS = Object.freeze(['harness.v1.sessions.load', 'harness.v1.sessions.follow', 'harness.v1.sessions.unfollow']);
|
|
14
|
+
export const SESSION_READ_METHODS = Object.freeze(['harness.v1.sessions.load', 'harness.v1.sessions.follow', 'harness.v1.sessions.unfollow', 'harness.v1.sessions.recorded_config', 'harness.v1.sessions.launch_agent']);
|
|
13
15
|
export const SESSION_READ_EVENT = 'harness.v1.sessions.event';
|
|
14
|
-
export const SESSION_READ_CAPABILITY = Object.freeze({ version: 1, owner: 'caller', language: 'typescript', native_parser: false });
|
|
16
|
+
export const SESSION_READ_CAPABILITY = Object.freeze({ version: 1, owner: 'caller', language: 'typescript', native_parser: false, recorded_config: true, launch_agent: true });
|
|
15
17
|
export { parseNativeJson as parseSessionWire, stringifyNativeJson as stringifySessionWire };
|
|
16
18
|
const invalid = (message) => { throw Object.assign(new Error(message), { rpcCode: -32602 }); };
|
|
17
19
|
const object = (value, name) => {
|
|
@@ -102,3 +104,20 @@ export async function openSessionReadFollower(params, context = {}) {
|
|
|
102
104
|
return SessionFollower.openLocator(locator, { ...context, fidelity, includeSubagents: view?.includeSubagents ?? true,
|
|
103
105
|
displayHistory: view?.displayHistory ?? false, messageLimit: view?.messageLimit, maxMessageChars: view?.maxMessageChars });
|
|
104
106
|
}
|
|
107
|
+
|
|
108
|
+
export async function loadRecordedSessionConfiguration(params, context = {}) {
|
|
109
|
+
object(params, 'recorded configuration params');
|
|
110
|
+
// LocatorParams ignores unrelated fields. This door always reads the full
|
|
111
|
+
// lossless source, independent of any supplied display/load-view knobs.
|
|
112
|
+
const { locator } = parseSessionReadParams({ locator: params.locator }, context);
|
|
113
|
+
return recordedSessionConfiguration(await loadSessionLocator(locator, { ...context, fidelity: 'byte_lossless', includeSubagents: true }), locator.harness);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export async function loadSessionLaunchAgent(params, context = {}) {
|
|
117
|
+
object(params, 'launch metadata params');
|
|
118
|
+
const { locator } = parseSessionReadParams({ locator: params.locator }, context);
|
|
119
|
+
// This door needs no transcript or parser. Preserve the original lexical
|
|
120
|
+
// path for the harness-home lookup, rather than the normalized load path.
|
|
121
|
+
locator.storage.path = params.locator.storage.path;
|
|
122
|
+
return { environment: recordedLaunchAgent(locator, context) };
|
|
123
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
// The permission reading is exposed only after the complete recorded runtime
|
|
2
|
+
// fold succeeds. Current defaults and a standalone permission line prove none.
|
|
3
|
+
import { parseNativeJson, stringifyNativeJson, stringifyNativeJsonMember } from './native-json.mjs';
|
|
4
|
+
import { field } from './session-codec-utils.mjs';
|
|
5
|
+
import { claudeRuntimeManifest } from './session-claude-runtime.mjs';
|
|
6
|
+
|
|
7
|
+
export function recordedSessionConfiguration(session, harness) {
|
|
8
|
+
const values = new Map([
|
|
9
|
+
['harness', stringifyNativeJson(harness)], ['session_id', stringifyNativeJson(session.meta.session_id)],
|
|
10
|
+
['cwd', stringifyNativeJson(session.meta.cwd)], ['model', stringifyNativeJson(session.meta.model)],
|
|
11
|
+
['approval_policy', 'null'], ['sandbox_policy', 'null'], ['permission_mode', 'null'],
|
|
12
|
+
]);
|
|
13
|
+
for (const header of session.meta.codex_headers) {
|
|
14
|
+
const payload = field(header, 'payload') === undefined ? header : header.payload;
|
|
15
|
+
for (const key of ['cwd', 'model', 'approval_policy', 'sandbox_policy']) {
|
|
16
|
+
if (field(payload, key) !== undefined) values.set(key, stringifyNativeJsonMember(payload, key));
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
try { values.set('permission_mode', stringifyNativeJson(claudeRuntimeManifest(session).posture.permission_mode)); }
|
|
20
|
+
catch { /* unsupported or malformed complete manifest leaves posture unknown */ }
|
|
21
|
+
// Member serialization preserves integer-versus-float facts and opaque
|
|
22
|
+
// policy values, including records whose values have unexpected JSON types.
|
|
23
|
+
return parseNativeJson(`{${[...values].map(([key, value]) => `${JSON.stringify(key)}:${value}`).join(',')}}`);
|
|
24
|
+
}
|