@xopcai/xopc 0.0.264 → 0.0.266

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 (209) 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-Qrp5ALCF.js} +1 -1
  10. package/dist/gateway/static/root/assets/{agent-defaults-settings-panel-BZvMqNrU.js → agent-defaults-settings-panel-DwinST48.js} +1 -1
  11. package/dist/gateway/static/root/assets/{agents-6foBELwt.js → agents-Dlr0YyNt.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-bLNvz93c.js} +1 -1
  14. package/dist/gateway/static/root/assets/{appearance-settings-C7nBHnkG.js → appearance-settings-D5qgIepF.js} +1 -1
  15. package/dist/gateway/static/root/assets/{apps-page-B_g-rL5b.js → apps-page-BQVlXhIb.js} +1 -1
  16. package/dist/gateway/static/root/assets/{archive-plugin-DfKK1iJO.js → archive-plugin-BEA7VAA4.js} +1 -1
  17. package/dist/gateway/static/root/assets/{attachment-load-BOU1pdnQ.js → attachment-load-B1RyF3uu.js} +1 -1
  18. package/dist/gateway/static/root/assets/{automations-page-BePzexAH.js → automations-page-BfH0FdKa.js} +1 -1
  19. package/dist/gateway/static/root/assets/{automations-workspace-7-m5pBEZ.js → automations-workspace-C37AlD3E.js} +1 -1
  20. package/dist/gateway/static/root/assets/{binary-plugins-Cly8XDyt.js → binary-plugins-EU884ZXi.js} +1 -1
  21. package/dist/gateway/static/root/assets/{block-editor-DzSu62js.js → block-editor-Bjr3naHb.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-CDrYrNGy.js} +1 -1
  24. package/dist/gateway/static/root/assets/{browser-settings-page-Dum3mTiz.js → browser-settings-page-DOmUkj2k.js} +1 -1
  25. package/dist/gateway/static/root/assets/{capabilities-settings-panel-Ci6qVS-O.js → capabilities-settings-panel-slt1o04n.js} +2 -2
  26. package/dist/gateway/static/root/assets/{channels-settings-BOGEZnSv.js → channels-settings-oHTTX6Jy.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-DEYQdv6n.js} +1 -1
  29. package/dist/gateway/static/root/assets/{connectors-page-DBvEvupa.js → connectors-page-olrbxHIT.js} +1 -1
  30. package/dist/gateway/static/root/assets/{cron-expression-BszT_gjK.js → cron-expression-DFrbDWpP.js} +1 -1
  31. package/dist/gateway/static/root/assets/{date-picker-D2MKBJ0G.js → date-picker-CYriYKpH.js} +1 -1
  32. package/dist/gateway/static/root/assets/{dependency-picker-DqigBx-G.js → dependency-picker-XY3qUErs.js} +1 -1
  33. package/dist/gateway/static/root/assets/{desktop-pet-CapspBGG.js → desktop-pet-Dsc__ynA.js} +1 -1
  34. package/dist/gateway/static/root/assets/{desktop-pet-settings-DMPk4CaK.js → desktop-pet-settings-EpuX3C08.js} +1 -1
  35. package/dist/gateway/static/root/assets/{extension-debug-page-CXLsDtrW.js → extension-debug-page-B_NeLV4K.js} +1 -1
  36. package/dist/gateway/static/root/assets/{extension-page-B2j87ROw.js → extension-page-D3JC6mLB.js} +1 -1
  37. package/dist/gateway/static/root/assets/{extension-settings-page-t1BApw-u.js → extension-settings-page-CXTQt1lp.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-yeYm_UPE.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-coh1LFsg.js} +1 -1
  43. package/dist/gateway/static/root/assets/{home-page-uItZMiBE.js → home-page-CAgoBZqp.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-Cn__9Un2.js +74 -0
  46. package/dist/gateway/static/root/assets/{keyboard-shortcuts-settings-Q_CM-ZCs.js → keyboard-shortcuts-settings-Cu1bI3Bg.js} +1 -1
  47. package/dist/gateway/static/root/assets/{local-app-workbench-page-DaiBWfOt.js → local-app-workbench-page-C9s2lFaI.js} +1 -1
  48. package/dist/gateway/static/root/assets/{local-apps-page-BALMNCSp.js → local-apps-page-CJ03Umjr.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-CY_DQKJ2.js} +1 -1
  51. package/dist/gateway/static/root/assets/{management-settings-BajkJeYl.js → management-settings-YPEemeMo.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-BjT6jlrA.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-A8eLtV6v.js} +1 -1
  56. package/dist/gateway/static/root/assets/{note-detail-panel-CCbPg_GP.js → note-detail-panel-DHT2QksJ.js} +3 -3
  57. package/dist/gateway/static/root/assets/{notes-page-BdxxcUKa.js → notes-page-BRFQiivM.js} +1 -1
  58. package/dist/gateway/static/root/assets/{page-tabs-BMYQef5D.js → page-tabs-DNUwulnl.js} +1 -1
  59. package/dist/gateway/static/root/assets/{preview-open-alternatives-CKZ-H5hJ.js → preview-open-alternatives-B33KsKkJ.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-CItEFVt9.js} +1 -1
  63. package/dist/gateway/static/root/assets/{projects-page-Wt7gF3Sx.js → projects-page-Bmy-c_lL.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-H6B1cA2V.js} +1 -1
  66. package/dist/gateway/static/root/assets/{runtime-tools-settings-panel-DcUB5h1f.js → runtime-tools-settings-panel-BsCovC39.js} +1 -1
  67. package/dist/gateway/static/root/assets/{schema-form-CquWhe7r.js → schema-form-NsPb_Vam.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-CyYLSXk_.js} +1 -1
  70. package/dist/gateway/static/root/assets/settings-advanced-gate-3T4rPk4A.js +1 -0
  71. package/dist/gateway/static/root/assets/{settings-form-section-DwswAefc.js → settings-form-section-BzPIPe1-.js} +1 -1
  72. package/dist/gateway/static/root/assets/{settings-page-CoAVmjjs.js → settings-page-4idpMhsw.js} +1 -1
  73. package/dist/gateway/static/root/assets/{setup-status-panel-C9EIcWtH.js → setup-status-panel-D2ds_TTD.js} +1 -1
  74. package/dist/gateway/static/root/assets/{share-preview-page-DWwy_4zh.js → share-preview-page-Djo3VTaC.js} +1 -1
  75. package/dist/gateway/static/root/assets/{shares-settings-CIUvq27-.js → shares-settings-fhRzNfRp.js} +1 -1
  76. package/dist/gateway/static/root/assets/skill-reload-api-Ct8tUju_.js +1 -0
  77. package/dist/gateway/static/root/assets/{skills-page-CqsgVtVh.js → skills-page-riM7FytZ.js} +1 -1
  78. package/dist/gateway/static/root/assets/{skills-page.utils-B-N73248.js → skills-page.utils-B3G8LBb-.js} +1 -1
  79. package/dist/gateway/static/root/assets/{system-settings-panel-BndjXuBZ.js → system-settings-panel-CW91jjnB.js} +1 -1
  80. package/dist/gateway/static/root/assets/{task-detail-page-CG6Ha9t7.js → task-detail-page-BBXKvl0k.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-DILER0Yb.js} +1 -1
  83. package/dist/gateway/static/root/assets/{use-autosave-mj9CUuz1.js → use-autosave-GxnP8vLO.js} +1 -1
  84. package/dist/gateway/static/root/assets/{user-model-page-DKeHMXI4.js → user-model-page-I1H4XRfA.js} +1 -1
  85. package/dist/gateway/static/root/assets/{voice-api-key-field-CzxVMYlr.js → voice-api-key-field-CI3sXyl4.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-DZC9Exn3.js} +1 -1
  88. package/dist/gateway/static/root/assets/{workflow-run-setup-panel-C82OXCJK.js → workflow-run-setup-panel-CaBaQER2.js} +1 -1
  89. package/dist/gateway/static/root/assets/{workflows-page-HLCUV3rQ.js → workflows-page-C-_Dxihw.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/agent-manager.js +4 -4
  101. package/dist/src/agent/background-review/run-background-review.js +1 -1
  102. package/dist/src/agent/embedded/model-runtime.js +1 -1
  103. package/dist/src/agent/embedded/run-for-session.js +1 -1
  104. package/dist/src/agent/image/tool-model-config.js +1 -1
  105. package/dist/src/agent/mcp/oauth/mcp-oauth-store.js +1 -1
  106. package/dist/src/agent/service.js +2 -2
  107. package/dist/src/agent/session/session-inspector.js +1 -1
  108. package/dist/src/agent/skills/hub-lock.js +1 -1
  109. package/dist/src/agent/source-context/browser-page.d.ts +3 -0
  110. package/dist/src/agent/source-context/browser-page.js +39 -0
  111. package/dist/src/agent/source-context/injector.js +9 -2
  112. package/dist/src/agent/source-context/types.d.ts +7 -1
  113. package/dist/src/agent/source-context/types.js +4 -1
  114. package/dist/src/agent/tasks/task-judge-service.js +3 -3
  115. package/dist/src/agent/tools/factory.js +9 -1
  116. package/dist/src/ai-assist/text-assist.js +1 -1
  117. package/dist/src/auth/credentials.js +2 -2
  118. package/dist/src/auth/oauth/xopc-cloud.js +1 -1
  119. package/dist/src/auth/profiles/store.js +1 -1
  120. package/dist/src/auth/sync-provider-auth.js +2 -2
  121. package/dist/src/browser/automations/schema.d.ts +1 -1
  122. package/dist/src/browser/automations/service.d.ts +1 -1
  123. package/dist/src/browser/native-messaging-host.d.ts +15 -0
  124. package/dist/src/browser/native-messaging-host.js +105 -0
  125. package/dist/src/browser/providers/browser-ext-install.d.ts +17 -1
  126. package/dist/src/browser/providers/browser-ext-install.js +82 -16
  127. package/dist/src/browser/providers/extension.d.ts +4 -0
  128. package/dist/src/browser/providers/extension.js +104 -38
  129. package/dist/src/browser/runtime/browser-runtime.d.ts +2 -1
  130. package/dist/src/browser/runtime/browser-runtime.js +6 -2
  131. package/dist/src/cli/commands/browser.js +13 -3
  132. package/dist/src/cli/commands/doctor/checks/database-schema.js +1 -1
  133. package/dist/src/cli/commands/doctor/checks/provider-auth.js +1 -1
  134. package/dist/src/cli/commands/models.js +1 -1
  135. package/dist/src/config/index.js +4 -4
  136. package/dist/src/config/loader.js +2 -2
  137. package/dist/src/config/models-json.js +1 -1
  138. package/dist/src/endpoint-tools/runtime.js +2 -0
  139. package/dist/src/extensions/lockfile.js +1 -1
  140. package/dist/src/gateway/agents-admin.js +1 -1
  141. package/dist/src/gateway/chat-stream/mapper.js +3 -2
  142. package/dist/src/gateway/file-path-classifier.js +1 -1
  143. package/dist/src/gateway/hono/app.js +19 -3
  144. package/dist/src/gateway/hono/lib/config-payload.js +1 -1
  145. package/dist/src/gateway/hono/middleware/auth.d.ts +4 -1
  146. package/dist/src/gateway/hono/middleware/auth.js +12 -1
  147. package/dist/src/gateway/hono/routes/browser-install.js +1 -1
  148. package/dist/src/gateway/hono/routes/browser.js +106 -7
  149. package/dist/src/gateway/hono/routes/devices.js +36 -3
  150. package/dist/src/gateway/hono/routes/endpoint-tools.js +15 -0
  151. package/dist/src/gateway/hono/routes/models.js +1 -1
  152. package/dist/src/gateway/hono/routes/session-input-handler.d.ts +20 -2
  153. package/dist/src/gateway/hono/routes/session-input-handler.js +18 -0
  154. package/dist/src/gateway/hono/routes/sessions.js +12 -6
  155. package/dist/src/gateway/hono/routes/voice.js +5 -4
  156. package/dist/src/gateway/security/gateway-scopes.d.ts +1 -0
  157. package/dist/src/gateway/security/gateway-scopes.js +10 -1
  158. package/dist/src/gateway/security/origin-check.d.ts +1 -0
  159. package/dist/src/gateway/security/origin-check.js +5 -1
  160. package/dist/src/gateway/service/agent-runner.js +2 -1
  161. package/dist/src/gateway/service/config-coordinator.js +1 -1
  162. package/dist/src/gateway/service/session-input-coordinator.d.ts +1 -0
  163. package/dist/src/gateway/service/session-input-coordinator.js +5 -4
  164. package/dist/src/gateway/service.js +22 -21
  165. package/dist/src/providers/auth-runtime/resolve-auth.js +1 -1
  166. package/dist/src/providers/index.js +2 -2
  167. package/dist/src/providers/model-registry.js +1 -1
  168. package/dist/src/providers/register-runtime-providers.js +1 -1
  169. package/dist/src/realtime/runtime.js +2 -1
  170. package/dist/src/session/client-history.d.ts +4 -1
  171. package/dist/src/session/client-history.js +6 -3
  172. package/dist/src/storage/sqlite/browser-tab-binding-repository.d.ts +7 -0
  173. package/dist/src/storage/sqlite/browser-tab-binding-repository.js +60 -0
  174. package/dist/src/storage/sqlite/device-access-repository.d.ts +3 -1
  175. package/dist/src/storage/sqlite/device-access-repository.js +4 -3
  176. package/dist/src/storage/sqlite/device-pairing-approval.js +6 -4
  177. package/dist/src/storage/sqlite/device-pairing-repository.d.ts +1 -1
  178. package/dist/src/storage/sqlite/index.d.ts +1 -0
  179. package/dist/src/storage/sqlite/index.js +2 -1
  180. package/dist/src/storage/sqlite/migrations/155_browser_extension.sql +120 -0
  181. package/dist/src/storage/sqlite/migrations/156_browser_tab_bindings.sql +14 -0
  182. package/dist/src/storage/sqlite/migrations/runner.d.ts +1 -1
  183. package/dist/src/storage/sqlite/migrations/runner.js +3 -3
  184. package/dist/src/storage/sqlite/migrations/runner.ts +1 -1
  185. package/dist/src/storage/sqlite/schema.js +1 -1
  186. package/dist/src/tui/tui-startup-resources.js +1 -1
  187. package/dist/src/tui/tui.js +1 -1
  188. package/dist/src/voice/realtime/agentBroker.d.ts +54 -0
  189. package/dist/src/voice/realtime/agentBroker.js +116 -0
  190. package/dist/src/voice/realtime/agentEngine.js +62 -15
  191. package/dist/src/voice/realtime/engine.d.ts +2 -1
  192. package/dist/src/voice/realtime/omniEngine.js +36 -7
  193. package/dist/src/voice/realtime/pcmFrameBuffer.d.ts +6 -0
  194. package/dist/src/voice/realtime/pcmFrameBuffer.js +29 -0
  195. package/dist/src/voice/realtime/runtime.js +97 -21
  196. package/dist/src/voice/realtime/runtime.types.d.ts +5 -21
  197. package/dist/src/voice/realtime/turnPolicy.d.ts +36 -0
  198. package/dist/src/voice/realtime/turnPolicy.js +134 -0
  199. package/dist/src/work-discovery/service.js +1 -1
  200. package/dist/src/workflows/service/workflow-run-service.js +1 -1
  201. package/package.json +2 -2
  202. package/dist/browser-ext/dist/content.js +0 -191
  203. package/dist/browser-ext/dist/popup.js +0 -32
  204. package/dist/browser-ext/popup.html +0 -37
  205. package/dist/gateway/static/root/assets/index-DQtjs2x3.js +0 -74
  206. package/dist/gateway/static/root/assets/settings-advanced-gate-DK1yTYE4.js +0 -1
  207. package/dist/gateway/static/root/assets/skill-reload-api-DfE3zFVw.js +0 -1
  208. package/dist/src/voice/realtime/conversationTurn.d.ts +0 -20
  209. package/dist/src/voice/realtime/conversationTurn.js +0 -83
@@ -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
@@ -1,4 +1,4 @@
1
- import type { CreateVoiceSessionRequest, VoiceProviderRoute } from '@xopcai/realtime-protocol/voice';
1
+ import type { CreateVoiceSessionRequest, VoiceMode, VoiceProviderRoute } from '@xopcai/realtime-protocol/voice';
2
2
  import type { Config } from '../../config/schema.js';
3
3
  import type { MediaUnderstandingProvider } from '../../media-understanding/types.js';
4
4
  import type { ResolvedSpeechProvider } from '../tts/factory.js';
@@ -6,6 +6,7 @@ import type { TTSConfig } from '../tts/types.js';
6
6
  import type { VoiceConversationContext } from './conversation-context.js';
7
7
  import type { OmniTranscript } from './omniEngine.js';
8
8
  import type { OmniRoute } from './omniRoute.js';
9
+ import type { VoiceAgentBroker } from './agentBroker.js';
9
10
  export interface ResolvedStreamingStt {
10
11
  plugin: MediaUnderstandingProvider & Required<Pick<MediaUnderstandingProvider, 'openAudioStream' | 'streamingAudio'>>;
11
12
  model: string;
@@ -18,7 +19,9 @@ export interface ResolvedStreamingStt {
18
19
  }
19
20
  export interface VoiceTicketClaim {
20
21
  sessionId: string;
22
+ connectionEpoch: number;
21
23
  principalId: string;
24
+ mode?: VoiceMode;
22
25
  request: CreateVoiceSessionRequest;
23
26
  inputMode: 'server_vad';
24
27
  idleTimeoutMs: number;
@@ -37,24 +40,6 @@ export interface ResolvedStreamingTts {
37
40
  config: TTSConfig;
38
41
  route: VoiceProviderRoute;
39
42
  }
40
- interface VoiceAgentEvent {
41
- type: string;
42
- payload?: {
43
- delta?: unknown;
44
- message?: unknown;
45
- status?: unknown;
46
- toolCallId?: unknown;
47
- toolName?: unknown;
48
- requestId?: unknown;
49
- kind?: unknown;
50
- question?: unknown;
51
- choices?: unknown;
52
- suggestedAnswer?: unknown;
53
- version?: unknown;
54
- createdAt?: unknown;
55
- expiresAt?: unknown;
56
- };
57
- }
58
43
  export interface VoiceRealtimeRuntimeOptions {
59
44
  recordOmniTranscript?: (sessionKey: string, callId: string, entry: OmniTranscript, expectedSessionId: string) => Promise<void>;
60
45
  getConversationContext?: (sessionKey: string, expectedSessionId: string) => Promise<VoiceConversationContext>;
@@ -62,7 +47,7 @@ export interface VoiceRealtimeRuntimeOptions {
62
47
  getConfig: () => Config;
63
48
  sessionExists: (sessionKey: string) => Promise<boolean>;
64
49
  sessionBusy: (sessionKey: string) => boolean;
65
- runAgent: (text: string, sessionKey: string, signal: AbortSignal) => AsyncIterable<VoiceAgentEvent>;
50
+ agentBroker: VoiceAgentBroker;
66
51
  recordInterruption: (entry: {
67
52
  sessionKey: string;
68
53
  responseId: string;
@@ -71,4 +56,3 @@ export interface VoiceRealtimeRuntimeOptions {
71
56
  interruptedDuring: 'thinking' | 'speaking';
72
57
  }) => Promise<void>;
73
58
  }
74
- export {};
@@ -0,0 +1,36 @@
1
+ export type TurnDisposition = 'complete' | 'incomplete' | 'backchannel' | 'wait';
2
+ export interface TurnSnapshot {
3
+ text: string;
4
+ silenceDurationMs: number;
5
+ }
6
+ export interface TurnDecision {
7
+ disposition: TurnDisposition;
8
+ confidence: number;
9
+ holdMs: number;
10
+ source: 'provider' | 'semantic' | 'heuristic';
11
+ }
12
+ export interface TurnPolicy {
13
+ decide(snapshot: TurnSnapshot): TurnDecision | Promise<TurnDecision>;
14
+ }
15
+ export declare function hasContinuationHint(text: string): boolean;
16
+ export declare const heuristicTurnPolicy: TurnPolicy;
17
+ /** Accumulates provider utterances and commits only after the selected policy settles. */
18
+ export declare class TurnCoordinator {
19
+ private readonly silenceDurationMs;
20
+ private readonly onCommit;
21
+ private readonly policy;
22
+ private readonly parts;
23
+ private timer;
24
+ private stoppedAt;
25
+ private decisionGeneration;
26
+ constructor(silenceDurationMs: number, onCommit: (text: string, decision: TurnDecision, turnId: string) => void, policy?: TurnPolicy);
27
+ start(id: string): void;
28
+ stop(id: string): void;
29
+ final(id: string, text: string): void;
30
+ reset(): void;
31
+ restore(id: string, text: string): void;
32
+ private text;
33
+ private invalidatePendingDecision;
34
+ private evaluate;
35
+ private schedule;
36
+ }
@@ -0,0 +1,134 @@
1
+ //#region src/voice/realtime/turnPolicy.ts
2
+ const MIN_TURN_SILENCE_MS = 1200;
3
+ const FINAL_SETTLE_MS = 350;
4
+ const INCOMPLETE_HOLD_MS = 1800;
5
+ const DECISION_TIMEOUT_MS = 750;
6
+ const MAX_TRANSCRIPT_CHARACTERS = 32e3;
7
+ const MAX_UTTERANCES = 256;
8
+ function isPromise(value) {
9
+ return typeof value.then === "function";
10
+ }
11
+ function hasContinuationHint(text) {
12
+ const tail = text.trim().replace(/[\s,。!?、;:,.!?;:]+$/u, "");
13
+ return /(?:…|\.\.\.)[\s,。!?,.!?]*$/u.test(text) || /(?:因为|但是|然后|而且|如果|所以|比如|或者|还有|我想|我要|我觉得|帮我|帮我查一下|帮我查查|请帮我|就是说|那个|这个|呃|嗯)$/u.test(tail) || /\b(?:because|but|and|or|if|so|then|um|uh|i want to|i would like to|can you|could you|let me)\s*$/iu.test(tail);
14
+ }
15
+ const heuristicTurnPolicy = { decide({ text }) {
16
+ const incomplete = hasContinuationHint(text);
17
+ return {
18
+ disposition: incomplete ? "incomplete" : "complete",
19
+ confidence: incomplete ? .6 : .5,
20
+ holdMs: incomplete ? INCOMPLETE_HOLD_MS : 0,
21
+ source: "heuristic"
22
+ };
23
+ } };
24
+ /** Accumulates provider utterances and commits only after the selected policy settles. */
25
+ var TurnCoordinator = class {
26
+ parts = /* @__PURE__ */ new Map();
27
+ timer;
28
+ stoppedAt = 0;
29
+ decisionGeneration = 0;
30
+ constructor(silenceDurationMs, onCommit, policy = heuristicTurnPolicy) {
31
+ this.silenceDurationMs = silenceDurationMs;
32
+ this.onCommit = onCommit;
33
+ this.policy = policy;
34
+ }
35
+ start(id) {
36
+ if (this.parts.has(id)) return;
37
+ this.invalidatePendingDecision();
38
+ this.parts.set(id, { stopped: false });
39
+ }
40
+ stop(id) {
41
+ const part = this.parts.get(id);
42
+ if (part?.stopped) return;
43
+ this.parts.set(id, {
44
+ ...part,
45
+ stopped: true
46
+ });
47
+ this.stoppedAt = Date.now();
48
+ this.evaluate();
49
+ }
50
+ final(id, text) {
51
+ const part = this.parts.get(id);
52
+ if (part?.text !== void 0) return;
53
+ if (!part?.stopped) this.stoppedAt = Date.now();
54
+ this.parts.set(id, {
55
+ text: text.trim(),
56
+ stopped: true
57
+ });
58
+ if (this.parts.size > MAX_UTTERANCES || this.text().length > MAX_TRANSCRIPT_CHARACTERS) {
59
+ this.reset();
60
+ throw new Error("Voice turn input limit reached");
61
+ }
62
+ this.evaluate();
63
+ }
64
+ reset() {
65
+ this.invalidatePendingDecision();
66
+ this.parts.clear();
67
+ }
68
+ restore(id, text) {
69
+ this.invalidatePendingDecision();
70
+ this.parts.set(id, {
71
+ text,
72
+ stopped: true
73
+ });
74
+ }
75
+ text() {
76
+ return [...this.parts.values()].map((part) => part.text ?? "").filter(Boolean).join(" ");
77
+ }
78
+ invalidatePendingDecision() {
79
+ this.decisionGeneration += 1;
80
+ clearTimeout(this.timer);
81
+ this.timer = void 0;
82
+ }
83
+ evaluate() {
84
+ this.invalidatePendingDecision();
85
+ if ([...this.parts.values()].some((part) => !part.stopped || part.text === void 0)) return;
86
+ const text = this.text();
87
+ if (!text) {
88
+ this.reset();
89
+ return;
90
+ }
91
+ const generation = this.decisionGeneration;
92
+ const snapshot = {
93
+ text,
94
+ silenceDurationMs: Math.max(0, Date.now() - this.stoppedAt)
95
+ };
96
+ let result;
97
+ try {
98
+ result = this.policy.decide(snapshot);
99
+ } catch {
100
+ this.schedule(text, heuristicTurnPolicy.decide(snapshot), generation);
101
+ return;
102
+ }
103
+ if (isPromise(result)) {
104
+ let settled = false;
105
+ const finish = (decision) => {
106
+ if (settled || generation !== this.decisionGeneration) return;
107
+ settled = true;
108
+ clearTimeout(timeout);
109
+ this.schedule(text, decision, generation);
110
+ };
111
+ const fallback = () => finish(heuristicTurnPolicy.decide(snapshot));
112
+ const timeout = setTimeout(fallback, DECISION_TIMEOUT_MS);
113
+ result.then(finish).catch(fallback);
114
+ return;
115
+ }
116
+ this.schedule(text, result, generation);
117
+ }
118
+ schedule(text, decision, generation) {
119
+ if (generation !== this.decisionGeneration) return;
120
+ const holdMs = Math.max(FINAL_SETTLE_MS, MIN_TURN_SILENCE_MS - this.silenceDurationMs, Number.isFinite(decision.holdMs) ? Math.max(0, decision.holdMs) : 0);
121
+ this.timer = setTimeout(() => {
122
+ if (generation !== this.decisionGeneration) return;
123
+ const turnId = [...this.parts.keys()][0];
124
+ if (decision.disposition === "wait") {
125
+ this.evaluate();
126
+ return;
127
+ }
128
+ this.reset();
129
+ if (decision.disposition !== "backchannel") this.onCommit(text, decision, turnId);
130
+ }, Math.max(FINAL_SETTLE_MS, this.stoppedAt + holdMs - Date.now()));
131
+ }
132
+ };
133
+ //#endregion
134
+ export { TurnCoordinator, hasContinuationHint, heuristicTurnPolicy };
@@ -6,12 +6,12 @@ import { getAgentDefaultModelRef, init_schema } from "../config/schema.js";
6
6
  import { buildSessionKey, init_session_key } from "../routing/session-key.js";
7
7
  import { getDefaultAgentId, init_resolve_route } from "../routing/resolve-route.js";
8
8
  import { init_providers, resolveModel } from "../providers/index.js";
9
+ import { isLocalModelBaseUrl } from "../providers/model-call.js";
9
10
  import { writeKnowledgeItem } from "../knowledge-memory/repository.js";
10
11
  import "../knowledge-memory/index.js";
11
12
  import { createPriorityWindow, createUserGoal } from "../user-model/goals.js";
12
13
  import { getAssertionSlot, getUserAssertion, linkAssertionEvidence, reconcileAssertion, setAssertionStatus } from "../user-model/repository.js";
13
14
  import "../user-model/index.js";
14
- import { isLocalModelBaseUrl } from "../providers/model-call.js";
15
15
  import { allowsRemoteSourceProcessing } from "../user-context/sources/processing-policy.js";
16
16
  import { analyzeUnderstandingSources, analyzeWorkContext, workDiscoveryResultMarkdown } from "./analyzer.js";
17
17
  import { createUnderstandingSourceRun, getUnderstandingSourceRun, updateUnderstandingSourceGrantCheckpoint, updateUnderstandingSourceGrantPolicies, updateUnderstandingSourceRun, upsertUnderstandingSourceGrant } from "../user-context/sources/repository.js";
@@ -1,7 +1,7 @@
1
1
  import { TaskRunRepository } from "../../tasks/task-run-repository.js";
2
2
  import { getDefaultAgentId, init_resolve_route } from "../../routing/resolve-route.js";
3
- import { extractProfileAgentId, resolveEffectiveAgentProfileForSession } from "../../config/agent-profile.js";
4
3
  import { init_providers, resolveModel } from "../../providers/index.js";
4
+ import { extractProfileAgentId, resolveEffectiveAgentProfileForSession } from "../../config/agent-profile.js";
5
5
  import { TaskRepository } from "../../tasks/task-repository.js";
6
6
  import { TaskConversationRepository } from "../../tasks/task-conversation-repository.js";
7
7
  import { getProjectWorkspacePathForSession } from "../../projects/workspace.js";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xopcai/xopc",
3
- "version": "0.0.264",
3
+ "version": "0.0.266",
4
4
  "description": "Local-first personal AI assistant with reviewable user understanding, durable tasks, workflows, automations, and local or cloud models.",
5
5
  "type": "module",
6
6
  "main": "dist/src/index.js",
@@ -163,9 +163,9 @@
163
163
  "vitepress": "^1.6.4",
164
164
  "vitest": "4.1.5",
165
165
  "vue": "^3.5.35",
166
+ "@xopcai/browser-control-contract": "0.1.0",
166
167
  "@xopcai/endpoint-tools-protocol": "0.0.0",
167
168
  "@xopcai/realtime-client": "0.0.0",
168
- "@xopcai/browser-control-contract": "0.1.0",
169
169
  "@xopcai/realtime-protocol": "0.0.0",
170
170
  "@xopcai/gateway-contract": "0.0.0"
171
171
  },