@volter/supercode-interchange 0.1.7 → 0.1.8

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
@@ -131,13 +131,31 @@ price. Costs use the pinned 2026-10-03 native rate snapshot, including cache TTL
131
131
  fast-mode and inference-geo factors; this is not a live pricing feed. Exact u64
132
132
  wire counts are retained independently of JavaScript number rounding. A separate
133
133
  `usage` marker refuses older read services without closing their catalog lane.
134
- Machine-wide spend and runtime budgets retain their separate native consumers.
134
+ Runtime budgets retain their separate native consumers.
135
135
  [Private Mac/Windows conversations](../../docs/evidence/session-usage-port-2026-10-08.md)
136
136
  record small Claude/Codex usage and refusal examples through npm stdio and
137
137
  loopback operator streams. Other-format/runtime corpus, concurrent replacement,
138
138
  scale, remote revocation, all pricing/time branches and a new Windows native
139
139
  artifact remain unqualified; these records do not establish an installed set.
140
140
 
141
+ `sessions.spend` reads recently modified Claude/Codex transcript tails in the
142
+ caller-owned load worker and counts child use on each parent. Its tail stop is
143
+ ten minutes before the requested window; Codex additionally keeps a preceding
144
+ total and model. Machine spend filters Claude usage before the last-response-id
145
+ fold, while per-session usage filters afterward, preserving their distinct
146
+ contracts. Costs use the same pinned rate snapshot. Counts stay exact on raw
147
+ wire frames; SDK JavaScript numbers can round. Totals count all sessions before
148
+ the requested row limit. Live registry names and enrolled-machine addresses are
149
+ display metadata, never proof of caller authority. A separate `spend` marker
150
+ refuses an older service. The npm `sessions usage` command uses this reader
151
+ without requiring a native accounting handler. Runtime budgets, shared native
152
+ record readers and pricing retain their own consumers.
153
+ [Private Mac/Windows spend records](../../docs/evidence/session-machine-spend-2026-10-08.md)
154
+ cover small parent/child, tail-boundary, sort/limit and refusal cases through
155
+ npm and loopback operator streams, plus the new Mac native CLI handoff.
156
+ Large corpus, concurrent changes, every live-name/address branch, installed
157
+ sets and new Windows native forwarding remain unqualified.
158
+
141
159
  `resolveHarnessHomes` follows native HOME-first defaults and preserves explicitly empty harness-specific
142
160
  home values. `projectMetadata` applies workspace scope, native lineage, repository-family, profile, time and
143
161
  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.7",
3
+ "version": "0.1.8",
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,10 +3,11 @@ 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; usage: 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; spend: 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;
10
+ export function prettySessionWire(value: unknown): string;
10
11
  export function parseSessionReadParams(params: unknown, context?: SessionReadContext): { locator: ReadLocator; fidelity: string; view: (SessionFollowOptions & { fromByte: number | null }) | null; options: import('./session-model.js').SessionLoadOptions | null };
11
12
  export function loadSessionRead(params: unknown, context?: SessionReadContext): Promise<ProjectedSessionResult | { session: NormalizedSession & { resolved_profile: RecordedSessionProfile; resolved_profile_history: RecordedSessionProfileChange[] } }>;
12
13
  export function openSessionReadFollower(params: unknown, context?: SessionReadContext): Promise<SessionFollower>;
@@ -23,3 +24,11 @@ export interface SessionTokenUsage {
23
24
  }
24
25
  /** Recorded tokens and the pinned rate snapshot; exact native integers survive wire serialization. */
25
26
  export function loadSessionUsage(params: unknown, context?: SessionReadContext): Promise<{ harness: string; session_id: string | null; since?: string; usage: SessionTokenUsage[] }>;
27
+ export type RecordedTokenSpend = Omit<SessionTokenUsage, 'at' | 'model'>;
28
+ export interface MachineTokenSpend extends RecordedTokenSpend {
29
+ harness: string; session_id: string; address: string | null; name: string | null;
30
+ cwd: string | null; subagents: number; last_at: string | null;
31
+ models: Array<RecordedTokenSpend & { model: string }>;
32
+ }
33
+ /** Recently modified Claude/Codex tails, including child use on the parent. */
34
+ export function loadSessionSpend(params: unknown, context?: SessionReadContext): Promise<{ since: string; until: string; session_count: number; sessions: MachineTokenSpend[]; total: RecordedTokenSpend }>;
package/session-read.mjs CHANGED
@@ -1,7 +1,7 @@
1
1
  // Caller-owned public read operations. Parsing never goes to a native service
2
2
  // or to a machine daemon carrying another caller's home/environment.
3
3
  import { resolve } from 'node:path';
4
- import { nativeInteger, nativeNumberKind, parseNativeJson, stringifyNativeJson } from './native-json.mjs';
4
+ import { nativeInteger, nativeNumberKind, parseNativeJson, stringifyNativeJson, stringifyNativeJsonPretty } from './native-json.mjs';
5
5
  import { loadSessionLocator } from './session-path.mjs';
6
6
  import { loadSessionDisplayLocator, loadSessionDisplayFrom } from './session-display.mjs';
7
7
  import { withClaudeReadIndex, supportsClaudeReadIndex } from './session-claude-index.mjs';
@@ -11,12 +11,15 @@ import { boundSessionView, projectedMessageWindow, projectedSessionJson, project
11
11
  import { recordedSessionConfiguration } from './session-recorded-config.mjs';
12
12
  import { recordedLaunchAgent } from './session-launch-agent.mjs';
13
13
  import { recordedSessionUsage } from './session-usage.mjs';
14
+ import { loadMachineSpend } from './session-spend.mjs';
14
15
  import { parseUsageSince, usageMoment } from './session-usage-time.mjs';
15
16
 
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']);
17
+ 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', 'harness.v1.sessions.spend']);
17
18
  export const SESSION_READ_EVENT = 'harness.v1.sessions.event';
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 });
19
+ 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 });
20
+ export { loadMachineSpend as loadSessionSpend };
19
21
  export { parseNativeJson as parseSessionWire, stringifyNativeJson as stringifySessionWire };
22
+ export { stringifyNativeJsonPretty as prettySessionWire };
20
23
  const invalid = (message) => { throw Object.assign(new Error(message), { rpcCode: -32602 }); };
21
24
  const object = (value, name) => {
22
25
  if (value == null || typeof value !== 'object' || Array.isArray(value)) invalid(`${name} must be an object`);
@@ -0,0 +1,97 @@
1
+ // The machine reading uses the retained native tail boundary, independently
2
+ // of the complete-session loader. Unread/malformed records contribute no use.
3
+ import { openSync, closeSync, fstatSync, readSync, opendirSync, statSync } from 'node:fs';
4
+ import { sep } from 'node:path';
5
+ import { parseNativeJson } from './native-json.mjs';
6
+ import { field, stringField } from './session-codec-utils.mjs';
7
+
8
+ const blockSize = 256 * 1024;
9
+ const utf8 = new TextDecoder('utf-8', { fatal: true, ignoreBOM: true });
10
+ export function textName(bytes) { try { return utf8.decode(bytes); } catch { return null; } }
11
+ export function childPath(directory, name) {
12
+ const root = Buffer.isBuffer(directory) ? directory : Buffer.from(directory);
13
+ const driveOnly = process.platform === 'win32' && root.length === 2 && /^[A-Za-z]:$/.test(root.toString('utf8'));
14
+ return Buffer.concat([root, root.length && !driveOnly && root.at(-1) !== 47 && (process.platform !== 'win32' || root.at(-1) !== 92) ? Buffer.from(sep) : Buffer.alloc(0), Buffer.isBuffer(name) ? name : Buffer.from(name)]);
15
+ }
16
+ export function* directoryEntries(path) {
17
+ let directory;
18
+ try { directory = opendirSync(path, { encoding: 'buffer' }); } catch { return; }
19
+ try {
20
+ for (;;) {
21
+ let entry; try { entry = directory.readSync(); } catch { return; }
22
+ if (entry == null) return;
23
+ yield { name: entry.name, path: childPath(path, entry.name) };
24
+ }
25
+ } finally { try { directory.closeSync(); } catch { /* already closed */ } }
26
+ }
27
+ export function isDirectory(path) { try { return statSync(path).isDirectory(); } catch { return false; } }
28
+ export function modifiedSince(path, since) {
29
+ try { const at = statSync(path, { bigint: true }).mtimeNs; return at >= 0n && BigInt.asIntN(64, at / 1000000n) >= since; }
30
+ catch { return false; }
31
+ }
32
+ export const jsonl = (name) => name.lastIndexOf(46) > 0 && name.subarray(name.lastIndexOf(46)).equals(Buffer.from('.jsonl'));
33
+ export const stem = (name) => name.subarray(0, name.lastIndexOf(46));
34
+
35
+ function readExact(fd, bytes, position) {
36
+ let done = 0;
37
+ while (done < bytes.length) {
38
+ const read = readSync(fd, bytes, done, bytes.length - done, position + done);
39
+ if (!read) return false;
40
+ done += read;
41
+ }
42
+ return true;
43
+ }
44
+ function parsed(bytes) { const text = textName(bytes); if (text == null) return undefined; try { return parseNativeJson(text); } catch { return undefined; } }
45
+
46
+ /** Newest-first boundary callbacks include the stopping record; results are oldest first. */
47
+ export function tailRecords(path, start) {
48
+ let fd;
49
+ try { fd = openSync(path, 'r'); } catch { return []; }
50
+ const newest = [];
51
+ try {
52
+ let end; try { end = fstatSync(fd).size; } catch { return []; }
53
+ if (!Number.isSafeInteger(end)) throw new Error('transcript size exceeds the filesystem reader position range');
54
+ let carry = Buffer.alloc(0);
55
+ while (end > 0) {
56
+ const begin = Math.max(0, end - blockSize), bytes = Buffer.alloc(end - begin);
57
+ try { if (!readExact(fd, bytes, begin)) break; } catch { break; }
58
+ const block = Buffer.concat([bytes, carry]);
59
+ const first = begin === 0 ? -1 : block.indexOf(10);
60
+ if (begin !== 0 && first < 0) { carry = block; end = begin; continue; }
61
+ const from = first + 1;
62
+ let lineEnd = block.length, reached = false;
63
+ for (let at = block.length - 1; at >= from - 1; at--) {
64
+ if (at >= from && block[at] !== 10) continue;
65
+ const record = parsed(block.subarray(at + 1, lineEnd)); lineEnd = at;
66
+ if (record === undefined) continue;
67
+ reached = start(record); newest.push(record);
68
+ if (reached) break;
69
+ }
70
+ if (reached) break;
71
+ carry = first < 0 ? Buffer.alloc(0) : block.subarray(0, first); end = begin;
72
+ }
73
+ return newest.reverse();
74
+ } finally { closeSync(fd); }
75
+ }
76
+
77
+ /** Codex's first session_meta line, bounded to the native 256 KiB header read. */
78
+ export function rolloutSession(path) {
79
+ let fd;
80
+ try {
81
+ fd = openSync(path, 'r'); const bytes = Buffer.alloc(blockSize);
82
+ let length = 0;
83
+ while (length < bytes.length) {
84
+ const count = readSync(fd, bytes, length, bytes.length - length, length);
85
+ if (!count) break;
86
+ const newline = bytes.indexOf(10, length); length += count;
87
+ if (newline >= 0 && newline < length) { length = newline + 1; break; }
88
+ }
89
+ const record = parsed(bytes.subarray(0, length));
90
+ if (stringField(record, 'type') !== 'session_meta') return null;
91
+ const payload = field(record, 'payload'), thread = stringField(payload, 'id'); if (thread == null) return null;
92
+ const nested = field(field(field(field(payload, 'source'), 'subagent'), 'thread_spawn'), 'parent_thread_id');
93
+ const parent = nested === undefined ? field(payload, 'parent_thread_id') : nested;
94
+ return { thread, parent: typeof parent === 'string' ? parent : null, cwd: stringField(payload, 'cwd') };
95
+ } catch { return null; }
96
+ finally { if (fd != null) closeSync(fd); }
97
+ }
@@ -0,0 +1,74 @@
1
+ // Display metadata only: these names and addresses establish no caller grant.
2
+ import { readFileSync } from 'node:fs';
3
+ import { dirname, isAbsolute, parse, sep } from 'node:path';
4
+ import { hostname, userInfo } from 'node:os';
5
+ import { execFile } from 'node:child_process';
6
+ import { nativeInteger, parseNativeJson } from './native-json.mjs';
7
+ import { field, stringField } from './session-codec-utils.mjs';
8
+ import { childPath, directoryEntries } from './session-spend-files.mjs';
9
+ import { nativeTrim } from './text.mjs';
10
+
11
+ function json(path) { try { return parseNativeJson(new TextDecoder('utf-8', { fatal: true, ignoreBOM: true }).decode(readFileSync(path))); } catch { return null; } }
12
+ function live(pid) {
13
+ try { process.kill(process.platform === 'win32' ? pid : Number(BigInt.asIntN(32, BigInt(pid))), 0); return true; }
14
+ catch (error) { return process.platform !== 'win32' && error.code === 'EPERM'; }
15
+ }
16
+ const optionalText = (row, key) => row[key] == null || typeof row[key] === 'string';
17
+ const words = (text) => nativeTrim(text).split(/[\u0009-\u000d\u0020\u0085\u00a0\u1680\u2000-\u200a\u2028\u2029\u202f\u205f\u3000]+/u).join(' ');
18
+ function path(...parts) {
19
+ let result = '';
20
+ for (const part of parts) result = isAbsolute(part) || process.platform === 'win32' && /^[A-Za-z]:/.test(part) ? part : childPath(result, part).toString('utf8');
21
+ return result;
22
+ }
23
+ const selected = (path, cwd) => !path || isAbsolute(path) || process.platform === 'win32' && /^[A-Za-z]:/.test(path) ? path : `${cwd}${sep}${path}`;
24
+
25
+ export async function claudeSpendNames(homes, env, cwd) {
26
+ const entries = [];
27
+ const root = homes.claude_code, parent = root && root !== parse(root).root ? dirname(root) : '.';
28
+ for (const file of directoryEntries(selected(path(parent, 'sessions'), cwd))) {
29
+ const dot = file.name.lastIndexOf(46);
30
+ if (dot < 1 || !file.name.subarray(dot).equals(Buffer.from('.json'))) continue;
31
+ const row = json(file.path); if (row == null || typeof row !== 'object' || Array.isArray(row)) continue;
32
+ const pid = nativeInteger(row, 'pid');
33
+ if (pid == null || pid > 0xffffffffn || typeof row.sessionId !== 'string' || !row.sessionId || !row.name
34
+ || typeof row.messagingSocketPath !== 'string' || row.parkedJobId != null
35
+ || !['cwd', 'name', 'messagingSocketPath', 'status', 'version', 'tmux', 'parkedJobId', 'procStart'].every(key => optionalText(row, key))
36
+ || row.updatedAt != null && nativeInteger(row, 'updatedAt') == null || !live(Number(pid))) continue;
37
+ entries.push({ pid: Number(pid), id: row.sessionId, name: row.name, start: row.procStart ?? null });
38
+ }
39
+ const starts = new Map();
40
+ if (process.platform === 'darwin' && entries.some(entry => entry.start != null)) {
41
+ // One process-table read for this display query; an unread start is not a mismatch.
42
+ await new Promise(resolve => execFile('ps', ['-o', 'pid=,lstart=', '-p', entries.map(entry => entry.pid).join(',')],
43
+ { env: { ...env, TZ: 'UTC', LC_ALL: 'C' }, timeout: 2000, maxBuffer: 1024 * 1024 }, (error, stdout) => {
44
+ if (!error) for (const line of stdout.split('\n')) { const match = /^\s*(\d+)\s+(.+)$/.exec(line); if (match) starts.set(Number(match[1]), words(match[2])); }
45
+ resolve();
46
+ }));
47
+ }
48
+ const names = new Map();
49
+ for (const entry of entries.sort((a, b) => a.pid - b.pid)) {
50
+ let actual = starts.get(entry.pid);
51
+ if (process.platform === 'linux' && entry.start != null) {
52
+ try { const stat = readFileSync(`/proc/${entry.pid}/stat`, 'utf8'); actual = stat.slice(stat.lastIndexOf(')') + 1).trim().split(/\s+/)[19]; } catch { /* unread establishes no mismatch */ }
53
+ }
54
+ if (entry.start != null && actual != null && actual !== (process.platform === 'darwin' ? words(entry.start) : nativeTrim(entry.start))) continue;
55
+ names.set(entry.id, entry.name);
56
+ }
57
+ return names;
58
+ }
59
+
60
+ export function spendMachineName(env, cwd) {
61
+ let home; try { home = env.HOME || (process.platform === 'win32' && env.USERPROFILE) || userInfo().homedir || '.'; } catch { home = '.'; }
62
+ const supercodeHome = env.SUPERCODE_HOME || (env.XDG_CONFIG_HOME ? path(env.XDG_CONFIG_HOME, 'supercode') : path(home, '.config', 'supercode'));
63
+ const workspaces = selected(path(env.SUPERCODE_TEAMS_HOME || path(supercodeHome, 'teams'), 'workspaces'), cwd);
64
+ const contexts = json(path(workspaces, 'contexts.json')), current = stringField(contexts, 'current');
65
+ const context = current == null ? null : field(field(contexts, 'contexts'), current), server = stringField(context, 'server_id'), team = stringField(context, 'team_id');
66
+ const enrolled = server != null && team != null ? stringField(field(json(path(workspaces, 'connectors', server, team, 'enrollment.json')), 'machine'), 'name') : null;
67
+ let host; try { host = process.platform === 'win32' ? env.COMPUTERNAME : nativeTrim(hostname()) || null; } catch { host = null; }
68
+ const name = (enrolled ?? host ?? 'localhost').split('.')[0];
69
+ return [...name].map(c => /^[A-Z]$/.test(c) ? c.toLowerCase() : /^[a-z0-9_-]$/.test(c) ? c : '-').join('') || 'localhost';
70
+ }
71
+
72
+ export function spendAddress(machine, harness, id) {
73
+ return id && !/[\u0009-\u000d\u0020\u0085\u00a0\u1680\u2000-\u200a\u2028\u2029\u202f\u205f\u3000]/u.test(id) ? `sc:${machine}:${harness}:${id}` : null;
74
+ }
@@ -0,0 +1,169 @@
1
+ // Machine-wide recorded spend, preserving token_spend's bounded tail and
2
+ // parent/child fold. Runtime-budget readers remain with their native owners.
3
+ import { isAbsolute, sep } from 'node:path';
4
+ import { nativeInteger, nativeNumberKind, setNativeFloat, setNativeInteger } from './native-json.mjs';
5
+ import { field, stringField } from './session-codec-utils.mjs';
6
+ import { resolveHarnessHomes } from './homes.mjs';
7
+ import { usageTimestamp, parseUsageSince, usageMoment } from './session-usage-time.mjs';
8
+ import { claudeUse, price, recorded, sorted, sub, wrap } from './session-usage.mjs';
9
+ import { directoryEntries, childPath, isDirectory, modifiedSince, jsonl, stem, textName, tailRecords, rolloutSession } from './session-spend-files.mjs';
10
+ import { claudeSpendNames, spendMachineName, spendAddress } from './session-spend-metadata.mjs';
11
+
12
+ const invalid = message => { throw Object.assign(new Error(message), { rpcCode: -32602 }); };
13
+ const outOfOrder = 600000n, signed = n => BigInt.asIntN(64, n);
14
+ const atMs = record => usageTimestamp(stringField(record, 'timestamp'));
15
+ function sum() { return { counts: [0n, 0n, 0n, 0n, 0n], cost: 0, priced: false, unpriced: false }; }
16
+ function add(target, source) {
17
+ for (let i = 0; i < 5; i++) target.counts[i] = wrap(target.counts[i] + source.counts[i]);
18
+ target.cost += source.cost; target.priced ||= source.priced; target.unpriced ||= source.unpriced;
19
+ }
20
+ function count(target, record) {
21
+ for (let i = 0; i < 5; i++) target.counts[i] = wrap(target.counts[i] + record.counts[i]);
22
+ const rate = price(record.model);
23
+ if (rate == null) { target.unpriced = true; return; }
24
+ let cost = Number(record.counts[0]) / 1e6 * rate[0];
25
+ for (let i = 1; i < 5; i++) cost += Number(record.counts[i]) / 1e6 * rate[i];
26
+ target.cost += cost * (record.fast ? rate[5] : 1) * (record.usOnly ? rate[6] : 1); target.priced = true;
27
+ }
28
+ function fields(sum, row = {}) {
29
+ for (const [key, n] of [['input_tokens', sum.counts[0]], ['output_tokens', sum.counts[1]], ['cache_read_tokens', sum.counts[2]],
30
+ ['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, n);
31
+ if (sum.priced) { row.cost = { currency: 'USD', source: 'rate_table' }; setNativeFloat(row.cost, 'amount', Math.round(sum.cost * 1e6) / 1e6); }
32
+ if (sum.unpriced) row.cost_unrecorded = true;
33
+ return row;
34
+ }
35
+ function sessionOf(sessions, harness, id) {
36
+ const key = JSON.stringify([harness, id]); let session = sessions.get(key);
37
+ if (!session) { session = { harness, id, models: new Map(), cwd: null, children: new Set(), last: 0n }; sessions.set(key, session); }
38
+ return session;
39
+ }
40
+ function recordUse(session, record) {
41
+ let model = session.models.get(record.model); if (!model) { model = sum(); session.models.set(record.model, model); }
42
+ count(model, record); if (record.atMs != null && record.atMs > session.last) session.last = record.atMs;
43
+ }
44
+ function claudeFile(path, since, id, child, sessions) {
45
+ const records = tailRecords(path, record => { const at = atMs(record); return at != null && at < signed(since - outOfOrder); });
46
+ const responses = new Map(); let cwd = null;
47
+ for (const record of records) {
48
+ cwd = stringField(record, 'cwd') ?? cwd;
49
+ const response = claudeUse(record);
50
+ // Machine spend filters before the last-record fold; per-session usage
51
+ // filters after that fold. They are distinct retained native contracts.
52
+ if (response && response[1].atMs != null && response[1].atMs >= since) responses.set(...response);
53
+ }
54
+ if (!responses.size) return;
55
+ const session = sessionOf(sessions, 'claude-code', id);
56
+ for (const id of [...responses.keys()].sort(sorted)) recordUse(session, responses.get(id));
57
+ if (child == null || session.cwd == null) session.cwd = cwd ?? session.cwd;
58
+ if (child != null) session.children.add(child);
59
+ }
60
+ function claudeSpend(root, since, sessions) {
61
+ for (const project of directoryEntries(root)) for (const entry of directoryEntries(project.path)) {
62
+ if (isDirectory(entry.path)) {
63
+ const parent = textName(entry.name); if (parent == null) continue;
64
+ for (const child of directoryEntries(childPath(entry.path, 'subagents'))) if (jsonl(child.name) && modifiedSince(child.path, since)) {
65
+ claudeFile(child.path, since, parent, stem(child.name).toString('utf8'), sessions);
66
+ }
67
+ } else if (jsonl(entry.name) && modifiedSince(entry.path, since)) {
68
+ const id = textName(stem(entry.name)); if (id != null) claudeFile(entry.path, since, id, null, sessions);
69
+ }
70
+ }
71
+ }
72
+ function codexFile(path, since, sessions) {
73
+ const meta = rolloutSession(path); if (!meta) return;
74
+ let totalBefore = false, modelBefore = false;
75
+ const records = tailRecords(path, record => {
76
+ const at = atMs(record);
77
+ if (at != null && at < signed(since - outOfOrder)) {
78
+ totalBefore ||= stringField(field(record, 'payload'), 'type') === 'token_count';
79
+ modelBefore ||= stringField(record, 'type') === 'turn_context';
80
+ }
81
+ return totalBefore && modelBefore;
82
+ });
83
+ let model = 'unknown', previous = [0n, 0n, 0n], session = null;
84
+ for (const record of records) {
85
+ const payload = field(record, 'payload');
86
+ if (stringField(record, 'type') === 'turn_context') { model = stringField(payload, 'model') ?? model; continue; }
87
+ if (stringField(payload, 'type') !== 'token_count') continue;
88
+ const total = field(field(payload, 'info'), 'total_token_usage'); if (total === undefined) continue;
89
+ const now = ['input_tokens', 'output_tokens', 'cached_input_tokens'].map(key => nativeInteger(total, key) ?? 0n);
90
+ if (now.every((n, i) => n === previous[i])) continue;
91
+ const [input, output, cached] = now.map((n, i) => sub(n, previous[i])); previous = now;
92
+ const used = recorded(record, model, [sub(input, cached), output, cached, 0n, 0n]);
93
+ if (used.atMs == null || used.atMs < since) continue;
94
+ session ??= sessionOf(sessions, 'codex', meta.parent ?? meta.thread); recordUse(session, used);
95
+ }
96
+ if (!session) return;
97
+ if (meta.parent != null) session.children.add(meta.thread);
98
+ if ((session.cwd == null || meta.parent == null) && meta.cwd != null) session.cwd = meta.cwd;
99
+ }
100
+ function codexSpend(root, since, sessions) {
101
+ const stack = [[root, 0]];
102
+ while (stack.length) {
103
+ const [path, depth] = stack.pop();
104
+ for (const entry of directoryEntries(path)) {
105
+ if (isDirectory(entry.path)) { if (depth < 4) stack.push([entry.path, depth + 1]); continue; }
106
+ const name = textName(entry.name);
107
+ if (name?.startsWith('rollout-') && name.endsWith('.jsonl') && modifiedSince(entry.path, since)) codexFile(entry.path, since, sessions);
108
+ }
109
+ }
110
+ }
111
+
112
+ function query(params, context) {
113
+ if (params == null || typeof params !== 'object' || Array.isArray(params)) invalid('spend params must be an object');
114
+ for (const key of Object.keys(params)) if (!['since', 'harnesses', 'sort', 'limit', 'homes'].includes(key)) invalid(`unknown field ${key}`);
115
+ if (typeof params.since !== 'string') invalid('since must be a string');
116
+ const harnesses = Object.hasOwn(params, 'harnesses') ? params.harnesses : [];
117
+ if (!Array.isArray(harnesses) || !harnesses.every(h => typeof h === 'string')) invalid('harnesses must be a string array');
118
+ const sort = params.sort ?? 'cost'; if (typeof sort !== 'string') invalid('sort must be a string');
119
+ if (!['cost', 'tokens'].includes(sort)) invalid(`sort \`${sort}\`: the spend reading sorts by \`cost\` or \`tokens\``);
120
+ const bad = harnesses.find(h => !['claude-code', 'codex'].includes(h));
121
+ if (bad != null) invalid(`harness \`${bad}\`: the spend reading reads \`claude-code\` and \`codex\``);
122
+ let limit = null;
123
+ if (params.limit != null) {
124
+ let n = nativeInteger(params, 'limit');
125
+ if (n == null && nativeNumberKind(params, 'limit') == null && Number.isSafeInteger(params.limit) && params.limit >= 0) n = BigInt(params.limit);
126
+ if (n == null) invalid('limit must be an unsigned 64-bit integer');
127
+ limit = Number(n > BigInt(Number.MAX_SAFE_INTEGER) ? BigInt(Number.MAX_SAFE_INTEGER) : n);
128
+ }
129
+ const homes = resolveHarnessHomes(context.env);
130
+ if (Object.hasOwn(params, 'homes')) {
131
+ if (params.homes == null || typeof params.homes !== 'object' || Array.isArray(params.homes)) invalid('homes must be an object');
132
+ // HarnessHomes ignores unknown fields but rejects a supplied known field
133
+ // that is not a path string, including null.
134
+ for (const key of Object.keys(homes)) if (Object.hasOwn(params.homes, key)) {
135
+ if (typeof params.homes[key] !== 'string') invalid(`homes.${key} must be a string`);
136
+ homes[key] = params.homes[key];
137
+ }
138
+ }
139
+ for (const key of Object.keys(homes)) {
140
+ const home = homes[key];
141
+ if (home && !isAbsolute(home) && !(process.platform === 'win32' && /^[A-Za-z]:/.test(home))) homes[key] = `${context.cwd}${sep}${home}`;
142
+ }
143
+ return { since: params.since, harnesses, sort, limit, homes };
144
+ }
145
+
146
+ export async function loadMachineSpend(params, { env = process.env, cwd = process.cwd() } = {}) {
147
+ const selected = query(params, { env, cwd }), now = BigInt(Date.now()), since = parseUsageSince(selected.since, now), sessions = new Map();
148
+ if (!selected.harnesses.length || selected.harnesses.includes('claude-code')) claudeSpend(selected.homes.claude_code, since, sessions);
149
+ if (!selected.harnesses.length || selected.harnesses.includes('codex')) codexSpend(selected.homes.codex, since, sessions);
150
+ const names = await claudeSpendNames(selected.homes, env, cwd), machine = spendMachineName(env, cwd), rows = [];
151
+ const ordered = [...sessions.values()].sort((a, b) => sorted(a.harness, b.harness) || sorted(a.id, b.id));
152
+ for (const session of ordered) {
153
+ const total = sum(), models = [];
154
+ for (const model of [...session.models.keys()].sort(sorted)) { const value = session.models.get(model); add(total, value); models.push(fields(value, { model })); }
155
+ const row = fields(total, { harness: session.harness, session_id: session.id, address: spendAddress(machine, session.harness, session.id),
156
+ name: session.harness === 'codex' ? `codex-${[...session.id].slice(0, 8).join('')}` : names.get(session.id) ?? null,
157
+ cwd: session.cwd, subagents: session.children.size, last_at: session.last > 0n ? usageMoment(session.last) : null, models });
158
+ rows.push({ total, row });
159
+ }
160
+ const tokens = value => value.counts.reduce((total, n) => wrap(total + n), 0n);
161
+ rows.sort((a, b) => {
162
+ if (selected.sort !== 'tokens' && a.total.priced !== b.total.priced) return a.total.priced ? -1 : 1;
163
+ if (selected.sort === 'tokens' || !a.total.priced) { const x = tokens(a.total), y = tokens(b.total); return x === y ? 0 : x > y ? -1 : 1; }
164
+ return a.total.cost === b.total.cost ? 0 : a.total.cost > b.total.cost ? -1 : 1;
165
+ });
166
+ const total = sum(); for (const value of rows) add(total, value.total);
167
+ return { since: usageMoment(since), until: usageMoment(now), session_count: rows.length,
168
+ sessions: rows.slice(0, selected.limit ?? rows.length).map(value => value.row), total: fields(total) };
169
+ }
package/session-usage.mjs CHANGED
@@ -1,6 +1,6 @@
1
1
  // The public session reader's accounting, ported from token_spend.rs and the
2
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.
3
+ // Native runtime budgets retain their independent consumers.
4
4
  import { nativeInteger, parseNativeJson, setNativeFloat, setNativeInteger } from './native-json.mjs';
5
5
  import { field, stringField } from './session-codec-utils.mjs';
6
6
  import { usageTimestamp } from './session-usage-time.mjs';
@@ -56,6 +56,9 @@ function claudeUse(record) {
56
56
  split === undefined ? written : wrap(write5m + sub(written, wrap(write5m + write1h))), write1h],
57
57
  stringField(usage, 'speed') === 'fast', stringField(usage, 'inference_geo') === 'us')];
58
58
  }
59
+
60
+ // Internal accounting primitives shared by the bounded machine-tail reader.
61
+ export { claudeUse, price, recorded, sorted, sub, wrap };
59
62
  function dayPrefix(at) {
60
63
  if (at == null) return 'unknown';
61
64
  let bytes = 0, result = '';