@inetafrica/open-claudia 3.1.28 → 3.2.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.
@@ -0,0 +1,171 @@
1
+ "use strict";
2
+
3
+ // C2: memory regression suite — every corrected recall miss becomes a
4
+ // permanent test case, re-graded nightly. Cases live in a JSONL store; the
5
+ // nightly runner grades each query against the DETERMINISTIC recall floor
6
+ // (matchPacks + matchEntities — the classic candidate stage every engine
7
+ // builds on; the discoverer's LLM walk only adds to it and is fail-open back
8
+ // to this). No model call anywhere: the suite must run — and mean the same
9
+ // thing — even on nights the providers are down. Pass-rate + delta land in
10
+ // the dream summary's memory-health section.
11
+
12
+ const fs = require("fs");
13
+ const path = require("path");
14
+ const { atomicWriteFileSync } = require("./fsutil");
15
+ const { resolveConfigDir, ensureConfigDir } = require("./config-dir");
16
+
17
+ const NIGHTLY_CAP = Number(process.env.RECALL_REGRESSION_CAP || 20);
18
+ const HISTORY_MAX = 90;
19
+
20
+ function casesPath() { return path.join(resolveConfigDir(), "recall-regressions.jsonl"); }
21
+ function historyPath() { return path.join(resolveConfigDir(), "recall-health-history.json"); }
22
+
23
+ function loadCases() {
24
+ try {
25
+ return fs.readFileSync(casesPath(), "utf-8")
26
+ .split("\n").filter(Boolean)
27
+ .map((l) => { try { return JSON.parse(l); } catch (e) { return null; } })
28
+ .filter(Boolean);
29
+ } catch (e) {
30
+ return [];
31
+ }
32
+ }
33
+
34
+ function saveCases(cases) {
35
+ ensureConfigDir();
36
+ atomicWriteFileSync(casesPath(), cases.map((c) => JSON.stringify(c)).join("\n") + (cases.length ? "\n" : ""));
37
+ }
38
+
39
+ function addCase({ query, expectSlugs, source = "manual", note = "" } = {}) {
40
+ const q = String(query || "").trim();
41
+ if (!q) throw new Error("query is required");
42
+ const expect = [].concat(expectSlugs || []).map((s) => String(s || "").trim()).filter(Boolean);
43
+ if (expect.length === 0) throw new Error("at least one expected slug is required");
44
+ if (source !== "manual" && source !== "live-miss") throw new Error(`source must be manual or live-miss (got ${source})`);
45
+ const entry = {
46
+ id: `rc_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`,
47
+ query: q,
48
+ expectSlugs: expect,
49
+ source,
50
+ note: String(note || ""),
51
+ addedAt: new Date().toISOString(),
52
+ lastRunAt: null,
53
+ lastResult: null,
54
+ };
55
+ const cases = loadCases();
56
+ cases.push(entry);
57
+ saveCases(cases);
58
+ return entry;
59
+ }
60
+
61
+ function listCases() {
62
+ return loadCases();
63
+ }
64
+
65
+ function removeCase(id) {
66
+ const cases = loadCases();
67
+ const next = cases.filter((c) => c.id !== id);
68
+ if (next.length === cases.length) return false;
69
+ saveCases(next);
70
+ return true;
71
+ }
72
+
73
+ // Grade one case against the deterministic floor. Expected slugs that no
74
+ // longer exist anywhere (pack merged away, entity removed) are not recall
75
+ // failures — a case with NO live target is "orphaned"; live targets are
76
+ // graded normally.
77
+ function gradeCase(c, { packLimit, entityLimit } = {}) {
78
+ const packs = require("./packs");
79
+ const entities = require("./entities");
80
+ const pl = packLimit || Number(process.env.PACK_MATCH_LIMIT || 6);
81
+ const el = entityLimit || 4;
82
+ const exists = (slug) => {
83
+ try { if (packs.readPack(slug)) return true; } catch (e) {}
84
+ try { if (entities.readEntity(slug)) return true; } catch (e) {}
85
+ return false;
86
+ };
87
+ const live = c.expectSlugs.filter(exists);
88
+ if (live.length === 0) return { status: "orphaned", missing: c.expectSlugs.slice() };
89
+ const found = new Set();
90
+ try { for (const m of packs.matchPacks(c.query, { limit: pl })) found.add(m.dir); } catch (e) {}
91
+ try { for (const m of entities.matchEntities(c.query, { limit: el })) found.add(m.slug); } catch (e) {}
92
+ const missing = live.filter((s) => !found.has(s));
93
+ return missing.length === 0
94
+ ? { status: "pass" }
95
+ : { status: "fail", missing, matched: [...found] };
96
+ }
97
+
98
+ // Nightly-capped, oldest-run-first rotation: a store bigger than the cap
99
+ // still gets full coverage over successive nights. Orphaned cases are
100
+ // dropped from the store (same policy as prospective-reminder GC).
101
+ function runSuite({ cap = NIGHTLY_CAP, packLimit, entityLimit, now = Date.now() } = {}) {
102
+ const all = loadCases();
103
+ if (all.length === 0) return { ran: 0, passed: 0, failed: 0, orphaned: 0, failures: [], total: 0 };
104
+ const order = [...all].sort((x, y) => String(x.lastRunAt || "").localeCompare(String(y.lastRunAt || "")));
105
+ const toRun = order.slice(0, Math.max(1, cap));
106
+ const stamp = new Date(now).toISOString();
107
+ const failures = [];
108
+ const orphanIds = new Set();
109
+ let passed = 0, failed = 0;
110
+ for (const c of toRun) {
111
+ const g = gradeCase(c, { packLimit, entityLimit });
112
+ c.lastRunAt = stamp;
113
+ c.lastResult = g.status;
114
+ if (g.status === "pass") passed++;
115
+ else if (g.status === "fail") { failed++; failures.push({ id: c.id, query: c.query, missing: g.missing }); }
116
+ else orphanIds.add(c.id);
117
+ }
118
+ const kept = all.filter((c) => !orphanIds.has(c.id));
119
+ saveCases(kept);
120
+ return { ran: toRun.length - orphanIds.size, passed, failed, orphaned: orphanIds.size, failures, total: kept.length };
121
+ }
122
+
123
+ function loadHistory() {
124
+ try {
125
+ const h = JSON.parse(fs.readFileSync(historyPath(), "utf-8"));
126
+ return Array.isArray(h) ? h : [];
127
+ } catch (e) {
128
+ return [];
129
+ }
130
+ }
131
+
132
+ // Dream entry point: grade, record the trend, and return announce lines for
133
+ // the consolidation report. Empty store → zero cost, zero lines.
134
+ function nightlyRun(opts = {}) {
135
+ const res = runSuite(opts);
136
+ if (res.ran === 0 && res.orphaned === 0) return { ...res, passRate: null, lines: [] };
137
+ const history = loadHistory();
138
+ const prev = history.length ? history[history.length - 1] : null;
139
+ const rate = res.ran > 0 ? Math.round((res.passed / res.ran) * 100) : null;
140
+ if (res.ran > 0) {
141
+ history.push({ at: new Date().toISOString(), ran: res.ran, passed: res.passed, failed: res.failed, orphaned: res.orphaned, passRate: rate });
142
+ ensureConfigDir();
143
+ atomicWriteFileSync(historyPath(), JSON.stringify(history.slice(-HISTORY_MAX), null, 2) + "\n");
144
+ }
145
+ const lines = [];
146
+ if (res.ran > 0) {
147
+ const delta = prev && typeof prev.passRate === "number" && rate !== null ? rate - prev.passRate : null;
148
+ const deltaTxt = delta === null ? "" : ` (${delta >= 0 ? "+" : ""}${delta} vs last run)`;
149
+ let line = `🧪 Memory health: ${res.passed}/${res.ran} recall regression case${res.ran === 1 ? "" : "s"} passing — ${rate}%${deltaTxt}.`;
150
+ if (res.failed > 0) {
151
+ const detail = res.failures.slice(0, 3).map((f) => `"${f.query.slice(0, 40)}" (missing ${f.missing.join(", ")})`).join("; ");
152
+ line += ` Failing: ${detail}${res.failures.length > 3 ? ` +${res.failures.length - 3} more` : ""} — open-claudia recall-case list`;
153
+ }
154
+ lines.push(line);
155
+ }
156
+ if (res.orphaned > 0) lines.push(`🧪 Dropped ${res.orphaned} recall regression case${res.orphaned === 1 ? "" : "s"} whose target no longer exists.`);
157
+ return { ...res, passRate: rate, lines };
158
+ }
159
+
160
+ module.exports = {
161
+ NIGHTLY_CAP,
162
+ addCase,
163
+ listCases,
164
+ removeCase,
165
+ gradeCase,
166
+ runSuite,
167
+ nightlyRun,
168
+ loadHistory,
169
+ casesPath,
170
+ historyPath,
171
+ };
@@ -0,0 +1,71 @@
1
+ "use strict";
2
+
3
+ // socket.io-client permanently stops reconnecting when the server REJECTS a
4
+ // handshake (auth middleware next(new Error())): connect_error fires with
5
+ // socket.active === false and the manager never dials again. During the
6
+ // 2026-08-08 outage chat-central booted before Central's MongoDB and rejected
7
+ // every handshake for a few minutes, terminally stranding the whole bot fleet.
8
+ // This wires a manual connect() retry with capped backoff so a rejected
9
+ // handshake is never terminal. A deliberate client-side disconnect() cancels
10
+ // any pending retry and stays final until connect() is called again.
11
+
12
+ const DEFAULT_INITIAL_DELAY_MS = 2000;
13
+ const DEFAULT_MAX_DELAY_MS = 30000;
14
+
15
+ function wireTerminalReconnect(socket, opts = {}) {
16
+ const label = opts.label || "socket";
17
+ const log = opts.log || ((msg) => console.error(msg));
18
+ const initialDelay = opts.initialDelayMs || DEFAULT_INITIAL_DELAY_MS;
19
+ const maxDelay = opts.maxDelayMs || DEFAULT_MAX_DELAY_MS;
20
+
21
+ let attempts = 0;
22
+ let timer = null;
23
+ let stopped = false;
24
+
25
+ const cancel = () => {
26
+ if (timer) { clearTimeout(timer); timer = null; }
27
+ };
28
+
29
+ const schedule = (why) => {
30
+ if (stopped || timer) return;
31
+ const delay = Math.min(initialDelay * 2 ** attempts, maxDelay);
32
+ attempts += 1;
33
+ log(`${label}: ${why} — socket.io will not retry on its own; manual connect() in ${Math.ceil(delay / 1000)}s (attempt ${attempts})`);
34
+ timer = setTimeout(() => {
35
+ timer = null;
36
+ if (stopped || socket.connected || socket.active) return;
37
+ try { socket.connect(); }
38
+ catch (e) { schedule(`reconnect attempt threw (${e.message})`); }
39
+ }, delay);
40
+ if (typeof timer.unref === "function") timer.unref();
41
+ };
42
+
43
+ socket.on("connect", () => { attempts = 0; cancel(); });
44
+ socket.on("connect_error", (err) => {
45
+ if (socket.active) return; // transport failure: the manager auto-retries
46
+ schedule(`handshake rejected (${err && err.message ? err.message : err})`);
47
+ });
48
+ socket.on("disconnect", (reason) => {
49
+ if (reason === "io client disconnect") { cancel(); return; }
50
+ if (socket.active) return; // the manager auto-reconnects
51
+ schedule(`disconnected (${reason})`);
52
+ });
53
+
54
+ // disconnect() while wedged emits no event, so intercept the call itself;
55
+ // connect() re-arms retries for the adapter-restart path.
56
+ const origDisconnect = socket.disconnect;
57
+ socket.disconnect = function (...args) {
58
+ stopped = true;
59
+ cancel();
60
+ return origDisconnect.apply(this, args);
61
+ };
62
+ const origConnect = socket.connect;
63
+ socket.connect = function (...args) {
64
+ stopped = false;
65
+ return origConnect.apply(this, args);
66
+ };
67
+
68
+ return socket;
69
+ }
70
+
71
+ module.exports = { wireTerminalReconnect, DEFAULT_INITIAL_DELAY_MS, DEFAULT_MAX_DELAY_MS };
@@ -4,10 +4,10 @@
4
4
  const fs = require("fs");
5
5
  const path = require("path");
6
6
  const { SOUL_FILE, CRONS_FILE, VAULT_FILE, FILES_DIR, BOT_DIR, config } = require("./config");
7
- const { canSpeak } = require("./media");
7
+ const { canSpeak, canTranscribe } = require("./media");
8
8
  const CONFIG_DIR = require("../config-dir");
9
9
  const { currentState, getProjectKey, getProviderSession } = require("./state");
10
- const { currentAdapter, currentChannelId, currentUserId } = require("./context");
10
+ const { currentAdapter, currentChannelId, currentUserId, currentSpeakerName } = require("./context");
11
11
  const { vault } = require("./vault-store");
12
12
  const {
13
13
  transcriptPointerNoteForRun,
@@ -202,6 +202,7 @@ function buildCoreInstructionsUncached() {
202
202
  // Gate on actual TTS capability, not local Whisper: hosted pods hear and
203
203
  // speak through the speech gateway with no whisper binary installed.
204
204
  const hasVoice = canSpeak();
205
+ const hasEars = canTranscribe();
205
206
  return `
206
207
  ${soul}
207
208
  ${buildPersonaBlock()}
@@ -235,6 +236,13 @@ Reply normally in your final answer. To send a file, image, or voice clip back t
235
236
  - \`open-claudia send-voice <path>\` — ogg/opus voice note
236
237
  - \`open-claudia say "<text>"\` — synthesize the text and send it as a voice note (use this when asked for a voice reply)
237
238
  Never print or embed bot tokens in prompts, commands, logs, or messages.
239
+ ${hasVoice || hasEars ? `
240
+ ## Speech services (TTS / STT)
241
+ Text-to-speech and speech-to-text are general capabilities, not just the voice-note reply path. Synthesis and transcription run inside the credentialed bot process — your own shell holds NO speech token or vendor key, so never call the speech gateway or TTS/STT vendor APIs directly and never conclude "the key is missing"; use these commands (they work from inside any bot-spawned task):
242
+ ${hasVoice ? `- \`open-claudia tts "<text>" --out <path>.mp3\` — synthesize narration to a FILE without sending it to chat: video voice-overs, explainer narration, IVR prompts, any audio artifact. The extension picks the container (.ogg native, .mp3/.wav transcoded); prints the final path. \`--voice <id>\` selects a specific gateway voice (e.g. a cloned voice).
243
+ - \`open-claudia say "<text>"\` — synthesize AND send as a voice note to the current chat (listed under Delivery above).` : ""}
244
+ ${hasEars ? `- \`open-claudia transcribe <file>\` — speech-to-text: prints the transcript of an audio OR video file (voice notes, meeting recordings, mp3/m4a/wav/ogg, mp4/mkv — the audio track is extracted automatically). Use it whenever the user sends a recording and wants its contents.` : ""}
245
+ For long narration, synthesize per-paragraph with \`tts\` and assemble with ffmpeg.` : ""}
238
246
 
239
247
  ## Background work
240
248
  You can persist work across turns and wake yourself up later. These survive bot restarts.
@@ -369,6 +377,20 @@ function buildSpeakerContextBlock() {
369
377
  const speaker = adapter && channelId
370
378
  ? people.findByHandle(adapter.type, String(currentUserId() || channelId))
371
379
  : null;
380
+ // Without a people record the speaker used to render as "no named profile"
381
+ // even when identity was known: an env-configured owner (KAZEE_OWNER_USER_ID,
382
+ // never auto-contacted) or a not-yet-recorded group member. In group chats
383
+ // the model then rightly refused to attribute their messages. Fall back to
384
+ // the canonical owner resolution the guard stack uses, then to the
385
+ // transport-reported display name. Names never confer privileges (R1):
386
+ // owner standing comes only from the canonical check, and a bare transport
387
+ // name is labelled unverified.
388
+ let canonicalSpeaker = null;
389
+ if (!speaker && adapter && channelId) {
390
+ try { canonicalSpeaker = require("./relationship").speakerFor(adapter.type, String(channelId), currentUserId()); } catch (e) {}
391
+ }
392
+ let transportName = "";
393
+ try { transportName = String(currentSpeakerName() || "").trim(); } catch (e) {}
372
394
  if (speaker) {
373
395
  const recentNotes = (speaker.notes || [])
374
396
  .slice(-3)
@@ -381,6 +403,23 @@ function buildSpeakerContextBlock() {
381
403
  speaker.bio ? `Bio: ${speaker.bio}` : "",
382
404
  recentNotes ? `Recent notes:\n${recentNotes}` : "",
383
405
  );
406
+ } else if (canonicalSpeaker && canonicalSpeaker.isOwner) {
407
+ // Prefer the live transport name: canonicalSpeaker.name falls back to
408
+ // owners()[0], which can be a different owner on multi-owner pods.
409
+ const name = transportName || canonicalSpeaker.name;
410
+ blocks.push(
411
+ "",
412
+ "## Speaker",
413
+ name
414
+ ? `You are talking to ${name} (the owner) on ${adapter.type}.`
415
+ : `You are talking to the owner on ${adapter.type}.`,
416
+ );
417
+ } else if (transportName) {
418
+ blocks.push(
419
+ "",
420
+ "## Speaker",
421
+ `The current message is from "${transportName}" on ${adapter.type} — transport-reported name, no saved profile; identity unverified.`,
422
+ );
384
423
  } else {
385
424
  blocks.push("", "## Speaker", "No named speaker profile is available for this turn.");
386
425
  }
@@ -651,6 +690,22 @@ function formatPackForContext(pack, packsLib, opts = {}) {
651
690
  return clip(parts.join("\n\n"), PACK_INJECT_MAX_CHARS);
652
691
  }
653
692
 
693
+ // C1 prospective memory: topic-bound reminders ride the recall injection of
694
+ // their target pack/entity. Fetched at render time but CONSUMED only after
695
+ // the budget accepts the committed block — a dropped render must never eat a
696
+ // one-shot reminder. All calls are defensive: reminder machinery failing can
697
+ // never break recall injection itself.
698
+ function pendingRemindersFor(type, slug) {
699
+ try { return require("./prospective").pendingFor(type, slug); } catch (e) { return []; }
700
+ }
701
+ function renderReminders(entries) {
702
+ try { return require("./prospective").renderPendingLines(entries); } catch (e) { return ""; }
703
+ }
704
+ function fireReminders(entries) {
705
+ if (!entries || entries.length === 0) return;
706
+ try { require("./prospective").fire(entries, "recall"); } catch (e) {}
707
+ }
708
+
654
709
  function buildPackBlock(matches, budget, opts = {}) {
655
710
  try {
656
711
  const packsLib = require("./packs");
@@ -671,17 +726,29 @@ function buildPackBlock(matches, budget, opts = {}) {
671
726
  used.push(m.dir);
672
727
  const key = `${adapter?.id || "?"}:${channelId || "?"}:${m.dir}`;
673
728
  const stamp = `${runIdentity}:${pack.updated}`;
729
+ const pending = pendingRemindersFor("pack", m.dir);
730
+ const pendingText = pending.length ? renderReminders(pending) : "";
674
731
  // Inject a pack's body once per (channel, session, version). A
675
732
  // compaction mints a new session id, which changes the stamp and
676
733
  // forces a fresh re-injection on the next turn (same mechanism
677
734
  // as the task tree), so the pack survives compaction without paying
678
735
  // to re-stamp an anchor on every intervening turn.
679
- if (packsInjectedFor.get(key) === stamp) continue;
680
- const block = formatPackForContext(pack, packsLib, { progressive, headline, extractive, matchedLines: extractive ? (matchedByDir.get(m.dir) || []) : undefined });
736
+ if (packsInjectedFor.get(key) === stamp) {
737
+ // C1: a reminder doesn't bump pack.updated, so the dedupe stamp
738
+ // would silently swallow it — surface a tiny standalone block.
739
+ if (pendingText) {
740
+ const note = `### Pack: ${pack.name || m.dir} (${m.dir})\n${pendingText}`;
741
+ if (tryUseRecallBudget(budget, note)) { blocks.push(note); fireReminders(pending); }
742
+ }
743
+ continue;
744
+ }
745
+ let block = formatPackForContext(pack, packsLib, { progressive, headline, extractive, matchedLines: extractive ? (matchedByDir.get(m.dir) || []) : undefined });
746
+ if (pendingText) block += `\n\n${pendingText}`;
681
747
  if (!tryUseRecallBudget(budget, block)) continue;
682
748
  packsInjectedFor.set(key, stamp);
683
749
  collector.packs.push(pack.name || m.dir);
684
750
  blocks.push(block);
751
+ if (pendingText) fireReminders(pending);
685
752
  }
686
753
  if (used.length > 0) setImmediate(() => { try { packsLib.touchUsed(used); } catch (e) {} });
687
754
  if (blocks.length === 0) return "";
@@ -738,12 +805,23 @@ function buildEntityBlock(matches, budget, opts = {}) {
738
805
  seen.push(m.slug);
739
806
  const key = `${adapter?.id || "?"}:${channelId || "?"}:${m.slug}`;
740
807
  const stamp = `${runIdentity}:${ent.updated}`;
741
- if (entitiesInjectedFor.get(key) === stamp) continue;
742
- const block = formatEntityForContext(ent, { headline: recallHeadline(), slug: m.slug });
808
+ const pending = pendingRemindersFor("entity", m.slug);
809
+ const pendingText = pending.length ? renderReminders(pending) : "";
810
+ if (entitiesInjectedFor.get(key) === stamp) {
811
+ // C1: same as packs — a reminder must survive the dedupe stamp.
812
+ if (pendingText) {
813
+ const note = `### ${ent.name} (${m.slug})\n${pendingText}`;
814
+ if (tryUseRecallBudget(budget, note)) { blocks.push(note); fireReminders(pending); }
815
+ }
816
+ continue;
817
+ }
818
+ let block = formatEntityForContext(ent, { headline: recallHeadline(), slug: m.slug });
819
+ if (pendingText) block += `\n\n${pendingText}`;
743
820
  if (!tryUseRecallBudget(budget, block)) continue;
744
821
  entitiesInjectedFor.set(key, stamp);
745
822
  collector.entities.push(ent.name || m.slug);
746
823
  blocks.push(block);
824
+ if (pendingText) fireReminders(pending);
747
825
  }
748
826
  if (seen.length > 0) setImmediate(() => { try { entitiesLib.touchSeen(seen); } catch (e) {} });
749
827
  if (blocks.length === 0) return "";
package/core/tools.js CHANGED
@@ -168,6 +168,82 @@ function fingerprint(name) {
168
168
  return { status: "changed", current, verified: s.verifiedHash, verifiedAt: s.verifiedAt || "" };
169
169
  }
170
170
 
171
+ // ── Skill certification (C3) ─────────────────────────────────────────────────
172
+ // Per-verb trust tier derived purely from the run telemetry above. Earned, not
173
+ // granted: "certified" needs ≥CERTIFY_CLEAN_RUNS clean runs with no failure in
174
+ // the probation window; any recent failure demotes to "probation"; everything
175
+ // else is "learning". Derived data + report-only proposals ONLY — certification
176
+ // never loosens a gate or grants anything by itself; the owner acts through the
177
+ // existing approval-card/guardrails flows or ignores it.
178
+ const CERTIFY_CLEAN_RUNS = 5;
179
+ const PROBATION_WINDOW_MS = 7 * 86400000;
180
+
181
+ // Mirrors recordRunResult's verb extraction so history lines re-attribute to
182
+ // the same verb key they were counted under.
183
+ function historyVerb(argLine) {
184
+ const first = String(argLine || "").split(/\s+/).find((a) => a && !a.startsWith("-")) || "(default)";
185
+ return first.slice(0, 40);
186
+ }
187
+
188
+ function cleanRunsSince(verb, history, sinceT) {
189
+ let n = 0;
190
+ for (const h of Array.isArray(history) ? history : []) {
191
+ const t = Date.parse(h.at || "");
192
+ if (!Number.isFinite(t) || t <= sinceT) continue;
193
+ if (h.exit !== 0) continue;
194
+ if (historyVerb(h.args) === verb) n += 1;
195
+ }
196
+ return n;
197
+ }
198
+
199
+ function verbCertification(verb, v = {}, history = [], now = Date.now()) {
200
+ const runs = Number(v.runs) || 0;
201
+ const lastFailureT = Date.parse(v.lastFailure || "");
202
+ const failed = Number.isFinite(lastFailureT);
203
+ if (failed && now - lastFailureT < PROBATION_WINDOW_MS) {
204
+ return { tier: "probation", cleanRuns: cleanRunsSince(verb, history, lastFailureT), runs };
205
+ }
206
+ // No failure ever → every recorded run was clean. After an old failure only
207
+ // successes SINCE it count toward re-certification (history-capped, so this
208
+ // is an honest lower bound).
209
+ const cleanRuns = failed ? cleanRunsSince(verb, history, lastFailureT) : runs;
210
+ return { tier: cleanRuns >= CERTIFY_CLEAN_RUNS ? "certified" : "learning", cleanRuns, runs };
211
+ }
212
+
213
+ function certificationForState(s = {}, now = Date.now()) {
214
+ const out = {};
215
+ for (const [verb, v] of Object.entries(s.verbs || {})) {
216
+ out[verb] = verbCertification(verb, v, s.history, now);
217
+ }
218
+ return out;
219
+ }
220
+
221
+ function certification(name, now = Date.now()) {
222
+ return certificationForState(readState(name), now);
223
+ }
224
+
225
+ // Dream phase (report-only, max 2/night): certified verbs on gated tiers are
226
+ // candidates for a standing Always-allow grant. Read-only verbs run freely and
227
+ // need no grant; probation/learning verbs haven't earned a proposal. The line
228
+ // is the ENTIRE output — no grant is created here, ever.
229
+ function certificationProposals({ now = Date.now(), max = 2 } = {}) {
230
+ const candidates = [];
231
+ for (const t of listTools()) {
232
+ if (!t.verbs || !t.manifest || !t.manifest.length) continue;
233
+ const certs = certificationForState({ verbs: t.verbs, history: t.history }, now);
234
+ for (const m of t.manifest) {
235
+ const tier = m.tier || t.risk || "write";
236
+ if (tier === "read-only") continue;
237
+ const c = certs[m.name];
238
+ if (!c || c.tier !== "certified") continue;
239
+ candidates.push({ tool: t.name, verb: m.name, tier, cleanRuns: c.cleanRuns });
240
+ }
241
+ }
242
+ candidates.sort((a, b) => b.cleanRuns - a.cleanRuns);
243
+ return candidates.slice(0, max).map((c) =>
244
+ `🎓 ${c.tool} ${c.verb} is certified (${c.cleanRuns} clean ${c.tier} runs) — candidate for a standing Always-allow grant if you want it; grant via the approval card / guardrails, or ignore.`);
245
+ }
246
+
171
247
  // Bump a tool's run counter without outcome detail. Still called at end of turn
172
248
  // from the runner's transcript parse; for directory tools that's a no-op (the
173
249
  // CLI already stamped the authoritative result — double-counting would lie),
@@ -1018,6 +1094,7 @@ module.exports = {
1018
1094
  runEnv, missingRequires, toolNameFromPath, sanitizeName, ensureDir, normalizeRisk,
1019
1095
  toolPaths, readState, writeState, recordRunResult, runGate, approvedTierFromArgv,
1020
1096
  resolveTier, invokedVerb, sourceHash, fingerprint,
1097
+ CERTIFY_CLEAN_RUNS, PROBATION_WINDOW_MS, verbCertification, certificationForState, certification, certificationProposals,
1021
1098
  listLib, readLib, writeLib, libDependents,
1022
1099
  matchTools, recordRun, recordCreate, readUsage,
1023
1100
  lintToolSource, checkSyntax, envRefsIn, assertNoSecrets,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@inetafrica/open-claudia",
3
- "version": "3.1.28",
3
+ "version": "3.2.1",
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": {
@@ -10,8 +10,8 @@
10
10
  "test:kazee-call": "node test-kazee-call-audio.js && node test-kazee-call-janus.js && node test-kazee-call-webrtc.js && node test-kazee-call-speech.js && node test-kazee-call-manager.js && node test-kazee-call-session.js",
11
11
  "setup": "node setup.js",
12
12
  "start": "node bot.js",
13
- "pretest": "node test-provider-fixture.js && node test-provider-core-prompt.js && node test-turn-observer.js && node test-provider-prompt-parity.js && node test-provider-env-isolation.js && node test-provider-resume-parity.js && node test-provider-bootstrap.js && node test-provider-registry.js && node test-provider-stream-decoder.js && node test-provider-events.js && node test-provider-claude.js && node test-provider-codex.js && node test-provider-runner.js && node test-provider-refusal-recovery.js && node test-provider-run-lifecycle.js && node test-provider-gateway.js && node test-provider-side-chat.js && node test-single-runtime.js && node test-provider-migration-backup.js && node test-provider-state-migration.js && node test-provider-session-history.js && node test-provider-session-commands.js && node test-provider-compaction.js && node test-provider-scheduler.js && node test-credential-capture.js && node test-cursor-removal.js && node test-utility-provider-policy.js && node test-provider-subagent.js && node test-provider-recall.js && node test-provider-pack-review.js && node test-memory-mutation-queue.js && node test-provider-dream.js && node test-provider-enforcer.js && node test-provider-tool-hooks.js && node test-provider-boot-matrix.js && node test-provider-capabilities.js && node test-provider-language.js && node test-provider-coupling-audit.js && node test-provider-parity-e2e.js && node test-kazee-channel-health.js && node test-kazee-media-caption.js && node test-prompt-budget.js && node test-agency-ledger.js && node test-agency-deliberate.js && node test-guardrail.js && node test-spaces-comment-content.js && node test-boot-health.js && node test-lexical-floor.js && node test-safe-mode.js && node test-logout.js && node test-kazee-call-audio.js && node test-kazee-call-janus.js && node test-kazee-call-webrtc.js && node test-kazee-call-speech.js && node test-kazee-call-manager.js && node test-kazee-call-session.js",
14
- "test": "OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node -e \"require('./vault'); console.log('OK')\" && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-runner-watchdog-static.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-usage-accounting.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-recall-engine.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-recall-graph.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-recall-discoverer.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-recall-query.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-walker-score.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-score-gate.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-cokept-warming.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-project-transcripts-smoke.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-abilities.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-ability-extraction.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-ability-couse.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-ability-transfer.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-ability-tiers.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-ability-merge-guard.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-learning-e2e.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-pack-nesting.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-pack-search.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-extractive-injection.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-journal-dedupe.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-read-signal.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-tools.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-tool-graph.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-tool-guard.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-tooling-mode.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-tool-manifest.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-recall-evolution.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-approval-async.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-unified-identity.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-queue-routing.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-persona-packs.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-speaker-persona.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-persona-pipeline.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-recall-relationship-gate.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-relationship.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-enforcer.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-mandate-hygiene.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-external-rule-scopes.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-approval-cards.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-identity-prune.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-kazee-message-id.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-kazee-group-gate.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-kazee-group-context.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-people-owner-seed.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-delivery-contract.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-run-lock.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-web-sessions.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-telegram-poll-recovery.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-telegram-409-grace.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-telegram-conflict-heal-guard.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-single-instance.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-streaming-split.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-spaces-autoenable.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-say-loopback.js"
13
+ "pretest": "node test-provider-fixture.js && node test-provider-core-prompt.js && node test-turn-observer.js && node test-provider-prompt-parity.js && node test-provider-env-isolation.js && node test-provider-resume-parity.js && node test-provider-bootstrap.js && node test-provider-registry.js && node test-provider-stream-decoder.js && node test-provider-events.js && node test-provider-claude.js && node test-provider-codex.js && node test-provider-runner.js && node test-provider-refusal-recovery.js && node test-provider-run-lifecycle.js && node test-provider-gateway.js && node test-provider-side-chat.js && node test-single-runtime.js && node test-provider-migration-backup.js && node test-provider-state-migration.js && node test-provider-session-history.js && node test-provider-session-commands.js && node test-provider-compaction.js && node test-provider-scheduler.js && node test-credential-capture.js && node test-cursor-removal.js && node test-utility-provider-policy.js && node test-provider-subagent.js && node test-provider-recall.js && node test-provider-pack-review.js && node test-memory-mutation-queue.js && node test-provider-dream.js && node test-provider-enforcer.js && node test-provider-tool-hooks.js && node test-provider-boot-matrix.js && node test-provider-capabilities.js && node test-provider-language.js && node test-provider-coupling-audit.js && node test-provider-parity-e2e.js && node test-kazee-channel-health.js && node test-kazee-media-caption.js && node test-socket-reconnect.js && node test-speech-loopback.js && node test-prompt-budget.js && node test-agency-ledger.js && node test-agency-deliberate.js && node test-guardrail.js && node test-spaces-comment-content.js && node test-boot-health.js && node test-lexical-floor.js && node test-safe-mode.js && node test-logout.js && node test-kazee-call-audio.js && node test-kazee-call-janus.js && node test-kazee-call-webrtc.js && node test-kazee-call-speech.js && node test-kazee-call-manager.js && node test-kazee-call-session.js",
14
+ "test": "OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node -e \"require('./vault'); console.log('OK')\" && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-runner-watchdog-static.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-usage-accounting.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-recall-engine.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-recall-graph.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-recall-discoverer.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-recall-query.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-walker-score.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-score-gate.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-cokept-warming.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-project-transcripts-smoke.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-abilities.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-ability-extraction.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-ability-couse.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-ability-transfer.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-ability-tiers.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-ability-merge-guard.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-learning-e2e.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-pack-nesting.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-pack-search.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-extractive-injection.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-journal-dedupe.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-read-signal.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-tools.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-tool-graph.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-tool-guard.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-tooling-mode.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-tool-manifest.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-recall-evolution.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-approval-async.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-unified-identity.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-queue-routing.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-persona-packs.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-speaker-persona.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-persona-pipeline.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-recall-relationship-gate.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-relationship.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-enforcer.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-mandate-hygiene.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-external-rule-scopes.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-approval-cards.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-identity-prune.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-kazee-message-id.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-kazee-group-gate.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-kazee-group-context.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-people-owner-seed.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-delivery-contract.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-run-lock.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-web-sessions.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-telegram-poll-recovery.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-telegram-409-grace.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-telegram-conflict-heal-guard.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-single-instance.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-streaming-split.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-spaces-autoenable.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-say-loopback.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-journal-union.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-subst-score.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-section-budgets.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-dedup-adjudicator.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-prospective.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-recall-regressions.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-cost-report.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-speaker-context.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-approval-mining.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-tool-certification.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-apprenticeship.js"
15
15
  },
16
16
  "files": [
17
17
  "bot.js",
@@ -487,8 +487,12 @@ async function main() {
487
487
  throw new Error("FAKE_AGENT_KIND must be claude or codex");
488
488
  }
489
489
  if (handleCompatibilityCommand()) return;
490
- captureInvocation();
490
+ // Arm signal handlers BEFORE writing the boot capture: the capture is the
491
+ // parent's green light to send SIGTERM, and on a loaded machine the child
492
+ // can be preempted between these calls — a TERM then hits node's default
493
+ // disposition and kills the process before the signal capture ever writes.
491
494
  installSignalControls();
495
+ captureInvocation();
492
496
  spawnDescendantFixture();
493
497
  const stdin = await readStdin();
494
498
  captureStdin(stdin);
@@ -11,7 +11,7 @@ class FakeSession {
11
11
  }
12
12
  function fixture() {
13
13
  const events = [], inbound = [];
14
- const socket = { on() {}, emit(name, payload) { events.push({ name, payload }); } };
14
+ const socket = { connected: true, on() {}, emit(name, payload) { events.push({ name, payload }); } };
15
15
  const adapter = { ownerUserId: "owner-1", botUserId: "bot-1", _socket: socket, _emit(name, payload) { inbound.push({ name, payload }); } };
16
16
  return { events, inbound, manager: new KazeeCallManager({ adapter, SessionImpl: FakeSession }) };
17
17
  }
@@ -36,5 +36,48 @@ function fixture() {
36
36
  assert.strictEqual(f.manager.session.spoken, "Your agenda is clear.");
37
37
  await f.manager.ended({ roomId: 12 });
38
38
  assert.strictEqual(f.manager.session, null);
39
+
40
+ // Outbound: session starts in caller mode, invite goes out, answer clears the ring timer.
41
+ f = fixture();
42
+ const res = await f.manager.outgoing({ chatId: "chat-2" });
43
+ assert.strictEqual(res.toUserId, "owner-1");
44
+ const outSession = f.manager.session;
45
+ assert.strictEqual(outSession.options.asCaller, true);
46
+ assert.ok(outSession.options.greeting);
47
+ assert.strictEqual(outSession.started, true);
48
+ const invite = f.events.find((e) => e.name === "call:invite");
49
+ assert.deepStrictEqual(invite.payload, { toUserId: "owner-1", callType: "audio", chatId: "chat-2", roomId: res.roomId });
50
+ assert.ok(f.manager._ringTimer);
51
+ // A callee device auto-ending its ring must NOT tear down the outbound call —
52
+ // chat-central revives the CallLog if the human answers late.
53
+ await f.manager.ended({ chatId: "chat-2" });
54
+ assert.ok(f.manager.session, "ended-while-ringing is ignored");
55
+ f.manager.accepted({ chatId: "chat-2" });
56
+ assert.strictEqual(f.manager._ringTimer, null);
57
+ // Outbound transcripts dispatch with the remote human as the "from" side.
58
+ outSession.options.onTranscript("Hello there");
59
+ assert.strictEqual(f.inbound[0].payload.channelId, "chat-2");
60
+ assert.strictEqual(f.inbound[0].payload.userId, "owner-1");
61
+ assert.strictEqual(f.inbound[0].payload.raw.ephemeralAudio, true);
62
+ await assert.rejects(() => f.manager.outgoing({ chatId: "chat-2" }), /busy/);
63
+ await f.manager.ended({ roomId: res.roomId });
64
+ assert.strictEqual(f.manager.session, null);
65
+
66
+ // A deliberate decline (call:rejected) still tears down mid-ring.
67
+ f = fixture();
68
+ await f.manager.outgoing({ chatId: "chat-4" });
69
+ assert.ok(f.manager._ringTimer);
70
+ await f.manager.rejected({ chatId: "chat-4" });
71
+ assert.strictEqual(f.manager.session, null, "reject-while-ringing closes the call");
72
+ assert.strictEqual(f.manager._ringTimer, null);
73
+
74
+ // Unanswered: ring timeout tells the server to finalize as missed and hangs up.
75
+ f = fixture();
76
+ const res2 = await f.manager.outgoing({ chatId: "chat-3", toUserId: "owner-1" });
77
+ f.manager._clearRingTimer();
78
+ await f.manager._ringTimeout("owner-1", "chat-3", res2.roomId);
79
+ const end = f.events.find((e) => e.name === "call:end");
80
+ assert.deepStrictEqual(end.payload, { toUserId: "owner-1", chatId: "chat-3" });
81
+ assert.strictEqual(f.manager.session, null);
39
82
  console.log("kazee call manager: ok");
40
83
  })().catch((error) => { console.error(error); process.exitCode = 1; });