@ljwei-stak/dsh-model-route-bot 0.66.3 → 0.67.0

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 (272) hide show
  1. package/.dsh-plugin/bot-product.mjs +81 -5
  2. package/.dsh-plugin/client.js +17490 -1791
  3. package/README.i18n.yaml +4 -4
  4. package/README.md +409 -364
  5. package/README.zh.md +409 -364
  6. package/apps/integration-client/README.md +31 -7
  7. package/apps/integration-client/api.mjs +14 -3
  8. package/apps/integration-client/binding-editor.jsx +308 -109
  9. package/apps/integration-client/binding-model.mjs +262 -0
  10. package/apps/integration-client/binding.css +144 -0
  11. package/apps/integration-client/bot-model-status.mjs +18 -0
  12. package/apps/integration-client/bot-profile-identity.jsx +197 -0
  13. package/apps/integration-client/bot-profile.css +57 -0
  14. package/apps/integration-client/bot-profile.mjs +97 -0
  15. package/apps/integration-client/bot-settings-dialog.css +18 -0
  16. package/apps/integration-client/bot-settings-dialog.jsx +111 -0
  17. package/apps/integration-client/bot-thread-header-bridge.css +20 -0
  18. package/apps/integration-client/bot-thread-header.jsx +278 -0
  19. package/apps/integration-client/bot-threads-panel.jsx +378 -0
  20. package/apps/integration-client/bot-threads.css +169 -0
  21. package/apps/integration-client/chat-design.mjs +250 -0
  22. package/apps/integration-client/chat-ui.jsx +175 -0
  23. package/apps/integration-client/chat.css +477 -0
  24. package/apps/integration-client/composer-draft-binding.mjs +50 -0
  25. package/apps/integration-client/composer-draft-bound.jsx +14 -0
  26. package/apps/integration-client/composer-draft-host.jsx +142 -0
  27. package/apps/integration-client/composer-draft-session.jsx +287 -0
  28. package/apps/integration-client/composer-draft-witness.mjs +115 -0
  29. package/apps/integration-client/composer-draft.css +4 -0
  30. package/apps/integration-client/composer-draft.mjs +322 -0
  31. package/apps/integration-client/contact-attention.jsx +54 -0
  32. package/apps/integration-client/data-panel.jsx +439 -0
  33. package/apps/integration-client/data.css +69 -0
  34. package/apps/integration-client/desktop-bridge.mjs +1 -0
  35. package/apps/integration-client/desktop-http-socket.mjs +49 -0
  36. package/apps/integration-client/desktop-keyboard.mjs +32 -0
  37. package/apps/integration-client/desktop-pane.jsx +99 -30
  38. package/apps/integration-client/desktop-stream.mjs +13 -3
  39. package/apps/integration-client/desktop-tools.jsx +19 -0
  40. package/apps/integration-client/desktop-touch.mjs +27 -0
  41. package/apps/integration-client/desktop.css +41 -0
  42. package/apps/integration-client/independent-chat-controller.mjs +224 -0
  43. package/apps/integration-client/independent-chat-entry.jsx +61 -0
  44. package/apps/integration-client/independent-chat-navigation.mjs +21 -0
  45. package/apps/integration-client/independent-chat-page.jsx +148 -0
  46. package/apps/integration-client/independent-chat-rich.jsx +64 -0
  47. package/apps/integration-client/independent-chat-rich.mjs +30 -0
  48. package/apps/integration-client/independent-chat.css +73 -0
  49. package/apps/integration-client/index.jsx +24 -16
  50. package/apps/integration-client/library-panel.jsx +4 -1
  51. package/apps/integration-client/licenses/jsqr-1.4.0-LICENSE.txt +202 -0
  52. package/apps/integration-client/main-route-editor.jsx +94 -0
  53. package/apps/integration-client/main-route-model.mjs +100 -0
  54. package/apps/integration-client/model-picker.css +37 -0
  55. package/apps/integration-client/model-picker.jsx +83 -0
  56. package/apps/integration-client/native-activity.css +141 -0
  57. package/apps/integration-client/native-activity.jsx +276 -0
  58. package/apps/integration-client/native-activity.mjs +384 -0
  59. package/apps/integration-client/notifications-panel.jsx +186 -0
  60. package/apps/integration-client/notifications.css +126 -0
  61. package/apps/integration-client/pairing-qr.jsx +79 -0
  62. package/apps/integration-client/panels.jsx +126 -115
  63. package/apps/integration-client/product-slots.jsx +46 -0
  64. package/apps/integration-client/routine-recorder.mjs +23 -2
  65. package/apps/integration-client/secure-pairing.jsx +84 -0
  66. package/apps/integration-client/tasks-panel.jsx +32 -10
  67. package/apps/integration-client/ui-icons.jsx +17 -0
  68. package/apps/integration-client/usage-explorer.jsx +43 -0
  69. package/apps/integration-client/usage-report.mjs +71 -0
  70. package/apps/integration-client/usage.css +34 -0
  71. package/apps/integration-client/web-panel-model.mjs +140 -0
  72. package/apps/integration-client/web-panel-page.jsx +221 -0
  73. package/apps/integration-client/web-panel-pins-live.jsx +28 -0
  74. package/apps/integration-client/web-panel.css +139 -0
  75. package/apps/integration-client/workspace-model.mjs +67 -0
  76. package/apps/integration-client/workspace-navigation.mjs +56 -0
  77. package/apps/integration-client/workspace-ui.jsx +173 -0
  78. package/apps/integration-client/workspace.css +332 -0
  79. package/modules/bot-runtime/SOURCE.json +176 -50
  80. package/modules/bot-runtime/index.mjs +169 -13
  81. package/modules/bot-runtime/product-overlay/OVERLAY.json +128 -2
  82. package/modules/bot-runtime/product-overlay/base/src/client/header.js +244 -0
  83. package/modules/bot-runtime/product-overlay/base/src/client/learning-card.js +202 -0
  84. package/modules/bot-runtime/product-overlay/base/src/client/model-menu.js +371 -0
  85. package/modules/bot-runtime/product-overlay/base/src/client/secret-card.js +363 -0
  86. package/modules/bot-runtime/product-overlay/base/src/client/sources.js +68 -0
  87. package/modules/bot-runtime/product-overlay/base/src/client/turn-error.js +260 -0
  88. package/modules/bot-runtime/product-overlay/base/src/host/channel.js +647 -0
  89. package/modules/bot-runtime/product-overlay/base/src/host/memory.js +2050 -0
  90. package/modules/bot-runtime/product-overlay/base/src/host/models.js +314 -0
  91. package/modules/bot-runtime/product-overlay/base/src/host/parts.js +1473 -0
  92. package/modules/bot-runtime/product-overlay/base/src/host/privacy.js +368 -0
  93. package/modules/bot-runtime/product-overlay/base/src/host/roster.js +776 -0
  94. package/modules/bot-runtime/product-overlay/base/src/host/store.js +512 -0
  95. package/modules/bot-runtime/product-overlay/base/src/host/usage.js +653 -0
  96. package/modules/bot-runtime/product-overlay/patches/src/client/details-drawer.js.patch +48 -2
  97. package/modules/bot-runtime/product-overlay/patches/src/client/header.js.patch +41 -0
  98. package/modules/bot-runtime/product-overlay/patches/src/client/index.js.patch +33 -1
  99. package/modules/bot-runtime/product-overlay/patches/src/client/learning-card.js.patch +20 -0
  100. package/modules/bot-runtime/product-overlay/patches/src/client/model-menu.js.patch +24 -0
  101. package/modules/bot-runtime/product-overlay/patches/src/client/secret-card.js.patch +25 -0
  102. package/modules/bot-runtime/product-overlay/patches/src/client/sources.js.patch +52 -0
  103. package/modules/bot-runtime/product-overlay/patches/src/client/turn-error.js.patch +23 -0
  104. package/modules/bot-runtime/product-overlay/patches/src/host/channel.js.patch +46 -0
  105. package/modules/bot-runtime/product-overlay/patches/src/host/memory.js.patch +11 -0
  106. package/modules/bot-runtime/product-overlay/patches/src/host/models.js.patch +16 -0
  107. package/modules/bot-runtime/product-overlay/patches/src/host/parts.js.patch +95 -0
  108. package/modules/bot-runtime/product-overlay/patches/src/host/privacy.js.patch +11 -0
  109. package/modules/bot-runtime/product-overlay/patches/src/host/roster.js.patch +145 -0
  110. package/modules/bot-runtime/product-overlay/patches/src/host/store.js.patch +68 -0
  111. package/modules/bot-runtime/product-overlay/patches/src/host/usage.js.patch +11 -0
  112. package/modules/bot-runtime/read-model.mjs +19 -4
  113. package/modules/bot-runtime/vendor/ds-bot/src/client/details-drawer.js +15 -5
  114. package/modules/bot-runtime/vendor/ds-bot/src/client/header.js +9 -2
  115. package/modules/bot-runtime/vendor/ds-bot/src/client/index.js +9 -2
  116. package/modules/bot-runtime/vendor/ds-bot/src/client/learning-card.js +2 -2
  117. package/modules/bot-runtime/vendor/ds-bot/src/client/model-menu.js +5 -3
  118. package/modules/bot-runtime/vendor/ds-bot/src/client/secret-card.js +3 -3
  119. package/modules/bot-runtime/vendor/ds-bot/src/client/sources.js +25 -2
  120. package/modules/bot-runtime/vendor/ds-bot/src/client/turn-error.js +3 -2
  121. package/modules/bot-runtime/vendor/ds-bot/src/host/channel.js +14 -2
  122. package/modules/bot-runtime/vendor/ds-bot/src/host/memory.js +1 -1
  123. package/modules/bot-runtime/vendor/ds-bot/src/host/models.js +7 -0
  124. package/modules/bot-runtime/vendor/ds-bot/src/host/parts.js +26 -9
  125. package/modules/bot-runtime/vendor/ds-bot/src/host/privacy.js +1 -1
  126. package/modules/bot-runtime/vendor/ds-bot/src/host/roster.js +112 -3
  127. package/modules/bot-runtime/vendor/ds-bot/src/host/store.js +34 -1
  128. package/modules/bot-runtime/vendor/ds-bot/src/host/usage.js +1 -1
  129. package/modules/independent-runtime/index.mjs +152 -62
  130. package/modules/independent-runtime/remote-tools.mjs +8 -5
  131. package/modules/openmaus-protocol/NOTICE +10 -0
  132. package/modules/openmaus-protocol/README.md +15 -0
  133. package/modules/openmaus-protocol/SOURCE.json +68 -0
  134. package/modules/openmaus-protocol/dist/protocol.mjs +1203 -0
  135. package/modules/openmaus-protocol/index.mjs +1 -0
  136. package/modules/openmaus-protocol/transport.mjs +77 -0
  137. package/modules/openmaus-protocol/vendor/LICENSE +202 -0
  138. package/modules/openmaus-protocol/vendor/companion/src/e2e/README.md +405 -0
  139. package/modules/openmaus-protocol/vendor/companion/src/e2e/credentials.ts +128 -0
  140. package/modules/openmaus-protocol/vendor/companion/src/e2e/descriptor.ts +84 -0
  141. package/modules/openmaus-protocol/vendor/companion/src/e2e/noise.ts +456 -0
  142. package/modules/openmaus-protocol/vendor/companion/src/e2e/phone.ts +104 -0
  143. package/modules/openmaus-protocol/vendor/companion/src/e2e/session.ts +417 -0
  144. package/modules/openmaus-protocol/vendor/companion/src/e2e/vectors/v1.json +2565 -0
  145. package/modules/openmaus-protocol/vendor/companion/src/e2e/vectors/v1.sha256 +1 -0
  146. package/modules/openmaus-protocol/vendor/companion/src/e2e/yamux.ts +335 -0
  147. package/package.json +20 -6
  148. package/packages/bot-acp/README.md +156 -0
  149. package/packages/bot-acp/licenses/LICENSE-OpenMausBot +202 -0
  150. package/packages/bot-acp/licenses/NOTICE.md +27 -0
  151. package/packages/bot-acp/src/acp-adapter.mjs +795 -0
  152. package/packages/bot-acp/src/acp-host.mjs +148 -0
  153. package/packages/bot-acp/src/native-launch.mjs +121 -0
  154. package/packages/bot-acp/src/permission-policy.mjs +283 -0
  155. package/packages/bot-acp/src/rpc-codec.mjs +265 -0
  156. package/packages/bot-acp/src/session-core.mjs +581 -0
  157. package/packages/bot-acp/src/stdio-rpc.mjs +329 -0
  158. package/packages/bot-engines/README.md +281 -0
  159. package/packages/bot-engines/licenses/LICENSE-OpenMausBot +202 -0
  160. package/packages/bot-engines/licenses/NOTICE.md +32 -0
  161. package/packages/bot-engines/src/claude-engine.mjs +772 -0
  162. package/packages/bot-engines/src/claude-llm-adapter.mjs +797 -0
  163. package/packages/bot-engines/src/claude-llm-turn.mjs +292 -0
  164. package/packages/bot-engines/src/claude-permission-broker.mjs +130 -0
  165. package/packages/bot-engines/src/claude-permission-proxy.mjs +87 -0
  166. package/packages/bot-engines/src/claude-protocol.mjs +177 -0
  167. package/packages/bot-engines/src/driver-catalog.mjs +334 -0
  168. package/packages/bot-engines/src/host-loop-request.mjs +95 -0
  169. package/packages/bot-engines/src/host-registrations.mjs +105 -0
  170. package/packages/capability-broker/src/cli-bootstrap.mjs +86 -16
  171. package/packages/capability-broker/src/cli-bridge-response.mjs +80 -0
  172. package/packages/capability-broker/src/cli-stdio-bridge.mjs +21 -8
  173. package/packages/cli-runtime/src/codex-session-core.mjs +4 -0
  174. package/packages/cli-runtime/src/native-cli-output.mjs +57 -0
  175. package/packages/cli-runtime/src/native-peer.ps1 +51 -3
  176. package/packages/cli-runtime/src/native-process.mjs +77 -14
  177. package/packages/model-router-plugin/README.md +3 -3
  178. package/packages/model-router-plugin/package.json +1 -1
  179. package/packages/orchestration/src/binding-product.mjs +19 -0
  180. package/packages/orchestration/src/bot-data-actions.mjs +45 -0
  181. package/packages/orchestration/src/bot-data-chart.mjs +109 -0
  182. package/packages/orchestration/src/bot-data-context.mjs +44 -0
  183. package/packages/orchestration/src/bot-data-engine.mjs +809 -0
  184. package/packages/orchestration/src/bot-data-io.mjs +186 -0
  185. package/packages/orchestration/src/bot-data-sql.mjs +195 -0
  186. package/packages/orchestration/src/bot-data-surface.mjs +144 -0
  187. package/packages/orchestration/src/bot-data-tools.mjs +68 -0
  188. package/packages/orchestration/src/bot-data-worker.mjs +228 -0
  189. package/packages/orchestration/src/bot-independent-execution.mjs +195 -0
  190. package/packages/orchestration/src/bot-independent-stream.mjs +74 -0
  191. package/packages/orchestration/src/bot-loop.mjs +152 -19
  192. package/packages/orchestration/src/bot-notifications.mjs +159 -0
  193. package/packages/orchestration/src/bot-policy.mjs +43 -4
  194. package/packages/orchestration/src/bot-profile.mjs +171 -0
  195. package/packages/orchestration/src/bot-routine.mjs +4 -4
  196. package/packages/orchestration/src/bot-runtime-assembly.mjs +255 -8
  197. package/packages/orchestration/src/bot-runtime-switch.mjs +13 -2
  198. package/packages/orchestration/src/bot-serving.mjs +164 -0
  199. package/packages/orchestration/src/bot-session-store.mjs +105 -9
  200. package/packages/orchestration/src/bot-settings.mjs +50 -11
  201. package/packages/orchestration/src/bot-thread-admission.mjs +84 -0
  202. package/packages/orchestration/src/bot-thread-host.mjs +187 -0
  203. package/packages/orchestration/src/bot-threads.mjs +790 -0
  204. package/packages/orchestration/src/data-product-access.mjs +37 -0
  205. package/packages/orchestration/src/data-tool-contract.mjs +291 -0
  206. package/packages/orchestration/src/desktop-http-carrier.mjs +120 -0
  207. package/packages/orchestration/src/desktop-proxy.mjs +311 -0
  208. package/packages/orchestration/src/desktop-sessions.mjs +43 -9
  209. package/packages/orchestration/src/ds-lifecycle.mjs +31 -4
  210. package/packages/orchestration/src/grant-refresh-queue.mjs +17 -0
  211. package/packages/orchestration/src/independent-chat-page.mjs +39 -0
  212. package/packages/orchestration/src/independent-chat-service.mjs +308 -0
  213. package/packages/orchestration/src/independent-context-state.mjs +44 -0
  214. package/packages/orchestration/src/independent-message-wire.mjs +84 -0
  215. package/packages/orchestration/src/independent-tool-catalog.mjs +175 -0
  216. package/packages/orchestration/src/independent-tool-families.mjs +73 -0
  217. package/packages/orchestration/src/independent-tool-host.mjs +344 -0
  218. package/packages/orchestration/src/main-bot-grant.mjs +15 -0
  219. package/packages/orchestration/src/main-model-routes.mjs +208 -0
  220. package/packages/orchestration/src/model-accounting.mjs +99 -35
  221. package/packages/orchestration/src/model-auxiliary-dispatch.mjs +187 -0
  222. package/packages/orchestration/src/model-loop-frame.mjs +179 -0
  223. package/packages/orchestration/src/model-prepared-generation.mjs +175 -0
  224. package/packages/orchestration/src/model-request-bounds.mjs +111 -0
  225. package/packages/orchestration/src/native-activity-contract.mjs +491 -0
  226. package/packages/orchestration/src/native-activity-service.mjs +102 -0
  227. package/packages/orchestration/src/native-activity-store.mjs +249 -0
  228. package/packages/orchestration/src/native-conversation-tools.mjs +78 -0
  229. package/packages/orchestration/src/native-data-tools.mjs +100 -0
  230. package/packages/orchestration/src/native-remote-actions.mjs +742 -0
  231. package/packages/orchestration/src/native-team-tools.mjs +73 -0
  232. package/packages/orchestration/src/pairing-invite.mjs +37 -0
  233. package/packages/orchestration/src/product-composition.mjs +1559 -1001
  234. package/packages/orchestration/src/remote-machines.mjs +82 -14
  235. package/packages/orchestration/src/routine-execution.mjs +5 -3
  236. package/packages/orchestration/src/routine-session.mjs +19 -13
  237. package/packages/orchestration/src/secure-pairing-host.mjs +112 -0
  238. package/packages/orchestration/src/task-journal.mjs +29 -0
  239. package/packages/orchestration/src/task-product.mjs +17 -0
  240. package/packages/orchestration/src/team-assignment-store.mjs +120 -0
  241. package/packages/orchestration/src/team-bridge.mjs +282 -0
  242. package/packages/orchestration/src/team-scheduler.mjs +83 -0
  243. package/packages/orchestration/src/team-sdk-tools.mjs +84 -0
  244. package/packages/orchestration/src/team-tool-contract.mjs +284 -0
  245. package/packages/orchestration/src/ui-host-namespace.mjs +23 -0
  246. package/packages/orchestration/src/web-panel-actions.mjs +103 -0
  247. package/packages/orchestration/src/web-panel-framing.mjs +227 -0
  248. package/packages/orchestration/src/web-panel-store.mjs +206 -0
  249. package/remote-computer-agent/README.md +39 -29
  250. package/remote-computer-agent/build.mjs +3 -0
  251. package/remote-computer-agent/dist/OPENMAUSBOT_LICENSE +202 -0
  252. package/remote-computer-agent/dist/OPENMAUSBOT_NOTICE +10 -0
  253. package/remote-computer-agent/dist/OPENMAUSBOT_SOURCE.json +68 -0
  254. package/remote-computer-agent/dist/cli-stdio-bridge.mjs +215 -25
  255. package/remote-computer-agent/dist/cli-stdio-bridge.mjs.map +4 -4
  256. package/remote-computer-agent/dist/cli.mjs +1688 -168
  257. package/remote-computer-agent/dist/cli.mjs.map +4 -4
  258. package/remote-computer-agent/dist/index.mjs +1579 -122
  259. package/remote-computer-agent/dist/index.mjs.map +4 -4
  260. package/remote-computer-agent/dist/native-peer.ps1 +51 -3
  261. package/remote-computer-agent/package-lock.json +308 -2
  262. package/remote-computer-agent/package.json +2 -1
  263. package/remote-computer-agent/pair-device.ps1 +20 -0
  264. package/remote-computer-agent/setup.ps1 +38 -16
  265. package/remote-computer-agent/src/cli.ts +24 -3
  266. package/remote-computer-agent/src/desktop-session.ts +11 -3
  267. package/remote-computer-agent/src/e2e-server.ts +81 -0
  268. package/remote-computer-agent/src/pairing-page.ts +35 -0
  269. package/remote-computer-agent/src/protocol.ts +25 -25
  270. package/remote-computer-agent/src/secure-pairing.ts +105 -0
  271. package/remote-computer-agent/src/store.ts +20 -5
  272. package/remote-computer-agent/src/transport.ts +39 -15
@@ -0,0 +1,647 @@
1
+ // Client channel: the team view the browser renders, and the POST /api/bot endpoints.
2
+ import { randomUUID } from 'node:crypto'
3
+ import { createUserMessage } from '@deepseek-ai/dsh-llm'
4
+ import { install as installBtw } from './btw.js'
5
+ import { install as installSide } from './side.js'
6
+ import { handoffReply, questionReply, viewReply } from './parts.js'
7
+ import { validateViewAnswer } from 'deep-view/core'
8
+ import { COLORS, EXTRA_COLORS, ROUTE } from './constants.js'
9
+
10
+ const LANGUAGES = { zh: 'Chinese (简体中文)', en: 'English' }
11
+ const RUN_STATS = ['sessionStats', 'tokenUsage', 'contextPressure']
12
+
13
+ const invalid = message => Object.assign(new Error(message), { code: 'bot/invalid' })
14
+ // Programming and file system errors are DS Bot's own faults, logged with their stack;
15
+ // any other error is a refusal whose message is meant for the user.
16
+ const isInternal = error => error instanceof TypeError || error instanceof ReferenceError || error instanceof RangeError
17
+ || error instanceof SyntaxError || (typeof error?.code === 'string' && /^E[A-Z_]+$/.test(error.code))
18
+ export function errorCode(error) {
19
+ if (typeof error?.code === 'string' && error.code.startsWith('bot/')) return error.code
20
+ return isInternal(error) ? 'bot/internal' : 'bot/refused'
21
+ }
22
+
23
+ export function install(rt) {
24
+ installBtw(rt)
25
+ installSide(rt)
26
+ const { ctx, state } = rt
27
+
28
+ // The DSH interface language of the last browser that asked for the team.
29
+ let uiLocale
30
+ rt.uiLanguage = () => (uiLocale === undefined ? undefined : LANGUAGES[uiLocale.split('-')[0].toLowerCase()] ?? uiLocale)
31
+
32
+ const ok = value => ({ ok: true, value })
33
+ const fail = (message, code = 'bot/error') => ({ ok: false, error: { code, message, details: {} } })
34
+ // Models come from DSH, not state.json, so a provider added in Harness settings
35
+ // changes the view without a new revision.
36
+ const modelsKey = () => `${rt.cachedCatalog().map(entry => `${entry.ref}${entry.needsKey ? '!' : ''}${entry.reasoning ? `:${JSON.stringify(entry.reasoning)}` : ''}`).join(',')}|${rt.cachedDefault() ?? ''}`
37
+ const view = () => ({
38
+ revision: state.revision,
39
+ workspaceId: state.workspaceId ?? null,
40
+ // `mainBotId` is the first Main Bot, which the sidebar shows as its tile.
41
+ mainBotId: state.mainBotIds[0] ?? null,
42
+ mainBotIds: state.mainBotIds,
43
+ // `sessionId` is the Bot's current own-chat part; `parts` its earlier ones.
44
+ // `waiting`: the Bot holds its greeting until the user picks its model or fixes
45
+ // what failed it. `blocked`: the facts of a failure only the user can fix (turns.js).
46
+ bots: Object.values(state.bots).map(({ segments, kickoff, ...bot }) => ({
47
+ ...bot, sessionId: rt.chatOf(bot.id), parts: (segments ?? []).map(segment => segment.sessionId), ...(kickoff ? { waiting: true } : {}),
48
+ })),
49
+ // `sessionId` is the group's own Session's current part, `ownParts` its earlier
50
+ // ones; `parts` the members' hidden group Sessions and their earlier segments,
51
+ // for usage.
52
+ rooms: Object.values(state.rooms).map(({ log, seen, paused, sessions, segments, parts: ownParts, ...room }) => {
53
+ const parts = [...Object.values(sessions ?? {}), ...Object.values(segments ?? {}).flat().map(segment => segment.sessionId), ...(ownParts ?? []).map(segment => segment.sessionId)]
54
+ const last = (log ?? []).filter(line => line.kind === undefined).at(-1)
55
+ return { ...room, sessionId: rt.sessionOfRoom(room), ownParts: (ownParts ?? []).map(segment => segment.sessionId), parts, ...(last ? { preview: last.who === 'User' ? last.text : `${last.who}:${last.text}` } : {}) }
56
+ }),
57
+ // Deleted private Bots whose folder, memory and chats stay closed to the others.
58
+ privateArchives: rt.archivesView(),
59
+ exchangeTurns: state.exchangeTurns,
60
+ questions: state.questions,
61
+ views: state.views ?? {},
62
+ // Aside answers of each Bot's current part, newest first. Not part of the
63
+ // model-visible history; earlier parts send their cards on their own pages.
64
+ btwCards: rt.btwView?.() ?? {},
65
+ // Each Bot's side chats, in creation order. The answers never join the
66
+ // model-visible history; the side panel reads them here.
67
+ sideChats: rt.sideView?.() ?? {},
68
+ // The team's DSH Agent (at most one): plain dsh Sessions, not Bots.
69
+ agents: Object.values(state.agents),
70
+ // Key names and who may use them; values never leave the host.
71
+ secrets: rt.secretsView(),
72
+ capabilities: rt.capabilityLibraryView(),
73
+ secretRequests: state.secretRequests ?? {},
74
+ colors: [...COLORS, ...EXTRA_COLORS],
75
+ models: rt.cachedCatalog(),
76
+ defaultModel: rt.cachedDefault() ?? null,
77
+ permissions: rt.permissionCatalog?.() ?? null,
78
+ modelsKey: modelsKey(),
79
+ prefs: state.prefs,
80
+ // Whether a Desktop window should serve Bot browser reads (see browser.js).
81
+ browser: rt.browserEnabled,
82
+ // False when config.memory turns memory off: no Memory row or panel.
83
+ memory: rt.memoryEnabled !== false,
84
+ // Set while another dsh host holds the team (store.js): why this one cannot change it.
85
+ readOnly: rt.readOnly() ?? null,
86
+ })
87
+ // What changes without a new revision, sent with every state answer. `learning`:
88
+ // each Bot's pending skill proposals. `memoryNews`:
89
+ // per Bot or group chat, its latest memory change; `memoryReviews`: memory reviews waiting or
90
+ // running; `work`: per Bot, the turn it runs and its tool; per group, the members whose
91
+ // turn there runs; the approvals waiting; per Bot, who it waits for to hand back work;
92
+ // and per Session, when its running turn began.
93
+ const live = () => ({
94
+ activity: rt.activityView(),
95
+ work: { bots: rt.workView(), rooms: rt.roomWorkView(), approvals: rt.approvalsView(), waiting: rt.waitingView?.() ?? {}, turns: rt.turnsView?.() ?? {}, steered: rt.steeredView?.() ?? {} },
96
+ memoryNews: rt.memoryNews(),
97
+ memoryReviews: rt.memoryReviews(),
98
+ learning: rt.learningView(),
99
+ connectors: rt.connectorsView(),
100
+ capabilityStatuses: rt.capabilityStatusesView(),
101
+ })
102
+ // The live fields' sources are spread across the host with no single change
103
+ // counter, so the serialized answer itself is the detector: a poll whose `live`
104
+ // still names it gets only the key back, not the fields again. The key carries
105
+ // a per-run prefix: a window can outlive the host, and a counter that restarts
106
+ // at 1 would let its stale key name a different answer.
107
+ const liveRun = randomUUID()
108
+ let liveSeq = 0
109
+ let liveCache = null
110
+ const liveAnswer = () => {
111
+ const value = live()
112
+ const json = JSON.stringify(value)
113
+ if (liveCache?.json === json) return liveCache
114
+ return liveCache = { key: `${liveRun}:${++liveSeq}`, value, json }
115
+ }
116
+ // What a host that only reads the team may still answer.
117
+ // Updates change the dsh profile, not the team, so a reading host may run them too.
118
+ const READS = new Set(['state', 'models', 'efforts', 'providers', 'provider-models', 'earlier', 'chat', 'run-stats', 'room-progress', 'exchange', 'exchange-text', 'file-text', 'usage', 'schedules', 'memory-view', 'browser-wait', 'browser-result', 'diagnostics', 'diagnostics-log', 'diagnostics-clear', 'update-check', 'update-install', 'dsh-update-install', 'repo-stars', 'approval-answer', 'thinking', 'avatar-library'])
119
+
120
+ async function setPrefs(body) {
121
+ if (body.theme !== undefined) {
122
+ if (typeof body.theme !== 'string' || !/^[a-z][\w-]{0,31}$/.test(body.theme)) throw invalid('Unknown theme')
123
+ state.prefs.theme = body.theme
124
+ }
125
+ if (body.accent !== undefined) {
126
+ if (body.accent === null || body.accent === '') delete state.prefs.accent
127
+ else if (/^#[0-9a-f]{6}$/i.test(body.accent)) state.prefs.accent = body.accent
128
+ else throw invalid('Accent must be a #rrggbb color')
129
+ }
130
+ if (body.motion !== undefined) {
131
+ if (body.motion === null || body.motion === 'normal') delete state.prefs.motion
132
+ else if (body.motion === 'quiet' || body.motion === 'lively') state.prefs.motion = body.motion
133
+ else throw invalid('Motion must be quiet, normal or lively')
134
+ }
135
+ if (body.layout !== undefined) {
136
+ if (body.layout === null || body.layout === 'bubble') delete state.prefs.layout
137
+ else if (body.layout === 'flat') state.prefs.layout = body.layout
138
+ else throw invalid('Layout must be bubble or flat')
139
+ }
140
+ if (body.width !== undefined) {
141
+ if (body.width === null || body.width === 80) delete state.prefs.width
142
+ else if (Number.isInteger(body.width) && body.width >= 50 && body.width <= 100) state.prefs.width = body.width
143
+ else throw invalid('Width must be a whole percent from 50 to 100')
144
+ }
145
+ if (body.flatWidth !== undefined) {
146
+ if (body.flatWidth === null || body.flatWidth === 100) delete state.prefs.flatWidth
147
+ else if (Number.isInteger(body.flatWidth) && body.flatWidth >= 50 && body.flatWidth <= 100) state.prefs.flatWidth = body.flatWidth
148
+ else throw invalid('Flat text width must be a whole percent from 50 to 100')
149
+ }
150
+ if (body.defaultBotModel !== undefined) {
151
+ // The model new Bots start on when a create names none. It resolves now, so the
152
+ // saved ref is one the catalog served at save time; a create still re-checks it.
153
+ if (body.defaultBotModel === null || body.defaultBotModel === '') delete state.prefs.defaultBotModel
154
+ else state.prefs.defaultBotModel = await rt.resolveModel(body.defaultBotModel)
155
+ }
156
+ if (body.thinking !== undefined) {
157
+ // A value this version does not know is ignored, not refused: the toggle
158
+ // may grow more states later.
159
+ if (body.thinking === true) state.prefs.thinking = true
160
+ else if (body.thinking === false || body.thinking === null) delete state.prefs.thinking
161
+ }
162
+ if (body.enterToSend !== undefined) {
163
+ if (body.enterToSend === false) state.prefs.enterToSend = false
164
+ else if (body.enterToSend === true || body.enterToSend === null) delete state.prefs.enterToSend
165
+ }
166
+ return rt.save()
167
+ }
168
+
169
+ const handle = async (endpoint, payload, signal) => {
170
+ try {
171
+ const body = payload ?? {}
172
+ await rt.refresh()
173
+ const readOnly = rt.readOnly()
174
+ if (readOnly !== undefined && !READS.has(endpoint)) return fail(readOnly, 'bot/read-only')
175
+ switch (endpoint) {
176
+ case 'state': {
177
+ if (typeof body.locale === 'string' && /^[a-z]{2,3}(-[\w-]+)?$/i.test(body.locale)) uiLocale = body.locale
178
+ if (readOnly === undefined) await rt.bootstrap()
179
+ await rt.catalog()
180
+ await rt.loadSecrets()
181
+ const answer = liveAnswer()
182
+ if (body.since === state.revision && (body.models === undefined || body.models === modelsKey())) {
183
+ if (body.live === answer.key) return ok({ revision: state.revision, unchanged: true, liveKey: answer.key })
184
+ return ok({ revision: state.revision, unchanged: true, liveKey: answer.key, ...answer.value })
185
+ }
186
+ return ok({ ...view(), liveKey: answer.key, ...answer.value })
187
+ }
188
+ case 'models':
189
+ await rt.catalog({ fresh: true })
190
+ return ok({ modelsKey: modelsKey() })
191
+ case 'efforts':
192
+ return ok(await rt.reasoningOf(String(body.ref ?? '')) ?? { efforts: [] })
193
+ // A Turn's exact thinking time, from the timed stream in its events; `ms`
194
+ // omitted when the stream carries no reasoning records.
195
+ case 'thinking': {
196
+ const sessionId = String(body.sessionId ?? '')
197
+ const turn = Number(body.turn)
198
+ if (!Number.isFinite(turn) || rt.selfOf(sessionId) === undefined) return ok(null)
199
+ await rt.load()
200
+ const ms = await rt.thinkingOf(sessionId, turn)
201
+ return ok(ms === undefined ? {} : { ms })
202
+ }
203
+ // Model providers and their keys, for a window without DSH's own Models page.
204
+ case 'providers':
205
+ return ok(await rt.providersView())
206
+ case 'provider-key':
207
+ if (body.value === null) await rt.clearProviderKey(String(body.provider ?? ''))
208
+ else await rt.setProviderKey(String(body.provider ?? ''), body.value)
209
+ return ok(await rt.providersView())
210
+ case 'provider-add':
211
+ return ok(await rt.addProvider(body))
212
+ case 'provider-models':
213
+ return ok(await rt.discoverModels(body))
214
+ case 'provider-remove':
215
+ return ok(await rt.removeProvider(String(body.provider ?? '')))
216
+ case 'duplicate-bot':
217
+ await rt.load()
218
+ return ok(await rt.duplicateBot(body.id))
219
+ case 'set-prefs':
220
+ await rt.load()
221
+ await setPrefs(body)
222
+ return ok(state.prefs)
223
+ case 'exchange': {
224
+ await rt.load()
225
+ const pair = new Set([body.a, body.b])
226
+ return ok(state.exchanges.filter(entry => pair.has(entry.from) && pair.has(entry.to)))
227
+ }
228
+ case 'exchange-text': {
229
+ await rt.load()
230
+ const entry = state.exchanges.find(item => item.id === body.id)
231
+ if (entry === undefined) return fail('This message is no longer kept', 'bot/stale')
232
+ return ok(await rt.exchangeText(entry))
233
+ }
234
+ // A long Bot post's file (attachLong), for the group chat's "Show all".
235
+ case 'file-text': {
236
+ if (typeof body.file?.attachmentId !== 'string') return fail('No file named', 'bot/invalid')
237
+ const read = await rt.fileText(body.file)
238
+ return read === undefined ? fail('This file cannot be read here', 'bot/unavailable') : ok(read)
239
+ }
240
+ case 'earlier':
241
+ await rt.load()
242
+ return ok(await rt.earlierPage(String(body.sessionId ?? ''), body.cursor ?? undefined))
243
+ case 'chat':
244
+ await rt.load()
245
+ return ok(await rt.chatPage(String(body.botId ?? ''), body.cursor ?? undefined))
246
+ // DSH's own whole-log figures for the current part of a Bot's chat, the ones its
247
+ // status bar shows; a window without that bar shows them in the Bot's details.
248
+ case 'run-stats': {
249
+ await rt.load()
250
+ const bot = rt.botOf(String(body.botId ?? ''))
251
+ if (bot === undefined) return fail('Unknown Bot', 'bot/not-found')
252
+ const projections = ctx.get?.('sessionProjections')
253
+ if (projections === undefined) return ok(null)
254
+ const agent = await rt.resolveAgent(rt.chatOf(bot.id))
255
+ return ok(projections.snapshot(agent.session, RUN_STATS).values)
256
+ }
257
+ case 'usage':
258
+ await rt.load()
259
+ return ok(await rt.usageView({ timeZone: typeof body.timeZone === 'string' ? body.timeZone.slice(0, 64) : undefined }))
260
+ case 'room-progress': {
261
+ // Any part id of the group's own Session means the group itself.
262
+ const room = rt.roomOf(body.roomId) ?? rt.roomOf(rt.pastOwners.get(body.roomId)?.roomId)
263
+ return ok([...rt.roomProgress.entries()].filter(([, entry]) => entry.roomId === (room?.id ?? body.roomId)).map(([callId, entry]) => ({ callId, replies: entry.replies, speaking: entry.slots.filter(slot => slot.status === 'speaking').map(slot => slot.id) })))
264
+ }
265
+ case 'create-bot': {
266
+ await rt.bootstrap()
267
+ const bot = await rt.createBot({ name: body.name, role: body.role, color: body.color, instructions: body.instructions, brief: body.brief, avatar: body.avatar, model: body.model, effort: body.effort, workDir: body.workDir })
268
+ return ok(bot)
269
+ }
270
+ case 'update-bot':
271
+ await rt.load()
272
+ return ok(await rt.updateBot(body.id, body))
273
+ // The repair card's Retry, once the user fixed what failed the Bot's turn.
274
+ case 'resume': {
275
+ await rt.load()
276
+ const bot = rt.botOf(String(body.botId ?? ''))
277
+ if (bot === undefined) return fail('Unknown Bot', 'bot/not-found')
278
+ await rt.resumeBot(bot)
279
+ return ok(true)
280
+ }
281
+ // The avatar library: photos the user imported once, kept beside the team.
282
+ case 'avatar-library':
283
+ await rt.load()
284
+ return ok(await rt.avatarLibraryView({ offset: body.offset, limit: body.limit }))
285
+ case 'avatar-import':
286
+ await rt.load()
287
+ return ok(await rt.avatarLibraryImport(Array.isArray(body.items) ? body.items : []))
288
+ case 'avatar-delete':
289
+ await rt.load()
290
+ return ok(await rt.avatarLibraryDelete(String(body.id ?? '')))
291
+ case 'delete-bot':
292
+ await rt.load()
293
+ await rt.deleteBot(body.id)
294
+ return ok(view())
295
+ case 'private-release':
296
+ await rt.load()
297
+ await rt.releaseArchive(String(body.id ?? ''))
298
+ return ok(view())
299
+ case 'set-main':
300
+ await rt.load()
301
+ await rt.setMain(body.id, body.main !== false, {
302
+ primary: body.primary === true,
303
+ replace: typeof body.replace === 'string' ? body.replace : undefined,
304
+ })
305
+ return ok(view())
306
+ case 'create-room': {
307
+ await rt.load()
308
+ const room = await rt.createRoom({ members: Array.isArray(body.members) ? body.members : [], name: body.name, ...(typeof body.admin === 'string' ? { admin: body.admin } : {}) })
309
+ if (body.greet === true) await rt.greetRoom(room.id).catch(error => rt.warn('ds-bot: greeting in %s failed: %s', room.name, error))
310
+ return ok(room)
311
+ }
312
+ case 'update-room': {
313
+ await rt.load()
314
+ const { room, changes } = await rt.updateRoom(body.id, {
315
+ name: body.name,
316
+ notice: body.notice,
317
+ mode: body.mode,
318
+ admin: body.admin,
319
+ members: Array.isArray(body.members) ? body.members : undefined,
320
+ add: Array.isArray(body.add) ? body.add : undefined,
321
+ remove: Array.isArray(body.remove) ? body.remove : undefined,
322
+ workDir: body.workDir,
323
+ permission: body.permission,
324
+ })
325
+ return ok({ room: view().rooms.find(entry => entry.id === room.id), changes })
326
+ }
327
+ case 'folders':
328
+ case 'folder-pick':
329
+ case 'folder-list':
330
+ await rt.load()
331
+ return ok(await rt.folderEndpoint(endpoint, body, signal))
332
+ case 'team-workspace':
333
+ await rt.load()
334
+ return ok({ team: await rt.setTeamWorkspace(String(body.path ?? '')) })
335
+ case 'add-agent':
336
+ await rt.load()
337
+ return ok(await rt.addAgent())
338
+ case 'agent-session':
339
+ await rt.load()
340
+ return ok({ sessionId: await rt.createAgentSession(body.agentId, body.workspaceId) })
341
+ case 'agent-adopt':
342
+ await rt.load()
343
+ return ok({ sessionId: await rt.adoptAgentSession(body.agentId, body.sessionId) })
344
+ case 'agent-archive':
345
+ await rt.load()
346
+ await rt.archiveAgentSession(body.agentId, body.sessionId)
347
+ return ok(true)
348
+ case 'agent-rename':
349
+ await rt.load()
350
+ await rt.renameAgentSession(body.agentId, body.sessionId, body.title)
351
+ return ok(true)
352
+ case 'remove-agent':
353
+ await rt.load()
354
+ await rt.removeAgent(body.agentId)
355
+ return ok(view())
356
+ case 'set-flags':
357
+ await rt.load()
358
+ return ok(await rt.setFlags(body.id, body))
359
+ case 'delete-room':
360
+ await rt.load()
361
+ await rt.deleteRoom(body.id)
362
+ return ok(view())
363
+ case 'approval-answer':
364
+ rt.answerApproval(body.id, body.choice)
365
+ return ok(true)
366
+ case 'stop-member-work': {
367
+ const stopped = await rt.stopMemberWork(String(body.roomId ?? ''), String(body.botId ?? ''))
368
+ rt.goalUserStopped?.(String(body.roomId ?? ''))
369
+ return ok(stopped)
370
+ }
371
+ case 'stop-room-work': {
372
+ const stopped = await rt.stopRoomWork(String(body.roomId ?? ''))
373
+ rt.goalUserStopped?.(String(body.roomId ?? ''))
374
+ return ok(stopped)
375
+ }
376
+ case 'goal-draft': {
377
+ await rt.load()
378
+ const result = await rt.goalDraft(String(body.roomId ?? ''), body.text)
379
+ return result.error !== undefined ? fail(result.error, result.code ?? 'bot/refused') : ok(view())
380
+ }
381
+ case 'goal-control': {
382
+ await rt.load()
383
+ const result = await rt.goalControl(String(body.roomId ?? ''), body)
384
+ return result.error !== undefined ? fail(result.error, result.code ?? 'bot/refused') : ok(view())
385
+ }
386
+ case 'dismiss-question':
387
+ await rt.load()
388
+ if (rt.own(state.questions, body.sessionId) !== undefined) {
389
+ delete state.questions[body.sessionId]
390
+ await rt.save()
391
+ }
392
+ return ok(true)
393
+ case 'dismiss-view':
394
+ await rt.load()
395
+ if (rt.own(state.views, body.sessionId) !== undefined) {
396
+ delete state.views[body.sessionId]
397
+ await rt.save()
398
+ }
399
+ return ok(true)
400
+ case 'secret-set':
401
+ case 'secret-allow':
402
+ case 'secret-cancel':
403
+ case 'secret-update':
404
+ case 'secret-delete':
405
+ await rt.load()
406
+ return ok(await rt.secretEndpoint(endpoint, body))
407
+ case 'connector-connect':
408
+ case 'connector-disconnect':
409
+ case 'connector-retry':
410
+ await rt.load()
411
+ return ok(await rt.connectorEndpoint(endpoint, body))
412
+ case 'capability-save':
413
+ case 'capability-delete':
414
+ case 'capability-approve':
415
+ case 'bot-capability':
416
+ await rt.load()
417
+ return ok(await rt.capabilityEndpoint(endpoint, body))
418
+ case 'schedules':
419
+ case 'schedule-create':
420
+ case 'schedule-delete':
421
+ await rt.load()
422
+ return ok(await rt.scheduleEndpoint(endpoint, body))
423
+ case 'memory-view':
424
+ case 'memory-ask':
425
+ case 'memory-forget':
426
+ case 'memory-history':
427
+ case 'memory-restore':
428
+ await rt.load()
429
+ return ok(await rt.memoryEndpoint(endpoint, body))
430
+ case 'learning-answer':
431
+ await rt.load()
432
+ return ok(await rt.learningEndpoint(endpoint, body))
433
+ case 'answer-question': {
434
+ if (typeof body.sessionId !== 'string') return fail('Nothing to send', 'bot/invalid')
435
+ await rt.load()
436
+ const past = rt.pastOwners.get(body.sessionId)
437
+ const sessionId = rt.botOf(body.sessionId) ? rt.chatOf(body.sessionId) : past && past.roomId === undefined ? rt.chatOf(past.botId) : body.sessionId
438
+ const question = rt.own(state.questions, sessionId)
439
+ if (question === undefined || question.id !== body.questionId) return fail('This question is no longer open', 'bot/stale')
440
+ const reply = questionReply(question, body.answer)
441
+ if (reply === undefined) return fail('Nothing to send', 'bot/invalid')
442
+ // Claim the question before queueing so a second answer is refused.
443
+ delete state.questions[sessionId]
444
+ try {
445
+ await rt.ensureCapabilities(sessionId)
446
+ const agent = await rt.resolveAgent(sessionId)
447
+ agent.followup(createUserMessage({ content: [{ type: 'text', text: reply.text }], source: reply.source }))
448
+ await ctx.sessions.flush(agent.session)
449
+ } catch (error) {
450
+ state.questions[sessionId] ??= question
451
+ throw error
452
+ }
453
+ await rt.save()
454
+ return ok(true)
455
+ }
456
+ case 'view-reply': {
457
+ if (typeof body.sessionId !== 'string') return fail('Nothing to send', 'bot/invalid')
458
+ await rt.load()
459
+ const past = rt.pastOwners.get(body.sessionId)
460
+ const sessionId = rt.botOf(body.sessionId) ? rt.chatOf(body.sessionId) : past && past.roomId === undefined ? rt.chatOf(past.botId) : body.sessionId
461
+ const pending = rt.own(state.views, sessionId)
462
+ if (pending === undefined || pending.id !== body.viewId) return fail('This view is no longer open', 'bot/stale')
463
+ const checked = validateViewAnswer(pending.view, body.action, body.values)
464
+ if (checked.error !== undefined) return fail(checked.error, 'bot/invalid')
465
+ const reply = viewReply(pending, body.action, body.values)
466
+ if (reply === undefined) return fail('Nothing to send', 'bot/invalid')
467
+ // Claim the view before queueing so a second answer is refused.
468
+ delete state.views[sessionId]
469
+ try {
470
+ await rt.ensureCapabilities(sessionId)
471
+ const agent = await rt.resolveAgent(sessionId)
472
+ agent.followup(createUserMessage({ content: [{ type: 'text', text: reply.text }], source: reply.source }))
473
+ await ctx.sessions.flush(agent.session)
474
+ } catch (error) {
475
+ state.views[sessionId] ??= pending
476
+ throw error
477
+ }
478
+ await rt.save()
479
+ return ok(true)
480
+ }
481
+ case 'send': {
482
+ const text = String(body.text ?? '').trim()
483
+ if (text === '' || typeof body.sessionId !== 'string') return fail('Nothing to send', 'bot/invalid')
484
+ await rt.load()
485
+ // A Bot id, a group chat's id, or an earlier part of either means the
486
+ // current part.
487
+ const past = rt.pastOwners.get(body.sessionId)
488
+ const room = rt.roomOf(body.sessionId)
489
+ const sessionId = rt.botOf(body.sessionId) ? rt.chatOf(body.sessionId)
490
+ : room !== undefined ? rt.sessionOfRoom(room)
491
+ : past?.roomId === undefined && past !== undefined ? rt.chatOf(past.botId)
492
+ : past !== undefined ? rt.sessionOfRoom(rt.roomOf(past.roomId)) : body.sessionId
493
+ await rt.ensureCapabilities(sessionId)
494
+ await ctx.sessionController.prompt({
495
+ requestId: randomUUID(),
496
+ sessionId,
497
+ mode: 'queue',
498
+ content: [{ type: 'text', text }],
499
+ }, AbortSignal.timeout(30_000))
500
+ return ok(true)
501
+ }
502
+ case 'btw-delete': {
503
+ if (typeof body.sessionId !== 'string' || body.sessionId === '' || typeof body.cardId !== 'string' || body.cardId === '') return fail('Nothing to delete', 'bot/invalid')
504
+ await rt.load()
505
+ return ok(await rt.btwDelete({ sessionId: body.sessionId, cardId: body.cardId }))
506
+ }
507
+ // Side chats in the Bot's panel: each question is an aside on a fresh
508
+ // snapshot of this Bot's private chat; the earlier finished pairs of
509
+ // that chat ride along. Nothing is written into the Session.
510
+ case 'side-new': {
511
+ if (typeof body.sessionId !== 'string' || body.sessionId === '') return fail('Nothing to open', 'bot/invalid')
512
+ await rt.load()
513
+ return ok(await rt.sideNew({ sessionId: body.sessionId }))
514
+ }
515
+ case 'side-ask': {
516
+ if (typeof body.sessionId !== 'string' || body.sessionId === '') return fail('Nothing to ask', 'bot/invalid')
517
+ if (body.chatId !== undefined && typeof body.chatId !== 'string') return fail('Nothing to ask', 'bot/invalid')
518
+ if (String(body.text ?? '').trim() === '') return fail('Write a question.', 'bot/invalid')
519
+ await rt.load()
520
+ return ok(await rt.sideAsk({ sessionId: body.sessionId, chatId: body.chatId, recent: body.recent === true, text: String(body.text) }))
521
+ }
522
+ case 'side-cancel': {
523
+ if (typeof body.chatId !== 'string' || body.chatId === '' || typeof body.entryId !== 'string' || body.entryId === '') return fail('Nothing to cancel', 'bot/invalid')
524
+ await rt.load()
525
+ return ok(await rt.sideCancel({ chatId: body.chatId, entryId: body.entryId }))
526
+ }
527
+ case 'side-close': {
528
+ if (typeof body.chatId !== 'string' || body.chatId === '') return fail('Nothing to close', 'bot/invalid')
529
+ await rt.load()
530
+ return ok(await rt.sideClose({ chatId: body.chatId }))
531
+ }
532
+ case 'update-check': {
533
+ const fresh = body.fresh === true
534
+ return ok({ ...(await rt.updateCheck({ fresh })), dsh: (await rt.dshUpdateCheck?.({ fresh })) ?? null })
535
+ }
536
+ case 'update-install':
537
+ return ok(rt.updateStart())
538
+ case 'dsh-update-install':
539
+ return ok(rt.dshUpdateStart?.())
540
+ case 'repo-stars':
541
+ return ok(await rt.repoStars())
542
+ // The Desktop window's long poll for read_browser, and its answers.
543
+ case 'browser-wait': {
544
+ if (rt.browserChannel === undefined) return fail('Bot browsers are off on this host', 'bot/browser-off')
545
+ const clientId = typeof body.clientId === 'string' ? body.clientId.slice(0, 100) : ''
546
+ if (clientId === '') return fail('A browser poll needs a clientId', 'bot/invalid')
547
+ await rt.load()
548
+ const requests = await rt.browserChannel.wait(clientId, rt.browserOpen(body.open))
549
+ return ok({ requests, holds: rt.browserChannel.holdSnapshot() })
550
+ }
551
+ case 'browser-result':
552
+ if (rt.browserChannel === undefined) return fail('Bot browsers are off on this host', 'bot/browser-off')
553
+ return ok(rt.browserChannel.result(body))
554
+ case 'browser-takeover': {
555
+ if (rt.browserChannel === undefined) return fail('Bot browsers are off on this host', 'bot/browser-off')
556
+ await rt.load()
557
+ const botId = String(body.botId ?? '')
558
+ if (!rt.botOf(botId)) return fail('Unknown Bot', 'bot/not-found')
559
+ // Taking over a handoff keeps its reason, note, and call, so its card stays up.
560
+ const asked = rt.browserChannel.holdOf(botId)
561
+ rt.browserChannel.setHold(botId, asked?.callId !== undefined
562
+ ? { by: 'user', reason: asked.reason, note: asked.note, callId: asked.callId }
563
+ : { by: 'user', reason: 'takeover', note: 'The user is controlling this browser.' })
564
+ return ok(true)
565
+ }
566
+ // A card's 'done' or 'skip' says how the handoff went. The panel's Give back
567
+ // sends no outcome, so its reply only says that control returned to the Bot.
568
+ case 'browser-give-back': {
569
+ if (rt.browserChannel === undefined) return fail('Bot browsers are off on this host', 'bot/browser-off')
570
+ await rt.load()
571
+ const botId = String(body.botId ?? '')
572
+ if (!rt.botOf(botId)) return fail('Unknown Bot', 'bot/not-found')
573
+ if (body.outcome !== undefined && !['done', 'skip', 'returned'].includes(body.outcome)) return fail('Nothing to send', 'bot/invalid')
574
+ const hold = rt.browserChannel.holdOf(botId)
575
+ if (hold === null) return fail('This browser is not held', 'bot/forbidden')
576
+ if (body.callId !== undefined && body.callId !== hold.callId) return fail('This handoff is no longer open', 'bot/stale')
577
+ // Claim the hold before queueing so a second answer is refused.
578
+ rt.browserChannel.setHold(botId, null)
579
+ if (hold.callId === undefined) return ok(true)
580
+ try {
581
+ const sessionId = rt.chatOf(botId)
582
+ const reply = handoffReply(hold, body.outcome)
583
+ await rt.ensureCapabilities(sessionId)
584
+ const agent = await rt.resolveAgent(sessionId)
585
+ agent.followup(createUserMessage({ content: [{ type: 'text', text: reply.text }], source: reply.source }))
586
+ await ctx.sessions.flush(agent.session)
587
+ } catch (error) {
588
+ if (rt.browserChannel.holdOf(botId) === null) rt.browserChannel.setHold(botId, hold)
589
+ throw error
590
+ }
591
+ return ok(true)
592
+ }
593
+ case 'browser-stop': {
594
+ await rt.load()
595
+ const botId = String(body.botId ?? '')
596
+ if (!rt.botOf(botId)) return fail('Unknown Bot', 'bot/not-found')
597
+ const sessionId = rt.chatOf(botId)
598
+ if (sessionId && rt.live.get(sessionId)?.turn !== undefined) {
599
+ const agent = await rt.resolveAgent(sessionId)
600
+ agent.cancel({ kind: 'user' })
601
+ }
602
+ for (const room of Object.values(rt.state.rooms ?? {})) {
603
+ if (room.members?.includes(botId)) await rt.stopMemberWork?.(room.id, botId)
604
+ }
605
+ rt.browserChannel?.setHold(botId, { by: 'user', reason: 'stopped', note: 'The user is controlling this browser.' })
606
+ return ok(true)
607
+ }
608
+ case 'diagnostics':
609
+ await rt.load()
610
+ return ok(await rt.diagnostics.report(body))
611
+ case 'diagnostics-log':
612
+ return ok(await rt.diagnostics.logText())
613
+ case 'diagnostics-clear':
614
+ await rt.diagnostics.clear()
615
+ return ok(true)
616
+ default:
617
+ return fail(`Unknown endpoint ${endpoint}`, 'bot/not-found')
618
+ }
619
+ } catch (error) {
620
+ const code = errorCode(error)
621
+ const message = error?.message ?? String(error)
622
+ if (code === 'bot/internal') rt.noteFailure({ source: 'api', text: `${endpoint}: ${message}`, code: error?.code ?? error?.name, stack: error?.stack })
623
+ return fail(message, code)
624
+ }
625
+ }
626
+
627
+ // An exact Fetch route on the shared /api channel; the browser authenticates with its session cookie.
628
+ ctx.effect(() => ctx.connection.fetch.register({
629
+ path: ROUTE,
630
+ methods: ['POST'],
631
+ requestBody: 'buffered',
632
+ fetch: async (request) => {
633
+ let body
634
+ try { body = await request.json() } catch { body = {} }
635
+ const result = await handle(String(body?.endpoint ?? ''), body?.payload, request.signal)
636
+ return new Response(JSON.stringify(result), { status: 200, headers: { 'content-type': 'application/json' } })
637
+ },
638
+ }), 'ds-bot: api route')
639
+
640
+ ctx.effect(() => {
641
+ void rt.load()
642
+ // Registered last, so this disposer runs before the state lock is released.
643
+ // In-flight side questions abort here; their entries stay answering for the
644
+ // next owner.
645
+ return () => Promise.all([rt.btwDispose?.(), rt.sideDispose?.()])
646
+ }, 'ds-bot: load state')
647
+ }