@inetafrica/open-claudia 3.4.6 → 3.4.11
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 +25 -0
- package/channels/kazee/call/access.js +53 -0
- package/channels/kazee/call/audio.js +1 -1
- package/channels/kazee/call/janus-client.js +3 -1
- package/channels/kazee/call/lease.js +70 -0
- package/channels/kazee/call/manager.js +150 -106
- package/channels/kazee/call/opus-codec.js +44 -0
- package/channels/kazee/call/session.js +58 -5
- package/core/scheduler.js +46 -26
- package/docs/PROVIDER_MIGRATION.md +5 -1
- package/package.json +2 -2
- package/test-cursor-removal.js +4 -1
- package/test-kazee-call-audio.js +22 -0
- package/test-kazee-call-janus.js +7 -2
- package/test-kazee-call-manager.js +136 -7
- package/test-kazee-call-session.js +62 -0
- package/test-provider-enforcer.js +29 -6
- package/test-provider-language.js +2 -1
- package/test-provider-scheduler.js +83 -0
|
@@ -79,6 +79,28 @@ async function testSpeakSerialization() {
|
|
|
79
79
|
await session.close();
|
|
80
80
|
}
|
|
81
81
|
|
|
82
|
+
async function testMeasuredMediaReady() {
|
|
83
|
+
const { session } = makeSession();
|
|
84
|
+
let ready = 0, sent = 0;
|
|
85
|
+
session.onMediaReady = () => { ready++; };
|
|
86
|
+
await session._subscribe(901, "peer");
|
|
87
|
+
const codec = new OpusAudioCodec();
|
|
88
|
+
session._onFeedRtp(901, { payload: toneOpus(codec, 30) });
|
|
89
|
+
assert.strictEqual(ready, 0, "receive-only media is insufficient");
|
|
90
|
+
session._writeFrame(Buffer.alloc(WEBRTC_SAMPLES * 2));
|
|
91
|
+
assert.strictEqual(ready, 0, "missing publisher track cannot count as sent media");
|
|
92
|
+
session.publisher = { track: { writeRtp: () => { sent++; } }, sender: { codec: { payloadType: 111 } } };
|
|
93
|
+
session._writeFrame(Buffer.alloc(WEBRTC_SAMPLES * 2));
|
|
94
|
+
assert.strictEqual(ready, 1);
|
|
95
|
+
session._writeFrame(Buffer.alloc(WEBRTC_SAMPLES * 2));
|
|
96
|
+
session._onFeedRtp(901, { payload: toneOpus(codec, 30) });
|
|
97
|
+
assert.strictEqual(ready, 1, "media-ready is reported once per session");
|
|
98
|
+
await session.close();
|
|
99
|
+
session._writeFrame(Buffer.alloc(WEBRTC_SAMPLES * 2));
|
|
100
|
+
assert.strictEqual(sent, 2, "closed sessions cannot send or activate media");
|
|
101
|
+
codec.close();
|
|
102
|
+
}
|
|
103
|
+
|
|
82
104
|
async function testSpeakCancellation() {
|
|
83
105
|
let frames = 0;
|
|
84
106
|
const synthCalls = [];
|
|
@@ -421,9 +443,49 @@ async function testCloseResolvesSpeech() {
|
|
|
421
443
|
assert.strictEqual(await session.speak("after"), false, "speak after close is a no-op");
|
|
422
444
|
}
|
|
423
445
|
|
|
446
|
+
async function testManagedRoomCredentials() {
|
|
447
|
+
class SignedJanus extends FakeJanus {
|
|
448
|
+
constructor(options) { super(); this.options = options; this.calls = []; }
|
|
449
|
+
async message(handle, body) {
|
|
450
|
+
this.calls.push(body);
|
|
451
|
+
if (body.request === "join" && body.ptype === "publisher") return { plugindata: { data: { videoroom: "joined", private_id: 999, publishers: [] } } };
|
|
452
|
+
return {};
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
let refreshes = 0;
|
|
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 }; },
|
|
459
|
+
JanusClientImpl: SignedJanus, PeerConnectionImpl: FakePC, SpeechStreamImpl: FakeSpeechStream });
|
|
460
|
+
session._maybePublish = async () => {};
|
|
461
|
+
await session.start();
|
|
462
|
+
assert.strictEqual(session.janus.options.token, "root-token");
|
|
463
|
+
assert.strictEqual(session.janus.calls[0].token, "publisher-token");
|
|
464
|
+
session.roomTokenExpiresAt = Date.now() - 1;
|
|
465
|
+
await Promise.all([session._subscribe(17, "owner"), session._subscribe(18, "other")]);
|
|
466
|
+
const subJoin = session.janus.calls.find(body => body.ptype === "subscriber");
|
|
467
|
+
assert.strictEqual(subJoin.token, "subscriber-token");
|
|
468
|
+
assert.strictEqual(subJoin.private_id, 999);
|
|
469
|
+
assert.strictEqual(refreshes, 1, "simultaneous subscriber joins share one token refresh");
|
|
470
|
+
session.janusTokenExpiresAt = Date.now() + 50000;
|
|
471
|
+
const handlesBefore = session.janus.handles;
|
|
472
|
+
await session._enforceLimits();
|
|
473
|
+
await wait(0);
|
|
474
|
+
assert.strictEqual(session.janus.token, "fresh-root");
|
|
475
|
+
assert.strictEqual(refreshes, 2, "root renewal does not wait for a new subscriber");
|
|
476
|
+
assert.strictEqual(session.janus.handles, handlesBefore, "renewal keeps existing session and handles");
|
|
477
|
+
session.janusTokenExpiresAt = Date.now() - 1;
|
|
478
|
+
await session._enforceLimits();
|
|
479
|
+
assert.strictEqual(session.closed, true, "expired transport credentials close media");
|
|
480
|
+
await session.close();
|
|
481
|
+
assert.ok(!session.janus.calls.some(body => ["create", "destroy"].includes(body.request)), "server allocated room ownership never transfers to the bot");
|
|
482
|
+
}
|
|
483
|
+
|
|
424
484
|
(async () => {
|
|
485
|
+
await testManagedRoomCredentials();
|
|
425
486
|
await testSpeakSerialization();
|
|
426
487
|
await testSpeakCancellation();
|
|
488
|
+
await testMeasuredMediaReady();
|
|
427
489
|
await testEchoGateAndBargeIn();
|
|
428
490
|
await testPrebufferAndCommit();
|
|
429
491
|
await testHoldForWords();
|
|
@@ -6,7 +6,8 @@ const assert = require("assert");
|
|
|
6
6
|
const fs = require("fs");
|
|
7
7
|
const os = require("os");
|
|
8
8
|
const path = require("path");
|
|
9
|
-
const { spawnSync } = require("child_process");
|
|
9
|
+
const { spawn, spawnSync } = require("child_process");
|
|
10
|
+
const { killProcessTree } = require("./core/process-tree");
|
|
10
11
|
|
|
11
12
|
const RESULT_PREFIX = "__PROVIDER_ENFORCER_RESULT__";
|
|
12
13
|
|
|
@@ -73,7 +74,6 @@ function auditSink() {
|
|
|
73
74
|
|
|
74
75
|
async function providerProbe(kind) {
|
|
75
76
|
const root = isolate(kind);
|
|
76
|
-
const timeoutSignal = path.join(root, `${kind}-timeout-signal.json`);
|
|
77
77
|
const controls = [
|
|
78
78
|
{ FAKE_AGENT_TEXT: '{"decision":"allow","reason":"inside mandate"}' },
|
|
79
79
|
{ FAKE_AGENT_TEXT: '{"decision":"block","reason":"outside mandate"}' },
|
|
@@ -82,16 +82,27 @@ async function providerProbe(kind) {
|
|
|
82
82
|
// The guard-reliability fix retries ONCE on a transient timeout before
|
|
83
83
|
// failing closed: BOTH attempts hang, so the verdict still escalates —
|
|
84
84
|
// proving the retry is bounded and never fails open at the transport level.
|
|
85
|
-
{ FAKE_AGENT_SCENARIO: "forced-termination", FAKE_AGENT_SIGNAL_CAPTURE: timeoutSignal },
|
|
86
85
|
{ FAKE_AGENT_SCENARIO: "forced-termination" },
|
|
86
|
+
{ FAKE_AGENT_SCENARIO: "forced-termination", FAKE_AGENT_IGNORE_SIGTERM: "1" },
|
|
87
87
|
];
|
|
88
88
|
const entry = makeProvider(kind, root, controls);
|
|
89
89
|
const model = `${kind}-enforcer-exact`;
|
|
90
90
|
const { createUtilityAgentService } = require("./core/utility-agent");
|
|
91
|
+
const children = [];
|
|
92
|
+
const terminations = [];
|
|
91
93
|
const utility = createUtilityAgentService({
|
|
92
94
|
registry: registry([entry]),
|
|
93
95
|
env: { ENFORCER_PROVIDER: kind, [`ENFORCER_MODEL_${kind.toUpperCase()}`]: model },
|
|
94
96
|
killGraceMs: 300,
|
|
97
|
+
spawnProcess(...args) {
|
|
98
|
+
const child = spawn(...args);
|
|
99
|
+
children.push(child);
|
|
100
|
+
return child;
|
|
101
|
+
},
|
|
102
|
+
killTree(pid, signal) {
|
|
103
|
+
terminations.push({ pid, signal });
|
|
104
|
+
return killProcessTree(pid, signal);
|
|
105
|
+
},
|
|
95
106
|
});
|
|
96
107
|
const enforcer = require("./core/enforcer");
|
|
97
108
|
assert.strictEqual(typeof enforcer.createEnforcerService, "function", "enforcer exposes an injectable utility-provider service");
|
|
@@ -114,8 +125,8 @@ async function providerProbe(kind) {
|
|
|
114
125
|
spawnUtilityAgent: utility.spawnUtilityAgent,
|
|
115
126
|
audit,
|
|
116
127
|
// Bounded (this deadline MUST fire — the child hangs by design), but with
|
|
117
|
-
// enough headroom for the hung
|
|
118
|
-
//
|
|
128
|
+
// enough headroom for the hung children to boot on a loaded CI runner
|
|
129
|
+
// before the deadline kills them; the retry deliberately ignores TERM.
|
|
119
130
|
timeoutMs: 5000,
|
|
120
131
|
});
|
|
121
132
|
const timedOut = await timeoutService.vetReply(speaker, "timeout verdict fixture");
|
|
@@ -154,7 +165,19 @@ async function providerProbe(kind) {
|
|
|
154
165
|
assert.ok(record.argv.includes(kind === "claude" ? "plan" : "read-only"));
|
|
155
166
|
assert.ok(record.argv.includes(kind === "claude" ? "--json-schema" : "--output-schema"));
|
|
156
167
|
}
|
|
157
|
-
|
|
168
|
+
// Observe shutdown from the surviving parent. A child-written receipt can
|
|
169
|
+
// be truncated by SIGKILL during synchronous I/O (CI pipeline 12017), even
|
|
170
|
+
// though the guard correctly failed closed and waited for process close.
|
|
171
|
+
assert.strictEqual(children.length, 6);
|
|
172
|
+
for (const child of children.slice(4)) {
|
|
173
|
+
const signals = terminations.filter((entry) => entry.pid === child.pid).map((entry) => entry.signal);
|
|
174
|
+
assert.strictEqual(signals[0], "SIGTERM", "both timeout attempts first request graceful termination");
|
|
175
|
+
assert.ok(["SIGTERM", "SIGKILL"].includes(child.signalCode), "the timed-out child has exited by signal before the verdict settles");
|
|
176
|
+
assert.throws(() => process.kill(child.pid, 0), { code: "ESRCH" }, "the timed-out provider must not survive its verdict");
|
|
177
|
+
}
|
|
178
|
+
assert.ok(terminations.some((entry) => entry.pid === children[5].pid && entry.signal === "SIGKILL"), "the retry that ignores TERM is forcibly terminated after the grace period");
|
|
179
|
+
assert.strictEqual(children[5].signalCode, "SIGKILL");
|
|
180
|
+
assert.deepStrictEqual(utility.stats(), { activeGlobal: 0, activeByPurpose: {} }, "both timed-out attempts release their concurrency slots after close");
|
|
158
181
|
|
|
159
182
|
return { kind, decisions: [allow.decision, block.decision, escalate.decision, malformed.decision, timedOut.decision] };
|
|
160
183
|
}
|
|
@@ -463,7 +463,8 @@ 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
|
-
|
|
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");
|
|
467
468
|
for (const keyword of ["claude", "codex", "coding-agent", "provider-agnostic"]) {
|
|
468
469
|
assert.ok(pkg.keywords.includes(keyword), `package keyword missing: ${keyword}`);
|
|
469
470
|
}
|
|
@@ -633,6 +633,88 @@ async function busyRestartAndMissedProbe(createSchedulerService) {
|
|
|
633
633
|
assert.strictEqual(missedTimers.length, 1, "a missed wakeup gets exactly one startup attempt");
|
|
634
634
|
}
|
|
635
635
|
|
|
636
|
+
async function durablePendingProbe(createSchedulerService, jobsApi) {
|
|
637
|
+
const configDir = fs.mkdtempSync(path.join(os.tmpdir(), "oc-pending-restart-"));
|
|
638
|
+
let clock = 1_720_000_000_500;
|
|
639
|
+
const openStore = () => jobsApi.createJobsStore({ configDir, jobsFile: path.join(configDir, "jobs.json"), now: () => clock });
|
|
640
|
+
const success = { ok: true, status: "succeeded", delivery: { ok: true } };
|
|
641
|
+
try {
|
|
642
|
+
for (const kind of ["wakeup", "watch", "expiry"]) {
|
|
643
|
+
const store = openStore();
|
|
644
|
+
store.add(baseJob({ id: kind, ...(kind === "wakeup" ? {} : {
|
|
645
|
+
kind: "cron", schedule: "* * * * *", check: "fixture-check", repeat: true,
|
|
646
|
+
expiresAt: kind === "expiry" ? clock - 1 : clock + 1000,
|
|
647
|
+
}) }));
|
|
648
|
+
let checks = 0;
|
|
649
|
+
const h = schedulerHarness(createSchedulerService, [], {
|
|
650
|
+
jobsStore: store, now: () => clock,
|
|
651
|
+
runWatchCheck: async () => { checks++; return { code: kind === "expiry" ? 1 : 0 }; },
|
|
652
|
+
});
|
|
653
|
+
h.state.runningProcess = { pid: 1 };
|
|
654
|
+
for (let tick = 0; tick < 12; tick++) {
|
|
655
|
+
clock += 60 * 60 * 1000;
|
|
656
|
+
const result = await h.service.fireJob(kind);
|
|
657
|
+
assert.strictEqual(result.deferred, true, "hours of busy time must remain queued");
|
|
658
|
+
}
|
|
659
|
+
assert.strictEqual(store.get(kind).attemptCount, 0);
|
|
660
|
+
assert.strictEqual(store.get(kind).disabled, false);
|
|
661
|
+
assert.strictEqual(checks, kind === "wakeup" ? 0 : 1);
|
|
662
|
+
assert.strictEqual(h.sends.length, 0);
|
|
663
|
+
h.service.stopAll();
|
|
664
|
+
|
|
665
|
+
// Reopen the real on-disk store and construct a new scheduler, as boot does.
|
|
666
|
+
const reloaded = openStore();
|
|
667
|
+
const terminal = deferred();
|
|
668
|
+
const restarted = schedulerHarness(createSchedulerService, [], {
|
|
669
|
+
jobsStore: reloaded, now: () => clock, outcomes: [terminal.promise, success],
|
|
670
|
+
runWatchCheck: async () => { throw new Error("pending watch must not poll after restart"); },
|
|
671
|
+
});
|
|
672
|
+
restarted.service.init();
|
|
673
|
+
const firing = restarted.scheduled[0].fn();
|
|
674
|
+
await nextTurn();
|
|
675
|
+
clock += 60 * 60 * 1000;
|
|
676
|
+
assert.strictEqual((await restarted.service.fireJob(kind)).inFlight, true, "long runs cannot re-enter after retry delay");
|
|
677
|
+
assert.strictEqual(restarted.runCalls.length, 1);
|
|
678
|
+
assert.strictEqual(restarted.sends.length, 1);
|
|
679
|
+
if (kind === "expiry") assert.match(restarted.runCalls[0].prompt, /Watch expired/);
|
|
680
|
+
terminal.resolve(success);
|
|
681
|
+
await firing;
|
|
682
|
+
if (kind === "watch") {
|
|
683
|
+
assert.strictEqual(reloaded.get(kind).status, "pending");
|
|
684
|
+
assert.strictEqual(reloaded.get(kind).watchTriggeredOccurrenceId, null);
|
|
685
|
+
restarted.service.removeJob(kind);
|
|
686
|
+
} else assert.strictEqual(reloaded.get(kind), null);
|
|
687
|
+
restarted.service.stopAll();
|
|
688
|
+
const finalBoot = schedulerHarness(createSchedulerService, [], { jobsStore: openStore(), now: () => clock });
|
|
689
|
+
finalBoot.service.init();
|
|
690
|
+
assert.strictEqual(finalBoot.scheduled.length, 0, "successful one-shot cannot replay on restart");
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
const store = openStore();
|
|
694
|
+
store.add(baseJob({ id: "failure", kind: "cron", schedule: "* * * * *", check: "fixture" }));
|
|
695
|
+
const failed = schedulerHarness(createSchedulerService, [], {
|
|
696
|
+
jobsStore: store, now: () => clock,
|
|
697
|
+
runWatchCheck: async () => ({ code: 0 }), outcomes: [{ ok: false, error: { code: "FIXTURE_FAILED" } }],
|
|
698
|
+
});
|
|
699
|
+
await failed.service.fireJob("failure");
|
|
700
|
+
failed.service.stopAll();
|
|
701
|
+
clock += 2000;
|
|
702
|
+
const retry = schedulerHarness(createSchedulerService, [], {
|
|
703
|
+
jobsStore: openStore(), now: () => clock,
|
|
704
|
+
runWatchCheck: async () => { throw new Error("do not recheck"); },
|
|
705
|
+
outcomes: [{ ok: false }, { ok: false }],
|
|
706
|
+
});
|
|
707
|
+
await retry.service.fireJob("failure");
|
|
708
|
+
await retry.service.fireJob("failure");
|
|
709
|
+
assert.strictEqual(retry.jobsStore.get("failure").status, "dead-letter", "real failures retain bounded retries");
|
|
710
|
+
assert.strictEqual(retry.jobsStore.get("failure").attemptCount, 3);
|
|
711
|
+
assert.strictEqual(retry.sends.length, 0, "advisory is not repeated across failure/restart");
|
|
712
|
+
retry.service.stopAll();
|
|
713
|
+
} finally {
|
|
714
|
+
fs.rmSync(configDir, { recursive: true, force: true });
|
|
715
|
+
}
|
|
716
|
+
}
|
|
717
|
+
|
|
636
718
|
async function unavailableAndFallbackProbe(createSchedulerService) {
|
|
637
719
|
const noFallback = schedulerHarness(createSchedulerService, [baseJob({ id: "unavailable" })], {
|
|
638
720
|
providerStatus: () => ({ availability: { state: "missing" }, auth: { state: "not_checked" } }),
|
|
@@ -960,6 +1042,7 @@ async function main() {
|
|
|
960
1042
|
await failureAndRetryProbe(schedulerApi.createSchedulerService);
|
|
961
1043
|
await detachedHistoricalBranchProbe(schedulerApi.createSchedulerService);
|
|
962
1044
|
await busyRestartAndMissedProbe(schedulerApi.createSchedulerService);
|
|
1045
|
+
await durablePendingProbe(schedulerApi.createSchedulerService, jobsApi);
|
|
963
1046
|
await unavailableAndFallbackProbe(schedulerApi.createSchedulerService);
|
|
964
1047
|
await cronOccurrenceProbe(schedulerApi.createSchedulerService);
|
|
965
1048
|
await watchGateProbe(schedulerApi.createSchedulerService);
|