@volter/supercode-interchange 0.0.0-stage → 0.1.0

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/query.mjs ADDED
@@ -0,0 +1,150 @@
1
+ import { selectedHarnesses } from './homes.mjs';
2
+ import { repositoryFamily, sameRepositoryFamily, workspaceScope } from './workspace.mjs';
3
+ import { hermesMirrorKey, includeHermesMirror } from './hermes-mirror.mjs';
4
+ import { finalizeNouns } from './metadata.mjs';
5
+ import { nativeTrim } from './text.mjs';
6
+ import { parseNativeJson, nativeInteger } from './native-json.mjs';
7
+
8
+ export const descriptorKey = (row) => JSON.stringify([row.locator.harness, row.locator.session_id]);
9
+ const cursorKey = (row) => [row.updated_at_ms ?? null, row.locator.harness, row.locator.session_id];
10
+ const utf8Order = (left, right) => Buffer.compare(Buffer.from(left), Buffer.from(right));
11
+
12
+ export function descriptorOrder(left, right) {
13
+ const a = left.updated_at_ms, b = right.updated_at_ms;
14
+ if (a !== b) {
15
+ if (a == null) return 1;
16
+ if (b == null) return -1;
17
+ return a > b ? -1 : 1;
18
+ }
19
+ return utf8Order(left.locator.harness, right.locator.harness) || utf8Order(left.locator.session_id, right.locator.session_id);
20
+ }
21
+
22
+ export function descriptorMatches(row, search) {
23
+ return [row.locator.harness, row.locator.session_id, row.title, row.cwd, row.model]
24
+ .some((value) => typeof value === 'string' && value.toLowerCase().includes(search));
25
+ }
26
+
27
+ /** Clone before rolling up; a query never changes the owner's raw rows. */
28
+ function rollUp(rows, includeChildren) {
29
+ const byId = new Map(rows.map((row) => [descriptorKey(row), row]));
30
+ const updates = new Map(), counts = new Map();
31
+ for (const row of rows) {
32
+ let parent = row.parent_session_id;
33
+ let root = null;
34
+ const seen = new Set();
35
+ while (parent != null && !seen.has(parent)) {
36
+ seen.add(parent);
37
+ const ancestor = byId.get(JSON.stringify([row.locator.harness, parent]));
38
+ if (!ancestor) break;
39
+ root = ancestor;
40
+ parent = ancestor.parent_session_id;
41
+ }
42
+ if (root) {
43
+ counts.set(root, (counts.get(root) ?? 0) + 1);
44
+ if (row.updated_at_ms != null) updates.set(root, Math.max(updates.get(root) ?? 0, row.updated_at_ms));
45
+ }
46
+ }
47
+ for (const [root, updated] of updates) root.updated_at_ms = Math.max(root.updated_at_ms ?? 0, updated);
48
+ for (const [root, count] of counts) root.child_session_count = count;
49
+ return includeChildren ? rows : rows.filter((row) => row.parent_session_id == null);
50
+ }
51
+
52
+ function familyRows(rows, rootId) {
53
+ // The native family selector addresses a native id rather than a locator.
54
+ const parents = new Map(rows.map((row) => [row.locator.session_id, row.parent_session_id]));
55
+ return rows.filter((row) => {
56
+ let current = row.locator.session_id;
57
+ const seen = new Set();
58
+ while (current != null && !seen.has(current)) {
59
+ if (current === rootId) return true;
60
+ seen.add(current); current = parents.get(current);
61
+ }
62
+ return false;
63
+ });
64
+ }
65
+
66
+ /** Filter the raw metadata index before page enrichment, matching the native
67
+ * catalog's scan scope, lineage projection, filter order and stable order. */
68
+ export function projectMetadata(query, descriptors, { hermesMirrors } = {}) {
69
+ if (query.search_previews) throw new Error('preview search requires discovery, not a metadata-only index projection');
70
+ const selected = new Set(selectedHarnesses(query));
71
+ const scope = workspaceScope(query);
72
+ let rows = [...descriptors].filter((row) => selected.has(row.locator.harness)
73
+ && (row.locator.harness !== 'hermes' || includeHermesMirror(hermesMirrors?.get(hermesMirrorKey(row)), selected))
74
+ && (!scope || scope(row))).map((row) => finalizeNouns(row));
75
+ rows = rollUp(rows, query.include_child_sessions === true);
76
+ if (query.root_session_id != null) rows = familyRows(rows, query.root_session_id);
77
+ if (query.workspace_family != null) {
78
+ const wanted = repositoryFamily(query.workspace_family);
79
+ const admitted = new Map();
80
+ rows = rows.filter((row) => {
81
+ if (row.cwd == null) return false;
82
+ if (!admitted.has(row.cwd)) admitted.set(row.cwd, sameRepositoryFamily(repositoryFamily(row.cwd), wanted));
83
+ return admitted.get(row.cwd);
84
+ });
85
+ }
86
+ const profile = query.profile == null ? null : nativeTrim(query.profile);
87
+ if (profile) rows = rows.filter((row) => row.profile === profile);
88
+ if (query.updated_after_ms != null) rows = rows.filter((row) => row.updated_at_ms != null && row.updated_at_ms >= query.updated_after_ms);
89
+ if (query.updated_before_ms != null) rows = rows.filter((row) => row.updated_at_ms != null && row.updated_at_ms <= query.updated_before_ms);
90
+ rows.sort(descriptorOrder);
91
+ const search = query.query == null ? null : nativeTrim(query.query).toLowerCase();
92
+ if (search) rows = rows.filter((row) => descriptorMatches(row, search));
93
+ return rows;
94
+ }
95
+
96
+ /** Discovery's existing hex JSON tuple cursor. It is unrelated to a database
97
+ * change-stream cursor and becomes stale when its exact row no longer matches. */
98
+ export function encodeDiscoveryCursor(row) {
99
+ return Buffer.from(JSON.stringify(cursorKey(row))).toString('hex');
100
+ }
101
+
102
+ export function decodeDiscoveryCursor(cursor) {
103
+ try {
104
+ if (typeof cursor !== 'string' || cursor.length % 2 !== 0 || !/^[0-9a-f]*$/i.test(cursor)) throw new Error();
105
+ // Buffer's UTF-8 decoder replaces invalid bytes; the native JSON parser
106
+ // refuses them. Decode fatally before parsing the tuple.
107
+ const value = parseNativeJson(new TextDecoder('utf-8', { fatal: true, ignoreBOM: true }).decode(Buffer.from(cursor, 'hex')));
108
+ if (!Array.isArray(value) || value.length !== 3 || !(value[0] === null || (nativeInteger(value, '0') != null && Number.isSafeInteger(value[0]) && value[0] >= 0))
109
+ || typeof value[1] !== 'string' || typeof value[2] !== 'string') throw new Error();
110
+ return value;
111
+ } catch { throw new Error('discovery cursor is invalid'); }
112
+ }
113
+
114
+ export function paginateDescriptors(query, rows) {
115
+ let start = 0;
116
+ if (query.cursor != null) {
117
+ const key = JSON.stringify(decodeDiscoveryCursor(query.cursor));
118
+ const at = rows.findIndex((row) => JSON.stringify(cursorKey(row)) === key);
119
+ if (at < 0) throw new Error('discovery cursor is stale or invalid');
120
+ start = at + 1;
121
+ }
122
+ if (query.limit != null && (!Number.isSafeInteger(query.limit) || query.limit < 0)) throw new Error('discovery limit must be a nonnegative integer');
123
+ const end = query.limit == null ? rows.length : Math.min(rows.length, start + query.limit);
124
+ const sessions = rows.slice(start, end);
125
+ return { sessions, next_cursor: end < rows.length && sessions.length ? encodeDiscoveryCursor(sessions.at(-1)) : null };
126
+ }
127
+
128
+ export function discoveryReceipt(query, sessions, totalMatched, truncated, searchedPreviews = false) {
129
+ let oldest = null, newest = null;
130
+ for (const row of sessions) {
131
+ if (row.updated_at_ms == null) continue;
132
+ oldest = oldest == null ? row.updated_at_ms : Math.min(oldest, row.updated_at_ms);
133
+ newest = newest == null ? row.updated_at_ms : Math.max(newest, row.updated_at_ms);
134
+ }
135
+ return {
136
+ ...(searchedPreviews ? { searched_previews: true } : {}),
137
+ ...(query.updated_after_ms != null ? { requested_after_ms: query.updated_after_ms } : {}),
138
+ ...(query.updated_before_ms != null ? { requested_before_ms: query.updated_before_ms } : {}),
139
+ ...(query.limit != null ? { requested_limit: query.limit } : {}),
140
+ ...(oldest != null ? { oldest_returned_ms: oldest } : {}),
141
+ ...(newest != null ? { newest_returned_ms: newest } : {}),
142
+ returned: sessions.length, total_matched: totalMatched, truncated,
143
+ };
144
+ }
145
+
146
+ export function projectMetadataPage(query, descriptors, options) {
147
+ const rows = projectMetadata(query, descriptors, options);
148
+ const page = paginateDescriptors(query, rows);
149
+ return { ...page, receipt: discoveryReceipt(query, page.sessions, rows.length, page.next_cursor != null) };
150
+ }
@@ -0,0 +1,128 @@
1
+ import { descriptorKey, descriptorOrder, projectMetadata, paginateDescriptors, discoveryReceipt } from './query.mjs';
2
+
3
+ const order = (a, b) => a.rank - b.rank || Buffer.compare(Buffer.from(a.part), Buffer.from(b.part))
4
+ || Buffer.compare(Buffer.from(a.row_key), Buffer.from(b.row_key));
5
+ const originKey = (row) => JSON.stringify([row.source, row.part, row.row_key]);
6
+ const partKey = (source, part) => JSON.stringify([source, part]);
7
+
8
+ /** A retained query owns one ordered metadata projection, shared by windows
9
+ * with the same filters. An ordinary part update replaces only its connected
10
+ * lineage components; pagination copies the requested window alone. Source
11
+ * rows are references to the owner's committed immutable descriptor values. */
12
+ export class RetainedMetadataProjection {
13
+ constructor(query, sources, records, metrics = {}) {
14
+ this.metrics = metrics; metrics.projection_rebuilds = (metrics.projection_rebuilds ?? 0) + 1;
15
+ this.query = query; this.ranks = new Map(sources.map((source, rank) => [source, rank]));
16
+ this.origins = new Map(); this.parts = new Map(); this.candidates = new Map(); this.raw = new Map();
17
+ this.ids = new Map(); this.edges = new Map(); this.projected = new Map(); this.ordered = [];
18
+ for (const row of records) this.#add(row);
19
+ for (const key of this.candidates.keys()) this.#winner(key);
20
+ for (const row of this.raw.values()) this.#link(row.descriptor, 1);
21
+ const initial = this.#project([...this.raw.keys()]);
22
+ this.ordered = initial;
23
+ for (const row of initial) this.projected.set(descriptorKey(row), row);
24
+ }
25
+
26
+ #add(record) {
27
+ const row = { ...record, rank: this.ranks.get(record.source) }, origin = originKey(row), key = descriptorKey(row.descriptor);
28
+ this.origins.set(origin, row);
29
+ if (!this.parts.has(partKey(row.source, row.part))) this.parts.set(partKey(row.source, row.part), new Set());
30
+ this.parts.get(partKey(row.source, row.part)).add(origin);
31
+ if (!this.candidates.has(key)) this.candidates.set(key, new Set());
32
+ this.candidates.get(key).add(origin);
33
+ }
34
+
35
+ #winner(key) {
36
+ const candidates = [...(this.candidates.get(key) ?? [])].map((origin) => this.origins.get(origin)).sort(order);
37
+ if (!candidates.length) { this.raw.delete(key); return; }
38
+ // snapshot's deduplication keeps the last value, but the first occurrence
39
+ // determines map iteration order (including native cross-harness families).
40
+ this.raw.set(key, { ...candidates.at(-1), first: candidates[0] });
41
+ }
42
+
43
+ #edge(from, to, delta) {
44
+ if (!this.edges.has(from)) this.edges.set(from, new Map());
45
+ const held = this.edges.get(from), count = (held.get(to) ?? 0) + delta;
46
+ if (count) held.set(to, count); else held.delete(to);
47
+ if (!held.size) this.edges.delete(from);
48
+ }
49
+
50
+ #link(row, delta) {
51
+ const id = row.locator.session_id, key = descriptorKey(row);
52
+ if (delta > 0) {
53
+ if (!this.ids.has(id)) this.ids.set(id, new Set());
54
+ this.ids.get(id).add(key);
55
+ } else {
56
+ const held = this.ids.get(id); held?.delete(key); if (!held?.size) this.ids.delete(id);
57
+ }
58
+ if (row.parent_session_id != null) {
59
+ this.#edge(id, row.parent_session_id, delta); this.#edge(row.parent_session_id, id, delta);
60
+ }
61
+ }
62
+
63
+ #component(ids) {
64
+ const pending = [...ids], seen = new Set(), keys = new Set();
65
+ while (pending.length) {
66
+ const id = pending.pop(); if (seen.has(id)) continue; seen.add(id);
67
+ for (const key of this.ids.get(id) ?? []) keys.add(key);
68
+ for (const parent of this.edges.get(id)?.keys() ?? []) pending.push(parent);
69
+ }
70
+ return keys;
71
+ }
72
+
73
+ #project(keys) {
74
+ const records = keys.map((key) => this.raw.get(key)).filter(Boolean).sort((a, b) => order(a.first, b.first));
75
+ this.metrics.projected_metadata_rows = (this.metrics.projected_metadata_rows ?? 0) + records.length;
76
+ const mirrors = new Map(records.filter((row) => row.mirror != null).map((row) =>
77
+ [JSON.stringify([row.descriptor.locator.storage.path, row.descriptor.locator.session_id]), row.mirror]));
78
+ return projectMetadata(this.query, records.map((row) => row.descriptor), { hermesMirrors: mirrors });
79
+ }
80
+
81
+ #at(row) {
82
+ let low = 0, high = this.ordered.length;
83
+ while (low < high) {
84
+ const mid = low + Math.floor((high - low) / 2);
85
+ if (descriptorOrder(this.ordered[mid], row) < 0) low = mid + 1; else high = mid;
86
+ }
87
+ return low;
88
+ }
89
+
90
+ updatePart(source, part, records) {
91
+ this.metrics.projection_part_updates = (this.metrics.projection_part_updates ?? 0) + 1;
92
+ const slot = partKey(source, part), before = [...(this.parts.get(slot) ?? [])], incoming = [...records], touched = new Set();
93
+ for (const origin of before) touched.add(descriptorKey(this.origins.get(origin).descriptor));
94
+ for (const row of incoming) touched.add(descriptorKey(row.descriptor));
95
+ const ids = new Set([...touched].map((key) => JSON.parse(key)[1])), affected = this.#component(ids);
96
+ for (const key of touched) if (this.raw.has(key)) this.#link(this.raw.get(key).descriptor, -1);
97
+ for (const origin of before) {
98
+ const row = this.origins.get(origin), key = descriptorKey(row.descriptor);
99
+ this.candidates.get(key).delete(origin); this.origins.delete(origin);
100
+ if (!this.candidates.get(key).size) this.candidates.delete(key);
101
+ }
102
+ this.parts.delete(slot);
103
+ for (const row of incoming) this.#add(row);
104
+ for (const key of touched) {
105
+ this.#winner(key); if (this.raw.has(key)) this.#link(this.raw.get(key).descriptor, 1);
106
+ }
107
+ for (const key of this.#component(ids)) affected.add(key);
108
+ for (const key of touched) affected.add(key);
109
+ for (const key of affected) {
110
+ const old = this.projected.get(key);
111
+ if (old) { this.ordered.splice(this.#at(old), 1); this.projected.delete(key); }
112
+ }
113
+ for (const row of this.#project([...affected])) {
114
+ this.ordered.splice(this.#at(row), 0, row); this.projected.set(descriptorKey(row), row);
115
+ }
116
+ }
117
+
118
+ partsFor(key) {
119
+ return new Set([...(this.candidates.get(key) ?? [])].map((origin) => {
120
+ const row = this.origins.get(origin); return partKey(row.source, row.part);
121
+ }));
122
+ }
123
+
124
+ page(query) {
125
+ const page = paginateDescriptors(query, this.ordered);
126
+ return { ...page, receipt: discoveryReceipt(query, page.sessions, this.ordered.length, page.next_cursor != null) };
127
+ }
128
+ }
@@ -0,0 +1,190 @@
1
+ import { stat } from 'node:fs/promises';
2
+ import { basename, dirname, isAbsolute, join, resolve } from 'node:path';
3
+ import { withSessionFile, parseJsonLine, fileFingerprint, sameFingerprint, appendableFingerprint, readHeaderRecords } from './file-io.mjs';
4
+ import { catalogTimestamp, finalizeNouns, applyHeaderRecord } from './metadata.mjs';
5
+ import { readColdMetadataTail, advanceMetadataTail } from './jsonl.mjs';
6
+ import { nativeObjectKeys, nativeInteger } from './native-json.mjs';
7
+ import { nativeTrim } from './text.mjs';
8
+
9
+ const missing = (error) => error.code === 'ENOENT' || error.code === 'ENOTDIR';
10
+ const string = (value) => typeof value === 'string' ? value : null;
11
+
12
+ /** Optional means absent, never unreadable. Malformed JSON has the native
13
+ * empty/default meaning; an unreadable source remains an owner failed part. */
14
+ export async function optionalFile(path, work, metrics) {
15
+ try { return { path, ...await withSessionFile(path, work, metrics) }; }
16
+ catch (error) { if (missing(error)) return { path, missing: true, result: null }; throw error; }
17
+ }
18
+
19
+ export function readSourceJson(path, metrics) {
20
+ return optionalFile(path, async (reader) => parseJsonLine(await reader.read(0, reader.size), { strictUtf8: true }), metrics);
21
+ }
22
+
23
+ /** A compound descriptor includes every auxiliary identity, including an
24
+ * absent optional source. Check them again before returning a commit unit. */
25
+ export async function verifySources(sources) {
26
+ for (const source of sources) {
27
+ let info;
28
+ try { info = await stat(source.path, { bigint: true }); }
29
+ catch (error) { if (!missing(error)) throw error; }
30
+ if (source.missing ? info == null : info != null && sameFingerprint(source.fingerprint, fileFingerprint(info))) continue;
31
+ throw Object.assign(new Error(`${source.path} changed during a compound metadata read; previous index rows are retained`), { code: 'ESTALE' });
32
+ }
33
+ }
34
+
35
+ export async function readGeminiProjects(root, metrics) {
36
+ const source = await readSourceJson(join(root, 'projects.json'), metrics);
37
+ const object = source.result?.projects, projects = new Map();
38
+ if (object != null && typeof object === 'object' && !Array.isArray(object)) {
39
+ // The shipped CLI enables preserve_order, including numeric object keys.
40
+ for (const cwd of nativeObjectKeys(object)) if (typeof object[cwd] === 'string') projects.set(object[cwd], cwd);
41
+ }
42
+ return { source, projects };
43
+ }
44
+
45
+ async function geminiHeader(reader) {
46
+ const bytes = await reader.read(0, Math.min(64 * 1024, reader.size));
47
+ const newline = bytes.indexOf(10), end = newline < 0 ? bytes.length : newline;
48
+ const header = parseJsonLine(bytes.subarray(0, end), { strictUtf8: true });
49
+ if (typeof header?.sessionId !== 'string') throw new Error(`${reader.path} has no Gemini sessionId header`);
50
+ const after = newline < 0 ? bytes.length : newline + 1;
51
+ const decoration = bytes.subarray(after, Math.min(bytes.length, after + 4 * 1024));
52
+ let model = null, modelTerminated = false;
53
+ for (const { line, terminated } of splitLines(decoration)) {
54
+ let value;
55
+ try { value = parseJsonLine(line, { strictUtf8: true }); } catch { break; }
56
+ if (value?.type !== 'user' && value?.type !== 'gemini') continue;
57
+ model = string(value.model);
58
+ if (model != null) { modelTerminated = terminated; break; }
59
+ }
60
+ const budgetEnded = after + decoration.length >= 64 * 1024 || decoration.length >= 4 * 1024;
61
+ return { header: { session_id: header.sessionId, model }, header_frozen: (newline >= 0 || bytes.length >= 64 * 1024) && (budgetEnded || (model != null && modelTerminated)) };
62
+ }
63
+
64
+ function* splitLines(bytes) {
65
+ let from = 0, end;
66
+ while ((end = bytes.indexOf(10, from)) >= 0) { yield { line: bytes.subarray(from, end), terminated: true }; from = end + 1; }
67
+ if (from < bytes.length) yield { line: bytes.subarray(from), terminated: false };
68
+ }
69
+
70
+ export async function readGemini(path, { projects, previous, metrics } = {}) {
71
+ if (basename(dirname(path)) !== 'chats') return null;
72
+ if (!(projects instanceof Map)) throw new Error('Gemini metadata requires its watched projects map');
73
+ return withSessionFile(path, async (reader) => {
74
+ const old = previous?.result?.state;
75
+ const unchanged = sameFingerprint(previous?.fingerprint, reader.fingerprint);
76
+ const append = old?.appendable && appendableFingerprint(previous?.fingerprint, reader.fingerprint);
77
+ const head = old && (unchanged || (append && old.header_frozen))
78
+ ? { header: old.header, header_frozen: old.header_frozen } : await geminiHeader(reader);
79
+ const tail = unchanged ? old : append ? await advanceMetadataTail(reader, 'gemini', old) : await readColdMetadataTail(reader, 'gemini');
80
+ const cwd = projects.get(basename(dirname(dirname(path)))) ?? null;
81
+ return {
82
+ descriptor: finalizeNouns({ locator: { harness: 'gemini', session_id: head.header.session_id, storage: { kind: 'file', path } },
83
+ cwd, title: null, model: head.header.model, updated_at_ms: tail.facts.last_turn_ms ?? reader.modified_ms, message_count: null }),
84
+ state: { ...tail, ...head },
85
+ };
86
+ }, metrics);
87
+ }
88
+
89
+ /** Percent decoding preserves '+' and rejects malformed escapes and UTF-8,
90
+ * matching the native folder decoder rather than form-url decoding. */
91
+ export function decodeWorkspaceFolder(encoded) {
92
+ try { return decodeURIComponent(encoded); } catch { return null; }
93
+ }
94
+
95
+ export async function readGrok(sessionDir, { metrics, previous } = {}) {
96
+ const cwd = decodeWorkspaceFolder(basename(dirname(sessionDir)));
97
+ if (cwd == null) return null;
98
+ const summary = await readSourceJson(join(sessionDir, 'summary.json'), metrics);
99
+ const path = join(sessionDir, 'chat_history.jsonl');
100
+ const transcript = await withSessionFile(path, (reader) => {
101
+ const value = summary.result;
102
+ const count = nativeInteger(value, 'num_chat_messages');
103
+ if (count != null && count > BigInt(Number.MAX_SAFE_INTEGER)) throw new Error(`${summary.path} has an inexact native message count`);
104
+ const title = string(value?.generated_title);
105
+ return {
106
+ descriptor: finalizeNouns({ locator: { harness: 'grok', session_id: basename(sessionDir), storage: { kind: 'file', path } },
107
+ cwd, title: title === '' ? null : title, model: string(value?.current_model_id),
108
+ updated_at_ms: catalogTimestamp(value?.updated_at) ?? reader.modified_ms,
109
+ message_count: count == null ? null : Number(count) }),
110
+ state: previous?.result?.state ?? {},
111
+ };
112
+ }, metrics);
113
+ await verifySources([summary]);
114
+ return { ...transcript, sources: [summary] };
115
+ }
116
+
117
+ async function nativeStoreHeader(reader) {
118
+ const result = { session_id: null, cwd: null, model: null, title: null, topic: null, parent_session_id: null, trigger: null };
119
+ let source = null;
120
+ const records = await readHeaderRecords(reader);
121
+ for (const value of records) {
122
+ if (source == null) {
123
+ source = string(value?.source);
124
+ if (source === 'claude_code') source = 'claude-code';
125
+ result.session_id ??= string(value?.session_id);
126
+ }
127
+ if (source != null) applyHeaderRecord(result, value, source);
128
+ if (result.cwd != null && result.model != null) break;
129
+ }
130
+ return { header: result, header_frozen: records.scan.terminated && (records.scan.exhausted || (result.cwd != null && result.model != null)) };
131
+ }
132
+
133
+ async function nativeStoreFile(path, previous, metrics, { header: wantHeader = true, tail: wantTail = true } = {}) {
134
+ return optionalFile(path, async (reader) => {
135
+ const unchanged = sameFingerprint(previous?.fingerprint, reader.fingerprint);
136
+ const old = previous?.result?.state;
137
+ if (unchanged && (!wantTail || old?.tail_read) && (!wantHeader || previous.result?.header_read)) return previous.result;
138
+ const append = old?.appendable && appendableFingerprint(previous?.fingerprint, reader.fingerprint);
139
+ const tail = unchanged && old?.tail_read ? old : wantTail
140
+ ? append ? await advanceMetadataTail(reader, 'supercode', old) : await readColdMetadataTail(reader, 'supercode')
141
+ : { facts: { last_turn_ms: null }, offset: reader.size, trailing: '', appendable: false };
142
+ const retainHead = previous?.result?.header_read && (unchanged || (append && old.header_frozen));
143
+ const head = retainHead ? { header: previous.result.header, header_frozen: old.header_frozen }
144
+ : wantHeader ? await nativeStoreHeader(reader) : { header: null, header_frozen: false };
145
+ return { header: head.header, header_read: retainHead || wantHeader,
146
+ state: { ...tail, tail_read: Boolean(wantTail || (unchanged && old?.tail_read)), header_frozen: head.header_frozen } };
147
+ }, metrics);
148
+ }
149
+
150
+ async function optionalStat(path) {
151
+ let info;
152
+ try { info = await stat(path, { bigint: true }); }
153
+ catch (error) { if (missing(error)) return { path, missing: true, result: null }; throw error; }
154
+ return { path, file: info.isFile(), fingerprint: fileFingerprint(info), result: null,
155
+ modified_ms: info.isFile() && info.mtimeNs >= 0n ? Number(info.mtimeNs / 1_000_000n) : null };
156
+ }
157
+
158
+ /** A .meta.json owns the native name/title. The sidecar owns the header and
159
+ * first-choice turn clock; the locator names the transcript while it exists,
160
+ * otherwise the surviving sidecar. No reduction content is translated here. */
161
+ export async function readSupercode(metaPath, root, { previous, metrics } = {}) {
162
+ const meta = await readSourceJson(metaPath, metrics);
163
+ const info = meta.result;
164
+ if (typeof info?.name !== 'string' || (info.title !== undefined && typeof info.title !== 'string')) return { descriptor: null, sources: [meta] };
165
+ const archived = resolve(dirname(metaPath)) !== resolve(root);
166
+ const name = `${info.name}.jsonl`, base = archived ? join(root, 'archived') : root;
167
+ const path = isAbsolute(name) ? name : join(base, name);
168
+ const sidecarPath = path.slice(0, -'.jsonl'.length) + '.sidecar.jsonl';
169
+ const previousByPath = new Map((previous?.sources ?? []).map((source) => [source.path, source]));
170
+ const transcriptInfo = await optionalStat(path);
171
+ // Preserve read_native_store_header_as's existing filename rule: when the
172
+ // locator is itself a sidecar, its adjacent authoritative sidecar has the
173
+ // doubled suffix. The usual case has no doubled file and uses the locator.
174
+ const doubledPath = sidecarPath.slice(0, -'.jsonl'.length) + '.sidecar.jsonl';
175
+ const doubled = transcriptInfo.file ? null : await nativeStoreFile(doubledPath, previousByPath.get(doubledPath), metrics, { tail: false });
176
+ const sidecar = await nativeStoreFile(sidecarPath, previousByPath.get(sidecarPath), metrics, { header: doubled == null || doubled.missing });
177
+ const transcript = !transcriptInfo.file || (!sidecar.missing && sidecar.result?.state.facts.last_turn_ms != null) ? transcriptInfo
178
+ : await nativeStoreFile(path, previousByPath.get(path), metrics, { header: sidecar.missing, tail: sidecar.result?.state.facts.last_turn_ms == null });
179
+ const files = [meta, transcriptInfo, sidecar, transcript, ...(doubled ? [doubled] : [])];
180
+ await verifySources(files);
181
+ const content = doubled && !doubled.missing ? doubled : sidecar.missing ? transcript : sidecar;
182
+ const header = content.result?.header;
183
+ return {
184
+ descriptor: finalizeNouns({ locator: { harness: 'supercode', session_id: info.name, storage: { kind: 'file', path: transcriptInfo.file ? path : sidecarPath } },
185
+ cwd: header?.cwd ?? null, title: info.title != null && nativeTrim(info.title) ? info.title : null, model: header?.model ?? null,
186
+ updated_at_ms: sidecar.result?.state.facts.last_turn_ms ?? transcript.result?.state.facts.last_turn_ms ?? transcript.modified_ms ?? sidecar.modified_ms ?? null,
187
+ message_count: null }),
188
+ sources: [...new Map(files.map((source) => [source.path, source])).values()],
189
+ };
190
+ }
@@ -0,0 +1,92 @@
1
+ import { realpath } from 'node:fs/promises';
2
+ import { basename, dirname, isAbsolute, join, relative, resolve, sep } from 'node:path';
3
+ import { sourceEntries, sourceStat } from './store-registry.mjs';
4
+ import { readCompressedCodex, refreshUncompressedJsonl } from './jsonl.mjs';
5
+ import { readGemini, readGrok, readSupercode } from './source-files.mjs';
6
+ import { readOpenClaw } from './openclaw.mjs';
7
+
8
+ export const FILE_HARNESSES = Object.freeze(['claude-code', 'codex', 'pi', 'openclaw', 'grok', 'gemini', 'supercode']);
9
+ export const STORE_HARNESSES = Object.freeze(['opencode', 'goose', 'hermes', 'orchestrator']);
10
+
11
+ /** Resolve existing ancestors too, so an absent root and its later creation
12
+ * use the same physical path as platform watch events. Refused metadata is
13
+ * not interpreted as absence. */
14
+ export async function normalizedSourcePath(path) {
15
+ let at = resolve(path), rest = [];
16
+ for (;;) {
17
+ try { return join(await realpath(at), ...rest.reverse()); }
18
+ catch (error) { if (!['ENOENT', 'ENOTDIR'].includes(error.code)) throw error; }
19
+ const parent = dirname(at);
20
+ if (parent === at) return resolve(path);
21
+ rest.push(basename(at)); at = parent;
22
+ }
23
+ }
24
+
25
+ export function pathUnder(root, path) {
26
+ const part = relative(root, path);
27
+ return part === '' || (!isAbsolute(part) && part !== '..' && !part.startsWith(`..${sep}`));
28
+ }
29
+
30
+ export function fileRoot(harness, homes) {
31
+ return FILE_HARNESSES.includes(harness) ? homes[harness === 'claude-code' ? 'claude_code' : harness] : null;
32
+ }
33
+
34
+ /** The key is the file that owns a metadata read, not every adjacent file
35
+ * whose event can invalidate it. Supercode's meta file owns its saved name. */
36
+ export async function sessionPartFor(harness, root, path) {
37
+ const name = basename(path);
38
+ if (harness === 'claude-code' || harness === 'pi') return name.endsWith('.jsonl') ? path : null;
39
+ if (harness === 'codex') return name.endsWith('.jsonl') || name.endsWith('.jsonl.zst') ? path : null;
40
+ if (harness === 'openclaw') return name.endsWith('.jsonl') && !name.endsWith('.trajectory.jsonl') ? path : null;
41
+ if (harness === 'gemini') return name.endsWith('.jsonl') && pathUnder(join(root, 'tmp'), path) ? path : null;
42
+ if (harness === 'grok') {
43
+ const part = relative(root, path).split(sep);
44
+ return pathUnder(root, path) && part.length >= 3 ? join(root, part[0], part[1], 'chat_history.jsonl') : null;
45
+ }
46
+ if (harness === 'supercode' && [root, join(root, 'archived')].includes(dirname(path))) {
47
+ const suffix = ['.meta.json', '.sidecar.jsonl', '.jsonl'].find((ending) => name.endsWith(ending));
48
+ return suffix ? join(dirname(path), `${name.slice(0, -suffix.length)}.meta.json`) : null;
49
+ }
50
+ return null;
51
+ }
52
+
53
+ /** The owner installs each directory watch before its listing. A refused
54
+ * child refuses this listing as a whole; its previously held rows stand.
55
+ * Following real directory identities ends symlink cycles. */
56
+ export async function listSessionParts(harness, root, { folder = root, beforeDirectory = async () => {}, metrics = {} } = {}) {
57
+ const walked = new Set(), parts = new Set(), directories = new Set();
58
+ async function visit(dir) {
59
+ const info = await sourceStat(dir);
60
+ if (!info?.isDirectory()) return;
61
+ const real = await normalizedSourcePath(dir);
62
+ if (walked.has(real)) return;
63
+ walked.add(real); directories.add(real);
64
+ await beforeDirectory(real);
65
+ metrics.directories_listed = (metrics.directories_listed ?? 0) + 1;
66
+ for (const name of await sourceEntries(dir)) {
67
+ const path = join(dir, name), entry = await sourceStat(path);
68
+ if (entry?.isDirectory()) await visit(path);
69
+ else if (entry?.isFile()) {
70
+ const part = await sessionPartFor(harness, root, await normalizedSourcePath(path));
71
+ if (part != null) parts.add(part);
72
+ }
73
+ }
74
+ }
75
+ await visit(folder);
76
+ return { parts, directories };
77
+ }
78
+
79
+ /** All readers return one cache/descriptor unit. Cache identities and offsets
80
+ * are committed beside the descriptor by the daemon, never by this library. */
81
+ export async function readFileMetadata(harness, root, path, { previous, projects, nativeTopic, metrics = {} } = {}) {
82
+ let cache;
83
+ if (['claude-code', 'codex', 'pi'].includes(harness)) cache = harness === 'codex' && path.endsWith('.zst')
84
+ ? await readCompressedCodex(path, { nativeTopic, metrics, previous })
85
+ : await refreshUncompressedJsonl(path, harness, previous, { nativeTopic, metrics });
86
+ else if (harness === 'openclaw') cache = await readOpenClaw(path, { previous, metrics });
87
+ else if (harness === 'gemini') cache = await readGemini(path, { previous, projects, metrics });
88
+ else if (harness === 'grok') cache = await readGrok(dirname(path), { previous, metrics });
89
+ else if (harness === 'supercode') cache = await readSupercode(path, root, { previous, metrics });
90
+ else throw new Error(`no file metadata reader for ${harness}`);
91
+ return { descriptor: cache?.result?.descriptor ?? cache?.descriptor ?? null, cache };
92
+ }
@@ -0,0 +1,75 @@
1
+ import { DatabaseSync } from 'node:sqlite';
2
+ import { fileFingerprint } from './file-io.mjs';
3
+ import { sourceStat } from './store-registry.mjs';
4
+ import { verifySources } from './source-files.mjs';
5
+ import { parseNativeJson } from './native-json.mjs';
6
+
7
+ /** Only DB and WAL mutations affect the stored read mark. SHM is SQLite's
8
+ * reader coordination file and never schedules transcript/index work. */
9
+ export async function storeFingerprints(path) {
10
+ const sources = [];
11
+ for (const file of [path, `${path}-wal`]) {
12
+ const info = await sourceStat(file);
13
+ sources.push(info ? { path: file, fingerprint: fileFingerprint(info) } : { path: file, missing: true });
14
+ }
15
+ return sources;
16
+ }
17
+
18
+ /** One read-only connection and one synchronous snapshot. A changed DB/WAL
19
+ * refuses the commit unit, leaving the owner's prior rows and retry visible.
20
+ * No foreign writer, journal-mode setter or extension loading is permitted. */
21
+ export async function readStore(path, work, metrics = {}) {
22
+ const sources = await storeFingerprints(path);
23
+ if (sources[0].missing) throw Object.assign(new Error(`${path} is not present`), { code: 'ENOENT' });
24
+ const db = new DatabaseSync(path, { readOnly: true, allowExtension: false });
25
+ metrics.stores_read = (metrics.stores_read ?? 0) + 1;
26
+ let result;
27
+ try {
28
+ db.exec('PRAGMA busy_timeout=0; BEGIN');
29
+ result = work(db);
30
+ if (result != null && typeof result.then === 'function') throw new Error('a foreign-store snapshot must be synchronous');
31
+ db.exec('COMMIT');
32
+ } finally { db.close(); }
33
+ await verifySources(sources);
34
+ return { result, sources };
35
+ }
36
+
37
+ export function statement(db, sql) {
38
+ const prepared = db.prepare(sql);
39
+ prepared.setReadBigInts(true);
40
+ return prepared;
41
+ }
42
+
43
+ export function tableColumns(db, table) {
44
+ // Callers provide only these fixed native schema identifiers.
45
+ if (!['session', 'sessions', 'messages'].includes(table)) throw new Error('unknown catalog schema table');
46
+ return new Set(db.prepare(`PRAGMA table_info(${table})`).all().map((row) => row.name));
47
+ }
48
+
49
+ export function text(value, nullable = false) {
50
+ if (nullable && value == null) return null;
51
+ if (typeof value !== 'string') throw new Error('foreign session store returned a non-text native column');
52
+ return value;
53
+ }
54
+
55
+ export function unsigned(value) {
56
+ if (value == null) return null;
57
+ if (typeof value !== 'bigint' && !Number.isSafeInteger(value)) throw new Error('foreign session store returned a non-integer native column');
58
+ if (value < 0) return null;
59
+ const number = Number(value);
60
+ if (!Number.isSafeInteger(number)) throw new Error('foreign session integer exceeds the exact Node wire range');
61
+ return number;
62
+ }
63
+
64
+ export function integer(value, nullable = false) {
65
+ if (nullable && value == null) return null;
66
+ // Every prepared native-column statement uses setReadBigInts. A Number is
67
+ // a SQLite REAL even when numerically integral; rusqlite's i64 read refuses it.
68
+ if (typeof value !== 'bigint') throw new Error('foreign session store returned a non-INTEGER native column');
69
+ return value;
70
+ }
71
+
72
+ export function jsonColumn(value) {
73
+ if (typeof value !== 'string') return null;
74
+ try { return parseNativeJson(value); } catch { return null; }
75
+ }