@inetafrica/open-claudia 3.4.12 → 3.4.14

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/CHANGELOG.md CHANGED
@@ -1,5 +1,17 @@
1
1
  # Changelog
2
2
 
3
+ ## 3.4.14
4
+
5
+ - Subscribe to managed call audio only when gateway-authenticated publisher metadata identifies a User. Exclude bots, the current bot's other feeds, and missing or malformed classifications before creating subscriber connections or speech streams.
6
+ - Preserve metadata through the initial room response and live publisher events. Managed calls use that authenticated classification; legacy calls retain their existing display-prefix exclusion because client-supplied metadata is untrusted.
7
+ - Exercise the actual session subscription and offer/answer path for allowed users and rejected bot, self and unknown feeds.
8
+
9
+ ## 3.4.13
10
+
11
+ - **Codex is now measured the way Claude is.** After each Codex turn the bot reads Codex's own per-request token log (`$CODEX_HOME/sessions/…/rollout-*-<thread>.jsonl`) and uses the last request's size as the live context, instead of the turn's input summed over every tool call. That sum (33M on one turn) tripped auto-compact after almost every substantial Codex turn — 8 needless compactions on 2026-09-05, each a summary run plus a cold restart — while Codex's real window occupancy never exceeded 245k of its 258k window. Codex sessions now compact at 80% of the window Codex reports.
12
+ - **Codex usage was mis-recorded three ways; all fixed.** (1) `turn.completed` usage is per turn by Codex's own definition, but the ledger treated it as a running total and subtracted the previous turn — 9 of 36 Codex turns that day were logged as zero. (2) OpenAI counts cached tokens inside `input_tokens`; the ledger and `/usage` now report uncached input for both providers (the raw comparison read 89.7M vs 4.5k for what was really 2.4M vs 0.8M). (3) Sub-agent threads Codex spawns with `spawn_agent` never appeared anywhere: the day's ten hidden threads read ~240M tokens and took a weekly quota from 75% to 100% in two hours. They are now attributed to the parent turn (`subagentThreads`, `subagentInputTokens`, …) and shown by `/usage`.
13
+ - **Quota and window visibility.** Ledger rows carry `contextWindowTokens`, `requests`, `providerCompactions` (Codex's own compactions) and the plan quota snapshot (`quotaUsedPercent`, `quotaResetsAt`); `/usage` prints window occupancy, sub-agent totals and quota used.
14
+
3
15
  ## 3.4.12
4
16
 
5
17
  - Keep incoming calls on their per-attempt admission policy when the organization canary setting changes; managed access supplies the assigned gateway and TURN configuration. Owner-only audio calling remains enforced.
@@ -256,8 +256,8 @@ class KazeeCallSession {
256
256
  }
257
257
  for (const publisher of data.publishers || []) {
258
258
  const feedId = Number(publisher.id);
259
- if (Number.isSafeInteger(feedId) && feedId > 0 && feedId !== this.publisherId && !this.subscribers.has(feedId) && !String(publisher.display || "").startsWith("open-claudia:")) {
260
- await this._subscribe(feedId, publisher.display);
259
+ if (Number.isSafeInteger(feedId) && feedId > 0 && feedId !== this.publisherId && !this.subscribers.has(feedId)) {
260
+ await this._subscribe(feedId, publisher.display, publisher.metadata);
261
261
  }
262
262
  }
263
263
  if (data.videoroom === "joined") {
@@ -357,8 +357,21 @@ class KazeeCallSession {
357
357
  this.speak(this.greeting).catch((error) => console.error("Kazee call greeting:", error.message));
358
358
  }
359
359
 
360
- async _subscribe(feedId, display) {
361
- if (this.subscribers.has(feedId)) return;
360
+ async _subscribe(feedId, display, metadata) {
361
+ if (this.closed || feedId === this.publisherId || this.subscribers.has(feedId)) return;
362
+ if (this.managedRoom) {
363
+ // Only the enforced gateway owns this metadata. Display remains an email
364
+ // for presentation, and cannot tell us whether a publisher is another bot.
365
+ // Missing or malformed classification must not start STT on an unknown feed.
366
+ const principal = metadata?.kazee;
367
+ if (!principal || Array.isArray(principal) || principal.version !== 1 ||
368
+ typeof principal.userId !== "string" || !/^[a-f\d]{24}$/i.test(principal.userId) ||
369
+ !["User", "Bot"].includes(principal.principalKind) || principal.principalKind === "Bot" ||
370
+ principal.userId.toLowerCase() === this.botUserId.toLowerCase()) return;
371
+ } else if (String(display || "").startsWith("open-claudia:")) {
372
+ // Legacy/direct rooms allow client metadata, so it is not a trusted source.
373
+ return;
374
+ }
362
375
  // Reserve the slot before the first await: the join response and the async
363
376
  // publishers event both list the same feed, and while attach() is in
364
377
  // flight the second frame passes the caller's has() check and subscribes
package/core/handlers.js CHANGED
@@ -1684,6 +1684,16 @@ register({
1684
1684
  `Cost: $${u.costUsd.toFixed(4)}`,
1685
1685
  `Last turn context: ${fmt(u.lastInputTokens)}`,
1686
1686
  ];
1687
+ const occupancy = u.liveContextTokens || u.lastInputTokens || 0;
1688
+ if (u.contextWindowTokens > 0) {
1689
+ lines.push(`Context window: ${fmt(occupancy)} of ${fmt(u.contextWindowTokens)} (${Math.round((100 * occupancy) / u.contextWindowTokens)}%)`);
1690
+ }
1691
+ if (u.subagentThreads > 0) {
1692
+ lines.push(`Provider sub-agents: ${u.subagentThreads} thread${u.subagentThreads === 1 ? "" : "s"} · ${fmt(u.subagentInputTokens || 0)} in / ${fmt(u.subagentOutputTokens || 0)} out (spawned by the model; not in the totals above)`);
1693
+ }
1694
+ if (Number.isFinite(u.quotaUsedPercent)) {
1695
+ lines.push(`Plan quota used: ${u.quotaUsedPercent}%${u.quotaResetsAt ? ` (resets ${String(u.quotaResetsAt).slice(0, 16).replace("T", " ")}Z)` : ""}`);
1696
+ }
1687
1697
  if (u.lastInputTokens > 200000) {
1688
1698
  const threshold = effectiveCompactThreshold(currentState());
1689
1699
  if (threshold === 0) {
@@ -102,7 +102,9 @@ function normalizeCodexEvent(rawEvent, state = createCodexParserState()) {
102
102
  if (state.terminal) return [];
103
103
  state.terminal = true;
104
104
  const events = [];
105
- const usage = normalizedUsage(rawEvent.usage, "session_total", state.seenUsage);
105
+ // Per-turn by Codex's own definition ("the usage of tokens during a turn",
106
+ // exec_events.rs) — not a session total; see runner.normalizeCodexUsage.
107
+ const usage = normalizedUsage(rawEvent.usage, "turn", state.seenUsage);
106
108
  if (usage) events.push(usage);
107
109
  events.push({
108
110
  type: "result",
@@ -0,0 +1,312 @@
1
+ "use strict";
2
+
3
+ // Codex writes one JSONL "rollout" per thread under $CODEX_HOME/sessions/
4
+ // YYYY/MM/DD/rollout-<local start>-<thread id>.jsonl. Every model request
5
+ // appends an event_msg/token_count record whose info.last_token_usage is that
6
+ // single request (input_tokens INCLUDES cached_input_tokens, so it is the
7
+ // window occupancy after the request), plus info.model_context_window and a
8
+ // rate_limits snapshot (the ChatGPT-plan quota). Codex's own compactions land
9
+ // as top-level "compacted" records, and threads the model spawned with
10
+ // spawn_agent get their own rollout whose session_meta carries
11
+ // thread_source "subagent" and parent_thread_id.
12
+ //
13
+ // `codex exec --json` itself only reports the per-turn total (turn.completed),
14
+ // so this reader is how Open Claudia measures Codex the way it measures
15
+ // Claude: peak/last single-request context instead of the turn-summed input,
16
+ // and sub-agent threads the turn total never mentions (probed live
17
+ // 2026-09-05: three parallel sub-agents took a weekly quota from 75% to 100%
18
+ // while the ledger saw none of it).
19
+
20
+ const fs = require("fs");
21
+ const os = require("os");
22
+ const path = require("path");
23
+
24
+ const DAY_MS = 24 * 60 * 60 * 1000;
25
+ const DEFAULT_LOOKBACK_DAYS = 3;
26
+ // session_meta carries the full base instructions (~22 KB observed on
27
+ // 0.153.4), so the first line is read in chunks until its newline.
28
+ const FIRST_LINE_CHUNK_BYTES = 65536;
29
+ const FIRST_LINE_MAX_BYTES = 4 * 1024 * 1024;
30
+
31
+ function resolveCodexHome(env = process.env) {
32
+ const raw = (env && env.CODEX_HOME) || process.env.CODEX_HOME;
33
+ return raw ? path.resolve(String(raw)) : path.join(os.homedir(), ".codex");
34
+ }
35
+
36
+ function pad(n) { return String(n).padStart(2, "0"); }
37
+
38
+ function dayDirs(codexHome, { now = Date.now(), days = DEFAULT_LOOKBACK_DAYS } = {}) {
39
+ // Rollout directories use the local date; include the UTC date too so a
40
+ // thread that straddles midnight in either clock is still found.
41
+ const seen = new Set();
42
+ const dirs = [];
43
+ for (let back = 0; back < days; back += 1) {
44
+ const d = new Date(now - back * DAY_MS);
45
+ const candidates = [
46
+ `${d.getFullYear()}/${pad(d.getMonth() + 1)}/${pad(d.getDate())}`,
47
+ `${d.getUTCFullYear()}/${pad(d.getUTCMonth() + 1)}/${pad(d.getUTCDate())}`,
48
+ ];
49
+ for (const rel of candidates) {
50
+ if (seen.has(rel)) continue;
51
+ seen.add(rel);
52
+ const full = path.join(codexHome, "sessions", rel);
53
+ try { if (fs.statSync(full).isDirectory()) dirs.push(full); } catch (_) {}
54
+ }
55
+ }
56
+ return dirs;
57
+ }
58
+
59
+ function listRollouts(codexHome, options = {}) {
60
+ const out = [];
61
+ for (const dir of dayDirs(codexHome, options)) {
62
+ let names = [];
63
+ try { names = fs.readdirSync(dir); } catch (_) { continue; }
64
+ for (const name of names) {
65
+ if (!name.startsWith("rollout-") || !name.endsWith(".jsonl")) continue;
66
+ out.push(path.join(dir, name));
67
+ }
68
+ }
69
+ return out;
70
+ }
71
+
72
+ function threadIdOf(filePath) {
73
+ const base = path.basename(filePath, ".jsonl");
74
+ // rollout-2026-09-05T19-19-01-<uuid>: the uuid is the trailing 36 chars.
75
+ const match = base.match(/-([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})$/i);
76
+ return match ? match[1] : null;
77
+ }
78
+
79
+ function findRolloutFile(threadId, { codexHome = resolveCodexHome(), now = Date.now(), days = DEFAULT_LOOKBACK_DAYS } = {}) {
80
+ if (!threadId) return null;
81
+ const suffix = `-${String(threadId)}.jsonl`;
82
+ for (const file of listRollouts(codexHome, { now, days })) {
83
+ if (file.endsWith(suffix)) return file;
84
+ }
85
+ return null;
86
+ }
87
+
88
+ function parseTs(value) {
89
+ const t = Date.parse(String(value || ""));
90
+ return Number.isFinite(t) ? t : null;
91
+ }
92
+
93
+ function readSessionMeta(filePath) {
94
+ let fd = null;
95
+ try {
96
+ fd = fs.openSync(filePath, "r");
97
+ const chunks = [];
98
+ let total = 0;
99
+ let firstLine = null;
100
+ while (total < FIRST_LINE_MAX_BYTES) {
101
+ const buffer = Buffer.alloc(FIRST_LINE_CHUNK_BYTES);
102
+ const read = fs.readSync(fd, buffer, 0, FIRST_LINE_CHUNK_BYTES, total);
103
+ if (read <= 0) break;
104
+ chunks.push(buffer.subarray(0, read));
105
+ total += read;
106
+ const newline = buffer.subarray(0, read).indexOf(0x0a);
107
+ if (newline >= 0) {
108
+ firstLine = Buffer.concat(chunks).toString("utf8", 0, total - read + newline);
109
+ break;
110
+ }
111
+ }
112
+ if (firstLine === null) firstLine = Buffer.concat(chunks).toString("utf8");
113
+ const record = JSON.parse(firstLine);
114
+ if (record && record.type === "session_meta" && record.payload) return normalizeSessionMeta(record);
115
+ return null;
116
+ } catch (_) {
117
+ return null;
118
+ } finally {
119
+ if (fd !== null) { try { fs.closeSync(fd); } catch (_) {} }
120
+ }
121
+ }
122
+
123
+ function normalizeSessionMeta(record) {
124
+ const p = record.payload || {};
125
+ return {
126
+ threadId: p.id || p.session_id || null,
127
+ timestamp: parseTs(p.timestamp || record.timestamp),
128
+ threadSource: typeof p.thread_source === "string" ? p.thread_source : (p.thread_source ? "subagent" : null),
129
+ parentThreadId: p.parent_thread_id || null,
130
+ agentNickname: p.agent_nickname || null,
131
+ cliVersion: p.cli_version || null,
132
+ originator: p.originator || null,
133
+ };
134
+ }
135
+
136
+ function num(value) {
137
+ const n = Number(value);
138
+ return Number.isFinite(n) ? n : 0;
139
+ }
140
+
141
+ function usageOf(raw) {
142
+ if (!raw || typeof raw !== "object") return null;
143
+ return {
144
+ input: num(raw.input_tokens),
145
+ cached: num(raw.cached_input_tokens),
146
+ cacheWrite: num(raw.cache_write_input_tokens),
147
+ output: num(raw.output_tokens),
148
+ reasoning: num(raw.reasoning_output_tokens),
149
+ };
150
+ }
151
+
152
+ function emptyTotals() {
153
+ return { input: 0, cached: 0, cacheWrite: 0, output: 0, reasoning: 0 };
154
+ }
155
+
156
+ function addTotals(target, usage) {
157
+ target.input += usage.input;
158
+ target.cached += usage.cached;
159
+ target.cacheWrite += usage.cacheWrite;
160
+ target.output += usage.output;
161
+ target.reasoning += usage.reasoning;
162
+ return target;
163
+ }
164
+
165
+ function rateLimitsOf(raw) {
166
+ if (!raw || typeof raw !== "object") return null;
167
+ const primary = raw.primary && typeof raw.primary === "object" ? raw.primary : null;
168
+ const secondary = raw.secondary && typeof raw.secondary === "object" ? raw.secondary : null;
169
+ const window = (w) => (w ? {
170
+ usedPercent: Number.isFinite(Number(w.used_percent)) ? Number(w.used_percent) : null,
171
+ windowMinutes: Number.isFinite(Number(w.window_minutes)) ? Number(w.window_minutes) : null,
172
+ resetsAt: Number.isFinite(Number(w.resets_at)) ? new Date(Number(w.resets_at) * 1000).toISOString() : null,
173
+ } : null);
174
+ return {
175
+ planType: raw.plan_type || null,
176
+ primary: window(primary),
177
+ secondary: window(secondary),
178
+ limitReached: raw.rate_limit_reached_type || null,
179
+ };
180
+ }
181
+
182
+ // Parse one rollout. `since` (ms epoch) keeps only records from the current
183
+ // turn — a rollout holds the whole thread, every turn since it was created.
184
+ function parseRollout(filePath, { since = 0 } = {}) {
185
+ let text;
186
+ try { text = fs.readFileSync(filePath, "utf8"); } catch (_) { return null; }
187
+ const result = {
188
+ file: filePath,
189
+ threadId: threadIdOf(filePath),
190
+ sessionMeta: null,
191
+ requests: [],
192
+ totals: emptyTotals(),
193
+ peakContextTokens: 0,
194
+ lastContextTokens: 0,
195
+ modelContextWindow: null,
196
+ compactions: 0,
197
+ rateLimits: null,
198
+ lastEventAt: null,
199
+ };
200
+ let previousFingerprint = null;
201
+ for (const line of text.split("\n")) {
202
+ if (!line) continue;
203
+ let record;
204
+ try { record = JSON.parse(line); } catch (_) { continue; }
205
+ if (!record || typeof record !== "object") continue;
206
+ if (record.type === "session_meta" && !result.sessionMeta) {
207
+ result.sessionMeta = normalizeSessionMeta(record);
208
+ if (!result.threadId) result.threadId = result.sessionMeta.threadId;
209
+ continue;
210
+ }
211
+ const ts = parseTs(record.timestamp);
212
+ if (ts !== null) result.lastEventAt = ts;
213
+ const inTurn = since ? (ts === null || ts >= since) : true;
214
+ if (record.type === "compacted") {
215
+ if (inTurn) result.compactions += 1;
216
+ continue;
217
+ }
218
+ const payload = record.payload;
219
+ if (!payload || payload.type !== "token_count") continue;
220
+ const info = payload.info || null;
221
+ if (info && Number.isFinite(Number(info.model_context_window))) {
222
+ result.modelContextWindow = Number(info.model_context_window);
223
+ }
224
+ const limits = rateLimitsOf(payload.rate_limits);
225
+ if (limits) result.rateLimits = limits;
226
+ if (!inTurn || !info) continue;
227
+ const last = usageOf(info.last_token_usage);
228
+ if (!last) continue;
229
+ const total = usageOf(info.total_token_usage) || last;
230
+ // Codex re-emits token_count for rate-limit refreshes with the same usage;
231
+ // one request must count once.
232
+ const fingerprint = JSON.stringify([last, total]);
233
+ if (fingerprint === previousFingerprint) continue;
234
+ previousFingerprint = fingerprint;
235
+ result.requests.push({ ts, ...last });
236
+ addTotals(result.totals, last);
237
+ if (last.input > result.peakContextTokens) result.peakContextTokens = last.input;
238
+ result.lastContextTokens = last.input;
239
+ }
240
+ return result;
241
+ }
242
+
243
+ function findChildRollouts(parentThreadId, { codexHome = resolveCodexHome(), since = 0, now = Date.now(), days = DEFAULT_LOOKBACK_DAYS, exclude = new Set() } = {}) {
244
+ if (!parentThreadId) return [];
245
+ const children = [];
246
+ for (const file of listRollouts(codexHome, { now, days })) {
247
+ if (exclude.has(file)) continue;
248
+ const meta = readSessionMeta(file);
249
+ if (!meta || meta.parentThreadId !== parentThreadId) continue;
250
+ if (since && meta.timestamp !== null && meta.timestamp < since) continue;
251
+ children.push({ file, threadId: meta.threadId || threadIdOf(file), meta });
252
+ }
253
+ return children;
254
+ }
255
+
256
+ // Everything the ledger and the compaction gate need for one Codex turn.
257
+ // Returns null when the rollout is not on disk (the caller falls back to the
258
+ // turn total Codex reported).
259
+ function collectCodexTurnUsage({ threadId, since = 0, env = process.env, codexHome = resolveCodexHome(env), now = Date.now(), includeSubagents = true, maxDepth = 2 } = {}) {
260
+ const file = findRolloutFile(threadId, { codexHome, now });
261
+ if (!file) return null;
262
+ const parent = parseRollout(file, { since });
263
+ if (!parent) return null;
264
+ const subagents = { threads: 0, requests: 0, totals: emptyTotals(), compactions: 0, threadIds: [] };
265
+ if (includeSubagents) {
266
+ const visited = new Set([file]);
267
+ let frontier = [threadId];
268
+ for (let depth = 0; depth < maxDepth && frontier.length; depth += 1) {
269
+ const next = [];
270
+ for (const parentId of frontier) {
271
+ for (const child of findChildRollouts(parentId, { codexHome, since, now, exclude: visited })) {
272
+ visited.add(child.file);
273
+ const parsed = parseRollout(child.file, { since: 0 });
274
+ if (!parsed) continue;
275
+ subagents.threads += 1;
276
+ subagents.requests += parsed.requests.length;
277
+ addTotals(subagents.totals, parsed.totals);
278
+ subagents.compactions += parsed.compactions;
279
+ subagents.threadIds.push(child.threadId);
280
+ if (child.threadId) next.push(child.threadId);
281
+ }
282
+ }
283
+ frontier = next;
284
+ }
285
+ }
286
+ return {
287
+ source: "rollout",
288
+ file,
289
+ threadId: parent.threadId || threadId,
290
+ requests: parent.requests.length,
291
+ peakContextTokens: parent.peakContextTokens,
292
+ lastContextTokens: parent.lastContextTokens,
293
+ modelContextWindow: parent.modelContextWindow,
294
+ totals: parent.totals,
295
+ compactions: parent.compactions,
296
+ rateLimits: parent.rateLimits,
297
+ subagents,
298
+ };
299
+ }
300
+
301
+ module.exports = {
302
+ DEFAULT_LOOKBACK_DAYS,
303
+ resolveCodexHome,
304
+ dayDirs,
305
+ listRollouts,
306
+ threadIdOf,
307
+ findRolloutFile,
308
+ readSessionMeta,
309
+ parseRollout,
310
+ findChildRollouts,
311
+ collectCodexTurnUsage,
312
+ };
@@ -119,6 +119,12 @@ function createCodexProvider(options = {}) {
119
119
  }
120
120
  }
121
121
 
122
+ function codexMultiAgentEnabled(settings) {
123
+ if (settings && settings.multiAgent === false) return false;
124
+ const raw = String(process.env.CODEX_MULTI_AGENT || "").trim().toLowerCase();
125
+ return !(raw === "0" || raw === "off" || raw === "false" || raw === "disabled");
126
+ }
127
+
122
128
  function buildInvocation(context, utility) {
123
129
  if (!context || typeof context !== "object") throw providerError("INVALID_PROVIDER_CONTEXT", "provider context must be an object");
124
130
  assertSupportedContext(context);
@@ -165,6 +171,12 @@ function createCodexProvider(options = {}) {
165
171
  if (preparedHook) args.push(...preparedHook.args);
166
172
 
167
173
  args.push("-c", `developer_instructions=${JSON.stringify(coreInstructions)}`);
174
+ // Codex 0.153 ships multi_agent on: the model may spawn parallel sub-agent
175
+ // threads (spawn_agent) whose usage its turn total never reports. Probed
176
+ // 2026-09-05: three parallel sub-agents took a weekly Pro quota from 75% to
177
+ // 100% in two hours. CODEX_MULTI_AGENT=off (or providerSettings.multiAgent
178
+ // false) disables the feature for every run the bot launches.
179
+ if (!codexMultiAgentEnabled(settings)) args.push("-c", "features.multi_agent=false");
168
180
 
169
181
  const effort = context.effort || settings.effort || null;
170
182
  if (effort) {
package/core/runner.js CHANGED
@@ -64,24 +64,36 @@ function usageNumber(usage, ...keys) {
64
64
  return 0;
65
65
  }
66
66
 
67
+ const { collectCodexTurnUsage } = require("./providers/codex-rollout");
68
+
69
+ // Never let a rollout read break a turn: the ledger falls back to the turn
70
+ // total Codex reported when the file is missing or unreadable.
71
+ function readCodexRollout(options) {
72
+ try { return collectCodexTurnUsage(options); } catch (_) { return null; }
73
+ }
74
+
67
75
  function usageParts(usage, backend) {
68
76
  if (!new Set(["claude", "codex"]).has(backend)) {
69
77
  throw Object.assign(new TypeError("usage accounting requires an explicit provider"), { code: "INVALID_PROVIDER" });
70
78
  }
71
- const input = usageNumber(usage, "input_tokens");
79
+ const rawInput = usageNumber(usage, "input_tokens");
72
80
  const cacheRead = backend === "codex"
73
81
  ? usageNumber(usage, "cached_input_tokens", "cache_read_input_tokens")
74
82
  : usageNumber(usage, "cache_read_input_tokens", "cached_input_tokens");
75
83
  const cacheCreation = usageNumber(usage, "cache_creation_input_tokens");
76
84
  const output = usageNumber(usage, "output_tokens");
85
+ // Codex/OpenAI reports cached input INSIDE input_tokens; Anthropic reports
86
+ // cache reads BESIDE input_tokens. Normalise so `input` is the uncached
87
+ // input for both providers (like-for-like in /usage and the ledger — on
88
+ // 2026-09-05 the raw numbers read 89.7M vs 4.5k for what was really 2.4M vs
89
+ // 0.8M) while `context` stays the full prefix each provider actually sent.
90
+ const input = backend === "codex" ? Math.max(0, rawInput - cacheRead) : rawInput;
77
91
  return {
78
92
  input,
79
93
  cacheRead,
80
94
  cacheCreation,
81
95
  output,
82
- // Codex/OpenAI reports cached input as part of input_tokens. Keep the
83
- // cache-read field for attribution, but do not add it again to context.
84
- context: backend === "codex" ? input : input + cacheRead + cacheCreation,
96
+ context: backend === "codex" ? rawInput : rawInput + cacheRead + cacheCreation,
85
97
  };
86
98
  }
87
99
 
@@ -109,24 +121,15 @@ function clampUsageDelta(current, previous) {
109
121
  };
110
122
  }
111
123
 
112
- function snapshotCodexUsage(state, sessionId, usage, options = {}) {
113
- if (!state || !sessionId || !usage) return null;
114
- const project = options.project || getProjectKey(state);
115
- if (!project) return null;
116
- const bucket = getUsageForSession(state, project, "codex", sessionId);
117
- const previous = bucket.providerUsageSnapshot || null;
118
- bucket.providerUsageSnapshot = {
119
- input_tokens: usageNumber(usage, "input_tokens"),
120
- cached_input_tokens: usageNumber(usage, "cached_input_tokens"),
121
- output_tokens: usageNumber(usage, "output_tokens"),
122
- };
123
- return previous;
124
- }
125
-
124
+ // Codex's turn.completed usage is "the usage of tokens during a turn"
125
+ // (codex-rs/exec/src/exec_events.rs) and every `codex exec resume` is its own
126
+ // process, so the number is already per turn. Until 3.4.13 it was treated as a
127
+ // cumulative session total and the previous turn subtracted: any turn smaller
128
+ // than its predecessor was recorded as 0 and the rest were undercounted (9 of
129
+ // 36 Codex turns on 2026-09-05 logged as zero). Pass it through unchanged.
126
130
  function normalizeCodexUsage(state, sessionId, usage, options = {}) {
127
- const previous = snapshotCodexUsage(state, sessionId, usage, options);
128
- if (!previous) return { usage, usageScope: "session_total", rawUsage: usage };
129
- return { usage: clampUsageDelta(usage, previous), usageScope: "turn_delta", rawUsage: usage };
131
+ void state; void sessionId; void options;
132
+ return { usage, usageScope: "turn", rawUsage: usage };
130
133
  }
131
134
 
132
135
  function applyUsageToState(state, usage, costUsd, opts = {}) {
@@ -147,6 +150,18 @@ function applyUsageToState(state, usage, costUsd, opts = {}) {
147
150
  u.lastUsageScope = opts.usageScope || "turn_delta";
148
151
  if (Number.isFinite(opts.liveContextTokens)) u.liveContextTokens = opts.liveContextTokens;
149
152
  else u.liveContextTokens = parts.context;
153
+ if (Number.isFinite(opts.contextWindowTokens) && opts.contextWindowTokens > 0) u.contextWindowTokens = opts.contextWindowTokens;
154
+ if (opts.quota && Number.isFinite(opts.quota.usedPercent)) {
155
+ u.quotaUsedPercent = opts.quota.usedPercent;
156
+ u.quotaResetsAt = opts.quota.resetsAt || null;
157
+ }
158
+ if (opts.subagents && opts.subagents.threads > 0) {
159
+ const totals = opts.subagents.totals || {};
160
+ u.subagentThreads = (u.subagentThreads || 0) + opts.subagents.threads;
161
+ u.subagentInputTokens = (u.subagentInputTokens || 0) + Math.max(0, (totals.input || 0) - (totals.cached || 0));
162
+ u.subagentCacheReadTokens = (u.subagentCacheReadTokens || 0) + (totals.cached || 0);
163
+ u.subagentOutputTokens = (u.subagentOutputTokens || 0) + (totals.output || 0);
164
+ }
150
165
  if (typeof costUsd === "number") u.costUsd += costUsd;
151
166
  return parts;
152
167
  }
@@ -185,6 +200,23 @@ function logTurnUsage(state, usage, costUsd, announce, opts = {}) {
185
200
  costUsd: typeof costUsd === "number" ? costUsd : 0,
186
201
  };
187
202
  if (typeof opts.coldStart === "boolean") record.coldStart = opts.coldStart;
203
+ if (opts.contextSource) record.contextSource = opts.contextSource;
204
+ if (Number.isFinite(opts.contextWindowTokens) && opts.contextWindowTokens > 0) record.contextWindowTokens = opts.contextWindowTokens;
205
+ if (Number.isFinite(opts.requests) && opts.requests > 0) record.requests = opts.requests;
206
+ if (Number.isFinite(opts.providerCompactions)) record.providerCompactions = opts.providerCompactions;
207
+ if (opts.quota && Number.isFinite(opts.quota.usedPercent)) {
208
+ record.quotaUsedPercent = opts.quota.usedPercent;
209
+ if (opts.quota.windowMinutes) record.quotaWindowMinutes = opts.quota.windowMinutes;
210
+ if (opts.quota.resetsAt) record.quotaResetsAt = opts.quota.resetsAt;
211
+ }
212
+ if (opts.subagents && opts.subagents.threads > 0) {
213
+ const totals = opts.subagents.totals || {};
214
+ record.subagentThreads = opts.subagents.threads;
215
+ record.subagentRequests = opts.subagents.requests || 0;
216
+ record.subagentInputTokens = Math.max(0, (totals.input || 0) - (totals.cached || 0));
217
+ record.subagentCacheReadTokens = totals.cached || 0;
218
+ record.subagentOutputTokens = totals.output || 0;
219
+ }
188
220
  if (opts.rawUsage && opts.rawUsage !== usage) {
189
221
  const raw = usageParts(opts.rawUsage, backend);
190
222
  record.rawInputTokens = raw.input;
@@ -207,7 +239,7 @@ function logTurnUsage(state, usage, costUsd, announce, opts = {}) {
207
239
  if (announce && usageOwnerOnly) {
208
240
  const baseline = result.trend?.baselineAvgContextTokens || 0;
209
241
  const rate = result.trend?.latestRate || 0;
210
- const contextLabel = hasPeak || usageScope === "turn_delta" ? "Context this turn" : "Session context total";
242
+ const contextLabel = usageScope === "session_total" && !hasPeak ? "Session context total" : "Context this turn";
211
243
  announce([
212
244
  "Token usage alert:",
213
245
  `${contextLabel}: ${fmtTokens(record.contextTokens)}`,
@@ -948,11 +980,22 @@ function resolveRunCwd(cwd) {
948
980
  return fallback;
949
981
  }
950
982
 
951
- function effectiveCompactThreshold(state = currentState()) {
983
+ const CODEX_WINDOW_COMPACT_FRACTION = 0.8;
984
+
985
+ function effectiveCompactThreshold(state = currentState(), opts = {}) {
952
986
  const override = state.settings?.compactWindow;
953
987
  if (override === 0) return 0;
954
- if (Number.isFinite(override) && override > 0) return override;
955
- return Number.isFinite(AUTO_COMPACT_TOKENS) ? AUTO_COMPACT_TOKENS : 380000;
988
+ const base = Number.isFinite(override) && override > 0
989
+ ? override
990
+ : (Number.isFinite(AUTO_COMPACT_TOKENS) ? AUTO_COMPACT_TOKENS : 380000);
991
+ // Codex's window (258k for gpt-6-astra) sits below the default threshold, so
992
+ // measured honestly the bot would never compact a Codex session — and would
993
+ // lose the brief that feeds memory. Compact at 80% of the reported window.
994
+ const window = Number(opts.contextWindowTokens ?? state.sessionUsage?.contextWindowTokens);
995
+ if (getActiveProvider(state) === "codex" && Number.isFinite(window) && window > 0) {
996
+ return Math.min(base, Math.floor(window * CODEX_WINDOW_COMPACT_FRACTION));
997
+ }
998
+ return base;
956
999
  }
957
1000
 
958
1001
  function shouldAutoCompact(state = currentState(), opts = {}) {
@@ -1511,13 +1554,32 @@ async function persistForegroundUsage(result, runContext, state = currentState()
1511
1554
  ?? usageParts(usage, runContext.provider).context;
1512
1555
  const usageSessionId = result.sessionId || runContext.sessionId;
1513
1556
  const usageProject = runContext.project?.name || null;
1557
+ let rollout = null;
1514
1558
  if (runContext.provider === "codex") {
1515
1559
  const normalized = normalizeCodexUsage(state, usageSessionId, usage, { project: usageProject });
1516
1560
  usage = normalized.usage;
1517
1561
  usageScope = normalized.usageScope;
1518
1562
  rawUsage = normalized.rawUsage;
1519
- liveContextTokens = usageParts(rawUsage, "codex").context;
1563
+ // Measure Codex like Claude: the last single-request window occupancy
1564
+ // from its rollout, never the turn-summed input — that sum tripped
1565
+ // auto-compact after almost every substantial turn (8 needless Codex
1566
+ // compactions on 2026-09-05) and hid the sub-agent threads entirely.
1567
+ rollout = readCodexRollout({
1568
+ threadId: usageSessionId,
1569
+ since: (result.timings?.startedAt || Date.now()) - 5000,
1570
+ });
1571
+ liveContextTokens = rollout && rollout.lastContextTokens > 0
1572
+ ? rollout.lastContextTokens
1573
+ : usageParts(rawUsage, "codex").context;
1520
1574
  }
1575
+ const rolloutOpts = rollout ? {
1576
+ contextSource: "rollout_last_request",
1577
+ contextWindowTokens: rollout.modelContextWindow || undefined,
1578
+ requests: rollout.requests,
1579
+ providerCompactions: rollout.compactions,
1580
+ quota: rollout.rateLimits?.primary || null,
1581
+ subagents: rollout.subagents,
1582
+ } : (runContext.provider === "codex" ? { contextSource: "turn_total_fallback" } : {});
1521
1583
  result.usage = usage;
1522
1584
  result.usageScope = usageScope;
1523
1585
  applyUsageToState(state, usage, result.costUsd || 0, {
@@ -1526,6 +1588,7 @@ async function persistForegroundUsage(result, runContext, state = currentState()
1526
1588
  liveContextTokens,
1527
1589
  project: usageProject,
1528
1590
  sessionId: usageSessionId,
1591
+ ...rolloutOpts,
1529
1592
  });
1530
1593
  logTurnUsage(
1531
1594
  state,
@@ -1544,6 +1607,7 @@ async function persistForegroundUsage(result, runContext, state = currentState()
1544
1607
  rawUsage,
1545
1608
  coldStart: usageSessionColdStart(usageSessionId),
1546
1609
  strict: true,
1610
+ ...rolloutOpts,
1547
1611
  },
1548
1612
  );
1549
1613
  saveState({ strict: true });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@inetafrica/open-claudia",
3
- "version": "3.4.12",
3
+ "version": "3.4.14",
4
4
  "description": "An always-on, provider-agnostic coding-agent harness for Claude Code and OpenAI Codex via chat",
5
5
  "main": "bot.js",
6
6
  "bin": {
@@ -11,7 +11,7 @@
11
11
  "setup": "node setup.js",
12
12
  "start": "node bot.js",
13
13
  "pretest": "node test-provider-fixture.js && node test-provider-core-prompt.js && node test-turn-observer.js && node test-provider-prompt-parity.js && node test-provider-env-isolation.js && node test-provider-resume-parity.js && node test-provider-bootstrap.js && node test-provider-registry.js && node test-provider-stream-decoder.js && node test-provider-events.js && node test-provider-claude.js && node test-provider-codex.js && node test-provider-runner.js && node test-provider-refusal-recovery.js && node test-provider-run-lifecycle.js && node test-provider-gateway.js && node test-provider-side-chat.js && node test-single-runtime.js && node test-provider-migration-backup.js && node test-provider-state-migration.js && node test-provider-session-history.js && node test-provider-session-commands.js && node test-provider-compaction.js && node test-provider-scheduler.js && node test-credential-capture.js && node test-cursor-removal.js && node test-utility-provider-policy.js && node test-provider-subagent.js && node test-provider-recall.js && node test-provider-pack-review.js && node test-memory-mutation-queue.js && node test-provider-dream.js && node test-provider-enforcer.js && node test-provider-tool-hooks.js && node test-provider-boot-matrix.js && node test-provider-capabilities.js && node test-provider-language.js && node test-provider-coupling-audit.js && node test-provider-parity-e2e.js && node test-kazee-channel-health.js && node test-kazee-media-caption.js && node test-kazee-typing-join.js && node test-kazee-inbound-dedup.js && node test-typing-phase-push.js && node test-socket-reconnect.js && node test-speech-loopback.js && node test-prompt-budget.js && node test-agency-ledger.js && node test-agency-deliberate.js && node test-guardrail.js && node test-spaces-comment-content.js && node test-boot-health.js && node test-lexical-floor.js && node test-safe-mode.js && node test-logout.js && node test-kazee-call-audio.js && node test-kazee-call-janus.js && node test-kazee-call-webrtc.js && node test-kazee-call-speech.js && node test-kazee-call-manager.js && node test-kazee-call-session.js",
14
- "test": "OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node -e \"require('./vault'); console.log('OK')\" && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-runner-watchdog-static.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-usage-accounting.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-recall-engine.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-recall-graph.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-recall-discoverer.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-recall-query.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-walker-score.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-score-gate.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-cokept-warming.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-project-transcripts-smoke.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-abilities.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-ability-extraction.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-ability-couse.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-ability-transfer.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-ability-tiers.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-ability-merge-guard.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-learning-e2e.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-pack-nesting.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-pack-search.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-extractive-injection.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-journal-dedupe.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-read-signal.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-tools.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-tool-graph.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-tool-guard.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-tooling-mode.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-tool-manifest.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-recall-evolution.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-approval-async.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-unified-identity.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-queue-routing.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-persona-packs.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-speaker-persona.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-persona-pipeline.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-recall-relationship-gate.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-relationship.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-enforcer.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-mandate-hygiene.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-external-rule-scopes.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-approval-cards.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-identity-prune.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-kazee-message-id.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-kazee-group-gate.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-kazee-group-context.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-people-owner-seed.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-delivery-contract.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-run-lock.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-typing-heartbeat.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-web-sessions.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-telegram-poll-recovery.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-telegram-409-grace.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-telegram-conflict-heal-guard.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-single-instance.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-streaming-split.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-spaces-autoenable.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-say-loopback.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-journal-union.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-subst-score.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-section-budgets.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-dedup-adjudicator.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-prospective.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-recall-regressions.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-cost-report.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-speaker-context.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-approval-mining.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-tool-certification.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-apprenticeship.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-speaker-attest.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-verify-contact.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-loopback-owner-gate.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-intro-adoption.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-grants.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-room-provider-settings.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-store-durability.js"
14
+ "test": "OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node -e \"require('./vault'); console.log('OK')\" && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-runner-watchdog-static.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-usage-accounting.js && node test-codex-rollout.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-recall-engine.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-recall-graph.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-recall-discoverer.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-recall-query.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-walker-score.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-score-gate.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-cokept-warming.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-project-transcripts-smoke.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-abilities.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-ability-extraction.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-ability-couse.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-ability-transfer.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-ability-tiers.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-ability-merge-guard.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-learning-e2e.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-pack-nesting.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-pack-search.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-extractive-injection.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-journal-dedupe.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-read-signal.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-tools.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-tool-graph.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-tool-guard.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-tooling-mode.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-tool-manifest.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-recall-evolution.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-approval-async.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-unified-identity.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-queue-routing.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-persona-packs.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-speaker-persona.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-persona-pipeline.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-recall-relationship-gate.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-relationship.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-enforcer.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-mandate-hygiene.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-external-rule-scopes.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-approval-cards.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-identity-prune.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-kazee-message-id.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-kazee-group-gate.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-kazee-group-context.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-people-owner-seed.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-delivery-contract.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-run-lock.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-typing-heartbeat.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-web-sessions.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-telegram-poll-recovery.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-telegram-409-grace.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-telegram-conflict-heal-guard.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-single-instance.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-streaming-split.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-spaces-autoenable.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-say-loopback.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-journal-union.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-subst-score.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-section-budgets.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-dedup-adjudicator.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-prospective.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-recall-regressions.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-cost-report.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-speaker-context.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-approval-mining.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-tool-certification.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-apprenticeship.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-speaker-attest.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-verify-contact.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-loopback-owner-gate.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-intro-adoption.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-grants.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-room-provider-settings.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-store-durability.js"
15
15
  },
16
16
  "files": [
17
17
  "bot.js",
@@ -462,10 +462,11 @@ async function testManagedRoomCredentials() {
462
462
  assert.strictEqual(session.janus.options.token, "root-token");
463
463
  assert.strictEqual(session.janus.calls[0].token, "publisher-token");
464
464
  await session._consumePublisherFrame({ plugindata: { data: { publishers: [{ id: 41, display: 'canonical-bot@calling.invalid' }, { id: 42, display: 'open-claudia:legacy-bot' }] } } });
465
- assert.strictEqual(session.subscribers.size, 0, 'own joined publisher ID is excluded independently of canonical display; legacy bot exclusion remains');
465
+ assert.strictEqual(session.subscribers.size, 0, 'own joined publisher ID and unclassified managed feeds are excluded independently of display');
466
466
 
467
467
  session.roomTokenExpiresAt = Date.now() - 1;
468
- await Promise.all([session._subscribe(17, "owner"), session._subscribe(18, "other")]);
468
+ const userMetadata = { kazee: { version: 1, userId: 'a'.repeat(24), principalKind: 'User' } };
469
+ await Promise.all([session._subscribe(17, "owner", userMetadata), session._subscribe(18, "other", userMetadata)]);
469
470
  const subJoin = session.janus.calls.find(body => body.ptype === "subscriber");
470
471
  assert.strictEqual(subJoin.token, "subscriber-token");
471
472
  assert.strictEqual(subJoin.private_id, 999);
@@ -484,6 +485,66 @@ async function testManagedRoomCredentials() {
484
485
  assert.ok(!session.janus.calls.some(body => ["create", "destroy"].includes(body.request)), "server allocated room ownership never transfers to the bot");
485
486
  }
486
487
 
488
+ async function testTrustedPublisherMetadata() {
489
+ const botId = 'b'.repeat(24), userId = 'a'.repeat(24);
490
+ const metadata = (principalKind, id = userId) => ({ kazee: { version: 1, userId: id, principalKind } });
491
+ const publisher = (id, meta, display = 'speaker@calling.invalid') => ({ id, display, metadata: meta });
492
+ let initial = [];
493
+ class MetadataJanus extends FakeJanus {
494
+ constructor() { super(); this.calls = []; this.listeners = new Map(); }
495
+ onHandle(handle, listener) { this.listeners.set(handle, listener); }
496
+ async message(handle, body) {
497
+ this.calls.push({ handle, ...body });
498
+ if (body.request === 'join' && body.ptype === 'publisher') return { plugindata: { data: { videoroom: 'joined', id: 41, private_id: 999, publishers: initial } } };
499
+ if (body.request === 'configure') return { jsep: { type: 'answer', sdp: 'fixture' } };
500
+ if (body.request === 'join' && body.ptype === 'subscriber') return { plugindata: { data: { videoroom: 'attached' } }, jsep: { type: 'offer', sdp: 'fixture' } };
501
+ return {};
502
+ }
503
+ }
504
+ const make = managedRoom => new KazeeCallSession({ roomId: 7, chatId: 'chat', callerId: userId, botUserId: botId, managedRoom,
505
+ roomToken: 'fixture', roomTokenExpiresAt: Date.now() + 300000,
506
+ iceServers: [{ urls: 'turn:relay:3478', username: 'fixture', credential: 'fixture' }], iceCredentialsExpiresAt: Date.now() + 3600000,
507
+ JanusClientImpl: MetadataJanus, PeerConnectionImpl: FakePC, SpeechStreamImpl: FakeSpeechStream });
508
+ const invalid = [undefined, null, {}, { kazee: null }, { kazee: [] },
509
+ { kazee: { version: 2, userId, principalKind: 'User' } }, { kazee: { version: '1', userId, principalKind: 'User' } },
510
+ metadata('Human'), metadata('bot'), metadata(undefined), metadata('User', 'invalid'), { kazee: { version: 1, principalKind: 'User' } },
511
+ { kazee: { version: 1, userId: { $ne: null }, principalKind: 'User' } }];
512
+ initial = [publisher(41, metadata('User')), publisher(42, metadata('Bot', 'c'.repeat(24)), 'other-bot@calling.invalid'),
513
+ publisher(43, metadata('User')), publisher(44, metadata('User', botId.toUpperCase())),
514
+ ...invalid.map((meta, i) => publisher(100 + i, meta))];
515
+ const managed = make(true);
516
+ try {
517
+ await managed.start();
518
+ assert.deepStrictEqual([...managed.subscribers.keys()], [43], 'initial join reaches real subscription only for a classified human, never self/bot/unknown');
519
+ assert.strictEqual(managed.subscribers.get(43).started, true, 'authorized human reaches the actual subscriber offer/answer path');
520
+ const emit = publishers => managed.janus.listeners.get(managed.publisherHandle)({ janus: 'event', plugindata: { data: { publishers } } });
521
+ await emit([publisher(50, metadata('Bot', 'd'.repeat(24)), 'another-bot@calling.invalid'),
522
+ publisher(51, metadata('User'), 'open-claudia:display-is-not-managed-authority'),
523
+ ...invalid.map((meta, i) => publisher(200 + i, meta))]);
524
+ assert.deepStrictEqual([...managed.subscribers.keys()], [43, 51], 'live publisher metadata survives forwarding; managed classification never falls back to display prefix');
525
+ assert.strictEqual(managed.subscribers.get(51).display, 'open-claudia:display-is-not-managed-authority', 'presentation remains unchanged');
526
+ const handles = managed.janus.handles;
527
+ await managed._subscribe(60, 'unclassified@calling.invalid');
528
+ await managed._subscribe(61, 'bot@calling.invalid', metadata('Bot'));
529
+ await managed._subscribe(41, 'own@calling.invalid', metadata('User'));
530
+ assert.strictEqual(managed.janus.handles, handles, 'the subscription method itself fails closed before creating handles or speech streams');
531
+ assert.deepStrictEqual(managed.janus.calls.filter(body => body.ptype === 'subscriber').map(body => body.feed), [43, 51]);
532
+ } finally { await managed.close(); }
533
+
534
+ initial = [publisher(41, metadata('User')), publisher(42, metadata('User'), 'open-claudia:legacy-bot'),
535
+ publisher(43, metadata('Bot'), 'human@calling.invalid')];
536
+ const legacy = make(false);
537
+ try {
538
+ await legacy.start();
539
+ assert.deepStrictEqual([...legacy.subscribers.keys()], [43], 'legacy metadata cannot hide a human or impersonate one for a prefixed bot; own numeric feed remains excluded');
540
+ await legacy.janus.listeners.get(legacy.publisherHandle)({ janus: 'event', plugindata: { data: { publishers: [
541
+ publisher(50, metadata('Bot'), 'untrusted-kind@calling.invalid'), publisher(51, metadata('User'), 'open-claudia:other-bot'),
542
+ publisher(52, undefined, 'older-client@calling.invalid'),
543
+ ] } } });
544
+ assert.deepStrictEqual([...legacy.subscribers.keys()], [43, 50, 52], 'legacy live publishers keep display-prefix behavior regardless of spoofed or absent metadata');
545
+ } finally { await legacy.close(); }
546
+ }
547
+
487
548
  async function testAssignedRelayTransportLifetime() {
488
549
  const { RTCPeerConnection } = require('werift');
489
550
  const originalExpiry = Date.now() + 3600000;
@@ -538,6 +599,7 @@ async function testAssignedRelayTransportLifetime() {
538
599
  }
539
600
 
540
601
  (async () => {
602
+ await testTrustedPublisherMetadata();
541
603
  await testAssignedRelayTransportLifetime();
542
604
  await testManagedRoomCredentials();
543
605
  await testSpeakSerialization();
@@ -134,6 +134,18 @@ const directResume = provider.buildMainInvocation(simpleContext({ sessionId: "th
134
134
  assert.ok(directResume.args.includes("--dangerously-bypass-approvals-and-sandbox"));
135
135
  assert.ok(directResume.args.indexOf("--dangerously-bypass-approvals-and-sandbox") < directResume.args.indexOf("thread-direct"));
136
136
 
137
+ {
138
+ const before = process.env.CODEX_MULTI_AGENT;
139
+ delete process.env.CODEX_MULTI_AGENT;
140
+ assert.ok(!provider.buildMainInvocation(simpleContext({})).args.includes("features.multi_agent=false"), "multi_agent stays as Codex ships it by default");
141
+ process.env.CODEX_MULTI_AGENT = "off";
142
+ const noAgents = provider.buildMainInvocation(simpleContext({}));
143
+ const at = noAgents.args.indexOf("features.multi_agent=false");
144
+ assert.ok(at > 0 && noAgents.args[at - 1] === "-c", "CODEX_MULTI_AGENT=off passes -c features.multi_agent=false");
145
+ assert.ok(at < noAgents.args.indexOf("--json"), "the override sits with the other -c overrides, before the exec flags");
146
+ if (before === undefined) delete process.env.CODEX_MULTI_AGENT; else process.env.CODEX_MULTI_AGENT = before;
147
+ }
148
+
137
149
  const continued = provider.buildMainInvocation(simpleContext({ continueSession: true, sessionId: "thread-continue" }));
138
150
  assert.strictEqual(continued.args[1], "resume");
139
151
  assert.ok(continued.args.includes("thread-continue"));
@@ -203,7 +203,7 @@ assert.deepStrictEqual(normalizeCodexEvent(samples.codex.assistant, codexState),
203
203
  { type: "text_final", text: "Hello from Codex" },
204
204
  ]);
205
205
  assert.deepStrictEqual(normalizeCodexEvent(samples.codex.result, codexState), [
206
- { type: "usage", usage: { input_tokens: 20, cached_input_tokens: 5, output_tokens: 4 }, scope: "session_total" },
206
+ { type: "usage", usage: { input_tokens: 20, cached_input_tokens: 5, output_tokens: 4 }, scope: "turn" },
207
207
  { type: "result", text: "", sessionId: "codex-thread-1" },
208
208
  ]);
209
209
  assert.deepStrictEqual(normalizeCodexEvent(samples.codex.result, codexState), [], "duplicate Codex terminal and usage settle once");
@@ -464,7 +464,10 @@ assert.match(pkg.description, /provider-agnostic coding-agent harness/i);
464
464
  // tool_name "Bash" and the deny gate caught the probe, so the gate no longer
465
465
  // requires unified_exec off. Push approved by Sumeet before release.
466
466
  // 3.4.12: Sumeet approved the full calling rollout on 2026-09-05; preserve assigned media configuration across canary changes.
467
- assert.strictEqual(pkg.version, "3.4.12", "release version must remain unchanged without explicit approval");
467
+ // 3.4.14: bot managed-call consumer (subscribe only to gateway-authenticated User publishers) — part of the Janus calling plan Sumeet approved on 2026-09-05.
468
+ // 3.4.13: Codex usage accounting fix — Sumeet approved the three-bug fix on 2026-09-05 ("Yes");
469
+ // prepared 2026-09-06 for his release go. Rollout-based Codex context, per-turn ledger, sub-agent + quota accounting.
470
+ assert.strictEqual(pkg.version, "3.4.14", "release version must remain unchanged without explicit approval");
468
471
  for (const keyword of ["claude", "codex", "coding-agent", "provider-agnostic"]) {
469
472
  assert.ok(pkg.keywords.includes(keyword), `package keyword missing: ${keyword}`);
470
473
  }
@@ -9,6 +9,7 @@ const {
9
9
  normalizeCodexUsage,
10
10
  applyUsageToState,
11
11
  shouldAutoCompact,
12
+ effectiveCompactThreshold,
12
13
  peakContextTokens,
13
14
  usageSessionColdStart,
14
15
  } = require("./core/runner");
@@ -48,7 +49,9 @@ function state(overrides = {}) {
48
49
 
49
50
  assert.deepStrictEqual(
50
51
  usageParts({ input_tokens: 100, cached_input_tokens: 60, output_tokens: 10 }, "codex"),
51
- { input: 100, cacheRead: 60, cacheCreation: 0, output: 10, context: 100 },
52
+ // OpenAI counts cached tokens inside input_tokens: `input` is the uncached
53
+ // part so it compares like-for-like with Claude; `context` is the full prefix.
54
+ { input: 40, cacheRead: 60, cacheCreation: 0, output: 10, context: 100 },
52
55
  );
53
56
 
54
57
  assert.deepStrictEqual(
@@ -62,7 +65,7 @@ const first = normalizeCodexUsage(s, "codex-session-1", {
62
65
  cached_input_tokens: 700,
63
66
  output_tokens: 50,
64
67
  }, { project: "alpha" });
65
- assert.strictEqual(first.usageScope, "session_total");
68
+ assert.strictEqual(first.usageScope, "turn", "Codex turn.completed usage is per turn, never a session total");
66
69
  assert.strictEqual(usageParts(first.usage, "codex").context, 1000);
67
70
 
68
71
  const second = normalizeCodexUsage(s, "codex-session-1", {
@@ -70,13 +73,13 @@ const second = normalizeCodexUsage(s, "codex-session-1", {
70
73
  cached_input_tokens: 800,
71
74
  output_tokens: 80,
72
75
  }, { project: "alpha" });
73
- assert.strictEqual(second.usageScope, "turn_delta");
76
+ assert.strictEqual(second.usageScope, "turn");
77
+ // No delta against the previous turn: a smaller turn after a bigger one used
78
+ // to be recorded as 0 (9 of 36 Codex turns on 2026-09-05).
74
79
  assert.deepStrictEqual(second.usage, {
75
- input_tokens: 250,
76
- cached_input_tokens: 100,
77
- cache_read_input_tokens: 0,
78
- cache_creation_input_tokens: 0,
79
- output_tokens: 30,
80
+ input_tokens: 1250,
81
+ cached_input_tokens: 800,
82
+ output_tokens: 80,
80
83
  });
81
84
 
82
85
  applyUsageToState(s, second.usage, 0, {
@@ -87,9 +90,9 @@ applyUsageToState(s, second.usage, 0, {
87
90
  liveContextTokens: usageParts(second.rawUsage, "codex").context,
88
91
  });
89
92
  assert.strictEqual(s.sessionUsage.turns, 1);
90
- assert.strictEqual(s.sessionUsage.inputTokens, 250);
91
- assert.strictEqual(s.sessionUsage.cacheReadTokens, 100);
92
- assert.strictEqual(s.sessionUsage.lastInputTokens, 250);
93
+ assert.strictEqual(s.sessionUsage.inputTokens, 450, "uncached input only");
94
+ assert.strictEqual(s.sessionUsage.cacheReadTokens, 800);
95
+ assert.strictEqual(s.sessionUsage.lastInputTokens, 1250);
93
96
  assert.strictEqual(s.sessionUsage.liveContextTokens, 1250);
94
97
 
95
98
  // The same native-looking ID in another project has an independent cumulative
@@ -99,7 +102,7 @@ const otherProject = normalizeCodexUsage(s, "codex-session-1", {
99
102
  cached_input_tokens: 1200,
100
103
  output_tokens: 100,
101
104
  }, { project: "beta" });
102
- assert.strictEqual(otherProject.usageScope, "session_total");
105
+ assert.strictEqual(otherProject.usageScope, "turn");
103
106
  s.currentSession = { name: "beta", dir: "/workspace/beta" };
104
107
  s.settings.backend = "claude";
105
108
  s.activeSessions.beta = { claude: "claude-beta", codex: "codex-session-1" };
@@ -154,6 +157,36 @@ assert.strictEqual(shouldAutoCompact(state({
154
157
  sessionUsage: { liveContextTokens: 250, lastInputTokens: 250, lastCompactedAt: Date.now() },
155
158
  })), true);
156
159
 
160
+ // Codex is compacted at 80% of the window it reports, because its window
161
+ // (258k on gpt-6-astra) is below the default threshold — measured honestly the
162
+ // bot would otherwise never compact a Codex session.
163
+ {
164
+ const windowed = state({ sessionUsage: { liveContextTokens: 210000, lastInputTokens: 210000, contextWindowTokens: 258400 } });
165
+ delete windowed.settings.compactWindow;
166
+ assert.strictEqual(effectiveCompactThreshold(windowed), 206720, "80% of the reported Codex window");
167
+ assert.strictEqual(shouldAutoCompact(windowed), true, "210k of a 258k window compacts");
168
+ windowed.sessionUsage.liveContextTokens = 200000;
169
+ assert.strictEqual(shouldAutoCompact(windowed), false, "200k of a 258k window does not");
170
+ windowed.settings.backend = "claude";
171
+ assert.strictEqual(effectiveCompactThreshold(windowed), 380000, "the Codex window cap never applies to Claude");
172
+ }
173
+
174
+ // Rollout-derived facts ride along into the session bucket.
175
+ {
176
+ const enriched = state();
177
+ applyUsageToState(enriched, { input_tokens: 1000, cached_input_tokens: 900, output_tokens: 10 }, 0, {
178
+ backend: "codex", project: "alpha", sessionId: "codex-session-1", usageScope: "turn", liveContextTokens: 1000,
179
+ contextWindowTokens: 258400, quota: { usedPercent: 31, resetsAt: "2026-09-12T13:57:00.000Z" },
180
+ subagents: { threads: 2, requests: 5, totals: { input: 70000, cached: 50000, output: 300 } },
181
+ });
182
+ assert.strictEqual(enriched.sessionUsage.contextWindowTokens, 258400);
183
+ assert.strictEqual(enriched.sessionUsage.quotaUsedPercent, 31);
184
+ assert.strictEqual(enriched.sessionUsage.subagentThreads, 2);
185
+ assert.strictEqual(enriched.sessionUsage.subagentInputTokens, 20000, "sub-agent input is stored uncached");
186
+ assert.strictEqual(enriched.sessionUsage.subagentCacheReadTokens, 50000);
187
+ assert.strictEqual(enriched.sessionUsage.subagentOutputTokens, 300);
188
+ }
189
+
157
190
  // peakContextTokens: the live context number must be the LARGEST single API
158
191
  // call, not the turn-cumulative sum (the v3.0.0 347k-vs-160k regression).
159
192
  assert.strictEqual(peakContextTokens([