@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 +13 -0
- package/channels/kazee/call/access.js +36 -3
- package/channels/kazee/call/manager.js +14 -12
- package/channels/kazee/call/session.js +59 -9
- package/core/handlers.js +10 -0
- package/core/providers/codex-events.js +3 -1
- package/core/providers/codex-rollout.js +312 -0
- package/core/providers/codex.js +12 -0
- package/core/runner.js +90 -26
- package/package.json +2 -2
- package/test-kazee-call-manager.js +33 -4
- package/test-kazee-call-session.js +61 -4
- package/test-provider-codex.js +12 -0
- package/test-provider-events.js +1 -1
- package/test-provider-language.js +4 -2
- package/test-usage-accounting.js +45 -12
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
|
|
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
|
|
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
|
-
|
|
83
|
-
|
|
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
|
-
|
|
137
|
-
|
|
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")
|
|
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
|
-
|
|
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 =
|
|
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, { ...
|
|
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 &&
|
|
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
|
-
|
|
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",
|