@volter/supercode-interchange 0.1.11 → 0.1.13

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/file-io.mjs CHANGED
@@ -3,6 +3,10 @@ import { parseNativeJson } from './native-json.mjs';
3
3
 
4
4
  const stale = (path) => Object.assign(new Error(`${path} changed during its metadata read; previous index rows are retained`), { code: 'ESTALE' });
5
5
  const same = (a, b) => a.dev === b.dev && a.ino === b.ino && a.size === b.size && a.mtimeNs === b.mtimeNs;
6
+ // A file its writer appended to while it was read is the same file with more after the bytes read: the facts read are
7
+ // those of the open's size, and the next read takes the rest. A live transcript is written more often than a large
8
+ // read takes, so refusing an append would refuse every read of it.
9
+ const sameOrAppended = (before, now) => same(before, now) || before.dev === now.dev && before.ino === now.ino && now.size > before.size;
6
10
 
7
11
  export function fileFingerprint(info) {
8
12
  return { device: String(info.dev), inode: String(info.ino), size: String(info.size), mtime_ns: String(info.mtimeNs) };
@@ -19,7 +23,9 @@ export function appendableFingerprint(previous, current) {
19
23
 
20
24
  /** Read one open identity; verify both the handle and its current path before
21
25
  * returning facts for a transaction. A changed path never commits new offsets
22
- * beside facts read from its predecessor. The caller owns failure retry. */
26
+ * beside facts read from its predecessor: a file replaced, truncated or rewritten
27
+ * in place is stale, while one only appended to answers for the size it was
28
+ * opened at. The caller owns failure retry. */
23
29
  export async function withSessionFile(path, work, metrics = {}) {
24
30
  const handle = await open(path, 'r');
25
31
  metrics.files_opened = (metrics.files_opened ?? 0) + 1;
@@ -51,7 +57,7 @@ export async function withSessionFile(path, work, metrics = {}) {
51
57
  const after = await handle.stat({ bigint: true });
52
58
  let current;
53
59
  try { current = await stat(path, { bigint: true }); } catch { throw stale(path); }
54
- if (!same(before, after) || !same(after, current)) throw stale(path);
60
+ if (!sameOrAppended(before, after) || !sameOrAppended(before, current) || current.size < after.size) throw stale(path);
55
61
  return { result, fingerprint: reader.fingerprint, size, modified_ms: reader.modified_ms };
56
62
  } finally { await handle.close(); }
57
63
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@volter/supercode-interchange",
3
- "version": "0.1.11",
3
+ "version": "0.1.13",
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",
@@ -2,7 +2,7 @@ import { readdir } from 'node:fs/promises';
2
2
  import { basename, dirname, extname, join } from 'node:path';
3
3
  import { withSessionFile } from './file-io.mjs';
4
4
  import { sourceStat } from './store-registry.mjs';
5
- import { decodeSessionUtf8, readSessionText } from './session-file.mjs';
5
+ import { sessionFileText, readSessionText } from './session-file.mjs';
6
6
  import { parseNativeJson, stringifyNativeJson } from './native-json.mjs';
7
7
  import { fromClaudeCodeStr } from './session-claude.mjs';
8
8
  import { nonEmptyLines } from './session-lines.mjs';
@@ -70,7 +70,7 @@ export async function attachClaudeSubagents(session, mainPath, mainText, fidelit
70
70
  * reconstruction can degrade only in a semantic view, never an export. */
71
71
  export async function readClaudeCodeFile(path, { fidelity = 'byte_lossless', metrics } = {}) {
72
72
  return withSessionFile(path, async (reader) => {
73
- const text = decodeSessionUtf8(await reader.read(0, reader.size));
73
+ const text = await sessionFileText(reader);
74
74
  const session = fromClaudeCodeStr(text, { fidelity });
75
75
  await attachClaudeSubagents(session, path, text, fidelity, metrics);
76
76
  return session;
@@ -151,63 +151,75 @@ function applyNotification({ notification: note, timestamp, line }, children) {
151
151
  child.finished_at = timestamp; child.summary = note.summary;
152
152
  }
153
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)`);
154
+ /** The manifest's fold, one parsed record at a time: every record's calls
155
+ * first, then every record again in order, so physically earlier async
156
+ * results can resolve their origins. `raw` is kept only in the residue. */
157
+ export function claudeRuntimeFold() {
159
158
  const state = { crons: new Map(), pending: new Map(), children: new Map(), wakeups: new Map(), notifications: [], residue: [] };
160
159
  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
160
  const queue = { enqueued: 0, dequeued: 0, removed: 0, pending: [] };
162
161
  let incoming = [], outgoing = [];
163
162
  let reported = null;
163
+ return {
164
+ call(value, line, raw) { if (stringField(value, 'type') === 'assistant') assistantCalls(value, line, raw, state); },
165
+ record(value, line, raw) {
166
+ for (const [key, target] of [['timestamp', 'timestamp'], ['entrypoint', 'entrypoint'], ['userType', 'user_type'], ['version', 'version'], ['cwd', 'cwd']]) {
167
+ const text = stringField(value, key); if (text != null) posture[target] = text;
168
+ }
169
+ const kind = stringField(value, 'type') ?? '';
170
+ if (kind === 'permission-mode') { posture.permission_mode = required(value, 'permissionMode', line, 'permission-mode'); residue(state, line, kind, raw); }
171
+ else if (kind === 'last-prompt') {
172
+ posture.last_prompt_leaf_uuid = stringField(value, 'leafUuid'); posture.last_prompt = stringField(value, 'lastPrompt'); residue(state, line, kind, raw);
173
+ } else if (kind === 'queue-operation') {
174
+ const operation = required(value, 'operation', line, kind);
175
+ if (operation === 'enqueue') {
176
+ const content = required(value, 'content', line, 'queue enqueue'); queue.enqueued++; incoming.push(content);
177
+ const fired = [...state.wakeups.keys()].sort(order).find(id => { const held = state.wakeups.get(id); return (held.prompt ?? held.reason) === content; });
178
+ if (fired !== undefined) state.wakeups.delete(fired);
179
+ const notification = taskNotification(content); if (notification != null) state.notifications.push({ notification, timestamp: stringField(value, 'timestamp'), line });
180
+ } else if (operation === 'dequeue' || operation === 'remove') {
181
+ queue[operation === 'dequeue' ? 'dequeued' : 'removed']++;
182
+ if (!outgoing.length) { outgoing = incoming.reverse(); incoming = []; }
183
+ if (!outgoing.length) fail(`malformed Claude queue reference at line ${line}: ${operation} with an empty queue`);
184
+ outgoing.pop();
185
+ } else { residue(state, line, `unknown-queue-operation:${operation}`, raw); return; }
186
+ residue(state, line, kind, raw);
187
+ } else if (kind === 'user') toolResults(value, line, raw, state);
188
+ else if (kind === 'system') {
189
+ const count = nativeInteger(value, 'pendingBackgroundAgentCount');
190
+ if (count != null) { reported = count; residue(state, line, 'background-count', raw); }
191
+ else if (stringField(value, 'subtype') === 'scheduled_task_fire') residue(state, line, 'scheduled-task-fire', raw);
192
+ } else if (kind !== 'assistant' && runtimeType(kind)) residue(state, line, 'unknown-runtime-record', raw);
193
+ },
194
+ finish() {
195
+ for (const notification of state.notifications) applyNotification(notification, state.children);
196
+ queue.pending = outgoing.reverse().concat(incoming);
197
+ state.residue.sort((left, right) => left.line - right.line);
198
+ const manifest = { schema_version: CLAUDE_RUNTIME_MANIFEST_VERSION, posture, active_crons: sorted(state.crons), pending_wakeups: sorted(state.wakeups), queue,
199
+ background_children: sorted(state.children), reported_pending_background_children: null, residue: state.residue };
200
+ if (reported != null) setNativeInteger(manifest, 'reported_pending_background_children', reported);
201
+ return manifest;
202
+ },
203
+ };
204
+ }
205
+
206
+ /** Reconstruct all runtime-affecting records before exposing any posture.
207
+ * Invalid unrelated queue/tool/notification references refuse the manifest. */
208
+ export function claudeRuntimeManifest(session) {
209
+ if (session.meta.source !== 'claude_code') fail('Claude runtime state can only be extracted from a Claude Code session');
210
+ if (session.parse_error_lines !== 0) fail(`cannot reconstruct Claude runtime state: ${session.parse_error_lines} malformed JSONL line(s)`);
211
+ const fold = claudeRuntimeFold();
164
212
  const decode = (raw, line) => {
165
213
  if (nativeTrim(raw) === '') return null;
166
214
  try { return { value: parseNativeJson(raw) }; }
167
215
  catch (error) { fail(`cannot reconstruct Claude runtime state: malformed JSON at line ${line}: ${error.message}`); }
168
216
  };
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.
217
+ // Each pass releases one parsed record at a time.
171
218
  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);
219
+ const raw = session.raw[offset], line = offset + 1, decoded = decode(raw, line); if (decoded != null) fold.call(decoded.value, line, raw);
174
220
  }
175
221
  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);
222
+ const raw = session.raw[offset], line = offset + 1, decoded = decode(raw, line); if (decoded != null) fold.record(decoded.value, line, raw);
205
223
  }
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;
224
+ return fold.finish();
213
225
  }
@@ -1,7 +1,7 @@
1
1
  import { withSessionFile } from './file-io.mjs';
2
2
  import { sourceStat } from './store-registry.mjs';
3
3
  import { verifySources } from './source-files.mjs';
4
- import { sessionFileLines, decodeSessionUtf8 } from './session-file.mjs';
4
+ import { sessionFileLines, sessionFileBytes, decodeSessionUtf8 } from './session-file.mjs';
5
5
  import { field, stringField, truncateMessagesWithAnchor } from './session-codec-utils.mjs';
6
6
  import { parseNativeJson } from './native-json.mjs';
7
7
  import { nativeTrim } from './text.mjs';
@@ -125,7 +125,7 @@ export async function loadSessionDisplayFrom(locator, fromByte, options = {}) {
125
125
  const captured = await withSessionFile(path, async (reader) => {
126
126
  const first = await firstLine(reader); if (detectSessionSource(first) !== 'codex') return null;
127
127
  if (fromByte >= reader.size) throw new Error(`${path} is shorter than its display anchor (${fromByte})`);
128
- let bytes = await reader.read(fromByte, reader.size - fromByte);
128
+ let bytes = await sessionFileBytes(reader, fromByte);
129
129
  const newline = bytes.indexOf(10); bytes = newline === -1 ? Buffer.alloc(0) : bytes.subarray(newline + 1);
130
130
  let tail; try { tail = decodeSessionUtf8(bytes); } catch (error) { throw new Error(`${path} contains non-UTF-8 data after its display anchor`, { cause: error }); }
131
131
  const session = fromCodexDisplayStr(first + tail, unbounded);
package/session-file.mjs CHANGED
@@ -1,7 +1,31 @@
1
+ import { constants } from 'node:buffer';
1
2
  import { withSessionFile } from './file-io.mjs';
2
3
 
4
+ const MAX_TEXT = constants.MAX_STRING_LENGTH;
5
+ /** The session-read doors' answer for a file this reader cannot read for what is in it (text that is not UTF-8,
6
+ * or too long to hold): the same bytes get the same answer from the same reader. Any other failure of a read (the
7
+ * disk, a file changed under it, a fault in the reader) answers the generic code and says nothing of the file. */
8
+ export const SESSION_UNREADABLE = -32050;
9
+ export const textTooLong = (path, cause) => Object.assign(new Error(`${path} holds more text than this runtime keeps in one string (${MAX_TEXT} UTF-16 units); it can be read record by record, never as one document`, { cause }), { code: 'EFBIG', rpcCode: SESSION_UNREADABLE });
3
10
  export const decodeSessionUtf8 = (bytes) => new TextDecoder('utf-8', { fatal: true, ignoreBOM: true }).decode(bytes);
4
11
 
12
+ /** The file from `from` to its end, for a reader that needs it as one text.
13
+ * Bytes whose text cannot be one string are refused as that before they are
14
+ * held: their UTF-16 length is counted a bounded block at a time. A compressed
15
+ * file's bytes are not its text, which is measured where it is decoded. */
16
+ export async function sessionFileBytes(reader, from = 0) {
17
+ if (reader.size - from > MAX_TEXT && !reader.path.endsWith('.zst')) {
18
+ let units = 0;
19
+ for (let offset = from; offset < reader.size && units <= MAX_TEXT;) {
20
+ const block = await reader.read(offset, Math.min(1024 * 1024, reader.size - offset)); offset += block.length;
21
+ for (let at = 0; at < block.length; at++) { const byte = block[at]; if ((byte & 0xc0) !== 0x80) units += byte >= 0xf0 ? 2 : 1; }
22
+ }
23
+ if (units > MAX_TEXT) throw textTooLong(reader.path);
24
+ }
25
+ return reader.read(from, reader.size - from);
26
+ }
27
+ export const sessionFileText = async (reader) => decodeSessionUtf8(await sessionFileBytes(reader));
28
+
5
29
  /** One physical line at a time, including an unterminated final line. LF and
6
30
  * CR remain in the bytes; offsets always address the verified open file. */
7
31
  export async function* sessionFileLines(reader) {
@@ -22,6 +46,6 @@ export async function* sessionFileLines(reader) {
22
46
  }
23
47
 
24
48
  export async function readSessionText(path, metrics) {
25
- const capture = await withSessionFile(path, async (reader) => decodeSessionUtf8(await reader.read(0, reader.size)), metrics);
49
+ const capture = await withSessionFile(path, sessionFileText, metrics);
26
50
  return capture.result;
27
51
  }
@@ -2,7 +2,7 @@ import { dirname, join } from 'node:path';
2
2
  import { readStore, statement, tableColumns, text, integer } from './sqlite-reader.mjs';
3
3
  import { sourceStat } from './store-registry.mjs';
4
4
  import { optionalFile, verifySources } from './source-files.mjs';
5
- import { decodeSessionUtf8 } from './session-file.mjs';
5
+ import { sessionFileText } from './session-file.mjs';
6
6
  import { parseNativeJson, setNativeInteger, setNativeFloat } from './native-json.mjs';
7
7
  import { isObject, stringField } from './session-codec-utils.mjs';
8
8
  import { fromOpenCodeStr, opencodeEnvelope } from './session-opencode.mjs';
@@ -90,7 +90,7 @@ export async function readOpenCodeSqlite(path, sessionId = null, options = {}) {
90
90
  }, options.metrics);
91
91
  const { id, lines } = captured.result, sidecarPath = join(dirname(path), 'storage', 'session_diff', `${id}.json`);
92
92
  const sidecar = await optionalFile(sidecarPath, async (reader) => {
93
- const source = decodeSessionUtf8(await reader.read(0, reader.size));
93
+ const source = await sessionFileText(reader);
94
94
  try { return { parsed: true, value: parseNativeJson(source) }; }
95
95
  catch (error) { diagnostic(options, { message: 'opencode session_diff sidecar failed to parse as JSON — skipping', path: sidecarPath, error: error.message }); return { parsed: false }; }
96
96
  }, options.metrics);
package/session-path.mjs CHANGED
@@ -6,7 +6,7 @@ import { withSessionFile, fileFingerprint } from './file-io.mjs';
6
6
  import { sourceStat } from './store-registry.mjs';
7
7
  import { optionalFile, verifySources, decodeWorkspaceFolder } from './source-files.mjs';
8
8
  import { readStore, statement } from './sqlite-reader.mjs';
9
- import { decodeSessionUtf8 } from './session-file.mjs';
9
+ import { decodeSessionUtf8, sessionFileBytes, sessionFileText, textTooLong, SESSION_UNREADABLE } from './session-file.mjs';
10
10
  import { field, stringField, valueString } from './session-codec-utils.mjs';
11
11
  import { parseNativeJson } from './native-json.mjs';
12
12
  import { nonEmptyLines } from './session-lines.mjs';
@@ -19,15 +19,17 @@ import { readGooseSqlite } from './session-goose.mjs';
19
19
  import { readHermesSqlite } from './session-hermes.mjs';
20
20
 
21
21
  export const isCompressedSessionPath = (path) => extname(path) === '.zst';
22
+ /** Text too long for one string is said to be that; any other undecodable text is not session text. */
23
+ export const sessionTextError = (path, cause) => cause?.code === 'ERR_STRING_TOO_LONG' ? textTooLong(path, cause)
24
+ : Object.assign(new Error(`${path} is not valid UTF-8 text, and is not a recognized OpenCode SQLite store (no \`SQLite format 3\` header) — supercode reads session logs as UTF-8 JSONL, or an OpenCode \`opencode*.db\` SQLite file`, { cause }), { rpcCode: SESSION_UNREADABLE });
22
25
  export async function sessionReaderText(reader) {
23
- let bytes = await reader.read(0, reader.size);
26
+ let bytes = await sessionFileBytes(reader);
24
27
  if (isCompressedSessionPath(reader.path)) {
25
28
  if (typeof zlib.zstdDecompress !== 'function') throw new Error('compressed transcripts require Node 22.15 or later with zstdDecompress');
26
29
  try { bytes = await promisify(zlib.zstdDecompress)(bytes); }
27
30
  catch (error) { throw new Error(`${reader.path} is not a readable zstd stream: ${error.message}`, { cause: error }); }
28
31
  }
29
- try { return decodeSessionUtf8(bytes); }
30
- catch (error) { throw new Error(`${reader.path} is not valid UTF-8 text, and is not a recognized OpenCode SQLite store (no \`SQLite format 3\` header) — supercode reads session logs as UTF-8 JSONL, or an OpenCode \`opencode*.db\` SQLite file`, { cause: error }); }
32
+ try { return decodeSessionUtf8(bytes); } catch (error) { throw sessionTextError(reader.path, error); }
31
33
  }
32
34
 
33
35
  export async function looksLikeSessionSqlite(path, metrics) {
@@ -55,7 +57,7 @@ export async function captureGrokPathMetadata(session, path, metrics) {
55
57
  const workspace = dirname(directory);
56
58
  session.meta.cwd = workspace === '.' || workspace === sep ? null : decodeWorkspaceFolder(basename(workspace));
57
59
  const summary = await optionalFile(join(directory, 'summary.json'), async (reader) => {
58
- const source = decodeSessionUtf8(await reader.read(0, reader.size));
60
+ const source = await sessionFileText(reader);
59
61
  try { return parseNativeJson(source); } catch { return null; }
60
62
  }, metrics);
61
63
  const model = stringField(summary.result, 'current_model_id'); if (model != null) session.meta.model = model;
@@ -73,7 +75,7 @@ export async function loadNativeStoreFamily(path, { metrics } = {}) {
73
75
  const sidecar = await sourceStat(sidecarPath), sources = [{ path: metaPath, fingerprint: fileFingerprint(meta) }];
74
76
  sources.push(sidecar ? { path: sidecarPath, fingerprint: fileFingerprint(sidecar) } : { path: sidecarPath, missing: true });
75
77
  const session = (await withSessionFile(sidecar?.isFile() ? sidecarPath : path, async (reader) => {
76
- const text = decodeSessionUtf8(await reader.read(0, reader.size));
78
+ const text = await sessionFileText(reader);
77
79
  return sidecar?.isFile() ? fromSidecarStr(text) : fromNativeMessages([...nonEmptyLines(text)].map((line) => deserializeMessage(parseNativeJson(line))));
78
80
  }, metrics)).result;
79
81
  await verifySources(sources); session.meta.session_id = name; return session;
package/session-read.d.ts CHANGED
@@ -3,6 +3,8 @@ 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
+ /** The read doors' error code for a file this reader cannot read for what is in it. */
7
+ export const SESSION_UNREADABLE: -32050;
6
8
  export const SESSION_READ_CAPABILITY: Readonly<{ version: 1; owner: 'caller'; language: 'typescript'; native_parser: false; recorded_config: true; launch_agent: true; usage: true; spend: true; resume_instructions: true }>;
7
9
  export interface SessionReadContext { cwd?: string; env?: Record<string, string | undefined> }
8
10
  export function parseSessionWire(text: string): unknown;
package/session-read.mjs CHANGED
@@ -8,9 +8,11 @@ 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';
11
+ import { recordedConfigurationFold, recordedSessionConfiguration } from './session-recorded-config.mjs';
12
+ import { claudeRuntimeFold } from './session-claude-runtime.mjs';
13
+ import { withSessionRecords, sessionMetaFold } from './session-records.mjs';
12
14
  import { recordedLaunchAgent } from './session-launch-agent.mjs';
13
- import { recordedSessionUsage } from './session-usage.mjs';
15
+ import { recordedSessionUsage, sessionUsageFold } from './session-usage.mjs';
14
16
  import { loadMachineSpend } from './session-spend.mjs';
15
17
  import { sessionResumeLaunch } from './session-resume-instructions.mjs';
16
18
  import { parseUsageSince, usageMoment } from './session-usage-time.mjs';
@@ -20,6 +22,7 @@ export const SESSION_READ_EVENT = 'harness.v1.sessions.event';
20
22
  export const SESSION_READ_CAPABILITY = Object.freeze({ version: 1, owner: 'caller', language: 'typescript', native_parser: false, recorded_config: true, launch_agent: true, usage: true, spend: true, resume_instructions: true });
21
23
  export { loadMachineSpend as loadSessionSpend };
22
24
  export { parseNativeJson as parseSessionWire, stringifyNativeJson as stringifySessionWire };
25
+ export { SESSION_UNREADABLE } from './session-file.mjs';
23
26
  export { stringifyNativeJsonPretty as prettySessionWire };
24
27
  const invalid = (message) => { throw Object.assign(new Error(message), { rpcCode: -32602 }); };
25
28
  const object = (value, name) => {
@@ -116,6 +119,25 @@ export async function loadRecordedSessionConfiguration(params, context = {}) {
116
119
  // LocatorParams ignores unrelated fields. This door always reads the full
117
120
  // lossless source, independent of any supplied display/load-view knobs.
118
121
  const { locator } = parseSessionReadParams({ locator: params.locator }, context);
122
+ const folded = await withSessionRecords(locator, async (source, records) => {
123
+ const fold = recordedConfigurationFold(), meta = sessionMetaFold(source, fold);
124
+ // The runtime fold takes every record's calls before any record: the file is read once more for it.
125
+ const runtime = source === 'claude_code' ? claudeRuntimeFold() : null;
126
+ let posture = runtime != null;
127
+ for await (const record of records()) {
128
+ meta.observe(record);
129
+ if (record.value === undefined) posture = false;
130
+ else if (posture) try { runtime.call(record.value, record.index + 1, null); } catch { posture = false; }
131
+ }
132
+ // Only the manifest's own refusal leaves the posture unknown: a read that fails fails the door.
133
+ if (posture) for await (const record of records()) {
134
+ try { runtime.record(record.value, record.index + 1, null); } catch { posture = false; break; }
135
+ }
136
+ let mode = null;
137
+ if (posture) try { mode = runtime.finish().posture.permission_mode; } catch { /* unsupported or malformed complete manifest */ }
138
+ return fold.finish(meta.meta, locator.harness, mode);
139
+ }, context);
140
+ if (folded != null) return folded.result;
119
141
  return recordedSessionConfiguration(await loadSessionLocator(locator, { ...context, fidelity: 'byte_lossless', includeSubagents: true }), locator.harness);
120
142
  }
121
143
 
@@ -132,10 +154,19 @@ export async function loadSessionUsage(params, context = {}) {
132
154
  object(params, 'session usage params');
133
155
  const { locator } = parseSessionReadParams({ locator: params.locator }, context);
134
156
  const since = params.since == null ? null : parseUsageSince(string(params.since, 'since'));
135
- // This door reads the complete lossless session, including child records,
136
- // and ignores unrelated load/view options rather than applying a window.
137
- const session = await loadSessionLocator(locator, { ...context, fidelity: 'byte_lossless', includeSubagents: true });
138
- const answer = { harness: locator.harness, session_id: session.meta.session_id, usage: recordedSessionUsage(session, since) };
157
+ // This door reads every record of the session and ignores unrelated
158
+ // load/view options rather than applying a window.
159
+ const folded = await withSessionRecords(locator, async (source, records) => {
160
+ const meta = sessionMetaFold(source), usage = sessionUsageFold(source, since);
161
+ for await (const record of records()) { meta.observe(record); if (record.value !== undefined) usage.record(record.value, meta.meta.model); }
162
+ return { session_id: meta.meta.session_id, usage: usage.finish() };
163
+ }, context);
164
+ let recorded = folded?.result;
165
+ if (recorded == null) {
166
+ const session = await loadSessionLocator(locator, { ...context, fidelity: 'byte_lossless', includeSubagents: true });
167
+ recorded = { session_id: session.meta.session_id, usage: recordedSessionUsage(session, since) };
168
+ }
169
+ const answer = { harness: locator.harness, ...recorded };
139
170
  if (since != null) answer.since = usageMoment(since);
140
171
  return answer;
141
172
  }
@@ -148,6 +179,11 @@ export async function loadSessionResumeInstructions(params, context = {}) {
148
179
  if (!['default', 'yolo'].includes(policy)) invalid(`unknown resume policy: ${String(policy)}`);
149
180
  // Native admission loads the source even when cwd is explicit, and its
150
181
  // launch identity comes from the locator rather than a transcript header.
151
- const session = await loadSessionLocator(locator, { ...context, fidelity: 'byte_lossless', includeSubagents: true });
152
- return { launch: sessionResumeLaunch(locator.harness, locator.session_id, cwd ?? session.meta.cwd ?? '.', policy, context) };
182
+ const folded = await withSessionRecords(locator, async (source, records) => {
183
+ const meta = sessionMetaFold(source);
184
+ for await (const record of records()) meta.observe(record);
185
+ return meta.meta;
186
+ }, context);
187
+ const meta = folded?.result ?? (await loadSessionLocator(locator, { ...context, fidelity: 'byte_lossless', includeSubagents: true })).meta;
188
+ return { launch: sessionResumeLaunch(locator.harness, locator.session_id, cwd ?? meta.cwd ?? '.', policy, context) };
153
189
  }
@@ -4,21 +4,36 @@ import { parseNativeJson, stringifyNativeJson, stringifyNativeJsonMember } from
4
4
  import { field } from './session-codec-utils.mjs';
5
5
  import { claudeRuntimeManifest } from './session-claude-runtime.mjs';
6
6
 
7
+ const HEADER_KEYS = ['cwd', 'model', 'approval_policy', 'sandbox_policy'];
8
+
9
+ /** Codex headers in recorded order, the last value of each key standing. */
10
+ export function recordedConfigurationFold() {
11
+ let recorded = new Map();
12
+ return {
13
+ clear() { recorded = new Map(); },
14
+ header(header) {
15
+ const payload = field(header, 'payload') === undefined ? header : header.payload;
16
+ for (const key of HEADER_KEYS) if (field(payload, key) !== undefined) recorded.set(key, stringifyNativeJsonMember(payload, key));
17
+ },
18
+ // Member serialization preserves integer-versus-float facts and opaque
19
+ // policy values, including records whose values have unexpected JSON types.
20
+ finish(meta, harness, permissionMode = null) {
21
+ const values = new Map([
22
+ ['harness', stringifyNativeJson(harness)], ['session_id', stringifyNativeJson(meta.session_id)],
23
+ ['cwd', stringifyNativeJson(meta.cwd)], ['model', stringifyNativeJson(meta.model)],
24
+ ['approval_policy', 'null'], ['sandbox_policy', 'null'], ['permission_mode', stringifyNativeJson(permissionMode)],
25
+ ]);
26
+ for (const [key, value] of recorded) values.set(key, value);
27
+ return parseNativeJson(`{${[...values].map(([key, value]) => `${JSON.stringify(key)}:${value}`).join(',')}}`);
28
+ },
29
+ };
30
+ }
31
+
7
32
  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)); }
33
+ const fold = recordedConfigurationFold();
34
+ for (const header of session.meta.codex_headers) fold.header(header);
35
+ let permissionMode = null;
36
+ try { permissionMode = claudeRuntimeManifest(session).posture.permission_mode; }
20
37
  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(',')}}`);
38
+ return fold.finish(session.meta, harness, permissionMode);
24
39
  }
@@ -0,0 +1,88 @@
1
+ // A door that folds over a transcript's records reads them one at a time: the
2
+ // file is never held as one buffer or one string, so its size bounds nothing.
3
+ // Only a plain Claude Code or Codex JSONL file is read this way. Every other
4
+ // thing a locator names answers null here and is loaded as its document.
5
+ import { basename, dirname, extname, join } from 'node:path';
6
+ import { withSessionFile } from './file-io.mjs';
7
+ import { sourceStat } from './store-registry.mjs';
8
+ import { sessionFileLines } from './session-file.mjs';
9
+ import { parseNativeJson } from './native-json.mjs';
10
+ import { nativeTrim } from './text.mjs';
11
+ import { field, stringField } from './session-codec-utils.mjs';
12
+ import { createSession } from './session-model.mjs';
13
+ import { sessionRecordSource } from './session-source.mjs';
14
+ import { captureClaudeMeta } from './session-claude.mjs';
15
+ import { captureCodexMeta } from './session-codex.mjs';
16
+ import { restoreTopLevelResidue } from './session-native-residue.mjs';
17
+ import { isCompressedSessionPath, looksLikeSessionSqlite, sessionTextError } from './session-path.mjs';
18
+
19
+ async function* lines(reader) {
20
+ try { yield* sessionFileLines(reader); }
21
+ catch (error) { throw ['ERR_ENCODING_INVALID_ENCODED_DATA', 'ERR_STRING_TOO_LONG'].includes(error?.code) ? sessionTextError(reader.path, error) : error; }
22
+ }
23
+
24
+ /** The document reader tries the whole text as one JSON value before its
25
+ * lines. A first line that is no complete value may open such a document,
26
+ * so only a file whose first line parses is decided from its records. */
27
+ async function recordedSource(reader) {
28
+ let first = true;
29
+ for await (const { text } of lines(reader)) {
30
+ const line = nativeTrim(text); if (line === '') continue;
31
+ let value; try { value = parseNativeJson(line); } catch { if (first) return null; continue; }
32
+ first = false;
33
+ const source = sessionRecordSource(value); if (source != null) return source;
34
+ }
35
+ return null;
36
+ }
37
+
38
+ /** Each non-blank line with its index among the file's lines: `value` is
39
+ * absent when the line is not JSON. Every call reads the file from its start. */
40
+ async function* records(reader) {
41
+ let index = 0;
42
+ for await (const { text } of lines(reader)) {
43
+ const raw = text.endsWith('\n') ? text.slice(0, -1) : text, at = index++;
44
+ if (nativeTrim(raw) === '') continue;
45
+ let value; try { value = parseNativeJson(raw); } catch { /* malformed */ }
46
+ yield { index: at, raw, value };
47
+ }
48
+ }
49
+
50
+ /** `work(source, records)` over the transcript a locator names, or null when
51
+ * it is not a plain Claude Code or Codex JSONL file. */
52
+ export async function withSessionRecords(locator, work, { metrics } = {}) {
53
+ const { path, kind } = locator.storage;
54
+ if (kind !== 'file' || locator.harness === 'hermes' || isCompressedSessionPath(path)) return null;
55
+ if ((await sourceStat(join(dirname(path), `${basename(path, extname(path))}.meta.json`)))?.isFile()) return null;
56
+ if (!(await sourceStat(path))?.isFile() || await looksLikeSessionSqlite(path, metrics)) return null;
57
+ return (await withSessionFile(path, async (reader) => {
58
+ const source = await recordedSource(reader);
59
+ return ['claude_code', 'codex'].includes(source) ? { result: await work(source, () => records(reader)) } : null;
60
+ }, metrics)).result;
61
+ }
62
+
63
+ /** The header facts the document codecs keep in `meta`, from the same records
64
+ * in the same order. `headers` takes each Codex header as it stands, and is
65
+ * cleared when a restored provenance (in a record of either dialect)
66
+ * replaces those read before it. */
67
+ export function sessionMetaFold(source, headers = null) {
68
+ const { meta } = createSession(source);
69
+ let restored = false, held = meta.codex_headers;
70
+ const replaced = () => {
71
+ if (meta.codex_headers === held) return;
72
+ held = meta.codex_headers; headers?.clear(); for (const header of held) headers?.header(header);
73
+ };
74
+ return {
75
+ meta,
76
+ observe({ raw, value }) {
77
+ if (source === 'claude_code') { if (value !== undefined) { captureClaudeMeta(value, meta, raw); replaced(); } return; }
78
+ // The Codex codec parses its trimmed line.
79
+ if (value === undefined) { try { value = parseNativeJson(nativeTrim(raw)); } catch { return; } }
80
+ const kind = stringField(value, 'type'), payload = field(value, 'payload');
81
+ if (!restored && kind === 'session_meta' && restoreTopLevelResidue(payload, meta)) { restored = true; replaced(); }
82
+ if (kind === 'session_meta') captureCodexMeta(payload, meta);
83
+ else if (kind === 'turn_context') meta.model ??= stringField(payload, 'model');
84
+ else return;
85
+ if (!restored) headers?.header(value);
86
+ },
87
+ };
88
+ }
@@ -3,28 +3,45 @@ import { field, stringField } from './session-codec-utils.mjs';
3
3
  import { nonEmptyLines } from './session-lines.mjs';
4
4
  import { nativeTrim } from './text.mjs';
5
5
 
6
+ function documentSource(document) {
7
+ if (Array.isArray(field(document, 'conversation')) && (field(document, 'working_dir') !== undefined || field(document, 'workingDir') !== undefined)) return 'goose';
8
+ if (field(document, 'info') !== undefined && Array.isArray(field(document, 'messages'))) return 'opencode';
9
+ return null;
10
+ }
11
+
12
+ /** `pi` is a session header: its file's body decides between Pi and OpenClaw. */
13
+ function recordSource(value) {
14
+ if (Array.isArray(field(value, 'key')) && field(value, 'value') !== undefined) return 'opencode';
15
+ if (field(value, 'payload') !== undefined) return 'codex';
16
+ const kind = stringField(value, 'type');
17
+ if (stringField(value, 'sessionId') != null && ['projectHash', 'startTime', 'lastUpdated'].some((key) => field(value, key) !== undefined) && field(value, 'type') === undefined) return 'gemini';
18
+ if (kind === 'gemini' && field(value, 'content') !== undefined) return 'gemini';
19
+ if (['message', 'uuid', 'sessionId'].every((key) => field(value, key) === undefined) && ['system', 'user', 'assistant', 'tool_result', 'reasoning', 'backend_tool_call'].includes(kind) && ['content', 'tool_calls', 'tool_call_id', 'encrypted_content', 'kind'].some((key) => field(value, key) !== undefined)) return 'grok';
20
+ if (kind === 'session' && stringField(value, 'id') != null && field(value, 'message') === undefined && field(value, 'uuid') === undefined) return 'pi';
21
+ if (field(value, 'type') !== undefined || field(value, 'message') !== undefined) return 'claude_code';
22
+ return null;
23
+ }
24
+
25
+ /** The dialect one record proves when it is read apart from the rest of its
26
+ * file, a whole-document store's included; null when it proves none. */
27
+ export const sessionRecordSource = (value) => documentSource(value) ?? recordSource(value);
28
+
6
29
  /** Source provenance is recovered structurally; discussing a harness never
7
30
  * selects its dialect. Corrupt lines are tolerated by the native detector. */
8
31
  export function detectSessionSource(text) {
9
32
  let document; try { document = parseNativeJson(nativeTrim(text)); } catch { /* JSONL is considered below. */ }
10
- if (Array.isArray(field(document, 'conversation')) && (field(document, 'working_dir') !== undefined || field(document, 'workingDir') !== undefined)) return 'goose';
11
- if (field(document, 'info') !== undefined && Array.isArray(field(document, 'messages'))) return 'opencode';
33
+ const whole = documentSource(document); if (whole != null) return whole;
12
34
  for (const line of nonEmptyLines(text)) {
13
35
  let value; try { value = parseNativeJson(line); } catch { continue; }
14
- if (Array.isArray(field(value, 'key')) && field(value, 'value') !== undefined) return 'opencode';
15
- if (field(value, 'payload') !== undefined) return 'codex';
16
- const kind = stringField(value, 'type');
17
- if (stringField(value, 'sessionId') != null && ['projectHash', 'startTime', 'lastUpdated'].some((key) => field(value, key) !== undefined) && field(value, 'type') === undefined) return 'gemini';
18
- if (kind === 'gemini' && field(value, 'content') !== undefined) return 'gemini';
19
- if (['message', 'uuid', 'sessionId'].every((key) => field(value, key) === undefined) && ['system', 'user', 'assistant', 'tool_result', 'reasoning', 'backend_tool_call'].includes(kind) && ['content', 'tool_calls', 'tool_call_id', 'encrypted_content', 'kind'].some((key) => field(value, key) !== undefined)) return 'grok';
20
- if (kind === 'session' && stringField(value, 'id') != null && field(value, 'message') === undefined && field(value, 'uuid') === undefined) {
36
+ const source = recordSource(value);
37
+ if (source === 'pi') {
21
38
  for (const body of nonEmptyLines(text)) {
22
39
  let entry; try { entry = parseNativeJson(body); } catch { continue; }
23
40
  if ((stringField(entry, 'type') === 'leaf' && field(entry, 'targetId') !== undefined) || (stringField(entry, 'type') === 'message' && field(field(entry, 'message'), '__openclaw') !== undefined)) return 'openclaw';
24
41
  }
25
42
  return 'pi';
26
43
  }
27
- if (field(value, 'type') !== undefined || field(value, 'message') !== undefined) return 'claude_code';
44
+ if (source != null) return source;
28
45
  }
29
46
  return null;
30
47
  }
package/session-usage.mjs CHANGED
@@ -69,7 +69,10 @@ function* records(raw) {
69
69
  for (const line of raw) { let record; try { record = parseNativeJson(line); } catch { continue; } yield record; }
70
70
  }
71
71
 
72
- export function recordedSessionUsage(session, since = null) {
72
+ /** A session's usage folded from its parsed records in recorded order.
73
+ * `model` is the session's recorded model where one is known by that record:
74
+ * a Codex count read before any model is priced once the first is recorded. */
75
+ export function sessionUsageFold(source, since = null) {
73
76
  const days = new Map();
74
77
  const count = (record) => {
75
78
  if (since != null && (record.atMs == null || record.atMs < since)) return;
@@ -85,29 +88,41 @@ export function recordedSessionUsage(session, since = null) {
85
88
  sum.cost += cost * (record.fast ? rate[5] : 1) * (record.usOnly ? rate[6] : 1); sum.priced = true;
86
89
  }
87
90
  };
88
- if (session.meta.source === 'claude_code') {
89
- const responses = new Map();
90
- for (const record of records(session.raw)) { const response = claudeUse(record); if (response) responses.set(...response); }
91
- for (const id of [...responses.keys()].sort(sorted)) count(responses.get(id));
92
- } else if (session.meta.source === 'codex') {
93
- let model = session.meta.model ?? 'unknown', previous = [0n, 0n, 0n];
94
- for (const record of records(session.raw)) {
91
+ const responses = new Map();
92
+ let turnModel = null, previous = [0n, 0n, 0n], unnamed = [];
93
+ const named = (model) => { for (const use of unnamed) count({ ...use, model }); unnamed = []; };
94
+ return {
95
+ record(record, model = null) {
96
+ if (source === 'claude_code') { const response = claudeUse(record); if (response) responses.set(...response); return; }
97
+ if (source !== 'codex') return;
98
+ if (model != null && unnamed.length) named(model);
95
99
  const payload = field(record, 'payload');
96
- if (stringField(record, 'type') === 'turn_context') { model = stringField(payload, 'model') ?? model; continue; }
97
- if (stringField(payload, 'type') !== 'token_count') continue;
98
- const total = field(field(payload, 'info'), 'total_token_usage'); if (total === undefined) continue;
100
+ if (stringField(record, 'type') === 'turn_context') { turnModel = stringField(payload, 'model') ?? turnModel; return; }
101
+ if (stringField(payload, 'type') !== 'token_count') return;
102
+ const total = field(field(payload, 'info'), 'total_token_usage'); if (total === undefined) return;
99
103
  const now = ['input_tokens', 'output_tokens', 'cached_input_tokens'].map((key) => tokens(total, key));
100
- if (now.every((n, i) => n === previous[i])) continue;
104
+ if (now.every((n, i) => n === previous[i])) return;
101
105
  const [input, output, cached] = now.map((n, i) => sub(n, previous[i])); previous = now;
102
- count(recorded(record, model, [sub(input, cached), output, cached, 0n, 0n]));
103
- }
104
- }
105
- return [...days.values()].sort((a, b) => sorted(a.day, b.day) || sorted(a.model, b.model)).map((sum) => {
106
- const row = { at: `${sum.day}T00:00:00Z`, model: sum.model };
107
- for (const [key, count] of [['input_tokens', sum.counts[0]], ['output_tokens', sum.counts[1]], ['cache_read_tokens', sum.counts[2]],
108
- ['cache_write_tokens', wrap(sum.counts[3] + sum.counts[4])], ['cache_write_5m_tokens', sum.counts[3]], ['cache_write_1h_tokens', sum.counts[4]]]) setNativeInteger(row, key, count);
109
- if (sum.priced) { row.cost = { currency: 'USD', source: 'rate_table' }; setNativeFloat(row.cost, 'amount', Math.round(sum.cost * 1e6) / 1e6); }
110
- if (sum.unpriced) row.cost_unrecorded = true;
111
- return row;
112
- });
106
+ const use = recorded(record, turnModel ?? model, [sub(input, cached), output, cached, 0n, 0n]);
107
+ if (use.model == null) unnamed.push(use); else count(use);
108
+ },
109
+ finish() {
110
+ named('unknown');
111
+ for (const id of [...responses.keys()].sort(sorted)) count(responses.get(id));
112
+ return [...days.values()].sort((a, b) => sorted(a.day, b.day) || sorted(a.model, b.model)).map((sum) => {
113
+ const row = { at: `${sum.day}T00:00:00Z`, model: sum.model };
114
+ for (const [key, count] of [['input_tokens', sum.counts[0]], ['output_tokens', sum.counts[1]], ['cache_read_tokens', sum.counts[2]],
115
+ ['cache_write_tokens', wrap(sum.counts[3] + sum.counts[4])], ['cache_write_5m_tokens', sum.counts[3]], ['cache_write_1h_tokens', sum.counts[4]]]) setNativeInteger(row, key, count);
116
+ if (sum.priced) { row.cost = { currency: 'USD', source: 'rate_table' }; setNativeFloat(row.cost, 'amount', Math.round(sum.cost * 1e6) / 1e6); }
117
+ if (sum.unpriced) row.cost_unrecorded = true;
118
+ return row;
119
+ });
120
+ },
121
+ };
122
+ }
123
+
124
+ export function recordedSessionUsage(session, since = null) {
125
+ const fold = sessionUsageFold(session.meta.source, since);
126
+ for (const record of records(session.raw)) fold.record(record, session.meta.model);
127
+ return fold.finish();
113
128
  }