@volter/supercode-interchange 0.1.7 → 0.1.9

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,19 @@ 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.resume_instructions` reads the complete selected transcript before
127
+ returning a structured launch. An explicit cwd wins over the transcript's
128
+ recorded cwd, then `.`; the locator supplies the harness and session id.
129
+ Default and explicit `yolo` policy use the shared generated startup table.
130
+ Grok receives its resolved default home only when that home is a symlink and
131
+ no explicit `GROK_HOME` was supplied. This returns arguments and environment;
132
+ it neither starts a harness nor writes a session or permission configuration.
133
+ The separate `resume_instructions` capability marker refuses older services.
134
+ The [private Mac/Windows record](../../docs/evidence/session-resume-instructions-2026-10-08.md)
135
+ retains eight harnesses' specifications, cwd/environment cases and older-service
136
+ refusal through npm and loopback operator streams. Installed sets, actual live
137
+ resume and full-corpus qualification remain open.
138
+
126
139
  `sessions.usage` reads a complete lossless session in the load worker and groups
127
140
  recorded tokens by the native day prefix and model. Claude keeps the last usage
128
141
  record for each response id; Codex separates successive cumulative totals and
@@ -131,13 +144,31 @@ price. Costs use the pinned 2026-10-03 native rate snapshot, including cache TTL
131
144
  fast-mode and inference-geo factors; this is not a live pricing feed. Exact u64
132
145
  wire counts are retained independently of JavaScript number rounding. A separate
133
146
  `usage` marker refuses older read services without closing their catalog lane.
134
- Machine-wide spend and runtime budgets retain their separate native consumers.
147
+ Runtime budgets retain their separate native consumers.
135
148
  [Private Mac/Windows conversations](../../docs/evidence/session-usage-port-2026-10-08.md)
136
149
  record small Claude/Codex usage and refusal examples through npm stdio and
137
150
  loopback operator streams. Other-format/runtime corpus, concurrent replacement,
138
151
  scale, remote revocation, all pricing/time branches and a new Windows native
139
152
  artifact remain unqualified; these records do not establish an installed set.
140
153
 
154
+ `sessions.spend` reads recently modified Claude/Codex transcript tails in the
155
+ caller-owned load worker and counts child use on each parent. Its tail stop is
156
+ ten minutes before the requested window; Codex additionally keeps a preceding
157
+ total and model. Machine spend filters Claude usage before the last-response-id
158
+ fold, while per-session usage filters afterward, preserving their distinct
159
+ contracts. Costs use the same pinned rate snapshot. Counts stay exact on raw
160
+ wire frames; SDK JavaScript numbers can round. Totals count all sessions before
161
+ the requested row limit. Live registry names and enrolled-machine addresses are
162
+ display metadata, never proof of caller authority. A separate `spend` marker
163
+ refuses an older service. The npm `sessions usage` command uses this reader
164
+ without requiring a native accounting handler. Runtime budgets, shared native
165
+ record readers and pricing retain their own consumers.
166
+ [Private Mac/Windows spend records](../../docs/evidence/session-machine-spend-2026-10-08.md)
167
+ cover small parent/child, tail-boundary, sort/limit and refusal cases through
168
+ npm and loopback operator streams, plus the new Mac native CLI handoff.
169
+ Large corpus, concurrent changes, every live-name/address branch, installed
170
+ sets and new Windows native forwarding remain unqualified.
171
+
141
172
  `resolveHarnessHomes` follows native HOME-first defaults and preserves explicitly empty harness-specific
142
173
  home values. `projectMetadata` applies workspace scope, native lineage, repository-family, profile, time and
143
174
  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.9",
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; resume_instructions: 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,13 @@ 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 }>;
35
+ /** Reads the complete source and returns a specification; starts no process. */
36
+ export function loadSessionResumeInstructions(params: unknown, context?: SessionReadContext): Promise<{ launch: { cwd: string; program: string; arguments: string[]; env: Record<string, string> } }>;
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,16 @@ 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';
15
+ import { sessionResumeLaunch } from './session-resume-instructions.mjs';
14
16
  import { parseUsageSince, usageMoment } from './session-usage-time.mjs';
15
17
 
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']);
18
+ 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', 'harness.v1.sessions.resume_instructions']);
17
19
  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 });
20
+ 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, resume_instructions: true });
21
+ export { loadMachineSpend as loadSessionSpend };
19
22
  export { parseNativeJson as parseSessionWire, stringifyNativeJson as stringifySessionWire };
23
+ export { stringifyNativeJsonPretty as prettySessionWire };
20
24
  const invalid = (message) => { throw Object.assign(new Error(message), { rpcCode: -32602 }); };
21
25
  const object = (value, name) => {
22
26
  if (value == null || typeof value !== 'object' || Array.isArray(value)) invalid(`${name} must be an object`);
@@ -135,3 +139,15 @@ export async function loadSessionUsage(params, context = {}) {
135
139
  if (since != null) answer.since = usageMoment(since);
136
140
  return answer;
137
141
  }
142
+
143
+ export async function loadSessionResumeInstructions(params, context = {}) {
144
+ object(params, 'resume instructions params');
145
+ const { locator } = parseSessionReadParams({ locator: params.locator }, context);
146
+ const cwd = params.cwd == null ? null : string(params.cwd, 'cwd');
147
+ const policy = Object.hasOwn(params, 'policy') ? params.policy : 'default';
148
+ if (!['default', 'yolo'].includes(policy)) invalid(`unknown resume policy: ${String(policy)}`);
149
+ // Native admission loads the source even when cwd is explicit, and its
150
+ // launch identity comes from the locator rather than a transcript header.
151
+ const session = await loadSessionLocator(locator, { ...context, fidelity: 'byte_lossless', includeSubagents: true });
152
+ return { launch: sessionResumeLaunch(locator.harness, locator.session_id, cwd ?? session.meta.cwd ?? '.', policy, context) };
153
+ }
@@ -0,0 +1,54 @@
1
+ // A launch specification only. The caller decides whether to run the harness.
2
+ import { lstatSync, realpathSync } from 'node:fs';
3
+ import { userInfo } from 'node:os';
4
+ import { isAbsolute } from 'node:path';
5
+ import { STARTUP_PROMPTS } from './startup-prompts.mjs';
6
+ import { childPath } from './session-spend-files.mjs';
7
+
8
+ function startupArguments(harness, cwd, unattended) {
9
+ const prompts = STARTUP_PROMPTS[harness];
10
+ if (!prompts) return [];
11
+ const where = JSON.stringify(cwd);
12
+ const arguments_ = prompts.startup.flatMap(pair => pair.map(flag => flag.replaceAll('{cwd}', where)));
13
+ if (unattended) arguments_.push(...prompts.unattended);
14
+ return arguments_;
15
+ }
16
+
17
+ function grokHomeEnvironment(env, cwd) {
18
+ if (env.GROK_HOME !== undefined) return {};
19
+ let home = env.HOME || (process.platform === 'win32' ? env.USERPROFILE : null);
20
+ if (!home) { try { home = userInfo().homedir; } catch { return {}; } }
21
+ if (!isAbsolute(home) && !(process.platform === 'win32' && /^[A-Za-z]:/.test(home))) home = childPath(cwd, home);
22
+ const path = childPath(home, '.grok');
23
+ try { if (lstatSync(path).isSymbolicLink()) return { GROK_HOME: realpathSync(path) }; }
24
+ catch { /* no recorded filesystem resolution to supply */ }
25
+ return {};
26
+ }
27
+
28
+ export function sessionResumeLaunch(harness, sessionId, cwd, policy, { env = process.env, cwd: readCwd = process.cwd() } = {}) {
29
+ const unattended = policy === 'yolo';
30
+ let program, arguments_;
31
+ switch (harness) {
32
+ case 'grok':
33
+ program = 'grok';
34
+ arguments_ = [...(unattended ? ['--sandbox', 'workspace', '--always-approve'] : []), '--resume', sessionId];
35
+ break;
36
+ case 'codex':
37
+ program = 'codex'; arguments_ = [...startupArguments(harness, cwd, unattended), 'resume', sessionId]; break;
38
+ case 'claude-code':
39
+ program = 'claude'; arguments_ = [...startupArguments(harness, cwd, unattended), '--resume', sessionId]; break;
40
+ case 'gemini':
41
+ program = 'gemini'; arguments_ = [...startupArguments(harness, cwd, unattended), '--resume', sessionId]; break;
42
+ case 'goose':
43
+ program = 'goose'; arguments_ = ['session', '--resume', '--session-id', sessionId]; break;
44
+ case 'pi':
45
+ program = 'pi'; arguments_ = [...startupArguments(harness, cwd, unattended), '--session', sessionId]; break;
46
+ case 'opencode':
47
+ program = 'opencode'; arguments_ = ['--session', sessionId]; break;
48
+ case 'supercode':
49
+ program = 'supercode'; arguments_ = [...(unattended ? ['--dangerous'] : []), 'resume', sessionId]; break;
50
+ default:
51
+ throw Object.assign(new Error(`no structured resume launch is registered for harness \`${harness}\``), { rpcCode: -32602 });
52
+ }
53
+ return { cwd, program, arguments: arguments_, env: program === 'grok' ? grokHomeEnvironment(env, readCwd) : {} };
54
+ }
@@ -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 = '';
@@ -0,0 +1,64 @@
1
+ // Generated from crates/harness/src/startup_prompts.json by scripts/startup-prompts.mjs (the pre-commit hook runs it):
2
+ // edit that table, not this file.
3
+ export const STARTUP_PROMPTS = Object.freeze({
4
+ "claude-code": {
5
+ "startup": [],
6
+ "unattended": [
7
+ "--dangerously-skip-permissions"
8
+ ],
9
+ "unattended_chosen_by": [
10
+ "--permission-mode",
11
+ "--permission-prompts",
12
+ "--dangerously-skip-permissions",
13
+ "--allow-dangerously-skip-permissions"
14
+ ]
15
+ },
16
+ "codex": {
17
+ "startup": [
18
+ [
19
+ "-c",
20
+ "check_for_update_on_startup=false"
21
+ ],
22
+ [
23
+ "-c",
24
+ "projects={{cwd}={trust_level=\"trusted\"}}"
25
+ ],
26
+ [
27
+ "--no-alt-screen"
28
+ ],
29
+ [
30
+ "--dangerously-bypass-hook-trust"
31
+ ]
32
+ ],
33
+ "unattended": [
34
+ "--dangerously-bypass-approvals-and-sandbox"
35
+ ],
36
+ "unattended_chosen_by": [
37
+ "-a",
38
+ "--ask-for-approval",
39
+ "-s",
40
+ "--sandbox",
41
+ "--approve-for-me",
42
+ "--dangerously-bypass-approvals-and-sandbox"
43
+ ]
44
+ },
45
+ "gemini": {
46
+ "startup": [],
47
+ "unattended": [
48
+ "--yolo"
49
+ ],
50
+ "unattended_chosen_by": [
51
+ "--yolo",
52
+ "--approval-mode"
53
+ ]
54
+ },
55
+ "pi": {
56
+ "startup": [],
57
+ "unattended": [
58
+ "--approve"
59
+ ],
60
+ "unattended_chosen_by": [
61
+ "--approve"
62
+ ]
63
+ }
64
+ });