@volter/supercode-interchange 0.1.4 → 0.1.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -110,6 +110,30 @@ cover recorded posture/policies through npm and operator streams, including
110
110
  unknown posture for malformed records. The manifest's other branches,
111
111
  corpus/scale, concurrent reads and independent source review remain open.
112
112
 
113
+ `sessions.launch_agent` reads `<harness home>/supercode/launches/<session id>.json`
114
+ in the same caller-owned load lane. Claude `projects` and Codex `sessions`
115
+ ancestors select the transcript's recorded home, independent of current vendor
116
+ defaults; no transcript must exist. Unsupported locators, unsafe ids, absent,
117
+ invalid UTF-8 or malformed records, and nonobject environments return null.
118
+ The environment is recorded metadata, never caller authority. The new
119
+ `launch_agent` capability marker refuses older read services explicitly.
120
+ Native runtime launch-record writers remain for their separate replacement.
121
+ [Private Mac/Windows conversations](../../docs/evidence/session-launch-metadata-2026-10-08.md)
122
+ record selected-home answers and null malformed metadata through npm and
123
+ operator streams; concurrent path/record replacement and runtime writing remain
124
+ unqualified.
125
+
126
+ `sessions.usage` reads a complete lossless session in the load worker and groups
127
+ recorded tokens by the native day prefix and model. Claude keeps the last usage
128
+ record for each response id; Codex separates successive cumulative totals and
129
+ cached prompt deltas. Unknown models retain `cost_unrecorded` rather than a zero
130
+ price. Costs use the pinned 2026-10-03 native rate snapshot, including cache TTL,
131
+ fast-mode and inference-geo factors; this is not a live pricing feed. Exact u64
132
+ wire counts are retained independently of JavaScript number rounding. A separate
133
+ `usage` marker refuses older read services without closing their catalog lane.
134
+ Machine-wide spend and runtime budgets retain their separate native consumers.
135
+ This source checkpoint has no runtime or Windows qualification yet.
136
+
113
137
  `resolveHarnessHomes` follows native HOME-first defaults and preserves explicitly empty harness-specific
114
138
  home values. `projectMetadata` applies workspace scope, native lineage, repository-family, profile, time and
115
139
  metadata-search filters with stable newest-first ordering. It clones raw rows before descendant roll-up.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@volter/supercode-interchange",
3
- "version": "0.1.4",
3
+ "version": "0.1.6",
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",
@@ -0,0 +1,30 @@
1
+ // Recorded launch metadata is a reading, never caller authority. The native
2
+ // runtime writer remains until its own replacement boundary.
3
+ import { readFileSync } from 'node:fs';
4
+ import { basename, dirname, isAbsolute, sep } from 'node:path';
5
+ import { parseNativeJson } from './native-json.mjs';
6
+
7
+ export function recordedLaunchAgent(locator, { cwd = process.cwd() } = {}) {
8
+ if (locator.storage.kind !== 'file' || !locator.session_id.length
9
+ || /[^A-Za-z0-9_-]/.test(locator.session_id)) return null;
10
+ const store = locator.harness === 'codex' ? 'sessions' : locator.harness === 'claude-code' ? 'projects' : null;
11
+ if (store == null) return null;
12
+ // Walk the lexical path, starting with the path itself as Path::ancestors
13
+ // does. Resolving '..' first could choose a different recorded harness home.
14
+ let at = locator.storage.path, home = null;
15
+ for (;;) {
16
+ if (basename(at) === store) { home = dirname(at); break; }
17
+ const parent = dirname(at);
18
+ if (parent === at || at === '.') break;
19
+ at = parent;
20
+ }
21
+ if (home == null) return null;
22
+ // Leave '..' for the filesystem, including its symlink semantics.
23
+ const path = `${isAbsolute(home) ? '' : `${cwd}${sep}`}${home}${sep}supercode${sep}launches${sep}${locator.session_id}.json`;
24
+ try {
25
+ const text = new TextDecoder('utf-8', { fatal: true, ignoreBOM: true }).decode(readFileSync(path));
26
+ const record = parseNativeJson(text);
27
+ const environment = record && !Array.isArray(record) && typeof record === 'object' ? record.environment : null;
28
+ return environment && !Array.isArray(environment) && typeof environment === 'object' ? environment : null;
29
+ } catch { return null; }
30
+ }
package/session-read.d.ts CHANGED
@@ -3,7 +3,7 @@ import { SessionFollower } from './session-codecs.js';
3
3
  import type { ProjectedSessionResult, NormalizedSession, RecordedSessionProfile, RecordedSessionProfileChange } from './session-model.js';
4
4
  export const SESSION_READ_METHODS: readonly string[];
5
5
  export const SESSION_READ_EVENT: 'harness.v1.sessions.event';
6
- export const SESSION_READ_CAPABILITY: Readonly<{ version: 1; owner: 'caller'; language: 'typescript'; native_parser: false; recorded_config: true }>;
6
+ export const SESSION_READ_CAPABILITY: Readonly<{ version: 1; owner: 'caller'; language: 'typescript'; native_parser: false; recorded_config: true; launch_agent: true; usage: true }>;
7
7
  export interface SessionReadContext { cwd?: string; env?: Record<string, string | undefined> }
8
8
  export function parseSessionWire(text: string): unknown;
9
9
  export function stringifySessionWire(value: unknown): string;
@@ -12,3 +12,14 @@ export function loadSessionRead(params: unknown, context?: SessionReadContext):
12
12
  export function openSessionReadFollower(params: unknown, context?: SessionReadContext): Promise<SessionFollower>;
13
13
  /** Recorded values only; malformed complete Claude runtime state leaves mode unknown. */
14
14
  export function loadRecordedSessionConfiguration(params: unknown, context?: SessionReadContext): Promise<{ harness: string; session_id: string | null; cwd: unknown; model: unknown; approval_policy: unknown; sandbox_policy: unknown; permission_mode: string | null }>;
15
+ /** Best-effort recorded environment only, independent of transcript existence or caller authority. */
16
+ export function loadSessionLaunchAgent(params: unknown, context?: SessionReadContext): Promise<{ environment: Record<string, unknown> | null }>;
17
+ export interface SessionTokenUsage {
18
+ at: string; model: string; input_tokens: number; output_tokens: number;
19
+ cache_read_tokens: number; cache_write_tokens: number;
20
+ cache_write_5m_tokens: number; cache_write_1h_tokens: number;
21
+ cost?: { amount: number | null; currency: 'USD'; source: 'rate_table' };
22
+ cost_unrecorded?: true;
23
+ }
24
+ /** Recorded tokens and the pinned rate snapshot; exact native integers survive wire serialization. */
25
+ export function loadSessionUsage(params: unknown, context?: SessionReadContext): Promise<{ harness: string; session_id: string | null; since?: string; usage: SessionTokenUsage[] }>;
package/session-read.mjs CHANGED
@@ -9,10 +9,13 @@ 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
11
  import { recordedSessionConfiguration } from './session-recorded-config.mjs';
12
+ import { recordedLaunchAgent } from './session-launch-agent.mjs';
13
+ import { recordedSessionUsage } from './session-usage.mjs';
14
+ import { parseUsageSince, usageMoment } from './session-usage-time.mjs';
12
15
 
13
- export const SESSION_READ_METHODS = Object.freeze(['harness.v1.sessions.load', 'harness.v1.sessions.follow', 'harness.v1.sessions.unfollow', 'harness.v1.sessions.recorded_config']);
16
+ export const SESSION_READ_METHODS = Object.freeze(['harness.v1.sessions.load', 'harness.v1.sessions.follow', 'harness.v1.sessions.unfollow', 'harness.v1.sessions.recorded_config', 'harness.v1.sessions.launch_agent', 'harness.v1.sessions.usage']);
14
17
  export const SESSION_READ_EVENT = 'harness.v1.sessions.event';
15
- export const SESSION_READ_CAPABILITY = Object.freeze({ version: 1, owner: 'caller', language: 'typescript', native_parser: false, recorded_config: true });
18
+ export const SESSION_READ_CAPABILITY = Object.freeze({ version: 1, owner: 'caller', language: 'typescript', native_parser: false, recorded_config: true, launch_agent: true, usage: true });
16
19
  export { parseNativeJson as parseSessionWire, stringifyNativeJson as stringifySessionWire };
17
20
  const invalid = (message) => { throw Object.assign(new Error(message), { rpcCode: -32602 }); };
18
21
  const object = (value, name) => {
@@ -111,3 +114,24 @@ export async function loadRecordedSessionConfiguration(params, context = {}) {
111
114
  const { locator } = parseSessionReadParams({ locator: params.locator }, context);
112
115
  return recordedSessionConfiguration(await loadSessionLocator(locator, { ...context, fidelity: 'byte_lossless', includeSubagents: true }), locator.harness);
113
116
  }
117
+
118
+ export async function loadSessionLaunchAgent(params, context = {}) {
119
+ object(params, 'launch metadata params');
120
+ const { locator } = parseSessionReadParams({ locator: params.locator }, context);
121
+ // This door needs no transcript or parser. Preserve the original lexical
122
+ // path for the harness-home lookup, rather than the normalized load path.
123
+ locator.storage.path = params.locator.storage.path;
124
+ return { environment: recordedLaunchAgent(locator, context) };
125
+ }
126
+
127
+ export async function loadSessionUsage(params, context = {}) {
128
+ object(params, 'session usage params');
129
+ const { locator } = parseSessionReadParams({ locator: params.locator }, context);
130
+ const since = params.since == null ? null : parseUsageSince(string(params.since, 'since'));
131
+ // This door reads the complete lossless session, including child records,
132
+ // and ignores unrelated load/view options rather than applying a window.
133
+ const session = await loadSessionLocator(locator, { ...context, fidelity: 'byte_lossless', includeSubagents: true });
134
+ const answer = { harness: locator.harness, session_id: session.meta.session_id, usage: recordedSessionUsage(session, since) };
135
+ if (since != null) answer.since = usageMoment(since);
136
+ return answer;
137
+ }
@@ -0,0 +1,65 @@
1
+ // Preserve the retained sidecar's UTC/i64 interpretation for recorded usage.
2
+ // In particular this is not Date.parse: native records accept signed time
3
+ // components, truncate fractions, and do not normalize day buckets to UTC.
4
+ import { nativeTrim } from './text.mjs';
5
+
6
+ const min = -(1n << 63n), max = (1n << 63n) - 1n;
7
+ const wrap = (n) => BigInt.asIntN(64, n);
8
+ const checked = (n) => n >= min && n <= max ? n : null;
9
+ const floor = (n, d) => n / d - (n % d < 0n ? 1n : 0n);
10
+ function integer(text) {
11
+ if (!/^[+-]?[0-9]+$/.test(text)) return null;
12
+ return checked(BigInt(text));
13
+ }
14
+ function split(text, delimiter) {
15
+ const a = text.indexOf(delimiter), b = text.indexOf(delimiter, a + 1);
16
+ return a < 0 || b < 0 ? null : [text.slice(0, a), text.slice(a + 1, b), text.slice(b + 1)];
17
+ }
18
+
19
+ export function usageTimestamp(text) {
20
+ if (typeof text !== 'string') return null;
21
+ text = nativeTrim(text); if (text.endsWith('Z')) text = text.slice(0, -1);
22
+ const t = text.indexOf('T'); if (t < 0) return null;
23
+ const date = split(text.slice(0, t), '-'); let time = text.slice(t + 1), millis = 0n;
24
+ const dot = time.indexOf('.');
25
+ if (dot >= 0) {
26
+ const digits = /^[0-9]+/.exec(time.slice(dot + 1))?.[0]; if (!digits) return null;
27
+ millis = BigInt(digits.slice(0, 3).padEnd(3, '0')); time = time.slice(0, dot);
28
+ }
29
+ const clock = split(time, ':'); if (!date || !clock) return null;
30
+ let [year, month, day, hour, minute, second] = [...date, ...clock].map(integer);
31
+ if ([year, month, day, hour, minute, second].some((n) => n == null) || month < 1n || month > 12n || day < 1n || day > 31n) return null;
32
+ year = month <= 2n ? wrap(year - 1n) : year;
33
+ const era = floor(year >= 0n ? year : wrap(year - 399n), 400n);
34
+ const yoe = wrap(year - wrap(era * 400n)), mp = month > 2n ? month - 3n : month + 9n;
35
+ const doy = (153n * mp + 2n) / 5n + day - 1n;
36
+ const doe = wrap(wrap(wrap(yoe * 365n) + yoe / 4n) - yoe / 100n + doy);
37
+ const days = wrap(wrap(era * 146097n) + doe - 719468n), daySeconds = checked(days * 86400n);
38
+ if (daySeconds == null) return null;
39
+ const seconds = checked(daySeconds + wrap(wrap(hour * 3600n + wrap(minute * 60n)) + second));
40
+ if (seconds == null) return null;
41
+ const whole = checked(seconds * 1000n); return whole == null ? null : checked(whole + millis);
42
+ }
43
+
44
+ export function parseUsageSince(text, now = BigInt(Date.now())) {
45
+ text = nativeTrim(text); const at = usageTimestamp(text); if (at != null) return at;
46
+ const digits = /^[0-9]+/.exec(text)?.[0];
47
+ const amount = digits == null ? null : integer(digits);
48
+ if (amount == null) throw new Error(`\`${text}\` is neither a duration (10m, 1h, 2d) nor an RFC 3339 moment`);
49
+ const units = new Map([['s', 1000n], ['m', 60000n], ['h', 3600000n], ['d', 86400000n]]), unit = units.get(text.slice(digits.length));
50
+ if (unit == null) throw new Error(`\`${text}\`: a duration's unit is s, m, h or d (10m, 1h, 2d)`);
51
+ return wrap(now - wrap(amount * unit));
52
+ }
53
+
54
+ export function usageMoment(ms) {
55
+ let seconds = floor(ms, 1000n); const millis = ms - seconds * 1000n;
56
+ if (seconds < 0n) seconds = 0n; // Retained sidecar clamps pre-epoch seconds.
57
+ const z = seconds / 86400n + 719468n, era = z / 146097n, doe = z - era * 146097n;
58
+ const yoe = (doe - doe / 1460n + doe / 36524n - doe / 146096n) / 365n;
59
+ let year = yoe + era * 400n;
60
+ const doy = doe - (365n * yoe + yoe / 4n - yoe / 100n), mp = (5n * doy + 2n) / 153n;
61
+ const day = doy - (153n * mp + 2n) / 5n + 1n, month = mp + (mp < 10n ? 3n : -9n);
62
+ year += month <= 2n ? 1n : 0n;
63
+ const tod = seconds % 86400n, pad = (n, width = 2) => String(n).padStart(width, '0');
64
+ return `${pad(year, 4)}-${pad(month)}-${pad(day)}T${pad(tod / 3600n)}:${pad(tod % 3600n / 60n)}:${pad(tod % 60n)}.${pad(millis, 3)}Z`;
65
+ }
@@ -0,0 +1,110 @@
1
+ // The public session reader's accounting, ported from token_spend.rs and the
2
+ // 2026-10-03 pricing.rs snapshot. These are parity rates, not a live price feed.
3
+ // Machine-wide accounting and native runtime budgets retain their consumers.
4
+ import { nativeInteger, parseNativeJson, setNativeFloat, setNativeInteger } from './native-json.mjs';
5
+ import { field, stringField } from './session-codec-utils.mjs';
6
+ import { usageTimestamp } from './session-usage-time.mjs';
7
+
8
+ const rates = [
9
+ ['claude-fable-5-1', [10, 50, .25, 12.5, 20, 1, 1.1]],
10
+ ['claude-mythos-5-1', [10, 50, .25, 12.5, 20, 1, 1.1]],
11
+ ['claude-fable-5', [10, 50, 1, 12.5, 20, 1, 1.1]],
12
+ ['claude-mythos-5', [10, 50, 1, 12.5, 20, 1, 1.1]],
13
+ ['claude-opus-5-5', [4, 20, .2, 5, 8, 2, 1.1]],
14
+ ['claude-opus-5', [5, 25, .5, 6.25, 10, 2, 1.1]],
15
+ ['claude-opus-4-8', [5, 25, .5, 6.25, 10, 2, 1.1]],
16
+ ['claude-opus-4-7', [5, 25, .5, 6.25, 10, 1, 1.1]],
17
+ ['claude-opus-4-6', [5, 25, .5, 6.25, 10, 1, 1.1]],
18
+ ['claude-opus-4-5', [5, 25, .5, 6.25, 10, 1, 1]],
19
+ ['claude-opus-4-1', [15, 75, 1.5, 18.75, 30, 1, 1]],
20
+ ['claude-opus-4-0', [15, 75, 1.5, 18.75, 30, 1, 1]],
21
+ ['claude-opus-4', [15, 75, 1.5, 18.75, 30, 1, 1]],
22
+ ['claude-sonnet-5-5', [2, 10, .2, 2.5, 4, 1, 1.1]],
23
+ ['claude-sonnet-5', [2, 10, .2, 2.5, 4, 1, 1.1]],
24
+ ['claude-sonnet-4-6', [3, 15, .3, 3.75, 6, 1, 1.1]],
25
+ ['claude-sonnet-4-5', [3, 15, .3, 3.75, 6, 1, 1]],
26
+ ['claude-sonnet-4-0', [3, 15, .3, 3.75, 6, 1, 1]],
27
+ ['claude-sonnet-4', [3, 15, .3, 3.75, 6, 1, 1]],
28
+ ['claude-haiku-4-5', [1, 5, .1, 1.25, 2, 1, 1]],
29
+ ['claude-3-5-haiku', [.8, 4, .08, 1, 1.6, 1, 1]],
30
+ ];
31
+ const wrap = (n) => BigInt.asUintN(64, n), sub = (a, b) => a > b ? a - b : 0n;
32
+ const tokens = (value, key) => nativeInteger(value, key) ?? 0n;
33
+ const sorted = (a, b) => Buffer.compare(Buffer.from(a), Buffer.from(b));
34
+ function price(model) {
35
+ model = model.replaceAll('.', '-');
36
+ for (const [pattern, rate] of rates) {
37
+ for (let from = 0, at; (at = model.indexOf(pattern, from)) >= 0; from = at + pattern.length) {
38
+ const rest = model.slice(at + pattern.length);
39
+ const digits = rest.startsWith('-') ? /^[0-9]*/.exec(rest.slice(1))[0].length : 0;
40
+ if (digits < 1 || digits > 2) return rate;
41
+ }
42
+ }
43
+ return null;
44
+ }
45
+ function recorded(record, model, counts, fast = false, usOnly = false) {
46
+ const at = stringField(record, 'timestamp');
47
+ return { at, atMs: usageTimestamp(at), model, counts, fast, usOnly };
48
+ }
49
+ function claudeUse(record) {
50
+ const message = field(record, 'message'), id = stringField(message, 'id'), usage = field(message, 'usage');
51
+ if (id == null || usage === undefined) return null;
52
+ const model = stringField(message, 'model') ?? 'unknown'; if (model === '<synthetic>') return null;
53
+ const written = tokens(usage, 'cache_creation_input_tokens'), split = field(usage, 'cache_creation');
54
+ const write1h = tokens(split, 'ephemeral_1h_input_tokens'), write5m = tokens(split, 'ephemeral_5m_input_tokens');
55
+ return [id, recorded(record, model, [tokens(usage, 'input_tokens'), tokens(usage, 'output_tokens'), tokens(usage, 'cache_read_input_tokens'),
56
+ split === undefined ? written : wrap(write5m + sub(written, wrap(write5m + write1h))), write1h],
57
+ stringField(usage, 'speed') === 'fast', stringField(usage, 'inference_geo') === 'us')];
58
+ }
59
+ function dayPrefix(at) {
60
+ if (at == null) return 'unknown';
61
+ let bytes = 0, result = '';
62
+ for (const scalar of at) { bytes += Buffer.byteLength(scalar); if (bytes > 10) break; result += scalar; if (bytes === 10) return result; }
63
+ return 'unknown';
64
+ }
65
+ function* records(raw) {
66
+ for (const line of raw) { let record; try { record = parseNativeJson(line); } catch { continue; } yield record; }
67
+ }
68
+
69
+ export function recordedSessionUsage(session, since = null) {
70
+ const days = new Map();
71
+ const count = (record) => {
72
+ if (since != null && (record.atMs == null || record.atMs < since)) return;
73
+ const day = dayPrefix(record.at), key = JSON.stringify([day, record.model]);
74
+ let sum = days.get(key);
75
+ if (!sum) { sum = { day, model: record.model, counts: [0n, 0n, 0n, 0n, 0n], cost: 0, priced: false, unpriced: false }; days.set(key, sum); }
76
+ for (let i = 0; i < 5; i++) sum.counts[i] = wrap(sum.counts[i] + record.counts[i]);
77
+ const rate = price(record.model);
78
+ if (rate == null) sum.unpriced = true;
79
+ else {
80
+ let cost = Number(record.counts[0]) / 1e6 * rate[0];
81
+ for (let i = 1; i < 5; i++) cost += Number(record.counts[i]) / 1e6 * rate[i];
82
+ sum.cost += cost * (record.fast ? rate[5] : 1) * (record.usOnly ? rate[6] : 1); sum.priced = true;
83
+ }
84
+ };
85
+ if (session.meta.source === 'claude_code') {
86
+ const responses = new Map();
87
+ for (const record of records(session.raw)) { const response = claudeUse(record); if (response) responses.set(...response); }
88
+ for (const id of [...responses.keys()].sort(sorted)) count(responses.get(id));
89
+ } else if (session.meta.source === 'codex') {
90
+ let model = session.meta.model ?? 'unknown', previous = [0n, 0n, 0n];
91
+ for (const record of records(session.raw)) {
92
+ const payload = field(record, 'payload');
93
+ if (stringField(record, 'type') === 'turn_context') { model = stringField(payload, 'model') ?? model; continue; }
94
+ if (stringField(payload, 'type') !== 'token_count') continue;
95
+ const total = field(field(payload, 'info'), 'total_token_usage'); if (total === undefined) continue;
96
+ const now = ['input_tokens', 'output_tokens', 'cached_input_tokens'].map((key) => tokens(total, key));
97
+ if (now.every((n, i) => n === previous[i])) continue;
98
+ const [input, output, cached] = now.map((n, i) => sub(n, previous[i])); previous = now;
99
+ count(recorded(record, model, [sub(input, cached), output, cached, 0n, 0n]));
100
+ }
101
+ }
102
+ return [...days.values()].sort((a, b) => sorted(a.day, b.day) || sorted(a.model, b.model)).map((sum) => {
103
+ const row = { at: `${sum.day}T00:00:00Z`, model: sum.model };
104
+ for (const [key, count] of [['input_tokens', sum.counts[0]], ['output_tokens', sum.counts[1]], ['cache_read_tokens', sum.counts[2]],
105
+ ['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);
106
+ if (sum.priced) { row.cost = { currency: 'USD', source: 'rate_table' }; setNativeFloat(row.cost, 'amount', Math.round(sum.cost * 1e6) / 1e6); }
107
+ if (sum.unpriced) row.cost_unrecorded = true;
108
+ return row;
109
+ });
110
+ }