@volter/supercode-interchange 0.1.20 → 0.1.22
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/discovery.mjs +18 -6
- package/index.d.ts +2 -0
- package/package.json +1 -1
- package/previews.mjs +25 -3
- package/session-claude-index.mjs +35 -5
package/discovery.mjs
CHANGED
|
@@ -10,7 +10,7 @@ export async function enrichDescriptor(query, descriptor, { codexTopics, metrics
|
|
|
10
10
|
const row = { ...descriptor };
|
|
11
11
|
const take = async (read) => {
|
|
12
12
|
try { return await read(); }
|
|
13
|
-
catch (error) { previewFailure?.(row.locator, error); return []; }
|
|
13
|
+
catch (error) { if (metrics) metrics.preview_failures = (metrics.preview_failures ?? 0) + 1; previewFailure?.(row.locator, error); return []; }
|
|
14
14
|
};
|
|
15
15
|
if (query.include_topic_candidates) {
|
|
16
16
|
const history = row.locator.harness === 'codex' ? codexTopics?.get(row.locator.session_id) : null;
|
|
@@ -30,8 +30,19 @@ export async function enrichDescriptor(query, descriptor, { codexTopics, metrics
|
|
|
30
30
|
export const readsPreviews = (query) => query.include_latest_candidates !== false || Boolean(query.include_topic_candidates);
|
|
31
31
|
|
|
32
32
|
export async function enrichIndexPage(query, rows, options) {
|
|
33
|
-
|
|
34
|
-
|
|
33
|
+
// A page used to await every open/read/stat/close before starting the next row. Keep a small number of
|
|
34
|
+
// independent reads in flight, preserve row order and retain every fingerprint/failure check. Never fan out
|
|
35
|
+
// with Promise.all(rows): a whole-index request can contain thousands of transcripts and expanded tails.
|
|
36
|
+
const result = new Array(rows.length);
|
|
37
|
+
let next = 0;
|
|
38
|
+
const read = async () => {
|
|
39
|
+
while (next < rows.length) {
|
|
40
|
+
const at = next++;
|
|
41
|
+
result[at] = await enrichDescriptor(query, rows[at], options);
|
|
42
|
+
}
|
|
43
|
+
};
|
|
44
|
+
const finished = await Promise.allSettled(Array.from({ length: Math.min(4, rows.length) }, read));
|
|
45
|
+
for (const worker of finished) if (worker.status === 'rejected') throw worker.reason;
|
|
35
46
|
return result;
|
|
36
47
|
}
|
|
37
48
|
|
|
@@ -46,9 +57,10 @@ export function namesWholeSessionId(query) {
|
|
|
46
57
|
* title supplier chooses one id by source mtime, before applying the other
|
|
47
58
|
* query filters, as native claude_session_named does. Neither is caller proof. */
|
|
48
59
|
export async function discoverFromIndex(query, descriptors, options = {}) {
|
|
60
|
+
const step = options.step ?? ((_name, work) => work());
|
|
49
61
|
const raw = [...descriptors];
|
|
50
|
-
if (query.search_previews) return previewPage(query, raw, options);
|
|
51
|
-
const page = projectMetadataPage(query, raw, options);
|
|
62
|
+
if (query.search_previews) return step('preview search', () => previewPage(query, raw, options));
|
|
63
|
+
const page = await step('metadata projection', () => projectMetadataPage(query, raw, options));
|
|
52
64
|
const name = queryName(query);
|
|
53
65
|
if (query.cursor == null && name) {
|
|
54
66
|
const selected = query.harnesses;
|
|
@@ -78,7 +90,7 @@ export async function discoverFromIndex(query, descriptors, options = {}) {
|
|
|
78
90
|
}
|
|
79
91
|
// Keep the native receipt from projection: its counts precede the exact-name
|
|
80
92
|
// post-filter and late-name fallback. Do not silently rewrite that contract.
|
|
81
|
-
page.sessions = await enrichIndexPage(query, page.sessions, options);
|
|
93
|
+
page.sessions = await step('preview reads', () => enrichIndexPage(query, page.sessions, options));
|
|
82
94
|
return page;
|
|
83
95
|
}
|
|
84
96
|
|
package/index.d.ts
CHANGED
|
@@ -108,6 +108,8 @@ export interface DiscoveryEnrichmentOptions extends MetadataProjectionOptions {
|
|
|
108
108
|
previewFailure?: (locator: SessionLocator, error: Error) => void;
|
|
109
109
|
liveClaudeName?: (name: string) => Promise<{ id: string; name: string } | null>;
|
|
110
110
|
lateClaudeName?: (name: string) => Promise<string | null>;
|
|
111
|
+
/** Optional request-local stage timing; it must preserve the work's value and errors. */
|
|
112
|
+
step?: <T>(name: string, work: () => T | Promise<T>) => T | Promise<T>;
|
|
111
113
|
}
|
|
112
114
|
export function enrichDescriptor(query: DiscoveryQuery, descriptor: SessionDescriptor, options?: DiscoveryEnrichmentOptions): Promise<SessionDescriptor>;
|
|
113
115
|
export function readsPreviews(query: DiscoveryQuery): boolean;
|
package/package.json
CHANGED
package/previews.mjs
CHANGED
|
@@ -66,6 +66,30 @@ function codexMessage(native) {
|
|
|
66
66
|
|
|
67
67
|
function parseLine(text) { try { return parseNativeJson(text); } catch { return null; } }
|
|
68
68
|
|
|
69
|
+
// Walk boundaries only as far as the eight candidates need. Splitting a validated 512 KiB/4 MiB window built
|
|
70
|
+
// an array of every progress/tool line for each returned session, even when its newest eight messages came first.
|
|
71
|
+
function* linesIn(text, latest) {
|
|
72
|
+
if (!text.length) return;
|
|
73
|
+
const limit = text.endsWith('\n') ? text.length - 1 : text.length;
|
|
74
|
+
if (latest) {
|
|
75
|
+
let end = limit;
|
|
76
|
+
for (;;) {
|
|
77
|
+
const newline = end === 0 ? -1 : text.lastIndexOf('\n', end - 1);
|
|
78
|
+
yield text.slice(newline + 1, end);
|
|
79
|
+
if (newline < 0) return;
|
|
80
|
+
end = newline;
|
|
81
|
+
}
|
|
82
|
+
} else {
|
|
83
|
+
let from = 0;
|
|
84
|
+
for (;;) {
|
|
85
|
+
const newline = text.indexOf('\n', from);
|
|
86
|
+
if (newline < 0 || newline >= limit) { yield text.slice(from, limit); return; }
|
|
87
|
+
yield text.slice(from, newline);
|
|
88
|
+
from = newline + 1;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
69
93
|
function codexCandidates(lines, latest) {
|
|
70
94
|
const candidates = [];
|
|
71
95
|
let pending = null;
|
|
@@ -90,9 +114,7 @@ function candidatesIn(bytes, harness, latest) {
|
|
|
90
114
|
// Validate the entire window before selecting eight records, matching the
|
|
91
115
|
// native String::from_utf8 refusal even for unused later preview bytes.
|
|
92
116
|
const text = new TextDecoder('utf-8', { fatal: true, ignoreBOM: true }).decode(bytes);
|
|
93
|
-
const lines = text
|
|
94
|
-
if (lines.at(-1) === '') lines.pop();
|
|
95
|
-
if (latest) lines.reverse();
|
|
117
|
+
const lines = linesIn(text, latest);
|
|
96
118
|
if (harness === 'codex') return codexCandidates(lines, latest);
|
|
97
119
|
const candidates = [];
|
|
98
120
|
for (const line of lines) {
|
package/session-claude-index.mjs
CHANGED
|
@@ -220,6 +220,16 @@ class ClaudeRecordScan extends AppendScan {
|
|
|
220
220
|
graph = new ClaudeReplayIndex(); meta = createSession('claude_code').meta; profile = recordedProfileFold('claude_code');
|
|
221
221
|
skeletons = new Map(); view = null;
|
|
222
222
|
|
|
223
|
+
/** Charge retained replay records and derived entries, including one entry for the scan itself. Progress and other discarded lines
|
|
224
|
+
* only advance `lines`: charging them as retained records made two long, mostly-progress transcripts evict one
|
|
225
|
+
* another on every alternating read, though their replay state together was far below the bound. */
|
|
226
|
+
get retainedRecords() {
|
|
227
|
+
// The record's graph node and linear-line index are views of that same retained record, not two more records.
|
|
228
|
+
// They are bounded by `records.size`; fork anchors and derived profile/message entries are charged separately.
|
|
229
|
+
return 1 + this.records.size + this.graph.segmentAnchors.size + this.profile.history.length + this.skeletons.size
|
|
230
|
+
+ (this.view?.skeleton.length ?? 0) + (this.view?.groups.length ?? 0);
|
|
231
|
+
}
|
|
232
|
+
|
|
223
233
|
/** The lines completed since the last pass. A last line with no LF yet stays outside the scan: `view` reads it. */
|
|
224
234
|
async extend(reader) {
|
|
225
235
|
const from = this.bytes;
|
|
@@ -267,7 +277,7 @@ class ClaudeRecordScan extends AppendScan {
|
|
|
267
277
|
}
|
|
268
278
|
}
|
|
269
279
|
|
|
270
|
-
// The scans of the files read most recently, at most this many
|
|
280
|
+
// The scans of the files read most recently, at most this many retained replay entries between them: a transcript read again is
|
|
271
281
|
// read from where its scan stopped, and one read long ago is scanned again. Of the files shorter than
|
|
272
282
|
// SCAN_KEPT_BYTES only the one read last is kept (a pass reads its file more than once): scanning a short file again
|
|
273
283
|
// costs less than keeping the scan of every one read.
|
|
@@ -278,11 +288,27 @@ async function scanOf(path, reader) {
|
|
|
278
288
|
let scan = scans.get(path); scans.delete(path);
|
|
279
289
|
if (!scan || !await scan.holds(reader)) scan = new ClaudeRecordScan(reader);
|
|
280
290
|
await scan.extend(reader);
|
|
291
|
+
return scan;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
// Project before admitting the scan: the projection adds skeletons and groups that the same finite bound charges.
|
|
295
|
+
// As before, the current scan is kept even when it alone exceeds the bound; every older scan then goes.
|
|
296
|
+
function retainScan(path, scan, metrics = {}) {
|
|
297
|
+
metrics.claude_scan_physical_lines = scan.lines;
|
|
298
|
+
metrics.claude_scan_record_positions = scan.records.size;
|
|
299
|
+
metrics.claude_scan_graph_nodes = scan.graph.nodes.length;
|
|
300
|
+
metrics.claude_scan_graph_lines = scan.graph.linearLines.length;
|
|
301
|
+
metrics.claude_scan_profile_changes = scan.profile.history.length;
|
|
302
|
+
metrics.claude_scan_skeletons = scan.skeletons.size;
|
|
281
303
|
if (scan.bytes < SCAN_KEPT_BYTES) for (const [other, item] of scans) if (item.bytes < SCAN_KEPT_BYTES) scans.delete(other);
|
|
282
304
|
scans.set(path, scan);
|
|
283
|
-
let held = 0; for (const item of scans.values()) held += item.
|
|
284
|
-
for (const [other, item] of scans) {
|
|
285
|
-
|
|
305
|
+
let held = 0; for (const item of scans.values()) held += item.retainedRecords;
|
|
306
|
+
for (const [other, item] of scans) {
|
|
307
|
+
if (held <= SCANNED_RECORDS || item === scan) break;
|
|
308
|
+
scans.delete(other); held -= item.retainedRecords;
|
|
309
|
+
metrics.claude_scan_evictions = (metrics.claude_scan_evictions ?? 0) + 1;
|
|
310
|
+
}
|
|
311
|
+
metrics.claude_scan_retained_entries = held;
|
|
286
312
|
}
|
|
287
313
|
|
|
288
314
|
/**
|
|
@@ -299,7 +325,11 @@ export async function loadClaudeRecordWindow(locator, { from, bytes = null, mess
|
|
|
299
325
|
if (!await supportsClaudeReadIndex(path, { metrics })) return null;
|
|
300
326
|
return (await withSessionFile(path, async (reader) => {
|
|
301
327
|
let view;
|
|
302
|
-
try {
|
|
328
|
+
try {
|
|
329
|
+
const scan = await scanOf(path, reader);
|
|
330
|
+
view = await scan.project(reader, fidelity);
|
|
331
|
+
retainScan(path, scan, metrics);
|
|
332
|
+
}
|
|
303
333
|
catch (error) {
|
|
304
334
|
scans.delete(path);
|
|
305
335
|
throw ['ERR_ENCODING_INVALID_ENCODED_DATA', 'ERR_STRING_TOO_LONG'].includes(error?.code) ? sessionTextError(path, error) : error;
|