@inetafrica/open-claudia 3.4.12 → 3.4.13

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,11 @@
1
1
  # Changelog
2
2
 
3
+ ## 3.4.13
4
+
5
+ - **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.
6
+ - **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`.
7
+ - **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.
8
+
3
9
  ## 3.4.12
4
10
 
5
11
  - 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.
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.13",
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",
@@ -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,9 @@ 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.13: Codex usage accounting fix — Sumeet approved the three-bug fix on 2026-09-05 ("Yes");
468
+ // prepared 2026-09-06 for his release go. Rollout-based Codex context, per-turn ledger, sub-agent + quota accounting.
469
+ assert.strictEqual(pkg.version, "3.4.13", "release version must remain unchanged without explicit approval");
468
470
  for (const keyword of ["claude", "codex", "coding-agent", "provider-agnostic"]) {
469
471
  assert.ok(pkg.keywords.includes(keyword), `package keyword missing: ${keyword}`);
470
472
  }
@@ -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([