@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 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 source and syntax checks only, with no runtime
46
- qualification claimed yet.
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 or runtime qualification.
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. No runtime qualification is claimed for this
68
- checkpoint.
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",
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
+ }