@volter/supercode-interchange 0.1.5 → 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
@@ -123,6 +123,17 @@ record selected-home answers and null malformed metadata through npm and
123
123
  operator streams; concurrent path/record replacement and runtime writing remain
124
124
  unqualified.
125
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
+
126
137
  `resolveHarnessHomes` follows native HOME-first defaults and preserves explicitly empty harness-specific
127
138
  home values. `projectMetadata` applies workspace scope, native lineage, repository-family, profile, time and
128
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.5",
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",
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; launch_agent: 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;
@@ -14,3 +14,12 @@ export function openSessionReadFollower(params: unknown, context?: SessionReadCo
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
15
  /** Best-effort recorded environment only, independent of transcript existence or caller authority. */
16
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
@@ -10,10 +10,12 @@ 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
12
  import { recordedLaunchAgent } from './session-launch-agent.mjs';
13
+ import { recordedSessionUsage } from './session-usage.mjs';
14
+ import { parseUsageSince, usageMoment } from './session-usage-time.mjs';
13
15
 
14
- export const SESSION_READ_METHODS = Object.freeze(['harness.v1.sessions.load', 'harness.v1.sessions.follow', 'harness.v1.sessions.unfollow', 'harness.v1.sessions.recorded_config', 'harness.v1.sessions.launch_agent']);
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']);
15
17
  export const SESSION_READ_EVENT = 'harness.v1.sessions.event';
16
- export const SESSION_READ_CAPABILITY = Object.freeze({ version: 1, owner: 'caller', language: 'typescript', native_parser: false, recorded_config: true, launch_agent: 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 });
17
19
  export { parseNativeJson as parseSessionWire, stringifyNativeJson as stringifySessionWire };
18
20
  const invalid = (message) => { throw Object.assign(new Error(message), { rpcCode: -32602 }); };
19
21
  const object = (value, name) => {
@@ -121,3 +123,15 @@ export async function loadSessionLaunchAgent(params, context = {}) {
121
123
  locator.storage.path = params.locator.storage.path;
122
124
  return { environment: recordedLaunchAgent(locator, context) };
123
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
+ }