@inetafrica/open-claudia 3.2.1 → 3.3.1
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 +27 -0
- package/bin/cli.js +1 -1
- package/bin/loopback-client.js +4 -0
- package/bin/schedule.js +24 -4
- package/bot.js +10 -1
- package/channels/kazee/call/audio.js +79 -0
- package/channels/kazee/call/manager.js +6 -0
- package/channels/kazee/call/session.js +359 -48
- package/channels/kazee/call/speech-client.js +38 -3
- package/core/actions.js +80 -0
- package/core/enforcer.js +22 -1
- package/core/handlers.js +52 -4
- package/core/loopback.js +41 -0
- package/core/media.js +71 -18
- package/core/pack-review.js +18 -0
- package/core/providers/utility-policy.js +4 -1
- package/core/relationship.js +18 -2
- package/core/router.js +28 -4
- package/core/runner.js +21 -4
- package/core/scheduler.js +60 -21
- package/core/speaker-attest.js +56 -0
- package/core/system-prompt.js +22 -4
- package/core/verify-contact.js +156 -0
- package/package.json +2 -2
- package/test-kazee-call-manager.js +4 -1
- package/test-kazee-call-session.js +100 -4
- package/test-provider-language.js +29 -1
- package/test-provider-scheduler.js +80 -0
- package/test-utility-provider-policy.js +22 -0
|
@@ -29,8 +29,9 @@ class FakePC {
|
|
|
29
29
|
}
|
|
30
30
|
|
|
31
31
|
class FakeSpeechStream {
|
|
32
|
-
constructor({ onTranscript } = {}) {
|
|
32
|
+
constructor({ onTranscript, onPartial } = {}) {
|
|
33
33
|
this.onTranscript = onTranscript;
|
|
34
|
+
this.onPartial = onPartial;
|
|
34
35
|
this.audioFrames = [];
|
|
35
36
|
this.commits = 0;
|
|
36
37
|
this.closedStream = false;
|
|
@@ -41,12 +42,12 @@ class FakeSpeechStream {
|
|
|
41
42
|
close() { this.closedStream = true; }
|
|
42
43
|
}
|
|
43
44
|
|
|
44
|
-
function makeSession({ synthesize, maxCallMs, idleMs, asCaller, greeting } = {}) {
|
|
45
|
+
function makeSession({ synthesize, maxCallMs, idleMs, finalWaitMs, asCaller, greeting } = {}) {
|
|
45
46
|
const ended = [];
|
|
46
47
|
const transcripts = [];
|
|
47
48
|
const session = new KazeeCallSession({
|
|
48
49
|
roomId: 7, chatId: "chat", callerId: "owner", botUserId: "bot", janusUrl: "ws://test",
|
|
49
|
-
maxCallMs, idleMs, asCaller, greeting,
|
|
50
|
+
maxCallMs, idleMs, finalWaitMs, asCaller, greeting,
|
|
50
51
|
synthesize: synthesize || (async () => ({ pcm: Buffer.alloc(WEBRTC_SAMPLES * 2 * 2), sampleRate: 48000 })),
|
|
51
52
|
onTranscript: (text, meta) => transcripts.push({ text, meta }),
|
|
52
53
|
onEnded: (reason) => ended.push(reason),
|
|
@@ -127,12 +128,98 @@ async function testPrebufferAndCommit() {
|
|
|
127
128
|
assert.strictEqual(sub.stt.audioFrames.length, 0, "silence stays out of STT");
|
|
128
129
|
for (let i = 0; i < 3; i += 1) session._onFeedRtp(101, { payload: toneOpus(gen, 12000) });
|
|
129
130
|
assert.strictEqual(sub.stt.audioFrames.length, 8, "onset flushes the prebuffer (5 quiet + 2 onset + current)");
|
|
131
|
+
sub.stt.onPartial("hello there"); // recogniser heard words, so the energy close commits directly
|
|
130
132
|
for (let i = 0; i < 36; i += 1) session._onFeedRtp(101, { payload: toneOpus(gen, 30) });
|
|
131
133
|
assert.strictEqual(sub.stt.commits, 1, "turn commits after sustained silence");
|
|
132
134
|
gen.close();
|
|
133
135
|
await session.close();
|
|
134
136
|
}
|
|
135
137
|
|
|
138
|
+
async function testHoldForWords() {
|
|
139
|
+
const { session } = makeSession({});
|
|
140
|
+
await session._subscribe(101, "caller");
|
|
141
|
+
const sub = session.subscribers.get(101);
|
|
142
|
+
const gen = new OpusAudioCodec();
|
|
143
|
+
for (let i = 0; i < 3; i += 1) session._onFeedRtp(101, { payload: toneOpus(gen, 12000) });
|
|
144
|
+
for (let i = 0; i < 36; i += 1) session._onFeedRtp(101, { payload: toneOpus(gen, 30) });
|
|
145
|
+
assert.strictEqual(sub.stt.commits, 0, "energy close with zero words holds the commit");
|
|
146
|
+
assert(sub.awaitingWordsSince > 0, "hold state armed");
|
|
147
|
+
sub.stt.onPartial("hello there");
|
|
148
|
+
assert.strictEqual(sub.stt.commits, 1, "first words release the held commit");
|
|
149
|
+
assert.strictEqual(sub.awaitingWordsSince, 0);
|
|
150
|
+
assert(session._thinkTimer, "released commit acks and thinks like a normal turn");
|
|
151
|
+
gen.close();
|
|
152
|
+
await session.close();
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
async function testHoldTimeoutDropsNoiseTurn() {
|
|
156
|
+
const { session, transcripts } = makeSession({});
|
|
157
|
+
await session._subscribe(101, "caller");
|
|
158
|
+
const sub = session.subscribers.get(101);
|
|
159
|
+
const gen = new OpusAudioCodec();
|
|
160
|
+
for (let i = 0; i < 3; i += 1) session._onFeedRtp(101, { payload: toneOpus(gen, 12000) });
|
|
161
|
+
for (let i = 0; i < 36; i += 1) session._onFeedRtp(101, { payload: toneOpus(gen, 30) });
|
|
162
|
+
sub.awaitingWordsSince = Date.now() - 5000; // hold expired
|
|
163
|
+
session._onFeedRtp(101, { payload: toneOpus(gen, 30) });
|
|
164
|
+
assert.strictEqual(sub.stt.commits, 1, "expired hold still finalizes the audio");
|
|
165
|
+
assert.strictEqual(sub.noiseCommit, true);
|
|
166
|
+
assert.strictEqual(session._thinkTimer, null, "noise commit stays silent — no ack, no ticking");
|
|
167
|
+
sub.stt.onTranscript("uh"); // babble leaked a stray word into the transcript
|
|
168
|
+
assert.strictEqual(transcripts.length, 0, "sub-threshold noise transcript never dispatches");
|
|
169
|
+
gen.close();
|
|
170
|
+
await session.close();
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
async function testWordStallRescue() {
|
|
174
|
+
const { session } = makeSession({});
|
|
175
|
+
await session._subscribe(101, "caller");
|
|
176
|
+
const sub = session.subscribers.get(101);
|
|
177
|
+
const gen = new OpusAudioCodec();
|
|
178
|
+
for (let i = 0; i < 3; i += 1) session._onFeedRtp(101, { payload: toneOpus(gen, 12000) });
|
|
179
|
+
sub.stt.onPartial("so the deploy");
|
|
180
|
+
sub.lastWordAt = Date.now() - 4500; // word timeline stalled well past its cadence
|
|
181
|
+
session._onFeedRtp(101, { payload: toneOpus(gen, 12000) }); // background keeps energy loud
|
|
182
|
+
assert.strictEqual(sub.stt.commits, 1, "stalled words commit even while energy stays loud");
|
|
183
|
+
assert.strictEqual(sub.vad.active, false, "rescue resets the energy turn");
|
|
184
|
+
gen.close();
|
|
185
|
+
await session.close();
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
async function testInterimFallbackAndLateFinal() {
|
|
189
|
+
const { session, transcripts } = makeSession({ finalWaitMs: 30 });
|
|
190
|
+
await session._subscribe(101, "caller");
|
|
191
|
+
const sub = session.subscribers.get(101);
|
|
192
|
+
const gen = new OpusAudioCodec();
|
|
193
|
+
for (let i = 0; i < 3; i += 1) session._onFeedRtp(101, { payload: toneOpus(gen, 12000) });
|
|
194
|
+
sub.stt.onPartial("here is my answer");
|
|
195
|
+
for (let i = 0; i < 36; i += 1) session._onFeedRtp(101, { payload: toneOpus(gen, 30) });
|
|
196
|
+
assert.strictEqual(sub.stt.commits, 1);
|
|
197
|
+
assert.strictEqual(transcripts.length, 0, "dispatch waits for the final first");
|
|
198
|
+
await wait(80);
|
|
199
|
+
assert.strictEqual(transcripts.length, 1, "missing final falls back to the interim");
|
|
200
|
+
assert.strictEqual(transcripts[0].text, "here is my answer");
|
|
201
|
+
sub.stt.onTranscript("here is my answer sir");
|
|
202
|
+
assert.strictEqual(transcripts.length, 1, "late final after fallback is dropped, not double-answered");
|
|
203
|
+
gen.close();
|
|
204
|
+
await session.close();
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
async function testTrickleBuffering() {
|
|
208
|
+
const { session } = makeSession({});
|
|
209
|
+
const added = [];
|
|
210
|
+
session.publisher = { pc: { addIceCandidate: async (c) => { added.push(c.candidate); } }, pending: [], remoteSet: false };
|
|
211
|
+
session._onPublisherEvent({ janus: "trickle", candidate: { candidate: "cand-early", sdpMid: "0" } });
|
|
212
|
+
assert.strictEqual(added.length, 0, "candidates before the answer are buffered");
|
|
213
|
+
assert.strictEqual(session.publisher.pending.length, 1);
|
|
214
|
+
session._flushTrickle(session.publisher);
|
|
215
|
+
assert.deepStrictEqual(added, ["cand-early"], "buffered candidates flush once the answer lands");
|
|
216
|
+
session._onPublisherEvent({ janus: "trickle", candidate: { candidate: "cand-late", sdpMid: "0" } });
|
|
217
|
+
assert.deepStrictEqual(added, ["cand-early", "cand-late"], "later candidates apply directly");
|
|
218
|
+
session._onPublisherEvent({ janus: "trickle", candidate: { completed: true } });
|
|
219
|
+
assert.strictEqual(added.length, 2, "end-of-candidates marker is ignored");
|
|
220
|
+
await session.close();
|
|
221
|
+
}
|
|
222
|
+
|
|
136
223
|
async function testTranscriptAttribution() {
|
|
137
224
|
const { session, transcripts } = makeSession({});
|
|
138
225
|
await session._subscribe(101, "alice@kazee");
|
|
@@ -245,8 +332,11 @@ async function testCallerModeRoomLifecycle() {
|
|
|
245
332
|
const spoken = [];
|
|
246
333
|
session.speak = async (text) => { spoken.push(text); return true; };
|
|
247
334
|
await session._subscribe(101, "sumeet@kazee");
|
|
335
|
+
assert.deepStrictEqual(spoken, [], "no greeting while the publisher media leg is still down");
|
|
336
|
+
session._publisherUp = true;
|
|
337
|
+
session._maybeGreet();
|
|
248
338
|
await session._subscribe(102, "second@kazee");
|
|
249
|
-
assert.deepStrictEqual(spoken, ["hi there"], "greeting speaks exactly once,
|
|
339
|
+
assert.deepStrictEqual(spoken, ["hi there"], "greeting speaks exactly once, after media-up + first remote feed");
|
|
250
340
|
await session.close();
|
|
251
341
|
const destroy = messages.find((m) => m?.request === "destroy");
|
|
252
342
|
assert.ok(destroy && destroy.room === 7, "created room is destroyed on close");
|
|
@@ -283,6 +373,7 @@ async function testThinkingCues() {
|
|
|
283
373
|
// plays an audible "heard you" chime, and starts ticking right away — STT
|
|
284
374
|
// finalization takes seconds and must not sound like a dead line.
|
|
285
375
|
for (let i = 0; i < 3; i += 1) session._onFeedRtp(101, { payload: toneOpus(gen, 12000) });
|
|
376
|
+
sub.stt.onPartial("what is the status");
|
|
286
377
|
for (let i = 0; i < 36; i += 1) session._onFeedRtp(101, { payload: toneOpus(gen, 30) });
|
|
287
378
|
assert.strictEqual(sub.stt.commits, 1);
|
|
288
379
|
await wait(60);
|
|
@@ -335,6 +426,11 @@ async function testCloseResolvesSpeech() {
|
|
|
335
426
|
await testSpeakCancellation();
|
|
336
427
|
await testEchoGateAndBargeIn();
|
|
337
428
|
await testPrebufferAndCommit();
|
|
429
|
+
await testHoldForWords();
|
|
430
|
+
await testHoldTimeoutDropsNoiseTurn();
|
|
431
|
+
await testWordStallRescue();
|
|
432
|
+
await testInterimFallbackAndLateFinal();
|
|
433
|
+
await testTrickleBuffering();
|
|
338
434
|
await testTranscriptAttribution();
|
|
339
435
|
await testIdleTimeout();
|
|
340
436
|
await testIdleBargeInKeepsCall();
|
|
@@ -351,7 +351,35 @@ assert.match(pkg.description, /provider-agnostic coding-agent harness/i);
|
|
|
351
351
|
// fix from the chat-central outage, the call-platform batch (outgoing calls,
|
|
352
352
|
// greetings, TURN via /webrtc/config, ring-phase teardown fixes), the
|
|
353
353
|
// received-media .bin extension fix, and the tts/transcribe agent verbs.
|
|
354
|
-
|
|
354
|
+
// 3.2.2 approved by Sumeet 2026-08-09 ("Yeh", answering "Want me to build the
|
|
355
|
+
// chip fix now and fold it in? Then a single /restart live-verifies everything
|
|
356
|
+
// and I cut"): the presence-chip batch — thinking-on-receipt (heartbeat starts
|
|
357
|
+
// the moment a turn-bound message arrives, covering enforcer triage/download/
|
|
358
|
+
// STT dead air), tool stretches re-enter "thinking" instead of decaying the
|
|
359
|
+
// chip, /stop emits an explicit typing:stop, and the dedup adjudicator's
|
|
360
|
+
// "dedup" utility purpose registered so the journal dedup gate actually runs.
|
|
361
|
+
// 3.2.3 approved by Sumeet 2026-08-13 ("While we wait can we update open
|
|
362
|
+
// Claudia so a watcher requires a timeout or defaults to something so that
|
|
363
|
+
// when it expires it acts as a wake up so no watcher can silently fail?"):
|
|
364
|
+
// watch expiry — every new watch carries expiresAt (--timeout flag, default
|
|
365
|
+
// 2h); a lapsed deadline with a still-failing check fires the wake anyway as
|
|
366
|
+
// a marked "Watch expired" prompt and removes the watch, so a watch whose
|
|
367
|
+
// condition never comes true can no longer end silently.
|
|
368
|
+
// 3.3.0 cut 2026-08-14 (verify-contact batch: owner-only verify flow, HMAC
|
|
369
|
+
// speaker attest on owner-gated loopback kinds, closed relay/transcript
|
|
370
|
+
// egress) — but the release commit bumped package.json WITHOUT this pin, so
|
|
371
|
+
// the tag's build job failed on this very assert: npm kept 3.2.3 and nothing
|
|
372
|
+
// was published. Never retag a failed version — re-cut as +1.
|
|
373
|
+
// 3.3.1 approved by Sumeet 2026-08-19 ("The tts timeout for long messages is
|
|
374
|
+
// getting to be a problem Please fix it", with the sales-bot failure
|
|
375
|
+
// screenshot): pocket TTS sentence-packs long text into ≤450-char chunks,
|
|
376
|
+
// synthesizes each inside its own 120s window (one retry per chunk), and
|
|
377
|
+
// ffmpeg-concats the wavs into ONE ogg — message length can no longer abort
|
|
378
|
+
// the single gateway request and strand pods (no ElevenLabs key, no macOS
|
|
379
|
+
// say) voiceless. Carries the unpublished 3.3.0 content plus the Kazee calls
|
|
380
|
+
// batch (trickle ICE + streaming TTS playback, ringing-on-invite/deferred
|
|
381
|
+
// publish, werift ICE rejection suppression). Plus this pin repair.
|
|
382
|
+
assert.strictEqual(pkg.version, "3.3.1", "release version must remain unchanged without explicit approval");
|
|
355
383
|
for (const keyword of ["claude", "codex", "coding-agent", "provider-agnostic"]) {
|
|
356
384
|
assert.ok(pkg.keywords.includes(keyword), `package keyword missing: ${keyword}`);
|
|
357
385
|
}
|
|
@@ -826,6 +826,40 @@ async function watchGateProbe(createSchedulerService) {
|
|
|
826
826
|
assert.strictEqual(repeatJob.status, "pending");
|
|
827
827
|
assert.strictEqual(repeatJob.occurrenceId, "next-occ-1", "repeat watch advances to a new occurrence");
|
|
828
828
|
|
|
829
|
+
// Failing check with a FUTURE expiry → still the normal silent idle skip.
|
|
830
|
+
const pending = schedulerHarness(createSchedulerService, [watchJob({ id: "watch-pending", expiresAt: 1_720_000_100_000 })], {
|
|
831
|
+
runWatchCheck: async () => ({ code: 1, timedOut: false }),
|
|
832
|
+
outcomes: [successOutcome()],
|
|
833
|
+
});
|
|
834
|
+
const pendingResult = await pending.service.fireJob("watch-pending");
|
|
835
|
+
assert.strictEqual(pendingResult.status, "watch-idle");
|
|
836
|
+
assert.strictEqual(pending.runCalls.length, 0, "unexpired failing check stays silent");
|
|
837
|
+
|
|
838
|
+
// Failing check with a LAPSED expiry → fires anyway as an expiry wake-up:
|
|
839
|
+
// distinct header, advisory says timed out, removed even though repeat=true.
|
|
840
|
+
const expired = schedulerHarness(createSchedulerService, [watchJob({ id: "watch-expired", repeat: true, expiresAt: 1_720_000_000_000 })], {
|
|
841
|
+
runWatchCheck: async () => ({ code: 1, timedOut: false }),
|
|
842
|
+
outcomes: [successOutcome()],
|
|
843
|
+
});
|
|
844
|
+
const expiredResult = await expired.service.fireJob("watch-expired");
|
|
845
|
+
assert.strictEqual(expiredResult.ok, true);
|
|
846
|
+
assert.strictEqual(expiredResult.status, "succeeded");
|
|
847
|
+
assert.strictEqual(expired.runCalls.length, 1, "expired watch wakes the agent exactly once");
|
|
848
|
+
assert.match(expired.runCalls[0].prompt, /Watch expired/, "expiry fire carries the Watch expired header");
|
|
849
|
+
assert.match(expired.runCalls[0].prompt, /did NOT happen/, "expiry fire tells the agent the condition never came true");
|
|
850
|
+
assert.ok(expired.sends.some((text) => /timed out without triggering/.test(text)), "expiry advisory says it timed out");
|
|
851
|
+
assert.strictEqual(expired.jobsStore.get("watch-expired"), null, "expired watch is removed even when repeat was requested");
|
|
852
|
+
|
|
853
|
+
// Check that passes ON the expiry tick → a normal trigger, not an expiry.
|
|
854
|
+
const lastGasp = schedulerHarness(createSchedulerService, [watchJob({ id: "watch-lastgasp", expiresAt: 1_720_000_000_000 })], {
|
|
855
|
+
runWatchCheck: async () => ({ code: 0, timedOut: false }),
|
|
856
|
+
outcomes: [successOutcome()],
|
|
857
|
+
});
|
|
858
|
+
await lastGasp.service.fireJob("watch-lastgasp");
|
|
859
|
+
assert.strictEqual(lastGasp.runCalls.length, 1);
|
|
860
|
+
assert.match(lastGasp.runCalls[0].prompt, /Watch triggered/, "a passing check at the deadline is a real trigger");
|
|
861
|
+
assert.ok(!/Watch expired/.test(lastGasp.runCalls[0].prompt), "a passing check never reports as expired");
|
|
862
|
+
|
|
829
863
|
// Global master switch OFF → skip before the check even runs.
|
|
830
864
|
fs.writeFileSync(watchesEnabledFile, "off");
|
|
831
865
|
let checkRan = false;
|
|
@@ -841,6 +875,45 @@ async function watchGateProbe(createSchedulerService) {
|
|
|
841
875
|
fs.rmSync(watchesEnabledFile, { force: true });
|
|
842
876
|
}
|
|
843
877
|
|
|
878
|
+
function watchExpiryStampProbe(createSchedulerService, DEFAULT_WATCH_TIMEOUT_MS) {
|
|
879
|
+
assert.strictEqual(typeof DEFAULT_WATCH_TIMEOUT_MS, "number", "scheduler must export the default watch timeout");
|
|
880
|
+
const state = {
|
|
881
|
+
userId: "fixture-user",
|
|
882
|
+
currentSession: { name: "alpha", dir: "/workspace/alpha" },
|
|
883
|
+
settings: { backend: "claude" },
|
|
884
|
+
activeSessions: { alpha: { claude: "thread-alpha" } },
|
|
885
|
+
providerSettings: { claude: {} },
|
|
886
|
+
};
|
|
887
|
+
const service = createSchedulerService({
|
|
888
|
+
jobsStore: memoryJobsStore([]),
|
|
889
|
+
currentState: () => state,
|
|
890
|
+
now: () => 1_720_000_000_500,
|
|
891
|
+
setTimer: () => ({}),
|
|
892
|
+
clearTimer: () => {},
|
|
893
|
+
scheduleCron: () => ({ stop() {} }),
|
|
894
|
+
});
|
|
895
|
+
const stamped = service.addWatch({
|
|
896
|
+
adapter: "telegram-fixture",
|
|
897
|
+
adapterType: "telegram",
|
|
898
|
+
channelId: "fixture-channel",
|
|
899
|
+
canonicalUserId: "fixture-user",
|
|
900
|
+
check: "true",
|
|
901
|
+
prompt: "watch prompt",
|
|
902
|
+
});
|
|
903
|
+
assert.strictEqual(stamped.expiresAt, 1_720_000_000_500 + DEFAULT_WATCH_TIMEOUT_MS, "watch-add stamps the default expiry when none is given");
|
|
904
|
+
const explicit = service.addWatch({
|
|
905
|
+
adapter: "telegram-fixture",
|
|
906
|
+
adapterType: "telegram",
|
|
907
|
+
channelId: "fixture-channel",
|
|
908
|
+
canonicalUserId: "fixture-user",
|
|
909
|
+
check: "true",
|
|
910
|
+
prompt: "watch prompt 2",
|
|
911
|
+
expiresAt: 1_720_000_999_999,
|
|
912
|
+
});
|
|
913
|
+
assert.strictEqual(explicit.expiresAt, 1_720_000_999_999, "an explicit --timeout expiry passes through unchanged");
|
|
914
|
+
service.stopAll();
|
|
915
|
+
}
|
|
916
|
+
|
|
844
917
|
function staticSchedulingProbe() {
|
|
845
918
|
const files = [
|
|
846
919
|
"core/jobs.js",
|
|
@@ -863,6 +936,12 @@ function staticSchedulingProbe() {
|
|
|
863
936
|
assert.match(source["core/loopback.js"], /watch-add/, "loopback must route watch-add");
|
|
864
937
|
assert.match(source["bin/schedule.js"], /runWatchAdd/, "schedule CLI must expose watch-add");
|
|
865
938
|
assert.match(source["bin/cli.js"], /watch-add/, "cli must dispatch watch-add");
|
|
939
|
+
|
|
940
|
+
// Watch expiry must be wired end to end: default stamp → gate → CLI flag.
|
|
941
|
+
assert.match(source["core/scheduler.js"], /DEFAULT_WATCH_TIMEOUT_MS/, "scheduler must define the default watch timeout");
|
|
942
|
+
assert.match(source["core/scheduler.js"], /watchExpired/, "scheduler must mark expiry fires distinctly");
|
|
943
|
+
assert.match(source["core/loopback.js"], /expiresAt/, "loopback must pass the watch expiry through");
|
|
944
|
+
assert.match(source["bin/schedule.js"], /--timeout/, "watch-add CLI must expose --timeout");
|
|
866
945
|
}
|
|
867
946
|
|
|
868
947
|
async function main() {
|
|
@@ -884,6 +963,7 @@ async function main() {
|
|
|
884
963
|
await unavailableAndFallbackProbe(schedulerApi.createSchedulerService);
|
|
885
964
|
await cronOccurrenceProbe(schedulerApi.createSchedulerService);
|
|
886
965
|
await watchGateProbe(schedulerApi.createSchedulerService);
|
|
966
|
+
watchExpiryStampProbe(schedulerApi.createSchedulerService, schedulerApi.DEFAULT_WATCH_TIMEOUT_MS);
|
|
887
967
|
creationEnrichmentProbe(schedulerApi.createSchedulerService);
|
|
888
968
|
loopbackMetadataProbe(require("./core/runner"), require("./bin/loopback-client"));
|
|
889
969
|
staticSchedulingProbe();
|
|
@@ -185,6 +185,28 @@ async function main() {
|
|
|
185
185
|
"legacy Claude model aliases never leak into Codex",
|
|
186
186
|
);
|
|
187
187
|
|
|
188
|
+
// v3.2.2: "dedup" (journal dedup adjudicator) used to be rejected outright
|
|
189
|
+
// with INVALID_UTILITY_PURPOSE, silently disabling the dedup gate on every
|
|
190
|
+
// memory review. It rides the review pipeline: MEMORY_PROVIDER steers it,
|
|
191
|
+
// PACK_DEDUP_MODEL is its legacy Claude model alias.
|
|
192
|
+
assert.ok(
|
|
193
|
+
createUtilityPolicy({ registry: allReady, env: { UTILITY_PROVIDER: "claude" } })
|
|
194
|
+
.resolve({ purpose: "dedup", tier: "low" }).providerId,
|
|
195
|
+
"dedup is a valid utility purpose",
|
|
196
|
+
);
|
|
197
|
+
assert.strictEqual(
|
|
198
|
+
createUtilityPolicy({ registry: allReady, env: { UTILITY_PROVIDER: "claude", MEMORY_PROVIDER: "codex" } })
|
|
199
|
+
.resolve({ purpose: "dedup", tier: "low" }).providerId,
|
|
200
|
+
"codex",
|
|
201
|
+
"dedup follows the review pipeline's MEMORY_PROVIDER override",
|
|
202
|
+
);
|
|
203
|
+
assert.strictEqual(
|
|
204
|
+
createUtilityPolicy({ registry: allReady, env: { UTILITY_PROVIDER: "claude", PACK_DEDUP_MODEL: "legacy-claude-dedup" } })
|
|
205
|
+
.resolve({ purpose: "dedup", tier: "low" }).model,
|
|
206
|
+
"legacy-claude-dedup",
|
|
207
|
+
"dedup honors its PACK_DEDUP_MODEL legacy alias",
|
|
208
|
+
);
|
|
209
|
+
|
|
188
210
|
const fallbackRegistry = registry(map, {
|
|
189
211
|
claude: { availability: { state: "missing", executable: null, reason: "fixture unavailable" } },
|
|
190
212
|
});
|