@xopcai/xopc 0.0.264 → 0.0.265

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 (175) hide show
  1. package/dist/browser-ext/dist/assets/auth-CknJ00ae.js +5844 -0
  2. package/dist/browser-ext/dist/assets/sidepanel-C69P3v9u.css +113 -0
  3. package/dist/browser-ext/dist/background.js +200 -68
  4. package/dist/browser-ext/dist/sidepanel.html +14 -0
  5. package/dist/browser-ext/dist/sidepanel.js +18832 -0
  6. package/dist/browser-ext/manifest.json +20 -20
  7. package/dist/extensions/telegram/xopc.extension.json +1 -1
  8. package/dist/gateway/static/root/assets/{activity-store-BYBCpt7_.js → activity-store-Bhyy6ENz.js} +1 -1
  9. package/dist/gateway/static/root/assets/{agent-avatar-dicebear-D20aBHPJ.js → agent-avatar-dicebear-DRTTU2H3.js} +1 -1
  10. package/dist/gateway/static/root/assets/{agent-defaults-settings-panel-BZvMqNrU.js → agent-defaults-settings-panel-BsXF7LVi.js} +1 -1
  11. package/dist/gateway/static/root/assets/{agents-6foBELwt.js → agents-DpjWfxYx.js} +1 -1
  12. package/dist/gateway/static/root/assets/{api-YbtjDbuG.js → api-B2y7ZjZk.js} +1 -1
  13. package/dist/gateway/static/root/assets/{app-management-settings-panel-4ps1RrDU.js → app-management-settings-panel-33Mc81h2.js} +1 -1
  14. package/dist/gateway/static/root/assets/{appearance-settings-C7nBHnkG.js → appearance-settings-C8JECSiJ.js} +1 -1
  15. package/dist/gateway/static/root/assets/{apps-page-B_g-rL5b.js → apps-page-t64FcxIZ.js} +1 -1
  16. package/dist/gateway/static/root/assets/{archive-plugin-DfKK1iJO.js → archive-plugin-PzZ6SzfY.js} +1 -1
  17. package/dist/gateway/static/root/assets/{attachment-load-BOU1pdnQ.js → attachment-load-OhLL6K__.js} +1 -1
  18. package/dist/gateway/static/root/assets/{automations-page-BePzexAH.js → automations-page-BpAYwuvx.js} +1 -1
  19. package/dist/gateway/static/root/assets/{automations-workspace-7-m5pBEZ.js → automations-workspace-B6aTT5IC.js} +1 -1
  20. package/dist/gateway/static/root/assets/{binary-plugins-Cly8XDyt.js → binary-plugins-B5XpGsHg.js} +1 -1
  21. package/dist/gateway/static/root/assets/{block-editor-DzSu62js.js → block-editor-BgEhoTnN.js} +1 -1
  22. package/dist/gateway/static/root/assets/{browser-automation-inputs-8bHj7Fv2.js → browser-automation-inputs-s5I78Iko.js} +1 -1
  23. package/dist/gateway/static/root/assets/{browser-automations-page-CzL7Aj2k.js → browser-automations-page-CBLunDRH.js} +1 -1
  24. package/dist/gateway/static/root/assets/{browser-settings-page-Dum3mTiz.js → browser-settings-page-CSfpPCuD.js} +1 -1
  25. package/dist/gateway/static/root/assets/{capabilities-settings-panel-Ci6qVS-O.js → capabilities-settings-panel-DftNnICK.js} +2 -2
  26. package/dist/gateway/static/root/assets/{channels-settings-BOGEZnSv.js → channels-settings-CpuWa5qu.js} +1 -1
  27. package/dist/gateway/static/root/assets/{channels-status-swr-BR8oQ_eP.js → channels-status-swr-_OsWr_vS.js} +1 -1
  28. package/dist/gateway/static/root/assets/{chat-terminal-dock-BF9ZXa5Z.js → chat-terminal-dock-mnPsS_GJ.js} +1 -1
  29. package/dist/gateway/static/root/assets/{connectors-page-DBvEvupa.js → connectors-page-D3nHGnNV.js} +1 -1
  30. package/dist/gateway/static/root/assets/{cron-expression-BszT_gjK.js → cron-expression-NcGBvIfe.js} +1 -1
  31. package/dist/gateway/static/root/assets/{date-picker-D2MKBJ0G.js → date-picker-BrVcl7H5.js} +1 -1
  32. package/dist/gateway/static/root/assets/{dependency-picker-DqigBx-G.js → dependency-picker--edd48Y9.js} +1 -1
  33. package/dist/gateway/static/root/assets/{desktop-pet-settings-DMPk4CaK.js → desktop-pet-settings-DWredJwO.js} +1 -1
  34. package/dist/gateway/static/root/assets/{desktop-pet-CapspBGG.js → desktop-pet-vuyyb6I6.js} +1 -1
  35. package/dist/gateway/static/root/assets/{extension-debug-page-CXLsDtrW.js → extension-debug-page-CtBW4ebj.js} +1 -1
  36. package/dist/gateway/static/root/assets/{extension-page-B2j87ROw.js → extension-page-CsUrmtW7.js} +1 -1
  37. package/dist/gateway/static/root/assets/{extension-settings-page-t1BApw-u.js → extension-settings-page-BUd31AuK.js} +1 -1
  38. package/dist/gateway/static/root/assets/{files-BArWTfr7.js → files-Be5viEFY.js} +1 -1
  39. package/dist/gateway/static/root/assets/{gateway-config-swr-BFSesvAC.js → gateway-config-swr-OObCK8jM.js} +1 -1
  40. package/dist/gateway/static/root/assets/{gateway-settings-BFAecoGy.js → gateway-settings-C6FQv7op.js} +1 -1
  41. package/dist/gateway/static/root/assets/{gateway-startup-retry-D1giYgI-.js → gateway-startup-retry-D_TxKN_A.js} +1 -1
  42. package/dist/gateway/static/root/assets/{heartbeat-settings-CyD9zDcU.js → heartbeat-settings-CDo1Gjp2.js} +1 -1
  43. package/dist/gateway/static/root/assets/{home-page-uItZMiBE.js → home-page-D3_uvVJY.js} +1 -1
  44. package/dist/gateway/static/root/assets/{image-generation-api-BeB-OPqO.js → image-generation-api-fPU9yGsw.js} +1 -1
  45. package/dist/gateway/static/root/assets/index-Dpu7EKTm.js +74 -0
  46. package/dist/gateway/static/root/assets/{keyboard-shortcuts-settings-Q_CM-ZCs.js → keyboard-shortcuts-settings-CNVmrCrV.js} +1 -1
  47. package/dist/gateway/static/root/assets/{local-app-workbench-page-DaiBWfOt.js → local-app-workbench-page-B0IVDvs-.js} +1 -1
  48. package/dist/gateway/static/root/assets/{local-apps-page-BALMNCSp.js → local-apps-page-DvkarvLt.js} +1 -1
  49. package/dist/gateway/static/root/assets/{locale-store-RJHYFoq9.js → locale-store-DFQX9kTl.js} +1 -1
  50. package/dist/gateway/static/root/assets/{logs-page-C-TS7oFg.js → logs-page-BknY0vZf.js} +1 -1
  51. package/dist/gateway/static/root/assets/{management-settings-BajkJeYl.js → management-settings-DW6hJINQ.js} +1 -1
  52. package/dist/gateway/static/root/assets/{markdown-view-BxoQJqHx.js → markdown-view-D-5jHRWg.js} +1 -1
  53. package/dist/gateway/static/root/assets/{media-plugins-C_8Hl6KG.js → media-plugins-DYautOWY.js} +1 -1
  54. package/dist/gateway/static/root/assets/{messages-Cf1h5WXB.js → messages-Dj8QuS6e.js} +2 -2
  55. package/dist/gateway/static/root/assets/{mobile-pairing-wizard-CsWIZrZW.js → mobile-pairing-wizard-Dgu03cv-.js} +1 -1
  56. package/dist/gateway/static/root/assets/{note-detail-panel-CCbPg_GP.js → note-detail-panel-Cv9uH6Jq.js} +3 -3
  57. package/dist/gateway/static/root/assets/{notes-page-BdxxcUKa.js → notes-page-DcIAiYJn.js} +1 -1
  58. package/dist/gateway/static/root/assets/{page-tabs-BMYQef5D.js → page-tabs-DF6Q5XvA.js} +1 -1
  59. package/dist/gateway/static/root/assets/{preview-open-alternatives-CKZ-H5hJ.js → preview-open-alternatives-CQJH_jAN.js} +1 -1
  60. package/dist/gateway/static/root/assets/{product-delivery-CV4pDOtG.js → product-delivery-CCC4FJ3Y.js} +1 -1
  61. package/dist/gateway/static/root/assets/{product-open-page-zXJIptXs.js → product-open-page-CIGLPFm7.js} +1 -1
  62. package/dist/gateway/static/root/assets/{project-detail-page-DRhhxhZB.js → project-detail-page-D65JfNSw.js} +1 -1
  63. package/dist/gateway/static/root/assets/{projects-page-Wt7gF3Sx.js → projects-page-BfWF7Sz-.js} +1 -1
  64. package/dist/gateway/static/root/assets/{registry-api-BSfFaxVN.js → registry-api-CsZLatKo.js} +1 -1
  65. package/dist/gateway/static/root/assets/{remote-access-hub-B6de5Zrb.js → remote-access-hub-DA1g4CUD.js} +1 -1
  66. package/dist/gateway/static/root/assets/{runtime-tools-settings-panel-DcUB5h1f.js → runtime-tools-settings-panel-0-bFSBB8.js} +1 -1
  67. package/dist/gateway/static/root/assets/{schema-form-CquWhe7r.js → schema-form-csGbhTUr.js} +1 -1
  68. package/dist/gateway/static/root/assets/{schemas-BHS7bo06.js → schemas-BaskVM5g.js} +2 -2
  69. package/dist/gateway/static/root/assets/{sessions-page-BNWO2EVF.js → sessions-page-DXPBNhrZ.js} +1 -1
  70. package/dist/gateway/static/root/assets/settings-advanced-gate-DxeZm67O.js +1 -0
  71. package/dist/gateway/static/root/assets/{settings-form-section-DwswAefc.js → settings-form-section-YdrdY8B3.js} +1 -1
  72. package/dist/gateway/static/root/assets/{settings-page-CoAVmjjs.js → settings-page-Cv9waRn3.js} +1 -1
  73. package/dist/gateway/static/root/assets/{setup-status-panel-C9EIcWtH.js → setup-status-panel-DPf-_05Z.js} +1 -1
  74. package/dist/gateway/static/root/assets/{share-preview-page-DWwy_4zh.js → share-preview-page-DBoEHTPn.js} +1 -1
  75. package/dist/gateway/static/root/assets/{shares-settings-CIUvq27-.js → shares-settings-BQP5yNcO.js} +1 -1
  76. package/dist/gateway/static/root/assets/skill-reload-api-B5T8cgjq.js +1 -0
  77. package/dist/gateway/static/root/assets/{skills-page-CqsgVtVh.js → skills-page-D1G8h2Lt.js} +1 -1
  78. package/dist/gateway/static/root/assets/{skills-page.utils-B-N73248.js → skills-page.utils-BNZYTop7.js} +1 -1
  79. package/dist/gateway/static/root/assets/{system-settings-panel-BndjXuBZ.js → system-settings-panel-ByuppbHi.js} +1 -1
  80. package/dist/gateway/static/root/assets/{task-detail-page-CG6Ha9t7.js → task-detail-page-DNmkrF6y.js} +1 -1
  81. package/dist/gateway/static/root/assets/{theme-store-C5ssn64t.js → theme-store-zRT6vaZc.js} +1 -1
  82. package/dist/gateway/static/root/assets/{tunnel-api-BxZdcp3h.js → tunnel-api-CxWGpbwr.js} +1 -1
  83. package/dist/gateway/static/root/assets/{use-autosave-mj9CUuz1.js → use-autosave-B0l4qubU.js} +1 -1
  84. package/dist/gateway/static/root/assets/{user-model-page-DKeHMXI4.js → user-model-page-DMsI6YI7.js} +1 -1
  85. package/dist/gateway/static/root/assets/{voice-api-key-field-CzxVMYlr.js → voice-api-key-field-Bcz7boJg.js} +1 -1
  86. package/dist/gateway/static/root/assets/{voice-config-api-COA0V1cK.js → voice-config-api-DpJEu2RQ.js} +1 -1
  87. package/dist/gateway/static/root/assets/{work-discovery-overlay-DMiPK8dp.js → work-discovery-overlay-Dujq1j_u.js} +1 -1
  88. package/dist/gateway/static/root/assets/{workflow-run-setup-panel-C82OXCJK.js → workflow-run-setup-panel-CxNYbLNB.js} +1 -1
  89. package/dist/gateway/static/root/assets/{workflows-page-HLCUV3rQ.js → workflows-page-Dk1jK2nv.js} +1 -1
  90. package/dist/gateway/static/root/index.html +12 -12
  91. package/dist/package.js +1 -1
  92. package/dist/packages/browser-control-contract/src/index.js +14 -0
  93. package/dist/packages/endpoint-tools-protocol/src/index.js +2 -1
  94. package/dist/packages/gateway-contract/src/browser-page-context.js +43 -0
  95. package/dist/packages/gateway-contract/src/browser-tab-binding.js +29 -0
  96. package/dist/packages/gateway-contract/src/device-pairing.js +18 -2
  97. package/dist/packages/realtime-protocol/src/index.js +1 -0
  98. package/dist/packages/realtime-protocol/src/voice-audio.js +70 -9
  99. package/dist/packages/realtime-protocol/src/voice.js +65 -17
  100. package/dist/src/agent/source-context/browser-page.d.ts +3 -0
  101. package/dist/src/agent/source-context/browser-page.js +39 -0
  102. package/dist/src/agent/source-context/injector.js +9 -2
  103. package/dist/src/agent/source-context/types.d.ts +7 -1
  104. package/dist/src/agent/source-context/types.js +4 -1
  105. package/dist/src/agent/tools/factory.js +9 -1
  106. package/dist/src/browser/automations/schema.d.ts +1 -1
  107. package/dist/src/browser/automations/service.d.ts +1 -1
  108. package/dist/src/browser/native-messaging-host.d.ts +15 -0
  109. package/dist/src/browser/native-messaging-host.js +105 -0
  110. package/dist/src/browser/providers/browser-ext-install.d.ts +17 -1
  111. package/dist/src/browser/providers/browser-ext-install.js +82 -16
  112. package/dist/src/browser/providers/extension.d.ts +4 -0
  113. package/dist/src/browser/providers/extension.js +104 -38
  114. package/dist/src/browser/runtime/browser-runtime.d.ts +2 -1
  115. package/dist/src/browser/runtime/browser-runtime.js +6 -2
  116. package/dist/src/cli/commands/browser.js +13 -3
  117. package/dist/src/cli/commands/doctor/checks/database-schema.js +1 -1
  118. package/dist/src/endpoint-tools/runtime.js +2 -0
  119. package/dist/src/gateway/chat-stream/mapper.js +3 -2
  120. package/dist/src/gateway/hono/app.js +19 -3
  121. package/dist/src/gateway/hono/middleware/auth.d.ts +4 -1
  122. package/dist/src/gateway/hono/middleware/auth.js +12 -1
  123. package/dist/src/gateway/hono/routes/browser-install.js +1 -1
  124. package/dist/src/gateway/hono/routes/browser.js +106 -7
  125. package/dist/src/gateway/hono/routes/devices.js +36 -3
  126. package/dist/src/gateway/hono/routes/endpoint-tools.js +15 -0
  127. package/dist/src/gateway/hono/routes/session-input-handler.d.ts +20 -2
  128. package/dist/src/gateway/hono/routes/session-input-handler.js +18 -0
  129. package/dist/src/gateway/hono/routes/sessions.js +12 -6
  130. package/dist/src/gateway/hono/routes/voice.js +5 -4
  131. package/dist/src/gateway/security/gateway-scopes.d.ts +1 -0
  132. package/dist/src/gateway/security/gateway-scopes.js +10 -1
  133. package/dist/src/gateway/security/origin-check.d.ts +1 -0
  134. package/dist/src/gateway/security/origin-check.js +5 -1
  135. package/dist/src/gateway/service/agent-runner.js +2 -1
  136. package/dist/src/gateway/service/session-input-coordinator.d.ts +1 -0
  137. package/dist/src/gateway/service/session-input-coordinator.js +5 -4
  138. package/dist/src/gateway/service.js +12 -11
  139. package/dist/src/realtime/runtime.js +2 -1
  140. package/dist/src/session/client-history.d.ts +4 -1
  141. package/dist/src/session/client-history.js +6 -3
  142. package/dist/src/storage/sqlite/browser-tab-binding-repository.d.ts +7 -0
  143. package/dist/src/storage/sqlite/browser-tab-binding-repository.js +60 -0
  144. package/dist/src/storage/sqlite/device-access-repository.d.ts +3 -1
  145. package/dist/src/storage/sqlite/device-access-repository.js +4 -3
  146. package/dist/src/storage/sqlite/device-pairing-approval.js +6 -4
  147. package/dist/src/storage/sqlite/device-pairing-repository.d.ts +1 -1
  148. package/dist/src/storage/sqlite/index.d.ts +1 -0
  149. package/dist/src/storage/sqlite/index.js +2 -1
  150. package/dist/src/storage/sqlite/migrations/155_browser_extension.sql +120 -0
  151. package/dist/src/storage/sqlite/migrations/156_browser_tab_bindings.sql +14 -0
  152. package/dist/src/storage/sqlite/migrations/runner.d.ts +1 -1
  153. package/dist/src/storage/sqlite/migrations/runner.js +3 -3
  154. package/dist/src/storage/sqlite/migrations/runner.ts +1 -1
  155. package/dist/src/storage/sqlite/schema.js +1 -1
  156. package/dist/src/voice/realtime/agentBroker.d.ts +54 -0
  157. package/dist/src/voice/realtime/agentBroker.js +116 -0
  158. package/dist/src/voice/realtime/agentEngine.js +62 -15
  159. package/dist/src/voice/realtime/engine.d.ts +2 -1
  160. package/dist/src/voice/realtime/omniEngine.js +36 -7
  161. package/dist/src/voice/realtime/pcmFrameBuffer.d.ts +6 -0
  162. package/dist/src/voice/realtime/pcmFrameBuffer.js +29 -0
  163. package/dist/src/voice/realtime/runtime.js +97 -21
  164. package/dist/src/voice/realtime/runtime.types.d.ts +5 -21
  165. package/dist/src/voice/realtime/turnPolicy.d.ts +36 -0
  166. package/dist/src/voice/realtime/turnPolicy.js +134 -0
  167. package/package.json +2 -2
  168. package/dist/browser-ext/dist/content.js +0 -191
  169. package/dist/browser-ext/dist/popup.js +0 -32
  170. package/dist/browser-ext/popup.html +0 -37
  171. package/dist/gateway/static/root/assets/index-DQtjs2x3.js +0 -74
  172. package/dist/gateway/static/root/assets/settings-advanced-gate-DK1yTYE4.js +0 -1
  173. package/dist/gateway/static/root/assets/skill-reload-api-DfE3zFVw.js +0 -1
  174. package/dist/src/voice/realtime/conversationTurn.d.ts +0 -20
  175. package/dist/src/voice/realtime/conversationTurn.js +0 -83
@@ -0,0 +1,116 @@
1
+ //#region src/voice/realtime/agentBroker.ts
2
+ const RUN_ASSIGNMENT_TIMEOUT_MS = 5e3;
3
+ const RUN_ASSIGNMENT_POLL_MS = 20;
4
+ const MAX_TRACKED_TASKS = 256;
5
+ function sleep(ms, signal) {
6
+ return new Promise((resolve, reject) => {
7
+ const finish = () => {
8
+ signal.removeEventListener("abort", abort);
9
+ resolve();
10
+ };
11
+ const timer = setTimeout(finish, ms);
12
+ const abort = () => {
13
+ clearTimeout(timer);
14
+ reject(signal.reason ?? /* @__PURE__ */ new Error("Voice task subscription cancelled"));
15
+ };
16
+ signal.addEventListener("abort", abort, { once: true });
17
+ if (signal.aborted) abort();
18
+ });
19
+ }
20
+ function eventData(delivery) {
21
+ if (delivery.kind !== "realtime.event") return void 0;
22
+ const value = delivery.payload.data;
23
+ return value && typeof value === "object" && typeof value.type === "string" ? value : void 0;
24
+ }
25
+ /** Durable voice-to-Agent boundary. Playback and socket cancellation only detach event delivery. */
26
+ var DurableVoiceAgentBroker = class {
27
+ runByTask = /* @__PURE__ */ new Map();
28
+ constructor(deps) {
29
+ this.deps = deps;
30
+ }
31
+ async delegate(input) {
32
+ const clientMessageId = `voice:${input.expectedSessionId}:${input.turnId}`;
33
+ const activeRunId = this.deps.snapshot(input.sessionKey).activeRunId;
34
+ const activeRunCursor = activeRunId ? this.deps.currentSequence(`run:${activeRunId}`) : 0;
35
+ const result = await this.deps.submit({
36
+ sessionKey: input.sessionKey,
37
+ expectedSessionId: input.expectedSessionId,
38
+ clientMessageId,
39
+ delivery: activeRunId ? "steer" : "next",
40
+ content: input.text,
41
+ origin: {
42
+ type: "channel",
43
+ channel: "voice"
44
+ }
45
+ });
46
+ if (result.ok === false) throw new Error(`Voice task submission failed: ${result.code}`);
47
+ const deadline = Date.now() + RUN_ASSIGNMENT_TIMEOUT_MS;
48
+ let row = this.deps.find(input.sessionKey, clientMessageId);
49
+ while (!row?.runId && !row?.targetRunId) {
50
+ if (row && [
51
+ "completed",
52
+ "cancelled",
53
+ "failed",
54
+ "interrupted"
55
+ ].includes(row.status)) throw new Error(row.error ?? `Voice task ended before assignment: ${row.status}`);
56
+ if (Date.now() >= deadline) throw new Error("Voice task run assignment timed out");
57
+ await sleep(RUN_ASSIGNMENT_POLL_MS, input.signal);
58
+ row = this.deps.find(input.sessionKey, clientMessageId);
59
+ }
60
+ const runId = row.runId ?? row.targetRunId;
61
+ this.runByTask.set(row.id, runId);
62
+ if (this.runByTask.size > MAX_TRACKED_TASKS) this.runByTask.delete(this.runByTask.keys().next().value);
63
+ const afterSeq = result.effectiveDelivery === "steer" && runId === activeRunId ? activeRunCursor : 0;
64
+ return {
65
+ taskId: row.id,
66
+ runId,
67
+ events: this.events(row.id, runId, afterSeq, input.signal)
68
+ };
69
+ }
70
+ async cancel(taskId) {
71
+ const runId = this.runByTask.get(taskId);
72
+ if (!runId) return false;
73
+ await this.deps.cancelRun(runId);
74
+ this.runByTask.delete(taskId);
75
+ return true;
76
+ }
77
+ async *events(taskId, runId, afterSeq, signal) {
78
+ const queued = [];
79
+ let wake;
80
+ const push = (event) => {
81
+ queued.push(event);
82
+ wake?.();
83
+ wake = void 0;
84
+ };
85
+ const subscription = this.deps.subscribe(`run:${runId}`, afterSeq, push);
86
+ subscription.initial.forEach(push);
87
+ const abort = () => {
88
+ wake?.();
89
+ wake = void 0;
90
+ };
91
+ signal.addEventListener("abort", abort, { once: true });
92
+ try {
93
+ while (!signal.aborted) {
94
+ if (!queued.length) await new Promise((resolve) => {
95
+ wake = resolve;
96
+ });
97
+ if (signal.aborted) return;
98
+ const delivery = queued.shift();
99
+ if (!delivery) continue;
100
+ if (delivery.kind === "realtime.gap") throw new Error("Voice task event history is unavailable");
101
+ const event = eventData(delivery);
102
+ if (!event) continue;
103
+ yield event;
104
+ if (event.type === "stream_end") {
105
+ this.runByTask.delete(taskId);
106
+ return;
107
+ }
108
+ }
109
+ } finally {
110
+ signal.removeEventListener("abort", abort);
111
+ subscription.unsubscribe();
112
+ }
113
+ }
114
+ };
115
+ //#endregion
116
+ export { DurableVoiceAgentBroker };
@@ -2,8 +2,9 @@ import { createLogger } from "../../utils/logger/index.js";
2
2
  import { init_logger } from "../../utils/logger.js";
3
3
  import { speakStream } from "../tts/speak-core.js";
4
4
  import { AudioPlaybackWindow } from "./audio-playback-window.js";
5
- import { ConversationTurn } from "./conversationTurn.js";
5
+ import { TurnCoordinator } from "./turnPolicy.js";
6
6
  import { isLikelyPlaybackEcho } from "./playback-echo.js";
7
+ import { PcmFrameBuffer } from "./pcmFrameBuffer.js";
7
8
  import { SpeakableSegmenter } from "./speakable-segmenter.js";
8
9
  import crypto from "node:crypto";
9
10
  //#region src/voice/realtime/agentEngine.ts
@@ -34,8 +35,24 @@ function createAgentVoiceEngine(options) {
34
35
  let committing = false;
35
36
  const finalizedUtterances = /* @__PURE__ */ new Set();
36
37
  let pendingTurn;
37
- const turn = new ConversationTurn(claim.silenceDurationMs, (text) => {
38
+ const turn = new TurnCoordinator(claim.silenceDurationMs, (text, decision, turnId) => {
38
39
  if (closed || muted) return;
40
+ log.info({
41
+ sessionId: claim.sessionId,
42
+ turnId,
43
+ disposition: decision.disposition,
44
+ confidence: decision.confidence,
45
+ source: decision.source,
46
+ transcriptCharacters: text.length
47
+ }, "Realtime voice turn committed");
48
+ send("turn.decision", {
49
+ turnId,
50
+ disposition: decision.disposition,
51
+ confidence: decision.confidence,
52
+ source: decision.source,
53
+ committed: true
54
+ });
55
+ send("turn.committed", { turnId });
39
56
  if (queuedTurns >= 8) {
40
57
  send("session.error", {
41
58
  code: "INPUT_BACKPRESSURE",
@@ -48,13 +65,14 @@ function createAgentVoiceEngine(options) {
48
65
  queuedTurns += 1;
49
66
  const generation = inputGeneration;
50
67
  const pending = {
68
+ turnId,
51
69
  text,
52
70
  cancelled: false
53
71
  };
54
72
  pendingTurn = pending;
55
73
  conversationTail = conversationTail.then(() => {
56
74
  if (pendingTurn === pending) pendingTurn = void 0;
57
- if (generation === inputGeneration && !pending.cancelled) return runConversationTurn(text);
75
+ if (generation === inputGeneration && !pending.cancelled) return runAssistantTurn(text, turnId);
58
76
  }).finally(() => {
59
77
  if (generation === inputGeneration) queuedTurns -= 1;
60
78
  });
@@ -129,11 +147,10 @@ function createAgentVoiceEngine(options) {
129
147
  }
130
148
  });
131
149
  }
132
- for (let offset = 0; offset < item.value.byteLength; offset += 24e3) {
133
- const chunk = item.value.subarray(offset, offset + 24e3);
134
- await response.playback.reserve(chunk.byteLength, response.abortController.signal);
150
+ for (const frame of response.pcm.push(item.value)) {
151
+ await response.playback.reserve(frame.byteLength, response.abortController.signal);
135
152
  if (activeResponse !== response || closed) return;
136
- options.sendAudio(response.id, chunk);
153
+ options.sendAudio(response.id, frame);
137
154
  }
138
155
  }
139
156
  } finally {
@@ -204,7 +221,7 @@ function createAgentVoiceEngine(options) {
204
221
  if (response.speechWorker) prefetchNextSpeech(response);
205
222
  else startSpeechWorker(response);
206
223
  }
207
- async function runConversationTurn(text) {
224
+ async function runAssistantTurn(text, turnId) {
208
225
  if (!claim.tts || !claim.request.sessionKey || !text.trim() || closed) return;
209
226
  cancelActiveResponse("barge_in");
210
227
  const response = {
@@ -219,14 +236,28 @@ function createAgentVoiceEngine(options) {
219
236
  queuedSpeechCharacters: 0,
220
237
  startedAt: Date.now(),
221
238
  firstTextSeen: false,
222
- awaitingClarification: false
239
+ awaitingClarification: false,
240
+ pcm: new PcmFrameBuffer()
223
241
  };
224
242
  activeResponse = response;
225
243
  send("response.created", { responseId: response.id });
226
244
  try {
227
245
  await interruptionWrites;
228
246
  if (response.abortController.signal.aborted || closed) return;
229
- for await (const event of options.runtime.runAgent(text, claim.request.sessionKey, response.abortController.signal)) {
247
+ if (!claim.conversationSessionId) throw new Error("Conversation identity is unavailable");
248
+ const task = await options.runtime.agentBroker.delegate({
249
+ text,
250
+ turnId,
251
+ sessionKey: claim.request.sessionKey,
252
+ expectedSessionId: claim.conversationSessionId,
253
+ signal: response.abortController.signal
254
+ });
255
+ response.taskId = task.taskId;
256
+ send("task.created", {
257
+ responseId: response.id,
258
+ taskId: task.taskId
259
+ });
260
+ for await (const event of task.events) {
230
261
  if (activeResponse !== response || response.abortController.signal.aborted) return;
231
262
  if (event.type === "assistant_delta" || event.type === "tool_end" && event.payload?.toolName !== "clarify") response.awaitingClarification = false;
232
263
  if (event.type === "assistant_delta" && typeof event.payload?.delta === "string") {
@@ -247,12 +278,19 @@ function createAgentVoiceEngine(options) {
247
278
  queuePhrases(response, response.segmenter.push(event.payload.delta));
248
279
  }
249
280
  if (event.type === "tool_start") queuePhrases(response, response.segmenter.flush());
250
- if ((event.type === "tool_start" || event.type === "tool_end") && typeof event.payload?.toolCallId === "string" && typeof event.payload?.toolName === "string") send("response.activity", {
251
- responseId: response.id,
281
+ if ((event.type === "tool_start" || event.type === "tool_end") && typeof event.payload?.toolCallId === "string" && typeof event.payload?.toolName === "string") send("task.activity", {
282
+ taskId: task.taskId,
252
283
  toolCallId: event.payload.toolCallId.slice(0, 160),
253
284
  toolName: event.payload.toolName.slice(0, 256),
254
285
  status: event.type === "tool_start" ? "running" : event.payload.status === "error" ? "failed" : "completed"
255
286
  });
287
+ if (event.type === "stream_end") {
288
+ const status = event.payload?.status;
289
+ send("task.done", {
290
+ taskId: task.taskId,
291
+ status: status === "cancelled" ? "cancelled" : status === "suspended" ? "suspended" : status === "error" ? "failed" : "completed"
292
+ });
293
+ }
256
294
  if (event.type === "clarify_request" && typeof event.payload?.requestId === "string" && typeof event.payload?.question === "string") {
257
295
  response.awaitingClarification = true;
258
296
  send("response.clarification", {
@@ -274,6 +312,12 @@ function createAgentVoiceEngine(options) {
274
312
  send("response.text.done", { responseId: response.id });
275
313
  await waitForSpeech(response);
276
314
  if (response.speechError) throw response.speechError;
315
+ const finalFrame = response.pcm.finish();
316
+ if (finalFrame) {
317
+ await response.playback.reserve(finalFrame.byteLength, response.abortController.signal);
318
+ if (activeResponse !== response || closed) return;
319
+ options.sendAudio(response.id, finalFrame);
320
+ }
277
321
  await response.playback.drain(response.abortController.signal);
278
322
  if (activeResponse !== response || response.abortController.signal.aborted) return;
279
323
  if (response.audioStarted) send("response.audio.done", { responseId: response.id });
@@ -367,7 +411,7 @@ function createAgentVoiceEngine(options) {
367
411
  if (claim.request.purpose === "conversation") {
368
412
  if (pendingTurn) {
369
413
  pendingTurn.cancelled = true;
370
- turn.restore(pendingTurn.text);
414
+ turn.restore(pendingTurn.turnId, pendingTurn.text);
371
415
  pendingTurn = void 0;
372
416
  }
373
417
  turn.start(event.utteranceId);
@@ -500,8 +544,11 @@ function createAgentVoiceEngine(options) {
500
544
  });
501
545
  return cancelActiveResponse(reason);
502
546
  },
503
- acknowledge(responseId, playedBytes) {
504
- if (activeResponse?.id === responseId) activeResponse.playback.acknowledge(playedBytes);
547
+ cancelTask(taskId) {
548
+ return options.runtime.agentBroker.cancel(taskId);
549
+ },
550
+ acknowledge(responseId, playedDurationMs) {
551
+ if (activeResponse?.id === responseId) activeResponse.playback.acknowledge(playedDurationMs * 48);
505
552
  },
506
553
  close() {
507
554
  if (closing) return closing;
@@ -8,6 +8,7 @@ export interface VoiceEngine {
8
8
  setInputMuted(muted: boolean): void | Promise<void>;
9
9
  commit(): Promise<void>;
10
10
  cancel(responseId: string, reason: 'client_cancelled' | 'barge_in'): boolean;
11
- acknowledge(responseId: string, playedBytes: number): void;
11
+ cancelTask(taskId: string): Promise<boolean>;
12
+ acknowledge(responseId: string, playedDurationMs: number): void;
12
13
  close(): void | Promise<void>;
13
14
  }
@@ -1,7 +1,8 @@
1
1
  import { createLogger } from "../../utils/logger/index.js";
2
2
  import { init_logger } from "../../utils/logger.js";
3
3
  import { AudioPlaybackWindow } from "./audio-playback-window.js";
4
- import { ConversationTurn } from "./conversationTurn.js";
4
+ import { TurnCoordinator } from "./turnPolicy.js";
5
+ import { PcmFrameBuffer } from "./pcmFrameBuffer.js";
5
6
  import crypto from "node:crypto";
6
7
  import WebSocket from "ws";
7
8
  //#region src/voice/realtime/omniEngine.ts
@@ -50,8 +51,16 @@ function createOmniVoiceEngine(options) {
50
51
  let uploadTimer;
51
52
  let turnSettled = false;
52
53
  const speaking = /* @__PURE__ */ new Set();
53
- const turn = new ConversationTurn(options.silenceDurationMs, () => {
54
+ const turn = new TurnCoordinator(options.silenceDurationMs, (_text, decision, turnId) => {
54
55
  if (closed || failed || muted || inputBlocked) return;
56
+ options.send("turn.decision", {
57
+ turnId,
58
+ disposition: decision.disposition,
59
+ confidence: decision.confidence,
60
+ source: decision.source,
61
+ committed: true
62
+ });
63
+ options.send("turn.committed", { turnId });
55
64
  turnSettled = true;
56
65
  if (active) publish(active);
57
66
  });
@@ -146,6 +155,23 @@ function createOmniVoiceEngine(options) {
146
155
  options.send("response.text.done", { responseId: response.id });
147
156
  await response.tail;
148
157
  if (active !== response || closed) return;
158
+ const finalFrame = response.pcm.finish();
159
+ if (finalFrame) {
160
+ await response.playback.reserve(finalFrame.byteLength, response.abort.signal);
161
+ if (active !== response || closed) return;
162
+ if (!response.audio) {
163
+ response.audio = true;
164
+ options.send("response.audio.started", {
165
+ responseId: response.id,
166
+ format: {
167
+ encoding: "pcm_s16le",
168
+ channels: 1,
169
+ sampleRate: 24e3
170
+ }
171
+ });
172
+ }
173
+ options.sendAudio(response.id, finalFrame);
174
+ }
149
175
  await response.playback.drain(response.abort.signal);
150
176
  if (active !== response || closed) return;
151
177
  save({
@@ -361,7 +387,8 @@ function createOmniVoiceEngine(options) {
361
387
  tail: Promise.resolve(),
362
388
  published: false,
363
389
  settled,
364
- release
390
+ release,
391
+ pcm: new PcmFrameBuffer()
365
392
  };
366
393
  if (turnSettled) publish(active);
367
394
  } else if (active && event.response_id === active.id && event.type === "response.audio_transcript.delta") {
@@ -392,9 +419,8 @@ function createOmniVoiceEngine(options) {
392
419
  response.queuedBytes += audio.length;
393
420
  response.tail = response.tail.then(async () => {
394
421
  await response.settled;
395
- for (let offset = 0; offset < audio.length; offset += 24e3) {
422
+ for (const chunk of response.pcm.push(audio)) {
396
423
  if (closed || active !== response) return;
397
- const chunk = audio.subarray(offset, offset + 24e3);
398
424
  await response.playback.reserve(chunk.length, response.abort.signal);
399
425
  if (closed || active !== response) return;
400
426
  if (!response.audio) {
@@ -471,8 +497,11 @@ function createOmniVoiceEngine(options) {
471
497
  if (reason === "client_cancelled") discardInput();
472
498
  return cancel(reason);
473
499
  },
474
- acknowledge(responseId, playedBytes) {
475
- if (active?.id === responseId) active.playback.acknowledge(playedBytes);
500
+ async cancelTask() {
501
+ return false;
502
+ },
503
+ acknowledge(responseId, playedDurationMs) {
504
+ if (active?.id === responseId) active.playback.acknowledge(playedDurationMs * 48);
476
505
  },
477
506
  close() {
478
507
  if (closed) return writes;
@@ -0,0 +1,6 @@
1
+ /** Converts arbitrary PCM16 chunks into exact 20 ms wire frames. */
2
+ export declare class PcmFrameBuffer {
3
+ private remainder;
4
+ push(bytes: Uint8Array): Uint8Array[];
5
+ finish(): Uint8Array | undefined;
6
+ }
@@ -0,0 +1,29 @@
1
+ //#region src/voice/realtime/pcmFrameBuffer.ts
2
+ const PCM_24KHZ_20MS_BYTES = 960;
3
+ /** Converts arbitrary PCM16 chunks into exact 20 ms wire frames. */
4
+ var PcmFrameBuffer = class {
5
+ remainder = new Uint8Array();
6
+ push(bytes) {
7
+ if (!bytes.byteLength || bytes.byteLength % 2) throw new Error("PCM16 audio must contain complete samples");
8
+ const pending = new Uint8Array(this.remainder.byteLength + bytes.byteLength);
9
+ pending.set(this.remainder);
10
+ pending.set(bytes, this.remainder.byteLength);
11
+ const frames = [];
12
+ let offset = 0;
13
+ while (offset + PCM_24KHZ_20MS_BYTES <= pending.byteLength) {
14
+ frames.push(pending.slice(offset, offset + PCM_24KHZ_20MS_BYTES));
15
+ offset += PCM_24KHZ_20MS_BYTES;
16
+ }
17
+ this.remainder = pending.slice(offset);
18
+ return frames;
19
+ }
20
+ finish() {
21
+ if (!this.remainder.byteLength) return void 0;
22
+ const frame = new Uint8Array(PCM_24KHZ_20MS_BYTES);
23
+ frame.set(this.remainder);
24
+ this.remainder = new Uint8Array();
25
+ return frame;
26
+ }
27
+ };
28
+ //#endregion
29
+ export { PcmFrameBuffer };
@@ -10,7 +10,7 @@ import { resolveSTTProviderChain } from "../stt/factory.js";
10
10
  import { mergeSttConfigFromAppConfig } from "../stt/config.js";
11
11
  import { voiceConversationInstructions } from "./conversation-context.js";
12
12
  import { createPreauthConnectionBudget } from "../../gateway/security/preauth-connection-budget.js";
13
- import { encodeVoiceAudioFrame } from "../../../packages/realtime-protocol/src/voice-audio.js";
13
+ import { decodeVoiceUplinkAudioFrame, encodeVoiceAudioFrame } from "../../../packages/realtime-protocol/src/voice-audio.js";
14
14
  import { VOICE_REALTIME_HEARTBEAT_INTERVAL_MS, VOICE_REALTIME_MAX_BINARY_FRAME_BYTES, VOICE_REALTIME_START_TIMEOUT_MS, VOICE_REALTIME_WS_PATH, parseVoiceClientJsonFrame, parseVoiceClientMessage } from "../../../packages/realtime-protocol/src/voice.js";
15
15
  import { createAgentVoiceEngine } from "./agentEngine.js";
16
16
  import { resolveOmniRoute } from "./omniRoute.js";
@@ -157,27 +157,28 @@ var VoiceRealtimeRuntime = class {
157
157
  const config = structuredClone(this.options.getConfig());
158
158
  this.pruneTickets(now);
159
159
  if (!config.voice?.realtime?.enabled) throw new VoiceSessionCreationError("VOICE_DISABLED", "Realtime voice is disabled", 503);
160
+ const defaultMode = config.voice.realtime.defaultEngine === "omni" ? "natural" : "assistant";
160
161
  request = request.purpose === "conversation" ? {
161
162
  ...request,
162
- engine: request.engine ?? config.voice.realtime.defaultEngine
163
+ mode: request.mode ?? defaultMode
163
164
  } : request;
164
165
  let omni;
165
- if (request.engine === "omni") try {
166
+ if (request.mode === "natural") try {
166
167
  omni = await resolveOmniRoute(config);
167
168
  } catch {
168
169
  throw new VoiceSessionCreationError("PROVIDER_UNAVAILABLE", "Natural conversation is unavailable. Check its model, endpoint and credentials.", 503);
169
170
  }
170
171
  if (omni && (!this.options.recordOmniTranscript || !this.options.getConversationContext)) throw new VoiceSessionCreationError("PROVIDER_UNAVAILABLE", "Natural conversation storage is unavailable", 503);
171
172
  const conversationSessionId = request.purpose === "conversation" && request.sessionKey ? await this.options.getSessionIdentity?.(request.sessionKey) : void 0;
172
- if (omni && !conversationSessionId) throw new VoiceSessionCreationError("SESSION_NOT_FOUND", "Conversation session was not found", 404);
173
+ if (request.purpose === "conversation" && !conversationSessionId) throw new VoiceSessionCreationError("SESSION_NOT_FOUND", "Conversation session was not found", 404);
173
174
  const stt = omni ? void 0 : resolveStreamingStt(config, request.language);
174
175
  if (!omni && !stt) throw new VoiceSessionCreationError("PROVIDER_UNAVAILABLE", "No configured streaming speech-to-text provider is available", 503);
175
176
  const inputMode = "server_vad";
176
177
  if (stt && !stt.plugin.streamingAudio.turnDetection.includes(inputMode)) throw new VoiceSessionCreationError("PROVIDER_UNAVAILABLE", `Streaming speech-to-text provider does not support ${inputMode}`, 503);
177
- const tts = request.engine === "agent" ? resolveStreamingTts(config) : void 0;
178
+ const tts = request.mode === "assistant" ? resolveStreamingTts(config) : void 0;
178
179
  if (request.purpose === "conversation") {
179
180
  if (!request.sessionKey || !await this.options.sessionExists(request.sessionKey)) throw new VoiceSessionCreationError("SESSION_NOT_FOUND", "Conversation session was not found", 404);
180
- if (this.options.sessionBusy(request.sessionKey)) throw new VoiceSessionCreationError("SESSION_CONFLICT", "Conversation session already has an active response", 409);
181
+ if (request.mode === "natural" && this.options.sessionBusy(request.sessionKey)) throw new VoiceSessionCreationError("SESSION_CONFLICT", "Conversation session already has an active response", 409);
181
182
  if (this.conversationReservations.has(request.sessionKey)) throw new VoiceSessionCreationError("SESSION_CONFLICT", "Conversation session already has a voice connection", 409);
182
183
  if (!omni && !tts) throw new VoiceSessionCreationError("PROVIDER_UNAVAILABLE", "No configured PCM streaming text-to-speech provider is available", 503);
183
184
  }
@@ -200,11 +201,14 @@ var VoiceRealtimeRuntime = class {
200
201
  request = resolvedRequest;
201
202
  const sessionId = crypto.randomUUID();
202
203
  const ticket = crypto.randomBytes(32).toString("base64url");
204
+ const connectionEpoch = crypto.randomInt(1, 4294967296);
203
205
  const maxSessionMs = request.purpose === "conversation" ? Math.min(config.voice.realtime.maxConversationMs, omni?.route.managed ? 30 * 6e4 : Number.MAX_SAFE_INTEGER) : config.voice.realtime.maxDictationMs;
204
206
  const claim = {
205
207
  conversationSessionId,
206
208
  sessionId,
209
+ connectionEpoch,
207
210
  principalId,
211
+ ...request.purpose === "conversation" && request.mode ? { mode: request.mode } : {},
208
212
  request,
209
213
  inputMode,
210
214
  idleTimeoutMs: config.voice.realtime.idleTimeoutMs,
@@ -224,8 +228,10 @@ var VoiceRealtimeRuntime = class {
224
228
  ticket,
225
229
  ticketExpiresAt: new Date(claim.expiresAt).toISOString(),
226
230
  websocketPath: VOICE_REALTIME_WS_PATH,
227
- protocolVersion: 2,
231
+ protocolVersion: 3,
232
+ connectionEpoch,
228
233
  purpose: request.purpose,
234
+ ...request.purpose === "conversation" && request.mode ? { mode: request.mode } : {},
229
235
  inputMode,
230
236
  bargeIn: config.voice.realtime.bargeIn,
231
237
  inputFormat: {
@@ -233,6 +239,11 @@ var VoiceRealtimeRuntime = class {
233
239
  sampleRate: 16e3,
234
240
  channels: 1
235
241
  },
242
+ media: {
243
+ transport: "websocket-pcm",
244
+ codec: "pcm_s16le",
245
+ frameDurationMs: 20
246
+ },
236
247
  limits: {
237
248
  maxBinaryFrameBytes: VOICE_REALTIME_MAX_BINARY_FRAME_BYTES,
238
249
  maxSessionMs,
@@ -252,7 +263,7 @@ var VoiceRealtimeRuntime = class {
252
263
  };
253
264
  }
254
265
  handleUpgrade(req, socket, head) {
255
- if (new URL(req.url ?? "/", "http://gateway.local").pathname !== "/api/voice/realtime/v2/ws") return false;
266
+ if (new URL(req.url ?? "/", "http://gateway.local").pathname !== "/api/voice/realtime/v3/ws") return false;
256
267
  if (this.closed || this.wss.clients.size >= MAX_CONNECTIONS) {
257
268
  socket.destroy();
258
269
  return true;
@@ -319,6 +330,8 @@ var VoiceRealtimeRuntime = class {
319
330
  let ready = false;
320
331
  let seq = 0;
321
332
  let audioSeq = 0;
333
+ let outputMediaTimestampMs = 0;
334
+ const inputSequences = /* @__PURE__ */ new Map();
322
335
  let closed = false;
323
336
  let lastActivityAt = Date.now();
324
337
  const abortController = new AbortController();
@@ -326,10 +339,11 @@ var VoiceRealtimeRuntime = class {
326
339
  let context;
327
340
  let unsubscribeMemory;
328
341
  let unsubscribeSession;
342
+ let inputMutationTail = Promise.resolve();
329
343
  const send = (type, payload) => {
330
344
  if (!claim || socket.readyState !== WebSocketState.OPEN) return;
331
345
  const event = {
332
- protocolVersion: 2,
346
+ protocolVersion: 3,
333
347
  eventId: crypto.randomUUID(),
334
348
  seq: ++seq,
335
349
  type,
@@ -434,11 +448,20 @@ var VoiceRealtimeRuntime = class {
434
448
  setupStage = "engine";
435
449
  const sendAudio = (responseId, bytes) => {
436
450
  if (socket.bufferedAmount > 1024 * 1024) throw new Error("Voice client audio backpressure limit exceeded");
437
- if (!closed && socket.readyState === WebSocketState.OPEN) socket.send(encodeVoiceAudioFrame({
438
- responseId,
439
- seq: ++audioSeq,
440
- audio: bytes
441
- }), { binary: true });
451
+ if (closed || socket.readyState !== WebSocketState.OPEN) return;
452
+ for (let offset = 0; offset < bytes.byteLength; offset += 960) {
453
+ const audio = bytes.subarray(offset, offset + 960);
454
+ if (audio.byteLength !== 960) throw new Error("Realtime output must align to 20 ms PCM frames");
455
+ socket.send(encodeVoiceAudioFrame({
456
+ connectionEpoch: consumed.connectionEpoch,
457
+ responseId,
458
+ seq: ++audioSeq,
459
+ mediaTimestampMs: outputMediaTimestampMs,
460
+ durationMs: 20,
461
+ audio
462
+ }), { binary: true });
463
+ outputMediaTimestampMs += 20;
464
+ }
442
465
  };
443
466
  engine = consumed.omni ? createOmniVoiceEngine({
444
467
  callId: consumed.sessionId,
@@ -469,12 +492,19 @@ var VoiceRealtimeRuntime = class {
469
492
  }, "Realtime voice session ready");
470
493
  send("session.ready", {
471
494
  purpose: consumed.request.purpose,
495
+ ...consumed.mode ? { mode: consumed.mode } : {},
496
+ connectionEpoch: consumed.connectionEpoch,
472
497
  inputMode: consumed.inputMode,
473
498
  inputFormat: {
474
499
  encoding: "pcm_s16le",
475
500
  sampleRate: 16e3,
476
501
  channels: 1
477
502
  },
503
+ media: {
504
+ transport: "websocket-pcm",
505
+ codec: "pcm_s16le",
506
+ frameDurationMs: 20
507
+ },
478
508
  route: consumed.omni ? {
479
509
  engine: "omni",
480
510
  omni: consumed.omni.route
@@ -512,7 +542,19 @@ var VoiceRealtimeRuntime = class {
512
542
  return;
513
543
  }
514
544
  const bytes = data instanceof ArrayBuffer ? new Uint8Array(data) : Array.isArray(data) ? Buffer.concat(data) : new Uint8Array(data.buffer, data.byteOffset, data.byteLength);
515
- if (bytes.byteLength === 0 || bytes.byteLength % 2 !== 0 || bytes.byteLength > 65536) {
545
+ let frame;
546
+ try {
547
+ frame = decodeVoiceUplinkAudioFrame(bytes);
548
+ } catch {
549
+ send("session.error", {
550
+ code: "INVALID_AUDIO",
551
+ message: "Invalid PCM audio frame",
552
+ recoverable: false
553
+ });
554
+ shutdown("invalid_audio", true);
555
+ return;
556
+ }
557
+ if (frame.connectionEpoch !== claim.connectionEpoch || frame.audio.byteLength !== 640) {
516
558
  send("session.error", {
517
559
  code: "INVALID_AUDIO",
518
560
  message: "Invalid PCM audio frame",
@@ -521,12 +563,24 @@ var VoiceRealtimeRuntime = class {
521
563
  shutdown("invalid_audio", true);
522
564
  return;
523
565
  }
566
+ const previous = inputSequences.get(frame.utteranceId) ?? 0;
567
+ if (frame.audioSeq <= previous) return;
568
+ if (frame.audioSeq !== previous + 1 || inputSequences.size >= 256) {
569
+ send("session.error", {
570
+ code: "INVALID_AUDIO",
571
+ message: "Invalid audio sequence",
572
+ recoverable: false
573
+ });
574
+ shutdown("invalid_audio", true);
575
+ return;
576
+ }
577
+ inputSequences.set(frame.utteranceId, frame.audioSeq);
524
578
  if (!ready || !engine) {
525
579
  socket.close(4401, "Voice session is not ready");
526
580
  return;
527
581
  }
528
582
  try {
529
- engine.appendAudio(bytes);
583
+ engine.appendAudio(frame.audio);
530
584
  } catch (error) {
531
585
  log.warn({
532
586
  err: error,
@@ -574,23 +628,45 @@ var VoiceRealtimeRuntime = class {
574
628
  metric,
575
629
  durationMs,
576
630
  source: "client",
577
- engine: claim.request.engine
631
+ mode: claim.mode
578
632
  }, "Voice client timing sample");
579
633
  }
580
- } else if (message.type === "response.cancel") {
634
+ } else if (message.type === "response.stop_playback") {
581
635
  if (!engine?.cancel(message.payload.responseId, "client_cancelled")) send("session.error", {
582
636
  code: "NO_ACTIVE_RESPONSE",
583
637
  message: "No matching response is active",
584
638
  recoverable: true
585
639
  });
586
- } else if (message.type === "response.audio.played") try {
587
- engine?.acknowledge(message.payload.responseId, message.payload.playedBytes);
640
+ } else if (message.type === "task.cancel") engine?.cancelTask(message.payload.taskId).then((cancelled) => {
641
+ if (!cancelled) send("session.error", {
642
+ code: "NO_ACTIVE_TASK",
643
+ message: "No matching task is active",
644
+ recoverable: true
645
+ });
646
+ }).catch((error) => {
647
+ log.warn({
648
+ err: error,
649
+ sessionId: claim?.sessionId,
650
+ taskId: message.payload.taskId
651
+ }, "Voice task cancellation failed");
652
+ send("session.error", {
653
+ code: "TASK_CANCEL_FAILED",
654
+ message: "Could not cancel the task",
655
+ recoverable: true
656
+ });
657
+ });
658
+ else if (message.type === "response.audio.played") try {
659
+ engine?.acknowledge(message.payload.responseId, message.payload.playedDurationMs);
588
660
  } catch {
589
661
  socket.close(4400, "Invalid playback acknowledgement");
590
662
  }
591
663
  else if (message.type === "input.mute") {
592
664
  if (!ready || !engine || claim.request.purpose !== "conversation") return;
593
- Promise.resolve().then(() => engine.setInputMuted(message.payload.muted)).catch((error) => {
665
+ const muted = message.payload.muted;
666
+ inputMutationTail = inputMutationTail.then(async () => {
667
+ if (closed || !ready || !engine) return;
668
+ await engine.setInputMuted(muted);
669
+ }).catch((error) => {
594
670
  log.warn({
595
671
  err: error,
596
672
  sessionId: claim?.sessionId