@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.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,18 @@
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
+
9
+ ## 3.4.12
10
+
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.
12
+ - Flatten ICE server URLs for Werift, whose installed parser silently omits TURN when given browser-style URL arrays. Verify actual peer-connection TURN configuration in the tests.
13
+ - Refresh Janus grants and relay credentials for future peer connections. Existing Werift TURN allocations keep their original credentials and expiry, so the bot closes before the earliest active allocation deadline. It does not hot-swap existing allocation authentication; the default one-hour TURN lifetime exceeds the bot's thirty-minute call limit.
14
+ - Exclude the bot's own publisher by its Janus-assigned feed ID, including when the gateway replaces display names with canonical email. Keep the legacy bot display-prefix exclusion.
15
+
3
16
  ## 3.4.11
4
17
 
5
18
  - Verify timed-out guard processes from their parent, including forced termination and released concurrency, without racing a child-written receipt.
@@ -1,12 +1,40 @@
1
1
  "use strict";
2
2
  const { randomUUID } = require("crypto");
3
3
  function validServer(value) {
4
- if (value === undefined) return true;
4
+ if (value === undefined) return false;
5
5
  if (typeof value !== 'string') return false;
6
- try { const url = new URL(value); return url.protocol === 'wss:' && !!url.hostname && !url.username && !url.password && !url.hash; }
6
+ try { const url = new URL(value); return url.protocol === 'wss:' && !!url.hostname && !url.username && !url.password && !url.hash && !url.search; }
7
7
  catch { return false; }
8
8
  }
9
9
 
10
+ function projectAssignedIce(reply, now = Date.now()) {
11
+ if (!Array.isArray(reply.iceServers) || !reply.iceServers.length || reply.iceServers.length > 12
12
+ || !Number.isFinite(reply.iceCredentialsExpiresAt) || reply.iceCredentialsExpiresAt <= now) throw new Error("Invalid assigned call relay");
13
+ const iceServers = reply.iceServers.map(server => {
14
+ if (!server || typeof server !== 'object') throw new Error("Invalid assigned call relay");
15
+ const urls = Array.isArray(server.urls) ? server.urls : [server.urls];
16
+ if (!urls.length || urls.length > 12 || urls.some(url => typeof url !== 'string' || url.length > 1024
17
+ || !/^(stun|turn|turns):[^\s@?#]+(?:\?transport=(udp|tcp))?$/.test(url))) throw new Error("Invalid assigned call relay");
18
+ if (urls.some(url => /^turns?:/.test(url)) && (typeof server.username !== 'string' || !server.username || server.username.length > 2048
19
+ || typeof server.credential !== 'string' || !server.credential || server.credential.length > 2048)) throw new Error("Invalid assigned call relay");
20
+ return { urls: [...urls], ...(server.username === undefined ? {} : { username: server.username }), ...(server.credential === undefined ? {} : { credential: server.credential }) };
21
+ });
22
+ if (!iceServers.some(server => server.urls.some(url => /^turns?:/.test(url)))) throw new Error("Assigned call relay is unavailable");
23
+ return { iceServers, iceCredentialsExpiresAt: reply.iceCredentialsExpiresAt };
24
+ }
25
+
26
+ /** The bot's media adapter supports UDP TURN. TLS-only assignment fails closed. */
27
+ function supportedIceServers(iceServers) {
28
+ const usable = iceServers.flatMap(server => {
29
+ const urls = (Array.isArray(server.urls) ? server.urls : [server.urls]).filter(url => typeof url === 'string' && (url.startsWith('stun:') || (url.startsWith('turn:') && !url.includes('transport=tcp'))));
30
+ // Werift's parser accepts one string URL per entry; arrays silently lose
31
+ // their TURN address/username despite matching the browser ICE shape.
32
+ return urls.map(url => ({ ...server, urls: url }));
33
+ });
34
+ if (!usable.some(server => server.urls.startsWith('turn:'))) throw new Error('Call configuration has no supported TURN route');
35
+ return usable;
36
+ }
37
+
10
38
  function publicCallIdentity(payload = {}) {
11
39
  return {
12
40
  ...(payload.callAttemptId ? { callAttemptId: payload.callAttemptId } : {}),
@@ -28,6 +56,8 @@ function requestRoomAccess(socket, operation, input, { signal, timeoutMs = 10000
28
56
  if (reply.requestId !== requestId) return;
29
57
  if (!reply.ok) return fail("Call room access denied");
30
58
  if (reply.protocolVersion !== 3 || typeof reply.callAttemptId !== "string" || !/^[\w-]{16,128}$/.test(reply.callAttemptId)
59
+ || reply.roomAdmissionRequired !== true
60
+ || typeof reply.nodeId !== 'string' || !/^[\w-]{1,64}$/.test(reply.nodeId)
31
61
  || typeof reply.organizationId !== "string" || !reply.organizationId || reply.deviceId !== input.deviceId
32
62
  || !Number.isSafeInteger(reply.roomId) || reply.roomId <= 0 || !Number.isSafeInteger(reply.generation) || reply.generation < 0
33
63
  || (input.callAttemptId && reply.callAttemptId !== input.callAttemptId)
@@ -38,8 +68,11 @@ function requestRoomAccess(socket, operation, input, { signal, timeoutMs = 10000
38
68
  || typeof reply.janusToken !== "string" || !reply.janusToken || typeof reply.roomToken !== "string" || !reply.roomToken
39
69
  || (reply.leaseExpiresAt !== undefined && (!Number.isFinite(reply.leaseExpiresAt) || reply.leaseExpiresAt <= Date.now()))
40
70
  || !Number.isFinite(reply.roomTokenExpiresAt) || reply.roomTokenExpiresAt <= Date.now()) return fail("Invalid call room access response");
71
+ let assignedIce;
72
+ try { assignedIce = projectAssignedIce(reply); } catch (_) { return fail("Invalid call room access response"); }
41
73
  cleanup();
42
74
  resolve({ ...publicCallIdentity(reply), protocolVersion: 3, deviceId: reply.deviceId, generation: reply.generation,
75
+ roomAdmissionRequired: true, ...assignedIce,
43
76
  ...(reply.janusTokenExpiresAt !== undefined ? {janusTokenExpiresAt: reply.janusTokenExpiresAt} : {}),
44
77
  chatId: reply.chatId, nodeId: reply.nodeId, ...(reply.server !== undefined ? { server: reply.server } : {}), janusToken: reply.janusToken, roomToken: reply.roomToken, roomTokenExpiresAt: reply.roomTokenExpiresAt, ...(reply.leaseExpiresAt !== undefined ? { leaseExpiresAt: reply.leaseExpiresAt } : {}) });
45
78
  };
@@ -50,4 +83,4 @@ function requestRoomAccess(socket, operation, input, { signal, timeoutMs = 10000
50
83
  catch (_) { fail("Call socket unavailable"); }
51
84
  });
52
85
  }
53
- module.exports = { publicCallIdentity, requestRoomAccess };
86
+ module.exports = { publicCallIdentity, requestRoomAccess, projectAssignedIce, supportedIceServers };
@@ -2,7 +2,7 @@
2
2
 
3
3
  const { randomUUID, randomInt } = require("crypto");
4
4
  const { CallLeaseHeartbeat } = require("./lease");
5
- const { publicCallIdentity, requestRoomAccess } = require("./access");
5
+ const { publicCallIdentity, requestRoomAccess, supportedIceServers } = require("./access");
6
6
  const { canonicalForChannel } = require("../../../core/identity");
7
7
  const { KazeeCallSession } = require("./session");
8
8
 
@@ -56,12 +56,9 @@ class KazeeCallManager {
56
56
  catch (_) { throw new Error("Call configuration unavailable"); }
57
57
  const config = res?.config;
58
58
  if (!config || !Array.isArray(config.iceServers)) throw new Error("Call configuration unavailable");
59
- const usable = config.iceServers.flatMap(server => {
60
- const urls = (Array.isArray(server.urls) ? server.urls : [server.urls]).filter(url => typeof url === "string" && (url.startsWith("stun:") || (url.startsWith("turn:") && !url.includes("transport=tcp"))));
61
- return urls.length ? [{ ...server, urls }] : [];
62
- });
63
- if (!usable.some(server => server.urls.some(url => url.startsWith("turn:")))) throw new Error("Call configuration has no supported TURN route");
59
+ const usable = supportedIceServers(config.iceServers);
64
60
  return { iceServers: usable, janusServer: config.janusServer || this.janusUrl, janusToken: config.janusToken,
61
+ iceCredentialsExpiresAt: config.iceCredentialsExpiresAt,
65
62
  roomAdmissionEnabled: config.roomAdmissionEnabled === true };
66
63
  }
67
64
 
@@ -78,12 +75,13 @@ class KazeeCallManager {
78
75
  }
79
76
 
80
77
  _sessionOptions(config, access, identity, signal) {
81
- return { janusUrl: access?.server || config.janusServer, iceServers: config.iceServers, janusToken: access?.janusToken || config.janusToken,
82
- janusTokenExpiresAt: access?.janusTokenExpiresAt, managedRoom: config.roomAdmissionEnabled, roomToken: access?.roomToken, roomTokenExpiresAt: access?.roomTokenExpiresAt,
83
- refreshRoomAccess: config.roomAdmissionEnabled ? async () => {
78
+ return { janusUrl: access?.server || config.janusServer, iceServers: access ? supportedIceServers(access.iceServers) : config.iceServers, janusToken: access?.janusToken || config.janusToken,
79
+ iceCredentialsExpiresAt: access?.iceCredentialsExpiresAt ?? config.iceCredentialsExpiresAt,
80
+ janusTokenExpiresAt: access?.janusTokenExpiresAt, managedRoom: !!access, roomToken: access?.roomToken, roomTokenExpiresAt: access?.roomTokenExpiresAt,
81
+ refreshRoomAccess: access ? async () => {
84
82
  const next = await requestRoomAccess(this.adapter._socket, "room-access", { ...identity, deviceId: this.deviceId }, { signal });
85
83
  if (next.server !== access?.server || next.nodeId !== access?.nodeId || next.generation !== access?.generation) throw new Error('Call room placement changed');
86
- return next;
84
+ return { ...next, iceServers: supportedIceServers(next.iceServers) };
87
85
  } : undefined };
88
86
  }
89
87
 
@@ -124,6 +122,7 @@ class KazeeCallManager {
124
122
  const identity = publicCallIdentity(payload);
125
123
  const reject = (reason) => this.adapter._socket?.emit("call:reject", { ...identity, ...(identity.callAttemptId ? { deviceId: this.deviceId, generation: Number.isSafeInteger(payload.generation) && payload.generation >= 0 ? payload.generation : 0 } : {}), toUserId: callerId, callType, chatId, roomId, reason });
126
124
  if (!callerId || !chatId || !Number.isSafeInteger(roomId) || roomId <= 0) return reject("invalid_call");
125
+ if (payload.roomAdmissionRequired !== undefined && typeof payload.roomAdmissionRequired !== 'boolean') return reject("invalid_call");
127
126
  if (callerId !== String(this.adapter.ownerUserId || "")) return reject("not_allowed");
128
127
  if (callType !== "audio") return reject("unsupported_media");
129
128
  if (this.session || this._pending) return reject("busy");
@@ -133,8 +132,11 @@ class KazeeCallManager {
133
132
  let session;
134
133
  try {
135
134
  const config = await this._fetchCallConfig();
136
- if (config.roomAdmissionEnabled && !identity.callAttemptId) throw new Error("Call identity required");
137
- const access = config.roomAdmissionEnabled ? await requestRoomAccess(this.adapter._socket, "room-access", { ...identity, chatId, deviceId: this.deviceId }, { signal: pending.controller.signal }) : null;
135
+ // Server event metadata belongs to this attempt. The latest org flag only
136
+ // controls new outgoing allocation, never an existing call's admission.
137
+ const managed = payload.roomAdmissionRequired === true;
138
+ if (managed && !identity.callAttemptId) throw new Error("Call identity required");
139
+ const access = managed ? await requestRoomAccess(this.adapter._socket, "room-access", { ...identity, chatId, deviceId: this.deviceId }, { signal: pending.controller.signal }) : null;
138
140
  if (pending.controller.signal.aborted) return;
139
141
  const boundIdentity = access ? { ...publicCallIdentity(access), deviceId: this.deviceId, generation: access.generation } : identity;
140
142
  session = new this.SessionImpl({ roomId, chatId, callerId, botUserId: this.adapter.botUserId,
@@ -2,6 +2,7 @@
2
2
 
3
3
  const { MediaStreamTrack, RTCPeerConnection, RtpHeader, RtpPacket } = require("werift");
4
4
  const { JanusClient } = require("./janus-client");
5
+ const { projectAssignedIce, supportedIceServers } = require("./access");
5
6
  const { AdaptiveTurnDetector, OpusAudioCodec, WEBRTC_SAMPLES, downsample48To16, upsampleTo48 } = require("./audio");
6
7
  const { SpeechStream, synthesizePcm, synthesizeStream: synthesizeStreamPcm } = require("./speech-client");
7
8
  const ttsVoice = require("../../../core/tts-voice");
@@ -73,7 +74,7 @@ const ACK_CHIME_PCM48 = tonePcm48([{ freq: 660, ms: 90 }, { freq: 990, ms: 110 }
73
74
  const THINK_TICK_PCM48 = tonePcm48([{ freq: 520, ms: 90 }], 0.045);
74
75
 
75
76
  class KazeeCallSession {
76
- constructor({ roomId, chatId, callerId, botUserId, janusUrl, janusToken, janusTokenExpiresAt, roomToken, roomTokenExpiresAt, managedRoom = false, refreshRoomAccess, iceServers = [], onTranscript, onEnded, onMediaReady,
77
+ constructor({ roomId, chatId, callerId, botUserId, janusUrl, janusToken, janusTokenExpiresAt, roomToken, roomTokenExpiresAt, managedRoom = false, refreshRoomAccess, iceServers = [], iceCredentialsExpiresAt, onTranscript, onEnded, onMediaReady,
77
78
  maxCallMs, idleMs, finalWaitMs, synthesize = synthesizePcm, synthesizeStream = null, asCaller = false, greeting = null,
78
79
  JanusClientImpl = JanusClient, PeerConnectionImpl = RTCPeerConnection, SpeechStreamImpl = SpeechStream } = {}) {
79
80
  this.roomId = Number(roomId);
@@ -87,6 +88,7 @@ class KazeeCallSession {
87
88
  this.roomTokenExpiresAt = roomTokenExpiresAt;
88
89
  this.refreshRoomAccess = refreshRoomAccess;
89
90
  this.privateId = null;
91
+ this.publisherId = null;
90
92
  this.greeting = greeting ? String(greeting) : null;
91
93
  this._greeted = false;
92
94
  this._publisherUp = false;
@@ -97,6 +99,9 @@ class KazeeCallSession {
97
99
  this.PeerConnectionImpl = PeerConnectionImpl;
98
100
  this.SpeechStreamImpl = SpeechStreamImpl;
99
101
  this.iceServers = iceServers;
102
+ this.iceCredentialsExpiresAt = iceCredentialsExpiresAt;
103
+ this._transportIceExpiry = new Map();
104
+ this._transportExpiryTimer = null;
100
105
  this.onTranscript = onTranscript || (() => {});
101
106
  this.onEnded = onEnded || (() => {});
102
107
  this.onMediaReady = onMediaReady || (() => {});
@@ -136,15 +141,22 @@ class KazeeCallSession {
136
141
 
137
142
  async _roomAuth(force = false) {
138
143
  if (!this.managedRoom) return this.roomToken ? { token: this.roomToken } : {};
139
- if (force || !this.roomToken || this.roomTokenExpiresAt <= Date.now() + 10000) {
144
+ if (force || !this.roomToken || this.roomTokenExpiresAt <= Date.now() + 10000 || this.iceCredentialsExpiresAt <= Date.now() + 60000) {
140
145
  if (!this.refreshRoomAccess) throw new Error("Call room credentials expired");
141
146
  if (!this._roomRefresh) {
142
147
  this._roomRefresh = Promise.resolve().then(() => this.refreshRoomAccess()).then(access => {
143
148
  if (this.closed) throw new Error("Call ended during credential refresh");
149
+ const assigned = projectAssignedIce(access);
150
+ const iceServers = supportedIceServers(assigned.iceServers);
144
151
  this.roomToken = access.roomToken;
145
152
  this.roomTokenExpiresAt = access.roomTokenExpiresAt;
146
153
  if (access.janusToken) this.janus.token = access.janusToken;
147
154
  if (access.janusTokenExpiresAt !== undefined) this.janusTokenExpiresAt = access.janusTokenExpiresAt;
155
+ // Werift captures TURN auth when each ICE transport is constructed.
156
+ // New credentials apply to future peers, never extend an existing
157
+ // allocation's immutable deadline or mutate its authentication.
158
+ this.iceServers = iceServers;
159
+ this.iceCredentialsExpiresAt = assigned.iceCredentialsExpiresAt;
148
160
  }).finally(() => { this._roomRefresh = null; });
149
161
  }
150
162
  await this._roomRefresh;
@@ -153,6 +165,29 @@ class KazeeCallSession {
153
165
  return { token: this.roomToken };
154
166
  }
155
167
 
168
+ _newPeerConnection() {
169
+ const expiresAt = this.iceCredentialsExpiresAt;
170
+ if (this.managedRoom && (!Number.isFinite(expiresAt) || expiresAt <= Date.now() + 1000)) throw new Error("Call relay credentials unavailable");
171
+ const pc = new this.PeerConnectionImpl({ iceServers: this.iceServers });
172
+ if (Number.isFinite(expiresAt)) {
173
+ this._transportIceExpiry.set(pc, expiresAt);
174
+ this._armTransportExpiry();
175
+ }
176
+ return pc;
177
+ }
178
+
179
+ _armTransportExpiry() {
180
+ if (this._transportExpiryTimer) clearTimeout(this._transportExpiryTimer);
181
+ this._transportExpiryTimer = null;
182
+ if (this.closed || !this._transportIceExpiry.size) return;
183
+ const deadline = Math.min(...this._transportIceExpiry.values());
184
+ this._transportExpiryTimer = setTimeout(() => {
185
+ this._transportExpiryTimer = null;
186
+ this.close("relay-credentials-expired").catch(() => console.error("Kazee call relay cleanup failed"));
187
+ }, Math.max(0, deadline - Date.now() - 1000));
188
+ this._transportExpiryTimer.unref?.();
189
+ }
190
+
156
191
  async start() {
157
192
  await this.janus.connect();
158
193
  if (this.closed) { await this.janus.close(); throw new Error("Call ended during setup"); }
@@ -213,10 +248,15 @@ class KazeeCallSession {
213
248
  if (data.error_code != null) error.code = Number(data.error_code);
214
249
  throw error;
215
250
  }
216
- if (data.videoroom === "joined") this.privateId = data.private_id ?? null;
251
+ if (data.videoroom === "joined") {
252
+ this.privateId = data.private_id ?? null;
253
+ const publisherId = Number(data.id);
254
+ if (Number.isSafeInteger(publisherId) && publisherId > 0) this.publisherId = publisherId;
255
+ else if (this.managedRoom) throw new Error("Managed publisher identity unavailable");
256
+ }
217
257
  for (const publisher of data.publishers || []) {
218
258
  const feedId = Number(publisher.id);
219
- if (!this.subscribers.has(feedId) && !String(publisher.display || "").startsWith("open-claudia:")) {
259
+ if (Number.isSafeInteger(feedId) && feedId > 0 && feedId !== this.publisherId && !this.subscribers.has(feedId) && !String(publisher.display || "").startsWith("open-claudia:")) {
220
260
  await this._subscribe(feedId, publisher.display);
221
261
  }
222
262
  }
@@ -264,7 +304,9 @@ class KazeeCallSession {
264
304
 
265
305
  async _publish() {
266
306
  if (this.publisher) return;
267
- const pc = new this.PeerConnectionImpl({ iceServers: this.iceServers });
307
+ await this._roomAuth();
308
+ if (this.closed || this.publisher) return;
309
+ const pc = this._newPeerConnection();
268
310
  pc.iceConnectionStateChange?.subscribe?.((state) => {
269
311
  clog(`Kazee call publisher ICE: ${state} room=${this.roomId}`);
270
312
  if (state === "connected" || state === "completed") {
@@ -324,9 +366,10 @@ class KazeeCallSession {
324
366
  this.subscribers.set(feedId, null);
325
367
  clog(`Kazee call subscribing: feed=${feedId} display=${String(display || "")}`);
326
368
  try {
369
+ const roomAuth = await this._roomAuth();
327
370
  const handle = await this.janus.attach();
328
371
  if (this.closed) { await this.janus.detach(handle); this.subscribers.delete(feedId); return; }
329
- const pc = new this.PeerConnectionImpl({ iceServers: this.iceServers });
372
+ const pc = this._newPeerConnection();
330
373
  pc.iceConnectionStateChange?.subscribe?.((state) => clog(`Kazee call subscriber ICE: ${state} feed=${feedId}`));
331
374
  const sub = {
332
375
  handle,
@@ -357,7 +400,7 @@ class KazeeCallSession {
357
400
  if (track.kind === "audio") track.onReceiveRtp.subscribe((packet) => this._onFeedRtp(feedId, packet));
358
401
  });
359
402
  this.janus.onHandle(handle, (frame) => this._onSubscriberEvent(feedId, frame));
360
- const attached = await this.janus.message(handle, { ...(await this._roomAuth()), request: "join", ptype: "subscriber", room: this.roomId, feed: feedId, ...(this.privateId != null ? { private_id: this.privateId } : {}) });
403
+ const attached = await this.janus.message(handle, { ...roomAuth, request: "join", ptype: "subscriber", room: this.roomId, feed: feedId, ...(this.privateId != null ? { private_id: this.privateId } : {}) });
361
404
  await this._consumeSubscriberFrame(feedId, attached);
362
405
  await sub.stt.connect().then(
363
406
  () => clog(`Kazee call STT connected: feed=${feedId}`),
@@ -758,8 +801,8 @@ class KazeeCallSession {
758
801
  async _enforceLimits() {
759
802
  if (this.closed || this._ending) return;
760
803
  const now = Date.now();
761
- if (this.managedRoom && this.janusTokenExpiresAt !== undefined && this.janusTokenExpiresAt <= now + 60000) {
762
- if (this.janusTokenExpiresAt <= now) { await this.close("credentials-expired"); return; }
804
+ if (this.managedRoom && [this.janusTokenExpiresAt, this.roomTokenExpiresAt, this.iceCredentialsExpiresAt].some(expiry => expiry !== undefined && expiry <= now + 60000)) {
805
+ if ([this.janusTokenExpiresAt, this.roomTokenExpiresAt, this.iceCredentialsExpiresAt].some(expiry => expiry !== undefined && expiry <= now)) { await this.close("credentials-expired"); return; }
763
806
  // Uses the same single in-flight request as subscriber room-token refresh.
764
807
  // The next limit tick still closes at expiry if this request stalls.
765
808
  this._roomAuth(true).catch(() => {});
@@ -785,6 +828,8 @@ class KazeeCallSession {
785
828
  const sub = this.subscribers.get(feedId);
786
829
  if (!sub) return;
787
830
  this.subscribers.delete(feedId);
831
+ this._transportIceExpiry.delete(sub.pc);
832
+ this._armTransportExpiry();
788
833
  if (sub.finalTimer) clearTimeout(sub.finalTimer);
789
834
  try { sub.stt.close(); } catch (_) {}
790
835
  try { sub.codec.close(); } catch (_) {}
@@ -795,6 +840,10 @@ class KazeeCallSession {
795
840
  async close(reason = "ended") {
796
841
  if (this.closed) return;
797
842
  this.closed = true;
843
+ if (this._transportExpiryTimer) clearTimeout(this._transportExpiryTimer);
844
+ this._transportExpiryTimer = null;
845
+ const trackedPeers = [...this._transportIceExpiry.keys()];
846
+ this._transportIceExpiry.clear();
798
847
  this._leaseHeartbeat?.close();
799
848
  console.log(`Kazee call session closed: room=${this.roomId} reason=${reason}`);
800
849
  if (this._limitTimer) { clearInterval(this._limitTimer); this._limitTimer = null; }
@@ -806,6 +855,7 @@ class KazeeCallSession {
806
855
  try { await this.janus.message(this.publisherHandle, { request: "destroy", room: this.roomId }); } catch (_) {}
807
856
  }
808
857
  try { await this.publisher?.pc?.close(); } catch (_) {}
858
+ for (const pc of trackedPeers) { try { await pc.close(); } catch (_) {} }
809
859
  try { await this.janus.detach(this.publisherHandle); } catch (_) {}
810
860
  try { await this.janus.close(); } catch (_) {}
811
861
  this.codec.close();
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",