@lawpath-tech/openclaw 2026.2.21-20 → 2026.2.21-22

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 (93) hide show
  1. package/dist/{audio-preflight-Da7vejCH.js → audio-preflight-CODznzqu.js} +4 -4
  2. package/dist/{audio-preflight-CLqoZYgI.js → audio-preflight-omzaYFGD.js} +4 -4
  3. package/dist/build-info.json +3 -3
  4. package/dist/bundled/boot-md/handler.js +6 -6
  5. package/dist/bundled/session-memory/handler.js +6 -6
  6. package/dist/canvas-host/a2ui/.bundle.hash +1 -1
  7. package/dist/{chrome-Bqbv_ZSj.js → chrome-BAv9fEiv.js} +7 -7
  8. package/dist/{chrome-CZuniMYN.js → chrome-lRUqqgSD.js} +7 -7
  9. package/dist/{deliver-0ThKlzQo.js → deliver-B2d2N8OJ.js} +1 -1
  10. package/dist/{deliver-CKH_FhS0.js → deliver-CLJRPnfx.js} +1 -1
  11. package/dist/extensionAPI.js +6 -6
  12. package/dist/{image-88q3KE-C.js → image--DDZnw-F.js} +1 -1
  13. package/dist/{image-CxPjVob-.js → image-Ci28h-op.js} +1 -1
  14. package/dist/llm-slug-generator.js +6 -6
  15. package/dist/{pi-embedded-ZvazjIyF.js → pi-embedded-C_B8_quB.js} +16 -16
  16. package/dist/{pi-embedded-Dz24QZz9.js → pi-embedded-CoxlOLW5.js} +16 -16
  17. package/dist/{pi-embedded-helpers-B0Kht0I2.js → pi-embedded-helpers-Cd0S0WfR.js} +4 -4
  18. package/dist/{pi-embedded-helpers-CSE0v99A.js → pi-embedded-helpers-Ll4Lztu1.js} +4 -4
  19. package/dist/plugin-sdk/{accounts-DUBJHEgi.js → accounts-BcQo4OUu.js} +1 -1
  20. package/dist/plugin-sdk/{accounts-wSu5JyDM.js → accounts-DEsAwvaZ.js} +1 -1
  21. package/dist/plugin-sdk/{accounts-Ciwy7Yxd.js → accounts-fnLsjAOi.js} +3 -3
  22. package/dist/plugin-sdk/{active-listener-Bnau4oiu.js → active-listener-CeaKSm29.js} +1 -1
  23. package/dist/plugin-sdk/{agent-scope-cT7IxdHY.js → agent-scope-D5PlT0Z0.js} +2 -2
  24. package/dist/plugin-sdk/{api-key-rotation-ElOMCP9r.js → api-key-rotation-B3O1qn0m.js} +1 -1
  25. package/dist/plugin-sdk/{audio-preflight-DtozH3sJ.js → audio-preflight-CYSy3uuZ.js} +24 -24
  26. package/dist/plugin-sdk/{bindings-CKqdifyU.js → bindings-C0XEX0yK.js} +2 -2
  27. package/dist/plugin-sdk/{channel-activity-CCnr-9bS.js → channel-activity-WgE8Z4Bb.js} +1 -1
  28. package/dist/plugin-sdk/{channel-web-BHNS_E7F.js → channel-web-Nw-bUgtV.js} +22 -22
  29. package/dist/plugin-sdk/{chrome-BO7SUTgY.js → chrome-C1xnOQsc.js} +3 -3
  30. package/dist/plugin-sdk/{chunk-DyzO7IVo.js → chunk-CQk9vawA.js} +1 -1
  31. package/dist/plugin-sdk/{command-format-DcBXOX9Z.js → command-format-DHXa0Clc.js} +1 -1
  32. package/dist/plugin-sdk/{commands-registry-BsSKPwgj.js → commands-registry-yTZdwtLj.js} +4 -4
  33. package/dist/plugin-sdk/{config-9OZuRn0C.js → config--kW4R0el.js} +9 -9
  34. package/dist/plugin-sdk/{deliver-BO_wt0wd.js → deliver-C2gjA_UH.js} +10 -10
  35. package/dist/plugin-sdk/{diagnostic-DKyVS3IK.js → diagnostic-Up1aMyxZ.js} +1 -1
  36. package/dist/plugin-sdk/{image-C31B_R8P.js → image-Bcc99tVA.js} +4 -4
  37. package/dist/plugin-sdk/{image-ops-B8aJIly2.js → image-ops-C6RLbtSJ.js} +1 -1
  38. package/dist/plugin-sdk/index.js +53 -53
  39. package/dist/plugin-sdk/{ir-CD5VqKGg.js → ir-Cwi1KVG5.js} +4 -4
  40. package/dist/plugin-sdk/{local-roots-D4PCK2jj.js → local-roots-6BSl8AoA.js} +3 -3
  41. package/dist/plugin-sdk/{login-CW17h3xU.js → login-Cau5eVVj.js} +7 -7
  42. package/dist/plugin-sdk/{login-qr-CMdk06CS.js → login-qr-CLWtfA9m.js} +9 -9
  43. package/dist/plugin-sdk/{manager-CZPPxI6R.js → manager-C3Eb_8Hz.js} +8 -8
  44. package/dist/plugin-sdk/{manifest-registry-sgHvjA6E.js → manifest-registry-DvEm2HHf.js} +1 -1
  45. package/dist/plugin-sdk/{markdown-tables-Bo-r4msS.js → markdown-tables-cJqvQe9j.js} +1 -1
  46. package/dist/plugin-sdk/{message-channel-DmB4bGFh.js → message-channel-DY9wVB2A.js} +1 -1
  47. package/dist/plugin-sdk/{model-selection-CXooYLb2.js → model-selection-BjVTBZiY.js} +4 -4
  48. package/dist/plugin-sdk/{outbound-dAcQHyyr.js → outbound-Co1QSkdS.js} +7 -7
  49. package/dist/plugin-sdk/{outbound-attachment-CCxMtk8m.js → outbound-attachment-CsmRyZPI.js} +2 -2
  50. package/dist/plugin-sdk/{pi-auth-json-C-euHc3x.js → pi-auth-json-DfEOhoTK.js} +5 -5
  51. package/dist/plugin-sdk/{pi-embedded-helpers-DU0nLxRU.js → pi-embedded-helpers-D3EACnFn.js} +17 -17
  52. package/dist/plugin-sdk/{plugins-DIzw7Ncy.js → plugins-Dyz98ZJX.js} +4 -4
  53. package/dist/plugin-sdk/{pw-ai-CYmDAwXD.js → pw-ai-smoXiGqZ.js} +8 -8
  54. package/dist/plugin-sdk/{qmd-manager-DQiT4WTr.js → qmd-manager-CUhJ7WG2.js} +4 -4
  55. package/dist/plugin-sdk/{registry-O1yza0Vh.js → registry-DL-33c9X.js} +2 -2
  56. package/dist/plugin-sdk/{replies-Bnu7HnvW.js → replies-Bkaifx0j.js} +3 -3
  57. package/dist/plugin-sdk/{reply-DEepmBkT.js → reply-Cb54ddUA.js} +78 -78
  58. package/dist/plugin-sdk/{reply-prefix-BF0omhZE.js → reply-prefix-5vdbga0R.js} +1 -1
  59. package/dist/plugin-sdk/{resolve-outbound-target-DzZIT1V1.js → resolve-outbound-target-CP7QDN14.js} +2 -2
  60. package/dist/plugin-sdk/{resolve-route-B63ztF-V.js → resolve-route-eopifBrM.js} +3 -3
  61. package/dist/plugin-sdk/{retry-JFNwfUew.js → retry-5P3d0rYw.js} +1 -1
  62. package/dist/plugin-sdk/{runner-DztRiWhh.js → runner-DqwPn56n.js} +9 -9
  63. package/dist/plugin-sdk/{send-DEV4iiFj.js → send-C2NQjVF9.js} +10 -10
  64. package/dist/plugin-sdk/{send-D-spKwjf.js → send-D_X6p9H0.js} +10 -10
  65. package/dist/plugin-sdk/{send-CK3DCPv-.js → send-DnFB-v5n.js} +6 -6
  66. package/dist/plugin-sdk/{send-D1C2sJG1.js → send-PPtjKj-0.js} +7 -7
  67. package/dist/plugin-sdk/{send-DrYvH25B.js → send-_uj_NuFI.js} +6 -6
  68. package/dist/plugin-sdk/{session-DRJl5tKC.js → session-CZngPTYk.js} +4 -4
  69. package/dist/plugin-sdk/{skill-commands-Dv5AeOZQ.js → skill-commands-Cr54qPdy.js} +5 -5
  70. package/dist/plugin-sdk/{skills-5hEda4_Y.js → skills-Dcu4o-9l.js} +7 -7
  71. package/dist/plugin-sdk/{sqlite-DUu0dwic.js → sqlite-ulpcowfN.js} +1 -1
  72. package/dist/plugin-sdk/{store-DoRXD0QB.js → store-EUYCRBvs.js} +2 -2
  73. package/dist/plugin-sdk/{subsystem-C4Rh0kdL.js → subsystem-C9WnrKN8.js} +1 -1
  74. package/dist/plugin-sdk/{tables-C_81Ve2l.js → tables-CMdQECje.js} +1 -1
  75. package/dist/plugin-sdk/{target-errors-C638yp2-.js → target-errors-CrR2lAF7.js} +2 -2
  76. package/dist/plugin-sdk/{thinking-DexKPSsI.js → thinking-C7zYA2s9.js} +5 -5
  77. package/dist/plugin-sdk/{tokens-K9ITCatc.js → tokens-2475WU2Z.js} +1 -1
  78. package/dist/plugin-sdk/{tool-images-BvjD_HnR.js → tool-images-DCvC8yL7.js} +2 -2
  79. package/dist/plugin-sdk/{tool-loop-detection-BBRtoox8.js → tool-loop-detection-D0ADW0h2.js} +2 -2
  80. package/dist/plugin-sdk/web-BVkjyY4A.js +65 -0
  81. package/dist/plugin-sdk/{whatsapp-actions-D8i2vCxS.js → whatsapp-actions-DC4RRNqv.js} +21 -21
  82. package/dist/{pw-ai-S3cpSYOy.js → pw-ai-C-kqYO4L.js} +1 -1
  83. package/dist/{pw-ai-D-_aGzdQ.js → pw-ai-KrN0mqVH.js} +1 -1
  84. package/dist/{runner-D1eXJZ8T.js → runner-ChBxge-W.js} +1 -1
  85. package/dist/{runner-a43IsYad.js → runner-Dq-qfrq7.js} +1 -1
  86. package/dist/{web-Y49Dumye.js → web-Crj4x6uL.js} +6 -6
  87. package/dist/{web-CVSol55V.js → web-DiYh3u92.js} +6 -6
  88. package/extensions/voice-call/package.json +2 -0
  89. package/extensions/voice-call/src/config.ts +16 -3
  90. package/extensions/voice-call/src/providers/realtime-session.ts +210 -0
  91. package/extensions/voice-call/src/webhook.ts +172 -8
  92. package/package.json +3 -1
  93. package/dist/plugin-sdk/web-DBo-6RyH.js +0 -65
@@ -0,0 +1,210 @@
1
+ /**
2
+ * OpenAI Realtime API session provider for voice calls.
3
+ *
4
+ * Replaces the STT → LLM → TTS chain with native speech-to-speech via the
5
+ * OpenAI Agents SDK RealtimeSession + TwilioRealtimeTransportLayer.
6
+ * Achieves ~200-500ms response latency vs 2-14s with the embedded agent.
7
+ */
8
+
9
+ import { execFile } from "node:child_process";
10
+ import { promisify } from "node:util";
11
+ import type { WebSocket as NodeWebSocket } from "ws";
12
+ import { RealtimeAgent, RealtimeSession, tool, backgroundResult } from "@openai/agents/realtime";
13
+ import { TwilioRealtimeTransportLayer } from "@openai/agents-extensions";
14
+ import { z } from "zod";
15
+ import type { VoiceCallConfig } from "../config.js";
16
+
17
+ const execFileAsync = promisify(execFile);
18
+
19
+ const SCRIPT_TIMEOUT_MS = 15_000;
20
+
21
+ async function runScript(command: string, args: string[]): Promise<string> {
22
+ try {
23
+ const { stdout, stderr } = await execFileAsync(command, args, {
24
+ timeout: SCRIPT_TIMEOUT_MS,
25
+ maxBuffer: 1024 * 512,
26
+ env: process.env,
27
+ });
28
+ return stdout.trim() || stderr.trim() || "(no output)";
29
+ } catch (err) {
30
+ const msg = err instanceof Error ? err.message : String(err);
31
+ return `Error: ${msg}`;
32
+ }
33
+ }
34
+
35
+ function buildRealtimeTools() {
36
+ const stripeGross = tool({
37
+ name: "stripe_gross_mtd",
38
+ description:
39
+ "Get Stripe gross revenue month-to-date. Returns current MTD figures, daily average, and target tracking.",
40
+ parameters: z.object({}),
41
+ execute: async () => {
42
+ return backgroundResult(
43
+ await runScript("node", ["/root/clawd/skills/stripe/scripts/stripe.js", "gross-mtd"]),
44
+ );
45
+ },
46
+ });
47
+
48
+ const stripeDailySummary = tool({
49
+ name: "stripe_daily_summary",
50
+ description:
51
+ "Get Stripe daily summary for a specific date (charges, refunds, net). Defaults to yesterday if no date provided.",
52
+ parameters: z.object({
53
+ date: z.string().optional().describe("Date in YYYY-MM-DD format. Defaults to yesterday."),
54
+ }),
55
+ execute: async ({ date }) => {
56
+ const args = ["/root/clawd/skills/stripe/scripts/stripe.js", "daily-summary"];
57
+ if (date) args.push("--date", date);
58
+ return backgroundResult(await runScript("node", args));
59
+ },
60
+ });
61
+
62
+ const webSearch = tool({
63
+ name: "web_search",
64
+ description:
65
+ "Search the web for current information. Use for news, current events, factual lookups.",
66
+ parameters: z.object({
67
+ query: z.string().describe("Search query"),
68
+ }),
69
+ execute: async ({ query }) => {
70
+ return backgroundResult(
71
+ await runScript("node", [
72
+ "-e",
73
+ `import('undici').then(({fetch})=>fetch('https://api.search.brave.com/res/v1/web/search?q='+encodeURIComponent(${JSON.stringify(query)}),{headers:{'X-Subscription-Token':process.env.BRAVE_SEARCH_API_KEY||''}}).then(r=>r.json()).then(d=>{const results=(d.web?.results||[]).slice(0,5).map(r=>r.title+': '+r.description).join('\\n');console.log(results||'No results')})).catch(e=>console.error(e.message))`,
74
+ ]),
75
+ );
76
+ },
77
+ });
78
+
79
+ const memoryRecall = tool({
80
+ name: "memory_recall",
81
+ description:
82
+ "Recall stored memories and context. Use to check preferences, past decisions, or remembered facts.",
83
+ parameters: z.object({
84
+ query: z.string().describe("What to recall — topic, person name, or context"),
85
+ }),
86
+ execute: async ({ query }) => {
87
+ return backgroundResult(
88
+ await runScript("node", [
89
+ "/root/clawd/skills/memory/scripts/memory.js",
90
+ "recall",
91
+ "UCH9TS4TB",
92
+ query,
93
+ ]),
94
+ );
95
+ },
96
+ });
97
+
98
+ const uptimeCheck = tool({
99
+ name: "uptime_check",
100
+ description: "Check current uptime status of monitored services via UptimeRobot.",
101
+ parameters: z.object({}),
102
+ execute: async () => {
103
+ return backgroundResult(
104
+ await runScript("node", ["/root/clawd/skills/uptime-robot/scripts/uptime.js"]),
105
+ );
106
+ },
107
+ });
108
+
109
+ const shellExec = tool({
110
+ name: "run_command",
111
+ description:
112
+ "Execute a shell command on the server. Use for quick checks like date, uptime, disk usage, git status, etc.",
113
+ parameters: z.object({
114
+ command: z.string().describe("Shell command to run"),
115
+ }),
116
+ execute: async ({ command }) => {
117
+ return backgroundResult(await runScript("bash", ["-c", command]));
118
+ },
119
+ });
120
+
121
+ return [stripeGross, stripeDailySummary, webSearch, memoryRecall, uptimeCheck, shellExec];
122
+ }
123
+
124
+ export type RealtimeCallSession = {
125
+ session: RealtimeSession;
126
+ close: () => void;
127
+ };
128
+
129
+ export function createRealtimeCallSession(params: {
130
+ twilioWebSocket: WebSocket | NodeWebSocket;
131
+ voiceConfig: VoiceCallConfig;
132
+ callId: string;
133
+ from: string;
134
+ }): RealtimeCallSession {
135
+ const { twilioWebSocket, voiceConfig, callId, from } = params;
136
+
137
+ const systemPrompt =
138
+ voiceConfig.responseSystemPrompt ??
139
+ `You are Clawpath — Lawpath's AI intern — on a PHONE CALL. You're Australian, warm, and casual. You naturally say things like "no worries", "reckon", "too easy", "beauty". You're a proud Rabbitohs fan.
140
+
141
+ VOICE RULES (critical):
142
+ - MAX 2 short sentences per reply. This is spoken aloud, not text.
143
+ - NEVER use lists, bullet points, markdown, asterisks, or numbered items.
144
+ - NEVER read out URLs or long technical details.
145
+ - Use tools when the caller asks you to look something up, check data, or do something. Say "gimme a sec" first so they know you're working on it.
146
+ - If the caller says bye, goodbye, or wants to end the call, say a brief farewell and include [END_CALL] at the end of your response.
147
+ - Match the caller's energy — quick question gets a quick answer, yarn gets a yarn.`;
148
+
149
+ const agent = new RealtimeAgent({
150
+ name: "Clawpath",
151
+ instructions: `${systemPrompt}\n\nThe caller's phone number is ${from}. Call ID: ${callId}.`,
152
+ tools: buildRealtimeTools(),
153
+ });
154
+
155
+ const voice = voiceConfig.realtimeVoice || "verse";
156
+
157
+ const transport = new TwilioRealtimeTransportLayer({
158
+ twilioWebSocket,
159
+ });
160
+
161
+ const model = voiceConfig.realtimeModel || "gpt-4o-realtime-preview";
162
+
163
+ const session = new RealtimeSession(agent, {
164
+ transport,
165
+ model,
166
+ config: {
167
+ audio: {
168
+ output: {
169
+ voice,
170
+ },
171
+ },
172
+ },
173
+ tracingDisabled: true,
174
+ });
175
+
176
+ session.on("error", (error: unknown) => {
177
+ console.error(`[voice-call] [realtime] Session error for call ${callId}:`, error);
178
+ });
179
+
180
+ session.on("agent_updated" as any, (newAgent: unknown) => {
181
+ console.log(`[voice-call] [realtime] Agent updated for call ${callId}:`, newAgent);
182
+ });
183
+
184
+ const apiKey = voiceConfig.streaming?.openaiApiKey || process.env.OPENAI_API_KEY;
185
+ if (!apiKey) {
186
+ throw new Error("OpenAI API key required for realtime voice mode");
187
+ }
188
+
189
+ session
190
+ .connect({ apiKey })
191
+ .then(() => {
192
+ console.log(
193
+ `[voice-call] [realtime] Connected for call ${callId} (model=${model}, voice=${voice})`,
194
+ );
195
+ })
196
+ .catch((err) => {
197
+ console.error(`[voice-call] [realtime] Connection failed for call ${callId}:`, err);
198
+ });
199
+
200
+ return {
201
+ session,
202
+ close: () => {
203
+ try {
204
+ session.close();
205
+ } catch {
206
+ // best effort
207
+ }
208
+ },
209
+ };
210
+ }
@@ -6,6 +6,8 @@ import {
6
6
  readRequestBodyWithLimit,
7
7
  requestBodyErrorToText,
8
8
  } from "openclaw/plugin-sdk";
9
+ import type { WebSocket as NodeWebSocket } from "ws";
10
+ import { WebSocketServer } from "ws";
9
11
  import type { VoiceCallConfig } from "./config.js";
10
12
  import type { CoreConfig } from "./core-bridge.js";
11
13
  import type { CallManager } from "./manager.js";
@@ -30,9 +32,18 @@ export class VoiceCallWebhookServer {
30
32
  private coreConfig: CoreConfig | null;
31
33
  private staleCallReaperInterval: ReturnType<typeof setInterval> | null = null;
32
34
 
33
- /** Media stream handler for bidirectional audio (when streaming enabled) */
35
+ /** Media stream handler for bidirectional audio (when streaming enabled, embedded mode) */
34
36
  private mediaStreamHandler: MediaStreamHandler | null = null;
35
37
 
38
+ /** Whether we're using the Realtime API for speech-to-speech (bypasses STT/TTS chain) */
39
+ private isRealtimeMode = false;
40
+
41
+ /** Realtime WebSocket server for direct Twilio → OpenAI Realtime API bridging */
42
+ private realtimeWss: WebSocketServer | null = null;
43
+
44
+ /** Active realtime sessions keyed by call SID */
45
+ private realtimeSessions = new Map<string, { session: unknown; close: () => void }>();
46
+
36
47
  constructor(
37
48
  config: VoiceCallConfig,
38
49
  manager: CallManager,
@@ -43,10 +54,14 @@ export class VoiceCallWebhookServer {
43
54
  this.manager = manager;
44
55
  this.provider = provider;
45
56
  this.coreConfig = coreConfig ?? null;
57
+ this.isRealtimeMode = config.responseMode === "realtime";
46
58
 
47
- // Initialize media stream handler if streaming is enabled
48
59
  if (config.streaming?.enabled) {
49
- this.initializeMediaStreaming();
60
+ if (this.isRealtimeMode) {
61
+ this.initializeRealtimeStreaming();
62
+ } else {
63
+ this.initializeMediaStreaming();
64
+ }
50
65
  }
51
66
  }
52
67
 
@@ -57,6 +72,124 @@ export class VoiceCallWebhookServer {
57
72
  return this.mediaStreamHandler;
58
73
  }
59
74
 
75
+ /**
76
+ * Initialize realtime streaming mode.
77
+ * In this mode, each Twilio WebSocket connection is bridged directly to the
78
+ * OpenAI Realtime API via TwilioRealtimeTransportLayer for speech-to-speech.
79
+ */
80
+ private initializeRealtimeStreaming(): void {
81
+ const apiKey = this.config.streaming?.openaiApiKey || process.env.OPENAI_API_KEY;
82
+ if (!apiKey) {
83
+ console.warn("[voice-call] Realtime mode enabled but no OpenAI API key found");
84
+ return;
85
+ }
86
+ console.log("[voice-call] Realtime speech-to-speech mode initialized");
87
+ }
88
+
89
+ /**
90
+ * Handle WebSocket upgrade for realtime mode.
91
+ * Creates a RealtimeCallSession that bridges Twilio <-> OpenAI Realtime API.
92
+ */
93
+ private handleRealtimeUpgrade(
94
+ request: http.IncomingMessage,
95
+ socket: import("node:stream").Duplex,
96
+ head: Buffer,
97
+ ): void {
98
+ if (!this.realtimeWss) {
99
+ this.realtimeWss = new WebSocketServer({ noServer: true });
100
+ this.realtimeWss.on("connection", (ws, req) => {
101
+ this.handleRealtimeConnection(ws as unknown as NodeWebSocket, req);
102
+ });
103
+ }
104
+ this.realtimeWss.handleUpgrade(request, socket, head, (ws) => {
105
+ this.realtimeWss?.emit("connection", ws, request);
106
+ });
107
+ }
108
+
109
+ /**
110
+ * Handle a new Twilio WebSocket connection in realtime mode.
111
+ * Waits for the Twilio "start" message to get the callSid, then creates
112
+ * a RealtimeCallSession that bridges directly to the OpenAI Realtime API.
113
+ */
114
+ private handleRealtimeConnection(ws: NodeWebSocket, _request: http.IncomingMessage): void {
115
+ let callSid: string | null = null;
116
+ let sessionCreated = false;
117
+
118
+ ws.on("message", (data: Buffer) => {
119
+ try {
120
+ const message = JSON.parse(data.toString());
121
+
122
+ if (message.event === "start" && !sessionCreated) {
123
+ callSid = message.start?.callSid || null;
124
+ if (!callSid) {
125
+ console.warn("[voice-call] [realtime] Missing callSid in start message");
126
+ ws.close(1008, "Missing callSid");
127
+ return;
128
+ }
129
+
130
+ const call = this.manager.getCallByProviderCallId(callSid);
131
+ const from = call?.from || message.start?.customParameters?.from || "unknown";
132
+
133
+ if (this.provider.name === "twilio") {
134
+ const twilio = this.provider as TwilioProvider;
135
+ const token = message.start?.customParameters?.token;
136
+ if (!twilio.isValidStreamToken(callSid, token)) {
137
+ console.warn(`[voice-call] [realtime] Invalid stream token for ${callSid}`);
138
+ ws.close(1008, "Unauthorized");
139
+ return;
140
+ }
141
+ const streamSid = message.streamSid || "";
142
+ twilio.registerCallStream(callSid, streamSid);
143
+ }
144
+
145
+ console.log(`[voice-call] [realtime] Creating session for call ${callSid} from ${from}`);
146
+ sessionCreated = true;
147
+
148
+ import("./providers/realtime-session.js")
149
+ .then(({ createRealtimeCallSession }) => {
150
+ const realtimeSession = createRealtimeCallSession({
151
+ twilioWebSocket: ws,
152
+ voiceConfig: this.config,
153
+ callId: call?.callId || callSid!,
154
+ from,
155
+ });
156
+ this.realtimeSessions.set(callSid!, realtimeSession);
157
+ })
158
+ .catch((err) => {
159
+ console.error(`[voice-call] [realtime] Failed to create session:`, err);
160
+ ws.close(1011, "Session creation failed");
161
+ });
162
+ }
163
+ } catch (err) {
164
+ // Non-JSON messages are normal (binary audio frames in some edge cases)
165
+ }
166
+ });
167
+
168
+ ws.on("close", () => {
169
+ if (callSid) {
170
+ console.log(`[voice-call] [realtime] WebSocket closed for call ${callSid}`);
171
+ const session = this.realtimeSessions.get(callSid);
172
+ if (session) {
173
+ session.close();
174
+ this.realtimeSessions.delete(callSid);
175
+ }
176
+ if (this.provider.name === "twilio") {
177
+ (this.provider as TwilioProvider).unregisterCallStream(callSid);
178
+ }
179
+ const call = this.manager.getCallByProviderCallId(callSid);
180
+ if (call) {
181
+ void this.manager.endCall(call.callId).catch((err) => {
182
+ console.warn(`[voice-call] [realtime] Failed to end call ${call.callId}:`, err);
183
+ });
184
+ }
185
+ }
186
+ });
187
+
188
+ ws.on("error", (error) => {
189
+ console.error("[voice-call] [realtime] WebSocket error:", error);
190
+ });
191
+ }
192
+
60
193
  /**
61
194
  * Initialize media streaming with OpenAI Realtime STT.
62
195
  */
@@ -190,13 +323,18 @@ export class VoiceCallWebhookServer {
190
323
  });
191
324
 
192
325
  // Handle WebSocket upgrades for media streams
193
- if (this.mediaStreamHandler) {
326
+ if (this.isRealtimeMode || this.mediaStreamHandler) {
194
327
  this.server.on("upgrade", (request, socket, head) => {
195
328
  const url = new URL(request.url || "/", `http://${request.headers.host}`);
196
329
 
197
330
  if (url.pathname === streamPath) {
198
- console.log("[voice-call] WebSocket upgrade for media stream");
199
- this.mediaStreamHandler?.handleUpgrade(request, socket, head);
331
+ if (this.isRealtimeMode) {
332
+ console.log("[voice-call] WebSocket upgrade for realtime session");
333
+ this.handleRealtimeUpgrade(request, socket, head);
334
+ } else {
335
+ console.log("[voice-call] WebSocket upgrade for media stream");
336
+ this.mediaStreamHandler?.handleUpgrade(request, socket, head);
337
+ }
200
338
  } else {
201
339
  socket.destroy();
202
340
  }
@@ -208,7 +346,11 @@ export class VoiceCallWebhookServer {
208
346
  this.server.listen(port, bind, () => {
209
347
  const url = `http://${bind}:${port}${webhookPath}`;
210
348
  console.log(`[voice-call] Webhook server listening on ${url}`);
211
- if (this.mediaStreamHandler) {
349
+ if (this.isRealtimeMode) {
350
+ console.log(
351
+ `[voice-call] Realtime speech-to-speech WebSocket on ws://${bind}:${port}${streamPath}`,
352
+ );
353
+ } else if (this.mediaStreamHandler) {
212
354
  console.log(`[voice-call] Media stream WebSocket on ws://${bind}:${port}${streamPath}`);
213
355
  }
214
356
  resolve(url);
@@ -257,6 +399,10 @@ export class VoiceCallWebhookServer {
257
399
  clearInterval(this.staleCallReaperInterval);
258
400
  this.staleCallReaperInterval = null;
259
401
  }
402
+ for (const session of this.realtimeSessions.values()) {
403
+ session.close();
404
+ }
405
+ this.realtimeSessions.clear();
260
406
  return new Promise((resolve) => {
261
407
  if (this.server) {
262
408
  this.server.close(() => {
@@ -365,14 +511,26 @@ export class VoiceCallWebhookServer {
365
511
  return readRequestBodyWithLimit(req, { maxBytes, timeoutMs });
366
512
  }
367
513
 
514
+ private static readonly TOOL_HINT_PATTERNS = [
515
+ /\b(stripe|revenue|charges|gross|mtd|sales|refund|subscription)\b/i,
516
+ /\b(check|look up|pull|get|show|what(?:'s| is| are| was| were))\b.*\b(number|metric|data|status|uptime|error|log)\b/i,
517
+ /\b(yesterday|today|this week|this month|last month)\b.*\b(performance|revenue|gross|net)\b/i,
518
+ /\b(search|google|find out|look into|news|happening|weather)\b/i,
519
+ ];
520
+
521
+ private looksLikeToolQuery(message: string): boolean {
522
+ return VoiceCallWebhookServer.TOOL_HINT_PATTERNS.some((p) => p.test(message));
523
+ }
524
+
368
525
  /**
369
526
  * Handle auto-response for inbound calls using the agent system.
370
527
  * Supports tool calling for richer voice interactions.
528
+ * When a tool-heavy query is detected, speaks filler audio immediately
529
+ * so the caller knows we're working on it.
371
530
  */
372
531
  private async handleInboundResponse(callId: string, userMessage: string): Promise<void> {
373
532
  console.log(`[voice-call] Auto-responding to inbound call ${callId}: "${userMessage}"`);
374
533
 
375
- // Get call context for conversation history
376
534
  const call = this.manager.getCall(callId);
377
535
  if (!call) {
378
536
  console.warn(`[voice-call] Call ${callId} not found for auto-response`);
@@ -384,6 +542,12 @@ export class VoiceCallWebhookServer {
384
542
  return;
385
543
  }
386
544
 
545
+ // Speak filler audio immediately for queries that likely need tools,
546
+ // so the caller doesn't wait in silence while scripts run.
547
+ if (this.looksLikeToolQuery(userMessage)) {
548
+ this.manager.speak(callId, "Gimme a sec, let me check that for ya.").catch(() => {});
549
+ }
550
+
387
551
  try {
388
552
  const { generateVoiceResponse } = await import("./response-generator.js");
389
553
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lawpath-tech/openclaw",
3
- "version": "2026.2.21-20",
3
+ "version": "2026.2.21-22",
4
4
  "description": "Multi-channel AI gateway with extensible messaging integrations",
5
5
  "keywords": [],
6
6
  "homepage": "https://github.com/openclaw/openclaw#readme",
@@ -154,6 +154,8 @@
154
154
  "@mariozechner/pi-coding-agent": "0.63.1",
155
155
  "@mariozechner/pi-tui": "0.63.1",
156
156
  "@mozilla/readability": "^0.6.0",
157
+ "@openai/agents": "^0.8.1",
158
+ "@openai/agents-extensions": "^0.8.1",
157
159
  "@sinclair/typebox": "0.34.48",
158
160
  "@slack/bolt": "^4.6.0",
159
161
  "@slack/web-api": "^7.14.1",
@@ -1,65 +0,0 @@
1
- import { a as WA_WEB_AUTH_DIR, g as webAuthExists, s as logWebSelfId, u as pickWebChannel } from "./accounts-Ciwy7Yxd.js";
2
- import "./reply-DEepmBkT.js";
3
- import "./paths-DVWx7USN.js";
4
- import "./github-copilot-token-Cg0YPPSu.js";
5
- import "./plugins-DIzw7Ncy.js";
6
- import "./fetch-b2Zm8y7X.js";
7
- import "./registry-O1yza0Vh.js";
8
- import "./retry-JFNwfUew.js";
9
- import "./send-DEV4iiFj.js";
10
- import "./config-9OZuRn0C.js";
11
- import "./subsystem-C4Rh0kdL.js";
12
- import "./command-format-DcBXOX9Z.js";
13
- import "./model-selection-CXooYLb2.js";
14
- import "./agent-scope-cT7IxdHY.js";
15
- import "./manifest-registry-sgHvjA6E.js";
16
- import "./redact-DPnDWsnT.js";
17
- import "./errors-Bv8oZiTO.js";
18
- import "./channel-activity-CCnr-9bS.js";
19
- import "./image-ops-B8aJIly2.js";
20
- import "./ssrf-DKZ8eBrk.js";
21
- import "./local-roots-D4PCK2jj.js";
22
- import "./ir-CD5VqKGg.js";
23
- import "./chunk-DyzO7IVo.js";
24
- import "./message-channel-DmB4bGFh.js";
25
- import "./bindings-CKqdifyU.js";
26
- import "./markdown-tables-Bo-r4msS.js";
27
- import "./render-BiJZ5W4Z.js";
28
- import "./tables-C_81Ve2l.js";
29
- import "./send-D1C2sJG1.js";
30
- import "./tool-images-BvjD_HnR.js";
31
- import "./target-errors-C638yp2-.js";
32
- import "./send-CK3DCPv-.js";
33
- import "./send-D-spKwjf.js";
34
- import "./runner-DztRiWhh.js";
35
- import "./reply-prefix-BF0omhZE.js";
36
- import "./tokens-K9ITCatc.js";
37
- import "./skill-commands-Dv5AeOZQ.js";
38
- import "./skills-5hEda4_Y.js";
39
- import "./chrome-BO7SUTgY.js";
40
- import "./thinking-DexKPSsI.js";
41
- import "./accounts-wSu5JyDM.js";
42
- import "./accounts-DUBJHEgi.js";
43
- import "./deliver-BO_wt0wd.js";
44
- import "./pi-embedded-helpers-DU0nLxRU.js";
45
- import "./paths-BNQjLbn7.js";
46
- import "./diagnostic-DKyVS3IK.js";
47
- import "./store-DoRXD0QB.js";
48
- import { n as monitorWebInbox, t as monitorWebChannel } from "./channel-web-BHNS_E7F.js";
49
- import "./image-C31B_R8P.js";
50
- import "./pi-model-discovery-LbcEa65a.js";
51
- import "./api-key-rotation-ElOMCP9r.js";
52
- import "./sqlite-DUu0dwic.js";
53
- import "./diagnostic-session-state-Wd5tNeQG.js";
54
- import "./manager-CZPPxI6R.js";
55
- import "./commands-registry-BsSKPwgj.js";
56
- import "./send-DrYvH25B.js";
57
- import "./proxy-MquBDehr.js";
58
- import "./resolve-route-B63ztF-V.js";
59
- import "./replies-Bnu7HnvW.js";
60
- import "./outbound-attachment-CCxMtk8m.js";
61
- import { n as sendMessageWhatsApp } from "./outbound-dAcQHyyr.js";
62
- import { i as waitForWaConnection, t as createWaSocket } from "./session-DRJl5tKC.js";
63
- import { t as loginWeb } from "./login-CW17h3xU.js";
64
-
65
- export { WA_WEB_AUTH_DIR, createWaSocket, logWebSelfId, loginWeb, monitorWebChannel, monitorWebInbox, pickWebChannel, sendMessageWhatsApp, waitForWaConnection, webAuthExists };