@inetafrica/open-claudia 3.5.47 → 3.5.59

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.
Files changed (43) hide show
  1. package/CHANGELOG.md +70 -10
  2. package/bin/cli.js +10 -3
  3. package/bot.js +11 -0
  4. package/channels/kazee/adapter.js +244 -23
  5. package/channels/kazee/call/camera.js +233 -0
  6. package/channels/kazee/call/job-steps.js +81 -0
  7. package/channels/kazee/call/manager.js +1056 -27
  8. package/channels/kazee/call/miss-capture.js +82 -0
  9. package/channels/kazee/call/quick-voice.js +610 -0
  10. package/channels/kazee/call/session.js +787 -36
  11. package/channels/kazee/call/thread-context.js +100 -0
  12. package/channels/kazee/catch-up.js +275 -0
  13. package/core/actions.js +13 -4
  14. package/core/enforcer.js +3 -0
  15. package/core/handlers.js +14 -16
  16. package/core/identity.js +15 -1
  17. package/core/loopback.js +4 -1
  18. package/core/media.js +39 -1
  19. package/core/relationship.js +41 -18
  20. package/core/run-context.js +27 -3
  21. package/core/runner.js +70 -1
  22. package/core/state.js +6 -2
  23. package/core/switchboard/brain-link.js +161 -0
  24. package/core/switchboard/call-host.js +298 -0
  25. package/core/switchboard/handoff.js +80 -0
  26. package/core/switchboard/ipc.js +130 -0
  27. package/core/switchboard/supervisor.js +225 -0
  28. package/core/system-prompt.js +64 -12
  29. package/core/transcripts.js +39 -0
  30. package/core/tts-voice.js +62 -4
  31. package/core/turn-journal.js +23 -1
  32. package/core/utility-agent.js +5 -2
  33. package/package.json +2 -2
  34. package/test-call-speech.js +7 -0
  35. package/test-external-rule-scopes.js +24 -0
  36. package/test-fixtures/switchboard-fake-brain.js +20 -0
  37. package/test-kazee-call-manager.js +22 -4
  38. package/test-kazee-call-session.js +345 -8
  39. package/test-provider-compaction.js +8 -0
  40. package/test-provider-language.js +19 -9
  41. package/test-provider-prompt-parity.js +1 -0
  42. package/test-provider-runner.js +2 -0
  43. package/test-run-lock.js +24 -0
@@ -4,7 +4,7 @@ const crypto = require("crypto");
4
4
  const path = require("path");
5
5
  const { currentConversationKey, currentThreadId } = require("./context");
6
6
  const { resolveProviderSettings, peekLane } = require("./state");
7
- const { parseConversationKey } = require("./identity");
7
+ const { parseConversationKey, callLaneKey } = require("./identity");
8
8
 
9
9
  function cloneSerializable(value, label = "run context value") {
10
10
  if (value === undefined) return undefined;
@@ -57,6 +57,26 @@ function projectSnapshot(state, cwd, requestedProject) {
57
57
  // whole main context in a session of its own. Default "fresh": a new thread
58
58
  // starts a new session, seeded only with the thread's own messages. Codex has
59
59
  // no fork, so it always starts fresh.
60
+ // A live call answers in speech, so its turn may run a faster model and effort
61
+ // than the room's chat turns: KAZEE_CALL_MODEL / KAZEE_CALL_EFFORT (Claude
62
+ // only). Unset keeps the room's own settings; an unknown effort is ignored.
63
+ const CALL_EFFORTS = new Set(["low", "medium", "high", "xhigh", "max"]);
64
+ function callSetting(name) {
65
+ let value = process.env[name];
66
+ try { value = require("./config").config?.[name] ?? value; } catch (_) {}
67
+ return String(value || "").trim();
68
+ }
69
+
70
+ function callProviderOverrides(provider) {
71
+ if (provider !== "claude") return {};
72
+ const overrides = {};
73
+ const model = callSetting("KAZEE_CALL_MODEL");
74
+ const effort = callSetting("KAZEE_CALL_EFFORT").toLowerCase();
75
+ if (model) overrides.model = model;
76
+ if (CALL_EFFORTS.has(effort)) overrides.effort = effort;
77
+ return overrides;
78
+ }
79
+
60
80
  function threadSeedMode() {
61
81
  let value = process.env.OC_THREAD_SEED;
62
82
  try { value = require("./config").config?.OC_THREAD_SEED ?? value; } catch (_) {}
@@ -127,8 +147,10 @@ function captureRunContext({
127
147
  const activeSessionId = state.activeSessions?.[project.name]?.[provider] || null;
128
148
  // The run's lane (its conversation: a thread is its own) and thread are
129
149
  // fixed here, at admission: queueing, /stop, preview, typing and delivery all
130
- // follow the lane the message arrived in, never a later one.
131
- const laneKey = String(opts.laneKey || currentConversationKey() || project.name || "Workspace");
150
+ // follow the lane the message arrived in, never a later one. A live call's
151
+ // turns get a lane of their own beside their chat's.
152
+ const conversationLane = String(opts.laneKey || currentConversationKey() || project.name || "Workspace");
153
+ const laneKey = liveCall ? callLaneKey(conversationLane) : conversationLane;
132
154
  const threadId = (store && store.threadId) || currentThreadId() || parseConversationKey(laneKey).threadId || null;
133
155
  const lane = peekLane(state, laneKey);
134
156
  const forkFromSessionId = threadForkSession(state, { provider, laneKey, threadId, activeSessionId, opts, liveCall });
@@ -154,6 +176,7 @@ function captureRunContext({
154
176
  // personal bucket, so non-room channels are unaffected.
155
177
  const providerSettings = cloneSerializable({
156
178
  ...(resolveProviderSettings(state, provider) || {}),
179
+ ...(liveCall ? callProviderOverrides(provider) : {}),
157
180
  ...(opts.providerSettings || {}),
158
181
  }, "provider settings");
159
182
  delete providerSettings.backend;
@@ -261,6 +284,7 @@ function bindRunInputs(runContext, inputs = {}) {
261
284
  }
262
285
 
263
286
  module.exports = {
287
+ projectSnapshot,
264
288
  captureRunContext,
265
289
  bindRunInputs,
266
290
  cloneSerializable,
package/core/runner.js CHANGED
@@ -20,6 +20,7 @@ const {
20
20
  resolveProviderSettings, getProjectSessions,
21
21
  } = require("./state");
22
22
  const { chatContext, currentChannelId, currentAdapter, currentThreadId } = require("./context");
23
+ const { isCallLaneKey } = require("./identity");
23
24
  const { buildTurnPrompt } = require("./system-prompt");
24
25
  const turnJournal = require("./turn-journal");
25
26
  const usageLimits = require("./usage-limits");
@@ -413,6 +414,11 @@ async function executeProviderInvocation(options = {}) {
413
414
  terminalSeen = true;
414
415
  if (event.sessionId) sessionId = event.sessionId;
415
416
  if (typeof event.text === "string" && event.text) resultText = event.text;
417
+ // The answer as it will be delivered (finalAnswerText, else the
418
+ // result's own text), known now: a live call speaks it before the
419
+ // process has exited (earlyCallAnswer below).
420
+ const segments = textSegments.map((blocks) => blocks.join("")).filter((t) => t.trim());
421
+ event.answerText = redactSensitive(String(segments.at(-1) || resultText || "").trim());
416
422
  } else if (event.type === "error") {
417
423
  terminalSeen = true;
418
424
  rawErrors.push(event);
@@ -1490,6 +1496,7 @@ function deriveCompactionRunContext(base, overrides = {}) {
1490
1496
  admittedSessionId: overrides.sessionId ?? null,
1491
1497
  previousSessionId: overrides.previousSessionId ?? base.sessionId ?? null,
1492
1498
  resumeSessionId: overrides.resumeSessionId ?? null,
1499
+ forkSession: overrides.forkSession === true,
1493
1500
  continueSession: false,
1494
1501
  fresh: !!overrides.fresh,
1495
1502
  recallQuery: overrides.recallQuery ?? null,
@@ -1580,6 +1587,11 @@ function createCompactionService(dependencies = {}) {
1580
1587
  sessionId: oldSessionId,
1581
1588
  resumeSessionId: oldSessionId,
1582
1589
  previousSessionId: oldSessionId,
1590
+ // Read the live conversation through a fork (Claude --fork-session): the
1591
+ // summarize prompt and its brief must never land in the live session. A
1592
+ // summary that fails (an OAuth refresh error, a timeout) would leave the
1593
+ // prompt there, and the next turn answers it into the chat (12 Oct).
1594
+ forkSession: true,
1583
1595
  fresh: false,
1584
1596
  });
1585
1597
  const operationId = opts.operationId || (typeof crypto.randomUUID === "function" ? crypto.randomUUID() : `compact-${Date.now()}`);
@@ -1851,6 +1863,11 @@ function providerRunDiagnostic(result, runContext) {
1851
1863
  async function persistForegroundTranscript(result, runContext, state = currentState()) {
1852
1864
  const view = capturedStateView(state, runContext);
1853
1865
  const session = () => result.sessionId || runContext.sessionId || null;
1866
+ // A run stopped on purpose did not fail: /stop, or a call job the quick
1867
+ // voice answered itself or the caller replaced (stopCallRunWhenDropped).
1868
+ // Its words are kept; a "run failed: exited with code 143" note is not
1869
+ // (11 Oct: five such notes in one call's transcript).
1870
+ const stopped = !result.ok && (result.error?.code === "RUN_CANCELLED" || cancelTargets(laneForRun(state, runContext).cancelRequested, runContext));
1854
1871
  appendProjectTranscript(
1855
1872
  runContext.transcriptRole || "user",
1856
1873
  stripTranscriptPointerForStorage(runContext.userPrompt),
@@ -1865,6 +1882,7 @@ async function persistForegroundTranscript(result, runContext, state = currentSt
1865
1882
  session,
1866
1883
  { strict: true },
1867
1884
  );
1885
+ if (stopped) return;
1868
1886
  const role = result.ok ? "assistant" : "system-note";
1869
1887
  const text = result.ok
1870
1888
  ? (result.text || "(no output)")
@@ -2452,9 +2470,12 @@ function createForegroundService({ state, store, lane, stopTyping, capture, owns
2452
2470
  // another run then claimed) must not touch that lane's run slot.
2453
2471
  const holds = () => (holdsLane ? holdsLane() : ownsAdmissionLock);
2454
2472
  const callSpeechLeases = new Map();
2473
+ const callStopListeners = new Map();
2455
2474
  const callStage = (stage, runContext) => {
2456
2475
  if (capture || runContext?.liveCall !== true) return;
2457
2476
  console.log(`${new Date().toISOString()} Kazee call provider: stage=${stage} run=${runContext.runId} elapsed=${Math.max(0, Date.now() - runContext.admittedAt)}ms`);
2477
+ // The call's job board: this job is now the one being worked on.
2478
+ try { store?.adapter?.onCallStage?.(stage); } catch (_) {}
2458
2479
  };
2459
2480
  const persistEffects = !capture || persistCapture;
2460
2481
  // The run's lane: given by executeAdmittedRun, else the captured run's own.
@@ -2502,6 +2523,10 @@ function createForegroundService({ state, store, lane, stopTyping, capture, owns
2502
2523
  if (ownsAdmissionLock) onOwnSpawn?.(proc);
2503
2524
  if (ownsAdmissionLock || !runLane.runningProcess) runLane.runningProcess = proc;
2504
2525
  if (ownsAdmissionLock) runLane.preparingRun = false;
2526
+ if (!capture && ownsAdmissionLock) {
2527
+ const remove = stopCallRunWhenDropped(store, runLane, proc, runContext);
2528
+ if (remove) callStopListeners.set(runContext.runId, remove);
2529
+ }
2505
2530
  },
2506
2531
  shouldAbort(runContext, { capture: isCapture } = {}) {
2507
2532
  if (isCapture || !ownsAdmissionLock) return false;
@@ -2535,6 +2560,23 @@ function createForegroundService({ state, store, lane, stopTyping, capture, owns
2535
2560
  if (observer && event.type === "tool_start" && runContext.purpose === "foreground") {
2536
2561
  try { observer.onToolStart(event.name, event.detail); } catch (_) {}
2537
2562
  }
2563
+ // A live call's job board hears what the full agent is doing (tool
2564
+ // steps), so the quick voice can answer "what's the status?".
2565
+ if (!capture && runContext?.liveCall === true && event.type === "tool_start") {
2566
+ try { store?.adapter?.onCallActivity?.(event); } catch (_) {}
2567
+ }
2568
+ // A live call hears the answer once it is written, not once the CLI has
2569
+ // exited: its end-of-turn work took ~0.8 s on both answers of the 11 Oct
2570
+ // 18:57 call. The normal delivery still runs after; the call skips the
2571
+ // words it was already handed. Never for a guarded speaker (their reply
2572
+ // goes through the outbound guard first).
2573
+ if (!capture && runContext?.liveCall === true && event.type === "result" && event.answerText && !PLACEHOLDER_REPLY.test(event.answerText)) {
2574
+ let guarded = true;
2575
+ try { guarded = require("./relationship").isCurrentSpeakerGuarded(); } catch (_) {}
2576
+ if (!guarded) {
2577
+ try { Promise.resolve(store?.adapter?.earlyCallAnswer?.(event.answerText)).catch(() => {}); } catch (_) {}
2578
+ }
2579
+ }
2538
2580
  if (preview && event.type === "text_final" && typeof event.text === "string" && event.text) {
2539
2581
  preview.append(event.text, runContext);
2540
2582
  }
@@ -2552,6 +2594,8 @@ function createForegroundService({ state, store, lane, stopTyping, capture, owns
2552
2594
  try { require("./speaker-attest").endRun(runContext.runId); } catch (_) {}
2553
2595
  callSpeechLeases.get(runContext.runId)?.close();
2554
2596
  callSpeechLeases.delete(runContext.runId);
2597
+ callStopListeners.get(runContext.runId)?.();
2598
+ callStopListeners.delete(runContext.runId);
2555
2599
  const runLane = laneOf(runContext);
2556
2600
  if (runLane.runningProcess === proc) runLane.runningProcess = null;
2557
2601
  // Keep the lane's lock armed while transcript/usage/session persistence
@@ -2637,6 +2681,29 @@ function obsoleteCallTurn(store) {
2637
2681
  return store?.adapter?.liveCall === true && store.adapter.callTurn?.signal.aborted === true;
2638
2682
  }
2639
2683
 
2684
+ // A live-call turn the call dropped while its provider runs: the caller
2685
+ // changed their mind ("replaced"), the quick voice handled it alone ("self"),
2686
+ // or (with no quick voice) a newer utterance took over ("superseded"). Stop
2687
+ // it the way /stop does (named cancel flag, then the process tree), so it
2688
+ // neither holds the call's lane nor speaks. A turn whose call ENDED keeps
2689
+ // running, so "do X" said just before hanging up still gets done. Ported from
2690
+ // the 4 Oct calls build. Returns the listener's remover, or null.
2691
+ const STOPPED_CALL_TURN_REASONS = new Set(["superseded", "cancelled", "replaced", "self"]);
2692
+ function stopCallRunWhenDropped(store, lane, proc, runContext) {
2693
+ const signal = runContext?.liveCall === true ? store?.adapter?.callTurn?.signal : null;
2694
+ if (!signal || !proc?.pid) return null;
2695
+ const stop = () => {
2696
+ if (!STOPPED_CALL_TURN_REASONS.has(signal.reason) || lane.runningProcess !== proc) return;
2697
+ lane.cancelRequested = runContext.runId || true;
2698
+ console.log(`${new Date().toISOString()} Kazee call provider: stage=${signal.reason} run=${runContext.runId} elapsed=${Math.max(0, Date.now() - runContext.admittedAt)}ms`);
2699
+ killProcessTree(proc.pid, "SIGTERM");
2700
+ setTimeout(() => killProcessTree(proc.pid, "SIGKILL"), 3000).unref?.();
2701
+ };
2702
+ if (signal.aborted) { stop(); return null; }
2703
+ signal.addEventListener("abort", stop, { once: true });
2704
+ return () => signal.removeEventListener("abort", stop);
2705
+ }
2706
+
2640
2707
  function cancelledCallRunResult(runContext) {
2641
2708
  return cancelledRunResult(runContext, { id: runContext.provider, label: runContext.provider }, "Call turn superseded or ended before execution");
2642
2709
  }
@@ -2669,7 +2736,7 @@ function cancelQueuedCallOnAbort(lane, item) {
2669
2736
  function drainQueuedMessages(_state) {
2670
2737
  for (const { state, lane } of waitingLanes()) {
2671
2738
  if (laneBusy(lane) || !lane.messageQueue.length) continue;
2672
- if (slotBlocker(state)) { lane.waitingForSlot = true; continue; }
2739
+ if (!isCallLaneKey(lane.key) && slotBlocker(state)) { lane.waitingForSlot = true; continue; }
2673
2740
  const batch = takeQueueBatch(lane);
2674
2741
  const item = batch.length > 1 ? mergeQueueBatch(lane, batch) : batch[0];
2675
2742
  item.disposeCallListener?.();
@@ -3302,6 +3369,8 @@ function runAgentSilent(prompt, cwd, label, opts = {}) {
3302
3369
  }
3303
3370
 
3304
3371
  module.exports = {
3372
+ stopCallRunWhenDropped,
3373
+ persistForegroundTranscript,
3305
3374
  queuedNotice,
3306
3375
  handleQueueRemove,
3307
3376
  takeQueueBatch,
package/core/state.js CHANGED
@@ -20,6 +20,7 @@ const {
20
20
  channelKey,
21
21
  identities,
22
22
  parseConversationKey,
23
+ isCallLaneKey,
23
24
  } = require("./identity");
24
25
  const { currentChannelId, currentCanonicalUserId, currentConversationKey, currentRoomKey } = require("./context");
25
26
  const { atomicWriteFileSync, readJsonWithFallback } = require("./fsutil");
@@ -1005,7 +1006,10 @@ function slotBlocker(state) {
1005
1006
  function admitLane(state = currentState(), laneKey = null) {
1006
1007
  const lane = laneFor(state, laneKey);
1007
1008
  if (laneBusy(lane)) return { ok: false, reason: "busy", lane };
1008
- const blocker = slotBlocker(state);
1009
+ // A live call is real time: its turns never wait for a free slot (one call
1010
+ // at a time, so at most one run over the caps).
1011
+ const realtime = isCallLaneKey(lane.key);
1012
+ const blocker = realtime ? null : slotBlocker(state);
1009
1013
  if (lane.messageQueue.length) return { ok: false, reason: "waiting", lane, running: blocker?.running || 0 };
1010
1014
  if (blocker) {
1011
1015
  lane.waitingForSlot = true;
@@ -1013,7 +1017,7 @@ function admitLane(state = currentState(), laneKey = null) {
1013
1017
  }
1014
1018
  // A freed slot goes to the lanes already waiting for one, oldest first: a
1015
1019
  // newcomer queues behind them (the runner then admits in order).
1016
- if (waitingLanes().some((waiting) => waiting.lane !== lane && !slotBlocker(waiting.state))) {
1020
+ if (!realtime && waitingLanes().some((waiting) => waiting.lane !== lane && !slotBlocker(waiting.state))) {
1017
1021
  lane.waitingForSlot = true;
1018
1022
  return { ok: false, reason: "cap", lane, running: runningLanes(state).length, scope: "queue" };
1019
1023
  }
@@ -0,0 +1,161 @@
1
+ // The brain's end of the switchboard link (only when OC_SWITCHBOARD started
2
+ // this process as the switchboard's child). The call room lives in the
3
+ // switchboard; a call job arrives here and becomes an ordinary call turn: a
4
+ // per-job adapter made from the real Kazee adapter, as the call manager
5
+ // always made it, whose speech and answer go back over the link.
6
+
7
+ const { Link } = require("./ipc");
8
+ const { isSwitchboardChild } = require("./handoff");
9
+
10
+ let link = null;
11
+ let goneHandled = false;
12
+ // The channel also closes as this process exits on its own; not news then.
13
+ let exiting = false;
14
+ process.once("exit", () => { exiting = true; });
15
+ let options = {};
16
+ const jobs = new Map();
17
+
18
+ function findAdapter(adapterId) {
19
+ if (options.findAdapter) return options.findAdapter(adapterId);
20
+ return require("../adapter-registry").findAdapter(adapterId);
21
+ }
22
+
23
+ // channel/findAdapter/onGone are test seams; the brain uses its own process.
24
+ function connect({ channel = null, findAdapter: find = null, onGone = null } = {}) {
25
+ if (link) return link;
26
+ if (!channel && !isSwitchboardChild()) return null;
27
+ options = { findAdapter: find, onGone };
28
+ link = new Link(channel || process, { label: "switchboard" });
29
+ link.on("call.job", (payload) => {
30
+ try { startJob(payload); } catch (error) { console.error(`Switchboard call job failed to start: ${error.message}`); }
31
+ });
32
+ link.on("call.job.mirror", ({ id, mirror } = {}) => {
33
+ const job = jobs.get(id);
34
+ if (job && mirror) job.mirror = mirror;
35
+ });
36
+ link.on("call.job.abort", ({ id, reason } = {}) => {
37
+ const job = jobs.get(id);
38
+ if (job && !job.controller.signal.aborted) job.controller.abort(reason || "aborted");
39
+ });
40
+ link.on("call.job.forget", ({ id } = {}) => { jobs.delete(id); });
41
+ link.handle("call.carryOn", (payload) => carryOn(payload));
42
+ link.handle("call.greetingContext", (payload) => greetingContext(payload));
43
+ // The switchboard is gone (it crashed or was killed): this brain must go
44
+ // too, so the switchboard launchd or the pod starts next gets a free lock.
45
+ link.on("close", () => {
46
+ if (goneHandled || exiting) return;
47
+ goneHandled = true;
48
+ if (options.onGone) { options.onGone(); return; }
49
+ console.error("Switchboard link closed: shutting the brain down");
50
+ try { process.kill(process.pid, "SIGTERM"); } catch (_) {}
51
+ });
52
+ return link;
53
+ }
54
+
55
+ function hello() {
56
+ const l = connect();
57
+ if (!l) return false;
58
+ let version = null;
59
+ try { version = require("../../package.json").version; } catch (_) {}
60
+ return l.notify("brain.hello", { pid: process.pid, version });
61
+ }
62
+
63
+ // Said before a planned exit (restart, upgrade, shutdown): the call hears
64
+ // "I'm restarting" instead of an unexplained gap.
65
+ function notifyExiting(reason) {
66
+ exiting = true;
67
+ const l = link || connect();
68
+ return l ? l.notify("brain.exiting", { reason: String(reason || "restart") }) : false;
69
+ }
70
+
71
+ function configChanged(what) {
72
+ const l = link || connect();
73
+ return l ? l.notify("config.changed", { what }) : false;
74
+ }
75
+
76
+ function call(id, name, args) {
77
+ const l = connect();
78
+ if (!l) return Promise.reject(new Error("switchboard not connected"));
79
+ return l.request("call.job.fn", { id, name, args }, { timeoutMs: 0 });
80
+ }
81
+
82
+ function startJob({ id, adapterId, envelope, fields = {}, mirror = {}, resent = false } = {}) {
83
+ const base = findAdapter(adapterId);
84
+ if (!base || typeof base._emit !== "function") throw new Error(`no adapter ${adapterId}`);
85
+ const controller = new AbortController();
86
+ const job = { id, controller, mirror };
87
+ jobs.set(id, job);
88
+ const callAdapter = Object.create(base);
89
+ Object.assign(callAdapter, fields);
90
+ callAdapter.callTurn = { signal: controller.signal };
91
+ callAdapter.liveCallHistory = () => (job.mirror.history || []).map((entry) => ({ ...entry }));
92
+ callAdapter.liveCallCamera = () => {
93
+ const camera = job.mirror.camera;
94
+ if (!camera?.path) return null;
95
+ return { path: camera.path, ageMs: Math.max(0, Date.now() - (Number(camera.at) || Date.now())), receiving: camera.receiving !== false };
96
+ };
97
+ callAdapter.isCallTurnActive = () => !controller.signal.aborted && !!job.mirror.active;
98
+ callAdapter.cancelCallSpeech = () => { call(id, "cancelCallSpeech", []).catch(() => {}); };
99
+ callAdapter.onCallStage = (stage) => { call(id, "onCallStage", [stage]).catch(() => {}); };
100
+ callAdapter.onCallActivity = (event) => {
101
+ call(id, "onCallActivity", [{ type: event?.type, name: event?.name, detail: event?.detail }]).catch(() => {});
102
+ };
103
+ callAdapter.speakSentence = async (text) => {
104
+ try { return !!(await call(id, "speakSentence", [String(text ?? "")])); } catch (_) { return false; }
105
+ };
106
+ callAdapter.send = async (channelId, text) => {
107
+ try { return (await call(id, "send", [String(channelId), String(text ?? "")])) !== false; } catch (_) { return false; }
108
+ };
109
+ callAdapter.earlyCallAnswer = async (text) => {
110
+ try { return (await call(id, "earlyCallAnswer", [String(text ?? "")])) !== false; } catch (_) { return false; }
111
+ };
112
+ callAdapter.edit = async () => true;
113
+ callAdapter.typing = async () => {};
114
+ callAdapter.sendVoice = async (_channelId, voicePath) => {
115
+ console.warn(`Kazee call ignored file voice response ${voicePath}; streamed TTS is active`);
116
+ return true;
117
+ };
118
+ if (resent) console.log(`Switchboard call job ${id} asked again after a brain restart`);
119
+ base._emit("message", { ...envelope, adapter: callAdapter });
120
+ return callAdapter;
121
+ }
122
+
123
+ function carryOn({ adapterId, chatId, userId, prompt, threadId = null } = {}) {
124
+ const adapter = findAdapter(adapterId) || { id: adapterId, type: "kazee" };
125
+ const job = require("../../channels/kazee/call/manager").carryOnInChat({ adapter, chatId, userId, prompt, threadId });
126
+ return { id: job?.id || null };
127
+ }
128
+
129
+ function greetingContext({ adapterId, chatId, userId, threadId = null } = {}) {
130
+ const adapter = findAdapter(adapterId) || { id: adapterId, type: "kazee" };
131
+ return require("../../channels/kazee/call/manager").greetingContext({ adapter, chatId, userId, ...(threadId ? { threadId } : {}) });
132
+ }
133
+
134
+ // The brain's stand-in for a Kazee adapter's call manager: calls are the
135
+ // switchboard's, so the brain's socket ignores call events, stopping the
136
+ // brain never ends a call, and `open-claudia call` is passed across.
137
+ function callManagerProxy(adapter) {
138
+ return {
139
+ session: null,
140
+ switchboard: true,
141
+ wire() {},
142
+ async stop() {},
143
+ async outgoing(args = {}) {
144
+ const l = connect();
145
+ if (!l) throw new Error("switchboard not connected");
146
+ return l.request("call.outgoing", { adapterId: adapter.id, args }, { timeoutMs: 90000 });
147
+ },
148
+ };
149
+ }
150
+
151
+ function _resetForTests() {
152
+ goneHandled = true;
153
+ link?.close();
154
+ link = null;
155
+ options = {};
156
+ goneHandled = false;
157
+ exiting = false;
158
+ jobs.clear();
159
+ }
160
+
161
+ module.exports = { connect, hello, notifyExiting, configChanged, startJob, callManagerProxy, carryOn, greetingContext, jobs, _resetForTests };