@inetafrica/open-claudia 3.1.16 → 3.1.17

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,12 @@
1
1
  # Changelog
2
2
 
3
+ ## v3.1.17 — Call bridge joins through the caller's room-setup race (fixes "Call declined")
4
+
5
+ - Answering a Kazee web call could fail deterministically fast: the caller creates the Janus videoroom during its own dial-time setup, and the bot answers quicker than any human — its `join` hit Janus before the room existed, got 426 "No such room", and the whole call was rejected as `connection_failed` (surfacing as "Call declined" in the web UI).
6
+ - `session.start()` now polls the publisher join through transient 426 (10 attempts × 500ms), mirroring the web receiver's own retry loop in `CallContext.startVideoRoom`. Persistent 426 still fails the call.
7
+ - Janus error frames now carry `error.code` alongside the message for future discrimination.
8
+ - New assertions: join retries through transient no-such-room and still gives up when the room never appears.
9
+
3
10
  ## v3.1.16 — CI-deflake: no live network in unit tests, CI-safe enforcer deadlines
4
11
 
5
12
  - Identical bot behaviour to v3.1.15 (which never shipped: its pipeline died twice on loaded-runner test flakes — the tag is retired unretagged per release hygiene).
@@ -15,6 +15,8 @@ const PREBUFFER_FRAMES = 12; // 240ms of pre-onset audio so first syllables reac
15
15
  const LIMIT_CHECK_MS = 10_000;
16
16
  const DEFAULT_MAX_CALL_MS = 30 * 60_000;
17
17
  const DEFAULT_IDLE_MS = 2 * 60_000;
18
+ const DEFAULT_JOIN_RETRIES = 10;
19
+ const DEFAULT_JOIN_RETRY_DELAY_MS = 500;
18
20
  const MAX_DURATION_GOODBYE = "We've hit the call time limit, so I'll hang up now. Message me any time.";
19
21
  const IDLE_GOODBYE = "I haven't heard anything for a while, so I'll hang up now. Message me any time.";
20
22
 
@@ -35,6 +37,8 @@ class KazeeCallSession {
35
37
  this.synthesize = synthesize;
36
38
  this.maxCallMs = Number(maxCallMs ?? process.env.KAZEE_CALL_MAX_MS) || DEFAULT_MAX_CALL_MS;
37
39
  this.idleMs = Number(idleMs ?? process.env.KAZEE_CALL_IDLE_MS) || DEFAULT_IDLE_MS;
40
+ this.joinRetries = DEFAULT_JOIN_RETRIES;
41
+ this.joinRetryDelayMs = DEFAULT_JOIN_RETRY_DELAY_MS;
38
42
  this.codec = new OpusAudioCodec();
39
43
  this.publisher = null;
40
44
  this.publisherHandle = null;
@@ -58,9 +62,20 @@ class KazeeCallSession {
58
62
  await this.janus.connect();
59
63
  this.publisherHandle = await this.janus.attach();
60
64
  this.janus.onHandle(this.publisherHandle, (frame) => this._onPublisherEvent(frame));
61
- const joined = await this.janus.message(this.publisherHandle, {
62
- request: "join", ptype: "publisher", room: this.roomId, display: `open-claudia:${this.botUserId}`,
63
- });
65
+ // The caller creates the Janus room during its own call setup, which can
66
+ // land a beat after the invite reaches us — and a bot answers faster than
67
+ // any human. Poll join through transient 426 "No such room" like the web
68
+ // receiver's retry loop in CallContext.startVideoRoom.
69
+ let joined;
70
+ for (let attempt = 0; ; attempt += 1) {
71
+ joined = await this.janus.message(this.publisherHandle, {
72
+ request: "join", ptype: "publisher", room: this.roomId, display: `open-claudia:${this.botUserId}`,
73
+ });
74
+ const data = joined?.plugindata?.data || {};
75
+ const noRoom = Number(data.error_code) === 426 || /no such room/i.test(String(data.error || ""));
76
+ if (!noRoom || attempt >= this.joinRetries || this.closed) break;
77
+ await wait(this.joinRetryDelayMs);
78
+ }
64
79
  await this._consumePublisherFrame(joined);
65
80
  this._startedAt = Date.now();
66
81
  this._lastHeard = this._startedAt;
@@ -76,7 +91,11 @@ class KazeeCallSession {
76
91
 
77
92
  async _consumePublisherFrame(frame) {
78
93
  const data = frame?.plugindata?.data || {};
79
- if (data.error) throw new Error(data.error);
94
+ if (data.error) {
95
+ const error = new Error(data.error);
96
+ if (data.error_code != null) error.code = Number(data.error_code);
97
+ throw error;
98
+ }
80
99
  for (const publisher of data.publishers || []) {
81
100
  const feedId = Number(publisher.id);
82
101
  if (!this.subscribers.has(feedId) && !String(publisher.display || "").startsWith("open-claudia:")) {
@@ -135,7 +154,11 @@ class KazeeCallSession {
135
154
  const sub = this.subscribers.get(feedId);
136
155
  if (!sub) return;
137
156
  const data = frame?.plugindata?.data || {};
138
- if (data.error) throw new Error(data.error);
157
+ if (data.error) {
158
+ const error = new Error(data.error);
159
+ if (data.error_code != null) error.code = Number(data.error_code);
160
+ throw error;
161
+ }
139
162
  if (frame.jsep && !sub.started) {
140
163
  sub.started = true;
141
164
  await sub.pc.setRemoteDescription(frame.jsep);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@inetafrica/open-claudia",
3
- "version": "3.1.16",
3
+ "version": "3.1.17",
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": {
@@ -202,6 +202,32 @@ async function testMaxDuration() {
202
202
  assert.strictEqual(ended[0], "max-duration", "cap closes the call even mid-conversation");
203
203
  }
204
204
 
205
+ async function testJoinRetriesNoSuchRoom() {
206
+ const { session } = makeSession({});
207
+ session.joinRetryDelayMs = 1;
208
+ let joins = 0;
209
+ session.janus.message = async (_handle, body) => {
210
+ if (body?.request !== "join") return {};
211
+ joins += 1;
212
+ if (joins < 3) return { plugindata: { data: { videoroom: "event", error_code: 426, error: "No such room (7)" } } };
213
+ return {};
214
+ };
215
+ await session.start();
216
+ assert.strictEqual(joins, 3, "join retried through transient no-such-room");
217
+ await session.close();
218
+ }
219
+
220
+ async function testJoinGivesUpAfterRetries() {
221
+ const { session } = makeSession({});
222
+ session.joinRetryDelayMs = 1;
223
+ session.joinRetries = 2;
224
+ session.janus.message = async (_handle, body) => body?.request === "join"
225
+ ? { plugindata: { data: { videoroom: "event", error_code: 426, error: "No such room (7)" } } }
226
+ : {};
227
+ await assert.rejects(() => session.start(), /No such room/, "persistent 426 still fails the call");
228
+ await session.close();
229
+ }
230
+
205
231
  async function testCloseResolvesSpeech() {
206
232
  const { session } = makeSession({
207
233
  synthesize: async () => ({ pcm: Buffer.alloc(WEBRTC_SAMPLES * 2 * 100), sampleRate: 48000 }),
@@ -226,6 +252,8 @@ async function testCloseResolvesSpeech() {
226
252
  await testIdleTimeout();
227
253
  await testIdleBargeInKeepsCall();
228
254
  await testMaxDuration();
255
+ await testJoinRetriesNoSuchRoom();
256
+ await testJoinGivesUpAfterRetries();
229
257
  await testCloseResolvesSpeech();
230
258
  console.log("kazee call session: ok");
231
259
  })().catch((error) => { console.error(error); process.exitCode = 1; });
@@ -222,7 +222,12 @@ assert.match(pkg.description, /provider-agnostic coding-agent harness/i);
222
222
  // deflake — heal-guard test stubs its live api.telegram.org probe and isolates
223
223
  // CONFIG_DIR; enforcer probe deadlines raised to CI-safe values (2000→8000ms
224
224
  // backstop, 2500→5000ms deliberate-timeout boot headroom). No runtime changes.
225
- assert.strictEqual(pkg.version, "3.1.16", "release version must remain unchanged without explicit approval");
225
+ // 3.1.17 approved by Sumeet 2026-07-23 (same calls-directive authority; his web
226
+ // call to an upgraded bot showed "Call declined"): the call bridge now retries
227
+ // the Janus publisher join through transient 426 "No such room" — the caller
228
+ // creates the room at dial time and the bot answers faster than any human, so
229
+ // the single naked join lost the race deterministically.
230
+ assert.strictEqual(pkg.version, "3.1.17", "release version must remain unchanged without explicit approval");
226
231
  for (const keyword of ["claude", "codex", "coding-agent", "provider-agnostic"]) {
227
232
  assert.ok(pkg.keywords.includes(keyword), `package keyword missing: ${keyword}`);
228
233
  }