@inetafrica/open-claudia 3.4.11 → 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.
@@ -94,7 +94,7 @@ function fixture() {
94
94
  if (["call:allocate", "call:room-access"].includes(name)) {
95
95
  setImmediate(() => originalEmit(`${name}-result`, { requestId: payload.requestId, ok: true, protocolVersion: 3,
96
96
  callAttemptId: "managed-attempt-123456789", organizationId: "tenant-a", roomId: 100, chatId: "managed-chat",
97
- deviceId: payload.deviceId, generation: 0, ...replyPlacement, janusToken: "root-token", roomToken: "room-token", roomTokenExpiresAt: Date.now() + 300000, leaseExpiresAt: Date.now() + 120000 }));
97
+ deviceId: payload.deviceId, generation: 0, roomAdmissionRequired: true, iceServers: [{ urls: ["turn:secondary:3478", "turns:secondary:5349"], username: "assigned", credential: "assigned" }], iceCredentialsExpiresAt: Date.now() + 3600000, ...replyPlacement, janusToken: "root-token", roomToken: "room-token", roomTokenExpiresAt: Date.now() + 300000, leaseExpiresAt: Date.now() + 120000 }));
98
98
  }
99
99
  return originalEmit(name, payload);
100
100
  };
@@ -105,7 +105,8 @@ function fixture() {
105
105
  assert.strictEqual(f.manager.session.options.janusToken, "root-token");
106
106
  assert.strictEqual(f.manager.session.options.roomToken, "room-token");
107
107
  assert.strictEqual(f.manager.session.options.janusUrl, "wss://assigned/janus");
108
- assert.deepStrictEqual(f.manager.session.options.iceServers[0].urls, ["turn:relay:3478"]);
108
+ assert.strictEqual(f.manager.session.options.iceServers[0].urls, "turn:secondary:3478");
109
+ assert.ok(f.manager.session.options.iceCredentialsExpiresAt > Date.now());
109
110
  const refreshed = await f.manager.session.options.refreshRoomAccess();
110
111
  assert.strictEqual(refreshed.server, "wss://assigned/janus");
111
112
  replyPlacement = {nodeId: "secondary", server: "wss://other/janus"};
@@ -139,8 +140,12 @@ function fixture() {
139
140
  assert.strictEqual(f.manager.session, null, "authoritative end also ends an unanswered modern call");
140
141
  assert.strictEqual(socket.listenerCount("call:allocate-result"), 0);
141
142
  assert.strictEqual(socket.listenerCount("call:heartbeat-result"), 0, "ended manager session releases lease listener");
142
- await f.manager.incoming({ fromUserId: "owner-1", chatId: "managed-chat", roomId: 100, callType: "audio", callAttemptId: managedInvite.callAttemptId, organizationId: "tenant-a" });
143
+ f.manager.adapter._request = async () => ({ config: { roomAdmissionEnabled: false, janusServer: "wss://legacy/janus", iceServers: [{ urls: "turn:legacy:3478", username: "legacy", credential: "legacy" }] } });
144
+ await f.manager.incoming({ fromUserId: "owner-1", chatId: "managed-chat", roomId: 100, callType: "audio", callAttemptId: managedInvite.callAttemptId, organizationId: "tenant-a", roomAdmissionRequired: true });
143
145
  const incomingManaged = f.manager.session;
146
+ assert.strictEqual(incomingManaged.options.managedRoom, true);
147
+ assert.strictEqual(incomingManaged.options.janusUrl, "wss://assigned/janus");
148
+ assert.strictEqual(incomingManaged.options.iceServers[0].urls, "turn:secondary:3478");
144
149
  const managedAccept = emitted.filter(event => event.name === "call:accept").at(-1).payload;
145
150
  assert.strictEqual(managedAccept.deviceId, f.manager.deviceId);
146
151
  assert.strictEqual(managedAccept.generation, 0);
@@ -156,6 +161,19 @@ function fixture() {
156
161
  assert.strictEqual(socket.listenerCount("call:heartbeat-result"), 0);
157
162
 
158
163
 
164
+ // Legacy attempts, including those with modern attempt IDs, keep baseline
165
+ // transport even when the organization now enables new managed allocations.
166
+ for (const roomAdmissionRequired of [undefined, false]) {
167
+ f = fixture();
168
+ f.manager.adapter._request = async () => ({ config: { roomAdmissionEnabled: true, janusServer: "wss://legacy/janus", iceServers: [{ urls: "turn:legacy:3478", username: "legacy", credential: "legacy" }] } });
169
+ await f.manager.incoming({ fromUserId: "owner-1", chatId: "legacy", roomId: 99, callType: "audio", callAttemptId: "legacy-attempt-123456789", roomAdmissionRequired });
170
+ assert.strictEqual(f.manager.session.options.managedRoom, false);
171
+ assert.strictEqual(f.manager.session.options.janusUrl, "wss://legacy/janus");
172
+ assert.strictEqual(f.manager.session.options.refreshRoomAccess, undefined);
173
+ assert.ok(!f.events.some(event => event.name === "call:room-access"));
174
+ await f.manager.stop();
175
+ }
176
+
159
177
  f = fixture();
160
178
  await f.manager.incoming({ fromUserId: "owner-1", chatId: "managed-chat", roomId: 100, callType: "video", callAttemptId: "managed-attempt-123456789", organizationId: "tenant-a", generation: 7 });
161
179
  assert.strictEqual(f.events[0].payload.generation, 7, "pre-access decline preserves recipient reinvite generation");
@@ -200,12 +218,23 @@ function fixture() {
200
218
  accessSocket.emit("call:allocate-result", { requestId: requested.requestId, ok: true, protocolVersion: 3, callAttemptId: "attempt-1234567890", organizationId: "tenant-a", roomId: 20, chatId: "chat", deviceId: "different-device", generation: 0, janusToken: "t", roomToken: "t", roomTokenExpiresAt: Date.now() + 300000 });
201
219
  await assert.rejects(accessPromise, /Invalid call room/);
202
220
  assert.strictEqual(accessSocket.listenerCount("call:allocate-result"), 0);
203
- for (const server of ["http://assigned/janus", "wss://user:password@assigned/janus", "wss://assigned/janus#fragment", 123]) {
221
+ for (const server of ["http://assigned/janus", "wss://user:password@assigned/janus", "wss://assigned/janus#fragment", "wss://assigned/janus?token=bad", 123]) {
204
222
  const invalid = requestRoomAccess(accessSocket, "allocate", { deviceId: "device-1234567890", chatId: "chat" });
205
223
  accessSocket.emit("call:allocate-result", { requestId: requested.requestId, ok: true, protocolVersion: 3, callAttemptId: "attempt-1234567890", organizationId: "tenant-a", roomId: 20, chatId: "chat", deviceId: "device-1234567890", generation: 0, server, janusToken: "t", roomToken: "t", roomTokenExpiresAt: Date.now() + 300000 });
206
224
  await assert.rejects(invalid, /Invalid call room/);
207
225
  assert.strictEqual(accessSocket.listenerCount("call:allocate-result"), 0);
208
226
  }
227
+ for (const malformed of [
228
+ { roomAdmissionRequired: undefined }, { roomAdmissionRequired: false },
229
+ { iceServers: undefined }, { iceServers: [] }, { iceCredentialsExpiresAt: Date.now() - 1 },
230
+ { iceServers: [{ urls: 'turn:user:secret@relay:3478', username: 'u', credential: 'p' }] },
231
+ { iceServers: [{ urls: 'turn:relay:3478' }] },
232
+ ]) {
233
+ const invalid = requestRoomAccess(accessSocket, "allocate", { deviceId: "device-1234567890", chatId: "chat" });
234
+ accessSocket.emit("call:allocate-result", { requestId: requested.requestId, ok: true, protocolVersion: 3, callAttemptId: "attempt-1234567890", organizationId: "tenant-a", roomId: 20, chatId: "chat", deviceId: "device-1234567890", generation: 0, server: "wss://assigned/janus", nodeId: "secondary", roomAdmissionRequired: true, iceServers: [{ urls: 'turn:relay:3478', username: 'u', credential: 'p' }], iceCredentialsExpiresAt: Date.now() + 3600000, janusToken: "t", roomToken: "t", roomTokenExpiresAt: Date.now() + 300000, ...malformed });
235
+ await assert.rejects(invalid, /Invalid call room/);
236
+ assert.strictEqual(accessSocket.listenerCount("call:allocate-result"), 0);
237
+ }
209
238
  const controller = new AbortController();
210
239
  const cancelledAccess = requestRoomAccess(accessSocket, "allocate", { deviceId: "device-1234567890", chatId: "chat" }, { signal: controller.signal });
211
240
  controller.abort();
@@ -19,7 +19,7 @@ class FakeJanus {
19
19
  }
20
20
 
21
21
  class FakePC {
22
- constructor() { this.onTrack = { subscribe: () => {} }; }
22
+ constructor(options) { this.options = options; this.onTrack = { subscribe: () => {} }; }
23
23
  addTrack() { return { codec: { payloadType: 111 } }; }
24
24
  async createOffer() { return {}; }
25
25
  async setLocalDescription() {}
@@ -448,19 +448,22 @@ async function testManagedRoomCredentials() {
448
448
  constructor(options) { super(); this.options = options; this.calls = []; }
449
449
  async message(handle, body) {
450
450
  this.calls.push(body);
451
- if (body.request === "join" && body.ptype === "publisher") return { plugindata: { data: { videoroom: "joined", private_id: 999, publishers: [] } } };
451
+ if (body.request === "join" && body.ptype === "publisher") return { plugindata: { data: { videoroom: "joined", id: 41, private_id: 999, publishers: [] } } };
452
452
  return {};
453
453
  }
454
454
  }
455
455
  let refreshes = 0;
456
456
  const session = new KazeeCallSession({ roomId: 7, chatId: "chat", callerId: "owner", botUserId: "bot", janusUrl: "ws://test",
457
- janusToken: "root-token", roomToken: "publisher-token", roomTokenExpiresAt: Date.now() + 300000, managedRoom: true, asCaller: true,
458
- refreshRoomAccess: async () => { refreshes += 1; return { janusToken: "fresh-root", janusTokenExpiresAt: Date.now() + 86400000, roomToken: "subscriber-token", roomTokenExpiresAt: Date.now() + 300000 }; },
457
+ janusToken: "root-token", roomToken: "publisher-token", roomTokenExpiresAt: Date.now() + 300000, managedRoom: true, asCaller: true, iceServers: [{ urls: "turn:relay:3478", username: "fixture", credential: "fixture" }], iceCredentialsExpiresAt: Date.now() + 3600000,
458
+ refreshRoomAccess: async () => { refreshes += 1; return { iceServers: [{ urls: "turn:relay:3478", username: "fresh", credential: "fresh" }], iceCredentialsExpiresAt: Date.now() + 3600000, janusToken: "fresh-root", janusTokenExpiresAt: Date.now() + 86400000, roomToken: "subscriber-token", roomTokenExpiresAt: Date.now() + 300000 }; },
459
459
  JanusClientImpl: SignedJanus, PeerConnectionImpl: FakePC, SpeechStreamImpl: FakeSpeechStream });
460
460
  session._maybePublish = async () => {};
461
461
  await session.start();
462
462
  assert.strictEqual(session.janus.options.token, "root-token");
463
463
  assert.strictEqual(session.janus.calls[0].token, "publisher-token");
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');
466
+
464
467
  session.roomTokenExpiresAt = Date.now() - 1;
465
468
  await Promise.all([session._subscribe(17, "owner"), session._subscribe(18, "other")]);
466
469
  const subJoin = session.janus.calls.find(body => body.ptype === "subscriber");
@@ -481,7 +484,61 @@ async function testManagedRoomCredentials() {
481
484
  assert.ok(!session.janus.calls.some(body => ["create", "destroy"].includes(body.request)), "server allocated room ownership never transfers to the bot");
482
485
  }
483
486
 
487
+ async function testAssignedRelayTransportLifetime() {
488
+ const { RTCPeerConnection } = require('werift');
489
+ const originalExpiry = Date.now() + 3600000;
490
+ const assigned = (username, expiresAt) => ({ iceServers: [{ urls: 'turn:127.0.0.1:3478', username, credential: username + '-password' }], iceCredentialsExpiresAt: expiresAt });
491
+ const session = new KazeeCallSession({ roomId: 7, chatId: 'chat', callerId: 'owner', botUserId: 'bot', managedRoom: true,
492
+ ...assigned('original-fixture', originalExpiry), roomToken: 'room', roomTokenExpiresAt: Date.now() + 300000,
493
+ refreshRoomAccess: async () => ({ ...assigned('renewed-fixture', originalExpiry + 3600000), roomToken: 'renewed-room', roomTokenExpiresAt: Date.now() + 300000, janusToken: 'renewed-root', janusTokenExpiresAt: Date.now() + 300000 }),
494
+ JanusClientImpl: FakeJanus, PeerConnectionImpl: RTCPeerConnection, SpeechStreamImpl: FakeSpeechStream });
495
+ try {
496
+ const old = session._newPeerConnection(); old.addTransceiver('audio');
497
+ assert.strictEqual(old.iceTransports[0].connection.options.turnUsername, 'original-fixture');
498
+ // Calling Werift's public setter cannot refresh an already-created ICE
499
+ // transport. Verify the installed implementation, without network gathering.
500
+ old.setConfiguration({ iceServers: assigned('setter-fixture', originalExpiry).iceServers });
501
+ assert.strictEqual(old.getConfiguration().iceServers[0].username, 'setter-fixture');
502
+ assert.strictEqual(old.iceTransports[0].connection.options.turnUsername, 'original-fixture');
503
+ await session._roomAuth(true);
504
+ assert.strictEqual(session._transportIceExpiry.get(old), originalExpiry, 'new access never extends an existing allocation deadline');
505
+ assert.strictEqual(old.iceTransports[0].connection.options.turnUsername, 'original-fixture');
506
+ const fresh = session._newPeerConnection(); fresh.addTransceiver('audio');
507
+ assert.strictEqual(fresh.iceTransports[0].connection.options.turnUsername, 'renewed-fixture');
508
+ assert.strictEqual(session._transportIceExpiry.get(fresh), originalExpiry + 3600000);
509
+ } finally { await session.close(); }
510
+ assert.strictEqual(session._transportExpiryTimer, null);
511
+ assert.strictEqual(session._transportIceExpiry.size, 0);
512
+
513
+ let ended;
514
+ const bounded = new KazeeCallSession({ roomId: 7, chatId: 'chat', callerId: 'owner', botUserId: 'bot', managedRoom: true,
515
+ ...assigned('short-fixture', Date.now() + 1500), roomToken: 'room', roomTokenExpiresAt: Date.now() + 300000,
516
+ refreshRoomAccess: async () => ({ ...assigned('fresh-fixture', Date.now() + 3600000), roomToken: 'fresh', roomTokenExpiresAt: Date.now() + 300000 }),
517
+ onEnded: reason => { ended = reason; }, JanusClientImpl: FakeJanus, PeerConnectionImpl: FakePC, SpeechStreamImpl: FakeSpeechStream });
518
+ try {
519
+ const old = bounded._newPeerConnection();
520
+ const deadline = bounded._transportIceExpiry.get(old);
521
+ await bounded._roomAuth(true);
522
+ assert.strictEqual(bounded._transportIceExpiry.get(old), deadline);
523
+ const until = Date.now() + 2000;
524
+ while (!bounded.closed && Date.now() < until) await wait(25);
525
+ assert.strictEqual(ended, 'relay-credentials-expired', 'existing transport closes on its original expiry despite successful refresh');
526
+ } finally { await bounded.close(); }
527
+
528
+ let publications = 0;
529
+ class PublishingPC extends FakePC { constructor(options) { super(options); publications += 1; } }
530
+ class PublishingJanus extends FakeJanus { async message() { return { jsep: { type: 'answer', sdp: 'fixture' } }; } }
531
+ const concurrent = new KazeeCallSession({ roomId: 7, chatId: 'chat', callerId: 'owner', botUserId: 'bot', managedRoom: true,
532
+ ...assigned('original-fixture', Date.now() + 3600000), roomToken: 'room', roomTokenExpiresAt: Date.now() + 300000,
533
+ JanusClientImpl: PublishingJanus, PeerConnectionImpl: PublishingPC });
534
+ try { await Promise.all([concurrent._publish(), concurrent._publish()]); assert.strictEqual(publications, 1, 'concurrent publisher events cannot race across async room authorization'); } finally { await concurrent.close(); }
535
+
536
+ const missing = new KazeeCallSession({ roomId: 7, chatId: 'chat', callerId: 'owner', botUserId: 'bot', managedRoom: true, JanusClientImpl: FakeJanus, PeerConnectionImpl: FakePC });
537
+ try { assert.throws(() => missing._newPeerConnection(), /relay credentials unavailable/); } finally { await missing.close(); }
538
+ }
539
+
484
540
  (async () => {
541
+ await testAssignedRelayTransportLifetime();
485
542
  await testManagedRoomCredentials();
486
543
  await testSpeakSerialization();
487
544
  await testSpeakCancellation();
@@ -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");
@@ -463,8 +463,10 @@ assert.match(pkg.description, /provider-agnostic coding-agent harness/i);
463
463
  // Codex turn. Live probe: unified-exec shell calls still reach PreToolUse as
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
- // 3.4.11: owner approved end-to-end calling rollout on 2026-09-05.
467
- assert.strictEqual(pkg.version, "3.4.11", "release version must remain unchanged without explicit approval");
466
+ // 3.4.12: Sumeet approved the full calling rollout on 2026-09-05; preserve assigned media configuration across canary changes.
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([