@openclaw/voice-call 2026.7.2-beta.5 → 2026.8.1-beta.2

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/dist/index.js CHANGED
@@ -1,18 +1,20 @@
1
1
  import { definePluginEntry, sleep } from "./runtime-api.js";
2
2
  import "./api.js";
3
3
  import { VOICE_CALL_CLI_DESCRIPTOR } from "./cli-output-mode.js";
4
- import { S as validateProviderConfig, b as resolveVoiceCallConfig, c as resolveWebhookExposureStatus, d as setupTailscaleExposureRoute, g as resolveUserPath, l as cleanupTailscaleExposureRoute, t as createVoiceCallRuntime, u as getTailscaleSelfInfo, y as VoiceCallConfigSchema } from "./runtime-entry-CQtitvRd.js";
5
- import { g as setVoiceCallStateRuntime, t as resolveDefaultVoiceCallStoreDir, u as getCallHistoryFromStore } from "./store-path-nYL-yM0S.js";
4
+ import { S as validateProviderConfig, b as resolveVoiceCallConfig, c as resolveWebhookExposureStatus, d as setupTailscaleExposureRoute, g as resolveUserPath, l as cleanupTailscaleExposureRoute, t as createVoiceCallRuntime, u as getTailscaleSelfInfo, y as VoiceCallConfigSchema } from "./runtime-entry-F13eJ4pp.js";
5
+ import { b as setVoiceCallStateRuntime, t as resolveDefaultVoiceCallStoreDir, u as getCallHistoryFromStore } from "./store-path-51_oC_xn.js";
6
6
  import { formatErrorMessage } from "openclaw/plugin-sdk/error-runtime";
7
7
  import { ErrorCodes, callGatewayFromCli, errorShape } from "openclaw/plugin-sdk/gateway-runtime";
8
- import { MAX_TCP_PORT, MAX_TIMER_TIMEOUT_MS, addTimerTimeoutGraceMs, clampTimerTimeoutMs, parseStrictNonNegativeInteger, resolveTimerTimeoutMs, timestampMsToIsoString } from "openclaw/plugin-sdk/number-runtime";
9
- import { normalizeAgentId } from "openclaw/plugin-sdk/routing";
10
- import { asOptionalRecord, isRecord, normalizeOptionalLowercaseString, normalizeOptionalString } from "openclaw/plugin-sdk/string-coerce-runtime";
8
+ import { resolveGlobalSingleton } from "openclaw/plugin-sdk/global-singleton";
9
+ import { normalizeAgentId, parseAgentSessionKey } from "openclaw/plugin-sdk/routing";
10
+ import { asNonArrayRecord, asOptionalRecord, isRecord, normalizeOptionalLowercaseString, normalizeOptionalString } from "openclaw/plugin-sdk/string-coerce-runtime";
11
11
  import { jsonResult } from "openclaw/plugin-sdk/tool-results";
12
12
  import { Type } from "typebox";
13
13
  import fs from "node:fs";
14
14
  import { randomUUID } from "node:crypto";
15
15
  import path from "node:path";
16
+ import { MAX_TCP_PORT, MAX_TIMER_TIMEOUT_MS, addTimerTimeoutGraceMs, clampTimerTimeoutMs, parseStrictNonNegativeInteger, resolveTimerTimeoutMs, timestampMsToIsoString } from "openclaw/plugin-sdk/number-runtime";
17
+ import { StringDecoder } from "node:string_decoder";
16
18
  import { format } from "node:util";
17
19
  //#region extensions/voice-call/src/cli.ts
18
20
  const VOICE_CALL_GATEWAY_DEFAULT_TIMEOUT_MS = 5e3;
@@ -429,22 +431,33 @@ function registerVoiceCallCli(params) {
429
431
  }
430
432
  };
431
433
  if (fs.existsSync(file) && path.basename(file) !== "calls.jsonl") {
432
- const initial = fs.readFileSync(file, "utf8");
433
- const lines = initial.split("\n").filter(Boolean);
434
+ const initial = fs.readFileSync(file);
435
+ let decoder = new StringDecoder("utf8");
436
+ const initialLines = decoder.write(initial).split("\n");
437
+ let pendingLine = initialLines.pop() ?? "";
438
+ const lines = initialLines.filter(Boolean);
434
439
  for (const line of lines.slice(Math.max(0, lines.length - since))) writeStdoutLine(line);
435
- let offset = Buffer.byteLength(initial, "utf8");
440
+ let offset = initial.length;
441
+ let lastObservedSize = initial.length;
436
442
  for (;;) {
437
443
  try {
438
444
  const stat = fs.statSync(file);
439
- if (stat.size < offset) offset = 0;
445
+ if (stat.size < lastObservedSize) {
446
+ offset = 0;
447
+ decoder = new StringDecoder("utf8");
448
+ pendingLine = "";
449
+ }
450
+ lastObservedSize = stat.size;
440
451
  if (stat.size > offset) {
441
452
  const fd = fs.openSync(file, "r");
442
453
  try {
443
454
  const buf = Buffer.alloc(stat.size - offset);
444
- fs.readSync(fd, buf, 0, buf.length, offset);
445
- offset = stat.size;
446
- const text = buf.toString("utf8");
447
- for (const line of text.split("\n").filter(Boolean)) writeStdoutLine(line);
455
+ const bytesRead = fs.readSync(fd, buf, 0, buf.length, offset);
456
+ offset += bytesRead;
457
+ const text = decoder.write(buf.subarray(0, bytesRead));
458
+ const completeLines = `${pendingLine}${text}`.split("\n");
459
+ pendingLine = completeLines.pop() ?? "";
460
+ for (const line of completeLines.filter(Boolean)) writeStdoutLine(line);
448
461
  } finally {
449
462
  fs.closeSync(fd);
450
463
  }
@@ -531,6 +544,126 @@ function registerVoiceCallCli(params) {
531
544
  });
532
545
  }
533
546
  //#endregion
547
+ //#region extensions/voice-call/src/command-service.ts
548
+ var VoiceCallCommandInputError = class extends Error {};
549
+ function toVoiceCallStatus(call) {
550
+ return {
551
+ callId: call.callId,
552
+ ...call.providerCallId !== void 0 ? { providerCallId: call.providerCallId } : {},
553
+ provider: call.provider,
554
+ direction: call.direction,
555
+ state: call.state,
556
+ startedAt: call.startedAt,
557
+ ...call.answeredAt !== void 0 ? { answeredAt: call.answeredAt } : {},
558
+ ...call.endedAt !== void 0 ? { endedAt: call.endedAt } : {},
559
+ ...call.endReason !== void 0 ? { endReason: call.endReason } : {}
560
+ };
561
+ }
562
+ function requireInput(value, message) {
563
+ if (!value) throw new VoiceCallCommandInputError(message);
564
+ return value;
565
+ }
566
+ function requireSuccess(result, fallback) {
567
+ if (!result.success) throw new Error(result.error || fallback);
568
+ }
569
+ function createVoiceCallCommandService(ensureRuntime) {
570
+ const describeHistoricalCall = async (rt, callId) => {
571
+ const call = await rt.manager.getCallFromMemoryOrStore(callId);
572
+ if (!call) return;
573
+ const endedAt = timestampMsToIsoString(call.endedAt);
574
+ return `call is not active (${[
575
+ `last state=${call.state}`,
576
+ call.endReason ? `endReason=${call.endReason}` : void 0,
577
+ endedAt ? `endedAt=${endedAt}` : void 0
578
+ ].filter(Boolean).join(", ")})`;
579
+ };
580
+ const resolveCallMessage = async (callId, message) => {
581
+ const resolvedCallId = requireInput(callId, "callId and message required");
582
+ const resolvedMessage = requireInput(message, "callId and message required");
583
+ const rt = await ensureRuntime();
584
+ const activeCall = rt.manager.getCall(resolvedCallId) ?? rt.manager.getCallByProviderCallId(resolvedCallId);
585
+ if (!activeCall) throw new VoiceCallCommandInputError(await describeHistoricalCall(rt, resolvedCallId) ?? "Call not found");
586
+ return {
587
+ rt,
588
+ callId: activeCall.callId,
589
+ message: resolvedMessage
590
+ };
591
+ };
592
+ const prepareContinue = async (callId, message) => {
593
+ const request = await resolveCallMessage(callId, message);
594
+ return {
595
+ rt: request.rt,
596
+ callId: request.callId,
597
+ run: async () => {
598
+ const result = await request.rt.manager.continueCall(request.callId, request.message);
599
+ requireSuccess(result, "continue failed");
600
+ return {
601
+ success: true,
602
+ transcript: result.transcript
603
+ };
604
+ }
605
+ };
606
+ };
607
+ return {
608
+ prepareContinue,
609
+ async initiate(params, missingToMessage = "to required") {
610
+ const rt = await ensureRuntime();
611
+ const to = requireInput(params.to ?? rt.config.toNumber, missingToMessage);
612
+ const result = await rt.manager.initiateCall(to, params.sessionKey, {
613
+ message: params.message,
614
+ mode: params.mode,
615
+ dtmfSequence: params.dtmfSequence,
616
+ ...params.requesterSessionKey ? { requesterSessionKey: params.requesterSessionKey } : {},
617
+ ...params.agentId ? { agentId: params.agentId } : {}
618
+ });
619
+ requireSuccess(result, "initiate failed");
620
+ return {
621
+ callId: result.callId,
622
+ initiated: true
623
+ };
624
+ },
625
+ async continueCall(callId, message) {
626
+ return await (await prepareContinue(callId, message)).run();
627
+ },
628
+ async speak(params) {
629
+ const request = await resolveCallMessage(params.callId, params.message);
630
+ if (request.rt.config.realtime.enabled) {
631
+ const realtimeResult = request.rt.webhookServer.speakRealtime(request.callId, request.message);
632
+ if (realtimeResult.success) return { success: true };
633
+ if (params.allowTwimlFallback === false) return {
634
+ success: false,
635
+ error: realtimeResult.error ?? "Realtime bridge is not active"
636
+ };
637
+ }
638
+ requireSuccess(await request.rt.manager.speak(request.callId, request.message), "speak failed");
639
+ return { success: true };
640
+ },
641
+ async sendDtmf(callId, digits) {
642
+ const resolvedCallId = requireInput(callId, "callId and digits required");
643
+ const resolvedDigits = requireInput(digits, "callId and digits required");
644
+ requireSuccess(await (await ensureRuntime()).manager.sendDtmf(resolvedCallId, resolvedDigits), "dtmf failed");
645
+ return { success: true };
646
+ },
647
+ async endCall(callId) {
648
+ const resolvedCallId = requireInput(callId, "callId required");
649
+ requireSuccess(await (await ensureRuntime()).manager.endCall(resolvedCallId), "end failed");
650
+ return { success: true };
651
+ },
652
+ async status(callId) {
653
+ const rt = await ensureRuntime();
654
+ if (!callId) return {
655
+ found: true,
656
+ calls: rt.manager.getActiveCalls().map(toVoiceCallStatus)
657
+ };
658
+ const call = await rt.manager.getCallFromMemoryOrStore(callId);
659
+ return call ? {
660
+ found: true,
661
+ call: toVoiceCallStatus(call)
662
+ } : { found: false };
663
+ }
664
+ };
665
+ }
666
+ //#endregion
534
667
  //#region extensions/voice-call/src/gateway-continue-operation.ts
535
668
  const VOICE_CALL_CONTINUE_OPERATION_BUFFER_MS = 3e4;
536
669
  const VOICE_CALL_CONTINUE_OPERATION_CLEANUP_MS = 300 * 1e3;
@@ -557,21 +690,9 @@ function createVoiceCallContinueOperationStore(params) {
557
690
  startedAtMs,
558
691
  pollTimeoutMs
559
692
  });
560
- request.rt.manager.continueCall(request.callId, request.message).then((result) => {
693
+ request.run().then((result) => {
561
694
  const current = operations.get(operationId);
562
695
  if (!current || current.status !== "pending") return;
563
- if (!result.success) {
564
- operations.set(operationId, {
565
- operationId,
566
- status: "failed",
567
- callId: request.callId,
568
- startedAtMs,
569
- completedAtMs: Date.now(),
570
- pollTimeoutMs,
571
- error: result.error || "continue failed"
572
- });
573
- return;
574
- }
575
696
  operations.set(operationId, {
576
697
  operationId,
577
698
  status: "completed",
@@ -649,223 +770,15 @@ function createVoiceCallContinueOperationStore(params) {
649
770
  //#region extensions/voice-call/index.ts
650
771
  const VOICE_CALL_WRITE_METHOD_SCOPE = { scope: "operator.write" };
651
772
  const VOICE_CALL_READ_METHOD_SCOPE = { scope: "operator.read" };
652
- const voiceCallConfigSchema = {
653
- parse(value) {
654
- const config = asOptionalRecord(value) ?? {};
655
- const enabled = typeof config.enabled === "boolean" ? config.enabled : true;
656
- return VoiceCallConfigSchema.parse({
657
- ...config,
658
- enabled,
659
- provider: config.provider ?? (enabled ? "mock" : void 0)
660
- });
661
- },
662
- uiHints: {
663
- provider: {
664
- label: "Provider",
665
- help: "Use twilio, telnyx, or mock for dev/no-network."
666
- },
667
- fromNumber: {
668
- label: "From Number",
669
- placeholder: "+15550001234"
670
- },
671
- toNumber: {
672
- label: "Default To Number",
673
- placeholder: "+15550001234"
674
- },
675
- inboundPolicy: { label: "Inbound Policy" },
676
- allowFrom: { label: "Inbound Allowlist" },
677
- inboundGreeting: {
678
- label: "Inbound Greeting",
679
- advanced: true
680
- },
681
- numbers: {
682
- label: "Per-number Routing",
683
- help: "Inbound overrides keyed by dialed E.164 number.",
684
- advanced: true
685
- },
686
- "telnyx.apiKey": {
687
- label: "Telnyx API Key",
688
- sensitive: true
689
- },
690
- "telnyx.connectionId": { label: "Telnyx Connection ID" },
691
- "telnyx.publicKey": {
692
- label: "Telnyx Public Key",
693
- sensitive: true
694
- },
695
- "twilio.accountSid": { label: "Twilio Account SID" },
696
- "twilio.authToken": {
697
- label: "Twilio Auth Token",
698
- sensitive: true
699
- },
700
- "twilio.region": {
701
- label: "Twilio Region",
702
- advanced: true
703
- },
704
- "outbound.defaultMode": { label: "Default Call Mode" },
705
- "outbound.notifyHangupDelaySec": {
706
- label: "Notify Hangup Delay (sec)",
707
- advanced: true
708
- },
709
- "serve.port": { label: "Webhook Port" },
710
- "serve.bind": { label: "Webhook Bind" },
711
- "serve.path": { label: "Webhook Path" },
712
- "tailscale.mode": {
713
- label: "Tailscale Mode",
714
- advanced: true
715
- },
716
- "tailscale.path": {
717
- label: "Tailscale Path",
718
- advanced: true
719
- },
720
- "tunnel.provider": {
721
- label: "Tunnel Provider",
722
- advanced: true
723
- },
724
- "tunnel.ngrokAuthToken": {
725
- label: "ngrok Auth Token",
726
- sensitive: true,
727
- advanced: true
728
- },
729
- "tunnel.ngrokDomain": {
730
- label: "ngrok Domain",
731
- advanced: true
732
- },
733
- "tunnel.allowNgrokFreeTierLoopbackBypass": {
734
- label: "Allow ngrok Free Tier (Loopback Bypass)",
735
- advanced: true
736
- },
737
- "streaming.enabled": {
738
- label: "Enable Streaming",
739
- help: "Classic streaming transcription currently requires the Twilio call provider.",
740
- advanced: true
741
- },
742
- "streaming.provider": {
743
- label: "Streaming Provider",
744
- help: "Uses the first registered realtime transcription provider when unset.",
745
- advanced: true
746
- },
747
- "streaming.providers": {
748
- label: "Streaming Provider Config",
749
- advanced: true
750
- },
751
- "streaming.streamPath": {
752
- label: "Media Stream Path",
753
- advanced: true
754
- },
755
- "realtime.enabled": {
756
- label: "Enable Realtime Voice",
757
- advanced: true
758
- },
759
- "realtime.provider": {
760
- label: "Realtime Voice Provider",
761
- help: "Uses the first registered realtime voice provider when unset.",
762
- advanced: true
763
- },
764
- "realtime.streamPath": {
765
- label: "Realtime Stream Path",
766
- advanced: true
767
- },
768
- "realtime.instructions": {
769
- label: "Realtime Instructions",
770
- advanced: true
771
- },
772
- "realtime.toolPolicy": {
773
- label: "Realtime Tool Policy",
774
- help: "Controls the shared openclaw_agent_consult tool.",
775
- advanced: true
776
- },
777
- "realtime.consultPolicy": {
778
- label: "Realtime Consult Policy",
779
- help: "Guides when the realtime voice model should call openclaw_agent_consult.",
780
- advanced: true
781
- },
782
- "realtime.fastContext.enabled": {
783
- label: "Enable Fast Realtime Context",
784
- help: "Searches memory/session context before the full consult agent.",
785
- advanced: true
786
- },
787
- "realtime.fastContext.timeoutMs": {
788
- label: "Fast Context Timeout",
789
- advanced: true
790
- },
791
- "realtime.fastContext.maxResults": {
792
- label: "Fast Context Result Limit",
793
- advanced: true
794
- },
795
- "realtime.fastContext.sources": {
796
- label: "Fast Context Sources",
797
- advanced: true
798
- },
799
- "realtime.fastContext.fallbackToConsult": {
800
- label: "Fallback To Full Consult",
801
- advanced: true
802
- },
803
- "realtime.agentContext.enabled": {
804
- label: "Enable Agent Voice Context",
805
- help: "Injects a compact agent identity and workspace context capsule into realtime voice instructions.",
806
- advanced: true
807
- },
808
- "realtime.agentContext.maxChars": {
809
- label: "Agent Voice Context Limit",
810
- advanced: true
811
- },
812
- "realtime.agentContext.includeIdentity": {
813
- label: "Include Agent Identity",
814
- advanced: true
815
- },
816
- "realtime.agentContext.includeWorkspaceFiles": {
817
- label: "Include Agent Workspace Files",
818
- advanced: true
819
- },
820
- "realtime.agentContext.files": {
821
- label: "Agent Voice Context Files",
822
- advanced: true
823
- },
824
- "realtime.providers": {
825
- label: "Realtime Provider Config",
826
- advanced: true
827
- },
828
- "tts.provider": {
829
- label: "TTS Provider Override",
830
- help: "Deep-merges with tts (Microsoft is ignored for calls).",
831
- advanced: true
832
- },
833
- "tts.providers": {
834
- label: "TTS Provider Config",
835
- advanced: true
836
- },
837
- publicUrl: {
838
- label: "Public Webhook URL",
839
- advanced: true
840
- },
841
- skipSignatureVerification: {
842
- label: "Skip Signature Verification",
843
- advanced: true
844
- },
845
- store: {
846
- label: "Call Log Store Path",
847
- advanced: true
848
- },
849
- agentId: {
850
- label: "Response Agent ID",
851
- help: "Agent workspace used for voice response generation. Defaults to \"main\".",
852
- advanced: true
853
- },
854
- responseModel: {
855
- label: "Response Model",
856
- help: "Optional override. Falls back to the runtime default model when unset.",
857
- advanced: true
858
- },
859
- responseSystemPrompt: {
860
- label: "Response System Prompt",
861
- advanced: true
862
- },
863
- responseTimeoutMs: {
864
- label: "Response Timeout (ms)",
865
- advanced: true
866
- }
867
- }
868
- };
773
+ const voiceCallConfigSchema = { parse(value) {
774
+ const config = asOptionalRecord(value) ?? {};
775
+ const enabled = typeof config.enabled === "boolean" ? config.enabled : true;
776
+ return VoiceCallConfigSchema.parse({
777
+ ...config,
778
+ enabled,
779
+ provider: config.provider ?? (enabled ? "mock" : void 0)
780
+ });
781
+ } };
869
782
  const VoiceCallToolSchema = Type.Union([
870
783
  Type.Object({
871
784
  action: Type.Literal("initiate_call"),
@@ -873,7 +786,6 @@ const VoiceCallToolSchema = Type.Union([
873
786
  message: Type.String({ description: "Intro message" }),
874
787
  mode: Type.Optional(Type.Union([Type.Literal("notify"), Type.Literal("conversation")])),
875
788
  sessionKey: Type.Optional(Type.String({ description: "OpenClaw session key for the call" })),
876
- requesterSessionKey: Type.Optional(Type.String({ description: "OpenClaw session key that initiated the call" })),
877
789
  dtmfSequence: Type.Optional(Type.String({ description: "DTMF digits to play before connect" }))
878
790
  }),
879
791
  Type.Object({
@@ -905,38 +817,28 @@ const VoiceCallToolSchema = Type.Union([
905
817
  sid: Type.Optional(Type.String({ description: "Call SID" })),
906
818
  message: Type.Optional(Type.String({ description: "Optional intro message" })),
907
819
  sessionKey: Type.Optional(Type.String({ description: "OpenClaw session key for the call" })),
908
- requesterSessionKey: Type.Optional(Type.String({ description: "OpenClaw session key that initiated the call" })),
909
820
  dtmfSequence: Type.Optional(Type.String({ description: "DTMF digits to play before connect" }))
910
821
  })
911
822
  ]);
912
- function asParamRecord(params) {
913
- return params && typeof params === "object" && !Array.isArray(params) ? params : {};
914
- }
915
823
  function isCliOnlyProcess() {
916
824
  return process.env.OPENCLAW_CLI === "1" && !process.argv.slice(2).includes("gateway");
917
825
  }
918
- function toVoiceCallStatus(call) {
919
- return {
920
- callId: call.callId,
921
- ...call.providerCallId !== void 0 ? { providerCallId: call.providerCallId } : {},
922
- provider: call.provider,
923
- direction: call.direction,
924
- state: call.state,
925
- startedAt: call.startedAt,
926
- ...call.answeredAt !== void 0 ? { answeredAt: call.answeredAt } : {},
927
- ...call.endedAt !== void 0 ? { endedAt: call.endedAt } : {},
928
- ...call.endReason !== void 0 ? { endReason: call.endReason } : {}
929
- };
826
+ const VOICE_CALL_RUNTIME_COORDINATOR_KEY = Symbol.for("openclaw.voice-call.runtimeCoordinator");
827
+ var VoiceCallRuntimeLifecycleError = class extends Error {};
828
+ function getVoiceCallRuntimeCoordinator() {
829
+ return resolveGlobalSingleton(VOICE_CALL_RUNTIME_COORDINATOR_KEY, () => ({
830
+ epochCounter: 0,
831
+ registeredEpoch: 0
832
+ }));
930
833
  }
931
- const VOICE_CALL_RUNTIME_KEY = Symbol.for("openclaw.voice-call.runtime");
932
- const VOICE_CALL_RUNTIME_PROMISE_KEY = Symbol.for("openclaw.voice-call.runtimePromise");
933
- const VOICE_CALL_RUNTIME_STOP_PROMISE_KEY = Symbol.for("openclaw.voice-call.runtimeStopPromise");
934
- function getVoiceCallRuntimeGlobalState() {
935
- const state = globalThis;
936
- state[VOICE_CALL_RUNTIME_KEY] ??= null;
937
- state[VOICE_CALL_RUNTIME_PROMISE_KEY] ??= null;
938
- state[VOICE_CALL_RUNTIME_STOP_PROMISE_KEY] ??= null;
939
- return state;
834
+ function activateVoiceCallRuntimeGeneration(coordinator, generation) {
835
+ if (generation.epoch < coordinator.registeredEpoch) throw new VoiceCallRuntimeLifecycleError("Voice call runtime generation was superseded; use the current plugin registration");
836
+ if (generation.retired) throw new VoiceCallRuntimeLifecycleError("Voice call runtime generation is retired; use the current plugin registration");
837
+ if (coordinator.current !== generation) {
838
+ if (coordinator.current) coordinator.current.retired = true;
839
+ coordinator.current = generation;
840
+ coordinator.registeredEpoch = generation.epoch;
841
+ }
940
842
  }
941
843
  var voice_call_default = definePluginEntry({
942
844
  id: "voice-call",
@@ -946,304 +848,122 @@ var voice_call_default = definePluginEntry({
946
848
  register(api) {
947
849
  const config = resolveVoiceCallConfig(voiceCallConfigSchema.parse(api.pluginConfig));
948
850
  const validation = validateProviderConfig(config);
949
- const runtimeState = getVoiceCallRuntimeGlobalState();
851
+ const runtimeCoordinator = getVoiceCallRuntimeCoordinator();
852
+ const runtimeGeneration = api.registrationMode !== "full" && runtimeCoordinator.current ? runtimeCoordinator.current : {
853
+ epoch: ++runtimeCoordinator.epochCounter,
854
+ retired: false
855
+ };
950
856
  const continueOperationStore = createVoiceCallContinueOperationStore({
951
857
  config,
952
858
  coreConfig: api.config
953
859
  });
954
860
  const ensureRuntime = async () => {
861
+ activateVoiceCallRuntimeGeneration(runtimeCoordinator, runtimeGeneration);
955
862
  if (!config.enabled) throw new Error("Voice call disabled in plugin config");
956
863
  if (!validation.valid) throw new Error(validation.errors.join("; "));
957
864
  while (true) {
958
- if (runtimeState[VOICE_CALL_RUNTIME_STOP_PROMISE_KEY]) {
959
- await runtimeState[VOICE_CALL_RUNTIME_STOP_PROMISE_KEY];
960
- continue;
961
- }
962
- const runtime = runtimeState[VOICE_CALL_RUNTIME_KEY];
963
- if (runtime) return runtime;
964
- let runtimePromise = runtimeState[VOICE_CALL_RUNTIME_PROMISE_KEY];
965
- if (!runtimePromise) {
966
- runtimePromise = createVoiceCallRuntime({
967
- config,
968
- coreConfig: api.config,
969
- fullConfig: api.config,
970
- agentRuntime: api.runtime.agent,
971
- stateRuntime: api.runtime.state,
972
- ttsRuntime: api.runtime.tts,
973
- logger: api.logger
974
- });
975
- runtimeState[VOICE_CALL_RUNTIME_PROMISE_KEY] = runtimePromise;
976
- }
977
- try {
978
- const createdRuntime = await runtimePromise;
979
- if (runtimeState[VOICE_CALL_RUNTIME_STOP_PROMISE_KEY]) continue;
980
- if (runtimeState[VOICE_CALL_RUNTIME_PROMISE_KEY] !== runtimePromise) continue;
981
- runtimeState[VOICE_CALL_RUNTIME_KEY] = createdRuntime;
982
- return createdRuntime;
983
- } catch (err) {
984
- if (runtimeState[VOICE_CALL_RUNTIME_PROMISE_KEY] === runtimePromise) {
985
- runtimeState[VOICE_CALL_RUNTIME_PROMISE_KEY] = null;
986
- runtimeState[VOICE_CALL_RUNTIME_KEY] = null;
865
+ activateVoiceCallRuntimeGeneration(runtimeCoordinator, runtimeGeneration);
866
+ const slot = runtimeCoordinator.slot;
867
+ if (slot) {
868
+ if (slot.owner !== runtimeGeneration) {
869
+ if (slot.state === "stopping") {
870
+ await slot.promise;
871
+ continue;
872
+ }
873
+ throw new VoiceCallRuntimeLifecycleError("A previous voice call runtime generation is still active; retry after it stops");
987
874
  }
988
- throw err;
989
- }
990
- }
991
- };
992
- const respondError = (respond, message, code = ErrorCodes.UNAVAILABLE) => {
993
- respond(false, void 0, errorShape(code, message));
994
- };
995
- const sendError = (respond, err) => {
996
- respondError(respond, formatErrorMessage(err));
997
- };
998
- const describeHistoricalCall = async (rt, callId) => {
999
- const call = await rt.manager.getCallFromMemoryOrStore(callId);
1000
- if (!call) return;
1001
- const endedAt = timestampMsToIsoString(call.endedAt);
1002
- return `call is not active (${[
1003
- `last state=${call.state}`,
1004
- call.endReason ? `endReason=${call.endReason}` : void 0,
1005
- endedAt ? `endedAt=${endedAt}` : void 0
1006
- ].filter(Boolean).join(", ")})`;
1007
- };
1008
- const resolveCallMessageRequest = async (params) => {
1009
- const callId = normalizeOptionalString(params?.callId) ?? "";
1010
- const message = normalizeOptionalString(params?.message) ?? "";
1011
- if (!callId || !message) return { error: "callId and message required" };
1012
- const rt = await ensureRuntime();
1013
- const activeCall = rt.manager.getCall(callId) ?? rt.manager.getCallByProviderCallId(callId);
1014
- if (activeCall) return {
1015
- rt,
1016
- callId: activeCall.callId,
1017
- message
1018
- };
1019
- return { error: await describeHistoricalCall(rt, callId) ?? "Call not found" };
1020
- };
1021
- const initiateCallAndRespond = async (params) => {
1022
- const result = await params.rt.manager.initiateCall(params.to, params.sessionKey, {
1023
- message: params.message,
1024
- mode: params.mode,
1025
- dtmfSequence: params.dtmfSequence,
1026
- ...params.requesterSessionKey ? { requesterSessionKey: params.requesterSessionKey } : {},
1027
- ...params.agentId ? { agentId: params.agentId } : {}
1028
- });
1029
- if (!result.success) {
1030
- respondError(params.respond, result.error || "initiate failed");
1031
- return;
1032
- }
1033
- params.respond(true, {
1034
- callId: result.callId,
1035
- initiated: true
1036
- });
1037
- };
1038
- const respondToCallMessageAction = async (params) => {
1039
- const request = await resolveCallMessageRequest(params.requestParams);
1040
- if ("error" in request) {
1041
- respondError(params.respond, request.error ?? "callId and message required", ErrorCodes.INVALID_REQUEST);
1042
- return;
1043
- }
1044
- const result = await params.action(request);
1045
- if (!result.success) {
1046
- respondError(params.respond, result.error || params.failure);
1047
- return;
1048
- }
1049
- params.respond(true, params.includeTranscript ? {
1050
- success: true,
1051
- transcript: result.transcript
1052
- } : { success: true });
1053
- };
1054
- api.registerGatewayMethod("voicecall.initiate", async ({ params, respond }) => {
1055
- try {
1056
- const message = normalizeOptionalString(params?.message) ?? "";
1057
- if (!message) {
1058
- respondError(respond, "message required", ErrorCodes.INVALID_REQUEST);
1059
- return;
1060
- }
1061
- const rt = await ensureRuntime();
1062
- const to = normalizeOptionalString(params?.to) ?? rt.config.toNumber;
1063
- if (!to) {
1064
- respondError(respond, "to required", ErrorCodes.INVALID_REQUEST);
1065
- return;
1066
- }
1067
- const mode = params?.mode === "notify" || params?.mode === "conversation" ? params.mode : void 0;
1068
- await initiateCallAndRespond({
1069
- rt,
1070
- respond,
1071
- to,
1072
- message,
1073
- mode,
1074
- sessionKey: normalizeOptionalString(params?.sessionKey),
1075
- requesterSessionKey: normalizeOptionalString(params?.requesterSessionKey)
1076
- });
1077
- } catch (err) {
1078
- sendError(respond, err);
1079
- }
1080
- }, VOICE_CALL_WRITE_METHOD_SCOPE);
1081
- api.registerGatewayMethod("voicecall.continue", async ({ params, respond }) => {
1082
- try {
1083
- await respondToCallMessageAction({
1084
- requestParams: params,
1085
- respond,
1086
- action: (request) => request.rt.manager.continueCall(request.callId, request.message),
1087
- failure: "continue failed",
1088
- includeTranscript: true
1089
- });
1090
- } catch (err) {
1091
- sendError(respond, err);
1092
- }
1093
- }, VOICE_CALL_WRITE_METHOD_SCOPE);
1094
- api.registerGatewayMethod("voicecall.continue.start", async ({ params, respond }) => {
1095
- try {
1096
- const request = await resolveCallMessageRequest(params);
1097
- if ("error" in request) {
1098
- respondError(respond, request.error ?? "callId and message required", ErrorCodes.INVALID_REQUEST);
1099
- return;
1100
- }
1101
- respond(true, continueOperationStore.start(request));
1102
- } catch (err) {
1103
- sendError(respond, err);
1104
- }
1105
- }, VOICE_CALL_WRITE_METHOD_SCOPE);
1106
- api.registerGatewayMethod("voicecall.continue.result", async ({ params, respond }) => {
1107
- try {
1108
- const operationId = normalizeOptionalString(params?.operationId) ?? "";
1109
- if (!operationId) {
1110
- respondError(respond, "operationId required", ErrorCodes.INVALID_REQUEST);
1111
- return;
1112
- }
1113
- const operation = continueOperationStore.read(operationId);
1114
- if (!operation.ok) {
1115
- respondError(respond, operation.error, ErrorCodes.INVALID_REQUEST);
1116
- return;
1117
- }
1118
- respond(true, operation.payload);
1119
- } catch (err) {
1120
- sendError(respond, err);
1121
- }
1122
- }, VOICE_CALL_READ_METHOD_SCOPE);
1123
- api.registerGatewayMethod("voicecall.speak", async ({ params, respond }) => {
1124
- try {
1125
- const request = await resolveCallMessageRequest(params);
1126
- if ("error" in request) {
1127
- respondError(respond, request.error ?? "callId and message required", ErrorCodes.INVALID_REQUEST);
1128
- return;
1129
- }
1130
- if (request.rt.config.realtime.enabled) {
1131
- const realtimeResult = request.rt.webhookServer.speakRealtime(request.callId, request.message);
1132
- if (realtimeResult.success) {
1133
- respond(true, { success: true });
1134
- return;
875
+ if (slot.state === "running") return slot.runtime;
876
+ if (slot.state === "stopping") {
877
+ await slot.promise;
878
+ continue;
1135
879
  }
1136
- if (params?.allowTwimlFallback === false) {
1137
- respond(true, {
1138
- success: false,
1139
- error: realtimeResult.error ?? "Realtime bridge is not active"
1140
- });
1141
- return;
880
+ let createdRuntime;
881
+ try {
882
+ createdRuntime = await slot.promise;
883
+ } catch (err) {
884
+ if (runtimeCoordinator.slot === slot) runtimeCoordinator.slot = void 0;
885
+ throw err;
1142
886
  }
887
+ activateVoiceCallRuntimeGeneration(runtimeCoordinator, runtimeGeneration);
888
+ if (runtimeCoordinator.slot !== slot) continue;
889
+ runtimeCoordinator.slot = {
890
+ state: "running",
891
+ owner: runtimeGeneration,
892
+ runtime: createdRuntime
893
+ };
894
+ return createdRuntime;
1143
895
  }
1144
- const result = await request.rt.manager.speak(request.callId, request.message);
1145
- if (!result.success) {
1146
- respondError(respond, result.error || "speak failed");
1147
- return;
1148
- }
1149
- respond(true, { success: true });
1150
- } catch (err) {
1151
- sendError(respond, err);
1152
- }
1153
- }, VOICE_CALL_WRITE_METHOD_SCOPE);
1154
- api.registerGatewayMethod("voicecall.dtmf", async ({ params, respond }) => {
1155
- try {
1156
- const callId = normalizeOptionalString(params?.callId) ?? "";
1157
- const digits = normalizeOptionalString(params?.digits) ?? "";
1158
- if (!callId || !digits) {
1159
- respondError(respond, "callId and digits required", ErrorCodes.INVALID_REQUEST);
1160
- return;
1161
- }
1162
- const result = await (await ensureRuntime()).manager.sendDtmf(callId, digits);
1163
- if (!result.success) {
1164
- respondError(respond, result.error || "dtmf failed");
1165
- return;
1166
- }
1167
- respond(true, { success: true });
1168
- } catch (err) {
1169
- sendError(respond, err);
896
+ const runtimePromise = createVoiceCallRuntime({
897
+ config,
898
+ coreConfig: api.config,
899
+ fullConfig: api.config,
900
+ agentRuntime: api.runtime.agent,
901
+ stateRuntime: api.runtime.state,
902
+ ttsRuntime: api.runtime.tts,
903
+ logger: api.logger
904
+ });
905
+ runtimeCoordinator.slot = {
906
+ state: "starting",
907
+ owner: runtimeGeneration,
908
+ promise: runtimePromise
909
+ };
1170
910
  }
1171
- }, VOICE_CALL_WRITE_METHOD_SCOPE);
1172
- api.registerGatewayMethod("voicecall.end", async ({ params, respond }) => {
1173
- try {
1174
- const callId = normalizeOptionalString(params?.callId) ?? "";
1175
- if (!callId) {
1176
- respondError(respond, "callId required", ErrorCodes.INVALID_REQUEST);
1177
- return;
1178
- }
1179
- const result = await (await ensureRuntime()).manager.endCall(callId);
1180
- if (!result.success) {
1181
- respondError(respond, result.error || "end failed");
1182
- return;
911
+ };
912
+ const commands = createVoiceCallCommandService(ensureRuntime);
913
+ const registerGatewayCommand = (method, handler, scope) => {
914
+ api.registerGatewayMethod(method, async (options) => {
915
+ try {
916
+ options.respond(true, await handler(options));
917
+ } catch (err) {
918
+ const code = err instanceof VoiceCallCommandInputError ? ErrorCodes.INVALID_REQUEST : ErrorCodes.UNAVAILABLE;
919
+ options.respond(false, void 0, errorShape(code, formatErrorMessage(err)));
1183
920
  }
1184
- respond(true, { success: true });
1185
- } catch (err) {
1186
- sendError(respond, err);
1187
- }
921
+ }, scope);
922
+ };
923
+ registerGatewayCommand("voicecall.initiate", async ({ params }) => {
924
+ const message = normalizeOptionalString(params?.message);
925
+ if (!message) throw new VoiceCallCommandInputError("message required");
926
+ return await commands.initiate({
927
+ to: normalizeOptionalString(params?.to),
928
+ message,
929
+ mode: params?.mode === "notify" || params?.mode === "conversation" ? params.mode : void 0,
930
+ sessionKey: normalizeOptionalString(params?.sessionKey),
931
+ requesterSessionKey: normalizeOptionalString(params?.requesterSessionKey)
932
+ });
1188
933
  }, VOICE_CALL_WRITE_METHOD_SCOPE);
1189
- api.registerGatewayMethod("voicecall.status", async ({ params, respond }) => {
1190
- try {
1191
- const raw = normalizeOptionalString(params?.callId) ?? normalizeOptionalString(params?.sid) ?? "";
1192
- const rt = await ensureRuntime();
1193
- if (!raw) {
1194
- respond(true, {
1195
- found: true,
1196
- calls: rt.manager.getActiveCalls().map(toVoiceCallStatus)
1197
- });
1198
- return;
1199
- }
1200
- const call = await rt.manager.getCallFromMemoryOrStore(raw);
1201
- if (!call) {
1202
- respond(true, { found: false });
1203
- return;
1204
- }
1205
- respond(true, {
1206
- found: true,
1207
- call: toVoiceCallStatus(call)
1208
- });
1209
- } catch (err) {
1210
- sendError(respond, err);
1211
- }
934
+ registerGatewayCommand("voicecall.continue", ({ params }) => commands.continueCall(normalizeOptionalString(params?.callId), normalizeOptionalString(params?.message)), VOICE_CALL_WRITE_METHOD_SCOPE);
935
+ registerGatewayCommand("voicecall.continue.start", async ({ params }) => continueOperationStore.start(await commands.prepareContinue(normalizeOptionalString(params?.callId), normalizeOptionalString(params?.message))), VOICE_CALL_WRITE_METHOD_SCOPE);
936
+ registerGatewayCommand("voicecall.continue.result", ({ params }) => {
937
+ const operationId = normalizeOptionalString(params?.operationId);
938
+ if (!operationId) throw new VoiceCallCommandInputError("operationId required");
939
+ const operation = continueOperationStore.read(operationId);
940
+ if (!operation.ok) throw new VoiceCallCommandInputError(operation.error);
941
+ return operation.payload;
1212
942
  }, VOICE_CALL_READ_METHOD_SCOPE);
1213
- api.registerGatewayMethod("voicecall.start", async ({ params, client, respond }) => {
1214
- try {
1215
- const to = normalizeOptionalString(params?.to) ?? "";
1216
- const message = normalizeOptionalString(params?.message) ?? "";
1217
- const dtmfSequence = normalizeOptionalString(params?.dtmfSequence);
1218
- const sessionKey = normalizeOptionalString(params?.sessionKey);
1219
- const requesterSessionKey = normalizeOptionalString(params?.requesterSessionKey);
1220
- const requestedAgentId = normalizeOptionalString(params?.agentId);
1221
- const normalizedAgentId = requestedAgentId ? normalizeAgentId(requestedAgentId) : void 0;
1222
- const pluginOwnerId = normalizeOptionalString(client?.internal?.pluginRuntimeOwnerId);
1223
- if (requestedAgentId && (!pluginOwnerId || normalizedAgentId !== requestedAgentId.toLowerCase())) {
1224
- respondError(respond, "agentId requires a trusted plugin caller and a valid agent id", ErrorCodes.INVALID_REQUEST);
1225
- return;
1226
- }
1227
- if (!to) {
1228
- respondError(respond, "to required", ErrorCodes.INVALID_REQUEST);
1229
- return;
1230
- }
1231
- const mode = params?.mode === "notify" || params?.mode === "conversation" ? params.mode : void 0;
1232
- const rt = await ensureRuntime();
1233
- await initiateCallAndRespond({
1234
- rt,
1235
- respond,
1236
- to,
1237
- message: message || void 0,
1238
- mode,
1239
- dtmfSequence,
1240
- sessionKey,
1241
- ...requesterSessionKey ? { requesterSessionKey } : {},
1242
- ...normalizedAgentId ? { agentId: normalizedAgentId } : {}
1243
- });
1244
- } catch (err) {
1245
- sendError(respond, err);
1246
- }
943
+ registerGatewayCommand("voicecall.speak", ({ params }) => commands.speak({
944
+ callId: normalizeOptionalString(params?.callId),
945
+ message: normalizeOptionalString(params?.message),
946
+ allowTwimlFallback: params?.allowTwimlFallback !== false
947
+ }), VOICE_CALL_WRITE_METHOD_SCOPE);
948
+ registerGatewayCommand("voicecall.dtmf", ({ params }) => commands.sendDtmf(normalizeOptionalString(params?.callId), normalizeOptionalString(params?.digits)), VOICE_CALL_WRITE_METHOD_SCOPE);
949
+ registerGatewayCommand("voicecall.end", ({ params }) => commands.endCall(normalizeOptionalString(params?.callId)), VOICE_CALL_WRITE_METHOD_SCOPE);
950
+ registerGatewayCommand("voicecall.status", ({ params }) => commands.status(normalizeOptionalString(params?.callId) ?? normalizeOptionalString(params?.sid)), VOICE_CALL_READ_METHOD_SCOPE);
951
+ registerGatewayCommand("voicecall.start", async ({ params, client }) => {
952
+ const to = normalizeOptionalString(params?.to);
953
+ const requestedAgentId = normalizeOptionalString(params?.agentId);
954
+ const normalizedAgentId = requestedAgentId ? normalizeAgentId(requestedAgentId) : void 0;
955
+ const pluginOwnerId = normalizeOptionalString(client?.internal?.pluginRuntimeOwnerId);
956
+ if (requestedAgentId && (!pluginOwnerId || normalizedAgentId !== requestedAgentId.toLowerCase())) throw new VoiceCallCommandInputError("agentId requires a trusted plugin caller and a valid agent id");
957
+ if (!to) throw new VoiceCallCommandInputError("to required");
958
+ return await commands.initiate({
959
+ to,
960
+ message: normalizeOptionalString(params?.message),
961
+ mode: params?.mode === "notify" || params?.mode === "conversation" ? params.mode : void 0,
962
+ dtmfSequence: normalizeOptionalString(params?.dtmfSequence),
963
+ sessionKey: normalizeOptionalString(params?.sessionKey),
964
+ requesterSessionKey: normalizeOptionalString(params?.requesterSessionKey),
965
+ agentId: normalizedAgentId
966
+ });
1247
967
  }, VOICE_CALL_WRITE_METHOD_SCOPE);
1248
968
  api.registerTool((toolContext) => ({
1249
969
  name: "voice_call",
@@ -1251,94 +971,52 @@ var voice_call_default = definePluginEntry({
1251
971
  description: "Make phone calls and have voice conversations via the voice-call plugin.",
1252
972
  parameters: VoiceCallToolSchema,
1253
973
  async execute(_toolCallId, params) {
1254
- const rawParams = asParamRecord(params);
1255
- const agentId = normalizeOptionalString(toolContext.agentId);
974
+ const rawParams = asNonArrayRecord(params);
975
+ const requesterSessionKey = normalizeOptionalString(toolContext.sessionKey);
976
+ const contextAgentId = normalizeOptionalString(toolContext.agentId) ?? parseAgentSessionKey(requesterSessionKey)?.agentId;
977
+ const agentId = contextAgentId ? normalizeAgentId(contextAgentId) : void 0;
1256
978
  try {
1257
- const rt = await ensureRuntime();
979
+ await ensureRuntime();
1258
980
  if (typeof rawParams.action === "string") switch (rawParams.action) {
1259
981
  case "initiate_call": {
1260
- const message = normalizeOptionalString(rawParams.message) ?? "";
1261
- if (!message) throw new Error("message required");
1262
- const to = normalizeOptionalString(rawParams.to) ?? rt.config.toNumber;
1263
- if (!to) throw new Error("to required");
1264
- const result = await rt.manager.initiateCall(to, void 0, {
982
+ const message = normalizeOptionalString(rawParams.message);
983
+ if (!message) throw new VoiceCallCommandInputError("message required");
984
+ return jsonResult(await commands.initiate({
985
+ to: normalizeOptionalString(rawParams.to),
1265
986
  message,
1266
987
  dtmfSequence: normalizeOptionalString(rawParams.dtmfSequence),
1267
988
  mode: rawParams.mode === "notify" || rawParams.mode === "conversation" ? rawParams.mode : void 0,
1268
- ...agentId ? { agentId } : {}
1269
- });
1270
- if (!result.success) throw new Error(result.error || "initiate failed");
1271
- return jsonResult({
1272
- callId: result.callId,
1273
- initiated: true
1274
- });
1275
- }
1276
- case "continue_call": {
1277
- const callId = normalizeOptionalString(rawParams.callId) ?? "";
1278
- const message = normalizeOptionalString(rawParams.message) ?? "";
1279
- if (!callId || !message) throw new Error("callId and message required");
1280
- const result = await rt.manager.continueCall(callId, message);
1281
- if (!result.success) throw new Error(result.error || "continue failed");
1282
- return jsonResult({
1283
- success: true,
1284
- transcript: result.transcript
1285
- });
1286
- }
1287
- case "speak_to_user": {
1288
- const callId = normalizeOptionalString(rawParams.callId) ?? "";
1289
- const message = normalizeOptionalString(rawParams.message) ?? "";
1290
- if (!callId || !message) throw new Error("callId and message required");
1291
- const result = await rt.manager.speak(callId, message);
1292
- if (!result.success) throw new Error(result.error || "speak failed");
1293
- return jsonResult({ success: true });
1294
- }
1295
- case "send_dtmf": {
1296
- const callId = normalizeOptionalString(rawParams.callId) ?? "";
1297
- const digits = normalizeOptionalString(rawParams.digits) ?? "";
1298
- if (!callId || !digits) throw new Error("callId and digits required");
1299
- const result = await rt.manager.sendDtmf(callId, digits);
1300
- if (!result.success) throw new Error(result.error || "dtmf failed");
1301
- return jsonResult({ success: true });
1302
- }
1303
- case "end_call": {
1304
- const callId = normalizeOptionalString(rawParams.callId) ?? "";
1305
- if (!callId) throw new Error("callId required");
1306
- const result = await rt.manager.endCall(callId);
1307
- if (!result.success) throw new Error(result.error || "end failed");
1308
- return jsonResult({ success: true });
989
+ sessionKey: normalizeOptionalString(rawParams.sessionKey),
990
+ agentId,
991
+ requesterSessionKey
992
+ }));
1309
993
  }
994
+ case "continue_call": return jsonResult(await commands.continueCall(normalizeOptionalString(rawParams.callId), normalizeOptionalString(rawParams.message)));
995
+ case "speak_to_user": return jsonResult(await commands.speak({
996
+ callId: normalizeOptionalString(rawParams.callId),
997
+ message: normalizeOptionalString(rawParams.message)
998
+ }));
999
+ case "send_dtmf": return jsonResult(await commands.sendDtmf(normalizeOptionalString(rawParams.callId), normalizeOptionalString(rawParams.digits)));
1000
+ case "end_call": return jsonResult(await commands.endCall(normalizeOptionalString(rawParams.callId)));
1310
1001
  case "get_status": {
1311
- const callId = normalizeOptionalString(rawParams.callId) ?? "";
1312
- if (!callId) throw new Error("callId required");
1313
- const call = await rt.manager.getCallFromMemoryOrStore(callId);
1314
- return jsonResult(call ? {
1315
- found: true,
1316
- call: toVoiceCallStatus(call)
1317
- } : { found: false });
1002
+ const callId = normalizeOptionalString(rawParams.callId);
1003
+ if (!callId) throw new VoiceCallCommandInputError("callId required");
1004
+ return jsonResult(await commands.status(callId));
1318
1005
  }
1319
1006
  }
1320
1007
  if ((rawParams.mode ?? "call") === "status") {
1321
1008
  const sid = normalizeOptionalString(rawParams.sid) ?? "";
1322
1009
  if (!sid) throw new Error("sid required for status");
1323
- const call = await rt.manager.getCallFromMemoryOrStore(sid);
1324
- return jsonResult(call ? {
1325
- found: true,
1326
- call: toVoiceCallStatus(call)
1327
- } : { found: false });
1010
+ return jsonResult(await commands.status(sid));
1328
1011
  }
1329
- const to = normalizeOptionalString(rawParams.to) ?? rt.config.toNumber;
1330
- if (!to) throw new Error("to required for call");
1331
- const result = await rt.manager.initiateCall(to, normalizeOptionalString(rawParams.sessionKey), {
1012
+ return jsonResult(await commands.initiate({
1013
+ to: normalizeOptionalString(rawParams.to),
1332
1014
  dtmfSequence: normalizeOptionalString(rawParams.dtmfSequence),
1333
1015
  message: normalizeOptionalString(rawParams.message),
1334
- ...agentId ? { agentId } : {},
1335
- ...normalizeOptionalString(rawParams.requesterSessionKey) ? { requesterSessionKey: normalizeOptionalString(rawParams.requesterSessionKey) } : {}
1336
- });
1337
- if (!result.success) throw new Error(result.error || "initiate failed");
1338
- return jsonResult({
1339
- callId: result.callId,
1340
- initiated: true
1341
- });
1016
+ sessionKey: normalizeOptionalString(rawParams.sessionKey),
1017
+ agentId,
1018
+ requesterSessionKey
1019
+ }, "to required for call"));
1342
1020
  } catch (err) {
1343
1021
  return jsonResult({ error: formatErrorMessage(err) });
1344
1022
  }
@@ -1358,33 +1036,46 @@ var voice_call_default = definePluginEntry({
1358
1036
  id: "voicecall",
1359
1037
  start: () => {
1360
1038
  if (isCliOnlyProcess()) return;
1039
+ try {
1040
+ activateVoiceCallRuntimeGeneration(runtimeCoordinator, runtimeGeneration);
1041
+ } catch (err) {
1042
+ if (err instanceof VoiceCallRuntimeLifecycleError) return;
1043
+ throw err;
1044
+ }
1361
1045
  if (!config.enabled) return;
1362
1046
  if (!validation.valid) {
1363
1047
  api.logger.warn(`[voice-call] Runtime not started; setup incomplete: ${validation.errors.join("; ")}`);
1364
1048
  return;
1365
1049
  }
1366
1050
  ensureRuntime().catch((err) => {
1051
+ const staleGeneration = runtimeGeneration.retired || runtimeGeneration.epoch < runtimeCoordinator.registeredEpoch;
1052
+ if (err instanceof VoiceCallRuntimeLifecycleError && staleGeneration) return;
1367
1053
  api.logger.error(`[voice-call] Failed to start runtime: ${formatErrorMessage(err)}`);
1368
1054
  });
1369
1055
  },
1370
1056
  stop: async () => {
1371
- if (runtimeState[VOICE_CALL_RUNTIME_STOP_PROMISE_KEY]) {
1372
- await runtimeState[VOICE_CALL_RUNTIME_STOP_PROMISE_KEY];
1373
- return;
1374
- }
1375
- const runtime = runtimeState[VOICE_CALL_RUNTIME_KEY];
1376
- const runtimePromise = runtimeState[VOICE_CALL_RUNTIME_PROMISE_KEY];
1377
- if (!runtime && !runtimePromise) return;
1378
- runtimeState[VOICE_CALL_RUNTIME_KEY] = null;
1379
- runtimeState[VOICE_CALL_RUNTIME_PROMISE_KEY] = null;
1380
- const stopPromise = (async () => {
1381
- await (runtime ?? await runtimePromise).stop();
1382
- })();
1383
- runtimeState[VOICE_CALL_RUNTIME_STOP_PROMISE_KEY] = stopPromise;
1057
+ if (runtimeGeneration.stopPromise) return await runtimeGeneration.stopPromise;
1058
+ runtimeGeneration.retired = true;
1059
+ const ownedSlot = runtimeCoordinator.slot?.owner === runtimeGeneration ? runtimeCoordinator.slot : void 0;
1060
+ let stoppingSlot;
1061
+ const stopPromise = ownedSlot?.state === "stopping" ? ownedSlot.promise : Promise.resolve().then(async () => {
1062
+ if (!ownedSlot) return;
1063
+ await (ownedSlot.state === "running" ? ownedSlot.runtime : await ownedSlot.promise).stop();
1064
+ });
1065
+ runtimeGeneration.stopPromise = stopPromise;
1066
+ if (ownedSlot && ownedSlot.state !== "stopping") {
1067
+ stoppingSlot = {
1068
+ state: "stopping",
1069
+ owner: runtimeGeneration,
1070
+ promise: stopPromise
1071
+ };
1072
+ if (runtimeCoordinator.slot === ownedSlot) runtimeCoordinator.slot = stoppingSlot;
1073
+ } else if (ownedSlot) stoppingSlot = ownedSlot;
1384
1074
  try {
1385
1075
  await stopPromise;
1386
1076
  } finally {
1387
- if (runtimeState[VOICE_CALL_RUNTIME_STOP_PROMISE_KEY] === stopPromise) runtimeState[VOICE_CALL_RUNTIME_STOP_PROMISE_KEY] = null;
1077
+ if (stoppingSlot && runtimeCoordinator.slot === stoppingSlot) runtimeCoordinator.slot = void 0;
1078
+ if (runtimeCoordinator.current === runtimeGeneration) runtimeCoordinator.current = void 0;
1388
1079
  }
1389
1080
  }
1390
1081
  });