@ljwei-stak/dsh-model-route-bot 0.66.4 → 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 (271) 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 +18 -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/package.json +1 -1
  178. package/packages/orchestration/src/binding-product.mjs +19 -0
  179. package/packages/orchestration/src/bot-data-actions.mjs +45 -0
  180. package/packages/orchestration/src/bot-data-chart.mjs +109 -0
  181. package/packages/orchestration/src/bot-data-context.mjs +44 -0
  182. package/packages/orchestration/src/bot-data-engine.mjs +809 -0
  183. package/packages/orchestration/src/bot-data-io.mjs +186 -0
  184. package/packages/orchestration/src/bot-data-sql.mjs +195 -0
  185. package/packages/orchestration/src/bot-data-surface.mjs +144 -0
  186. package/packages/orchestration/src/bot-data-tools.mjs +68 -0
  187. package/packages/orchestration/src/bot-data-worker.mjs +228 -0
  188. package/packages/orchestration/src/bot-independent-execution.mjs +195 -0
  189. package/packages/orchestration/src/bot-independent-stream.mjs +74 -0
  190. package/packages/orchestration/src/bot-loop.mjs +152 -19
  191. package/packages/orchestration/src/bot-notifications.mjs +159 -0
  192. package/packages/orchestration/src/bot-policy.mjs +43 -4
  193. package/packages/orchestration/src/bot-profile.mjs +171 -0
  194. package/packages/orchestration/src/bot-routine.mjs +4 -4
  195. package/packages/orchestration/src/bot-runtime-assembly.mjs +255 -8
  196. package/packages/orchestration/src/bot-runtime-switch.mjs +13 -2
  197. package/packages/orchestration/src/bot-serving.mjs +164 -0
  198. package/packages/orchestration/src/bot-session-store.mjs +105 -9
  199. package/packages/orchestration/src/bot-settings.mjs +50 -11
  200. package/packages/orchestration/src/bot-thread-admission.mjs +84 -0
  201. package/packages/orchestration/src/bot-thread-host.mjs +187 -0
  202. package/packages/orchestration/src/bot-threads.mjs +790 -0
  203. package/packages/orchestration/src/data-product-access.mjs +37 -0
  204. package/packages/orchestration/src/data-tool-contract.mjs +291 -0
  205. package/packages/orchestration/src/desktop-http-carrier.mjs +120 -0
  206. package/packages/orchestration/src/desktop-proxy.mjs +311 -0
  207. package/packages/orchestration/src/desktop-sessions.mjs +43 -9
  208. package/packages/orchestration/src/ds-lifecycle.mjs +31 -4
  209. package/packages/orchestration/src/grant-refresh-queue.mjs +17 -0
  210. package/packages/orchestration/src/independent-chat-page.mjs +39 -0
  211. package/packages/orchestration/src/independent-chat-service.mjs +308 -0
  212. package/packages/orchestration/src/independent-context-state.mjs +44 -0
  213. package/packages/orchestration/src/independent-message-wire.mjs +84 -0
  214. package/packages/orchestration/src/independent-tool-catalog.mjs +175 -0
  215. package/packages/orchestration/src/independent-tool-families.mjs +73 -0
  216. package/packages/orchestration/src/independent-tool-host.mjs +344 -0
  217. package/packages/orchestration/src/main-bot-grant.mjs +15 -0
  218. package/packages/orchestration/src/main-model-routes.mjs +208 -0
  219. package/packages/orchestration/src/model-accounting.mjs +99 -35
  220. package/packages/orchestration/src/model-auxiliary-dispatch.mjs +187 -0
  221. package/packages/orchestration/src/model-loop-frame.mjs +179 -0
  222. package/packages/orchestration/src/model-prepared-generation.mjs +175 -0
  223. package/packages/orchestration/src/model-request-bounds.mjs +111 -0
  224. package/packages/orchestration/src/native-activity-contract.mjs +491 -0
  225. package/packages/orchestration/src/native-activity-service.mjs +102 -0
  226. package/packages/orchestration/src/native-activity-store.mjs +249 -0
  227. package/packages/orchestration/src/native-conversation-tools.mjs +78 -0
  228. package/packages/orchestration/src/native-data-tools.mjs +100 -0
  229. package/packages/orchestration/src/native-remote-actions.mjs +742 -0
  230. package/packages/orchestration/src/native-team-tools.mjs +73 -0
  231. package/packages/orchestration/src/pairing-invite.mjs +37 -0
  232. package/packages/orchestration/src/product-composition.mjs +1559 -1001
  233. package/packages/orchestration/src/remote-machines.mjs +82 -14
  234. package/packages/orchestration/src/routine-execution.mjs +5 -3
  235. package/packages/orchestration/src/routine-session.mjs +19 -13
  236. package/packages/orchestration/src/secure-pairing-host.mjs +112 -0
  237. package/packages/orchestration/src/task-journal.mjs +29 -0
  238. package/packages/orchestration/src/task-product.mjs +17 -0
  239. package/packages/orchestration/src/team-assignment-store.mjs +120 -0
  240. package/packages/orchestration/src/team-bridge.mjs +282 -0
  241. package/packages/orchestration/src/team-scheduler.mjs +83 -0
  242. package/packages/orchestration/src/team-sdk-tools.mjs +84 -0
  243. package/packages/orchestration/src/team-tool-contract.mjs +284 -0
  244. package/packages/orchestration/src/ui-host-namespace.mjs +23 -0
  245. package/packages/orchestration/src/web-panel-actions.mjs +103 -0
  246. package/packages/orchestration/src/web-panel-framing.mjs +227 -0
  247. package/packages/orchestration/src/web-panel-store.mjs +206 -0
  248. package/remote-computer-agent/README.md +39 -29
  249. package/remote-computer-agent/build.mjs +3 -0
  250. package/remote-computer-agent/dist/OPENMAUSBOT_LICENSE +202 -0
  251. package/remote-computer-agent/dist/OPENMAUSBOT_NOTICE +10 -0
  252. package/remote-computer-agent/dist/OPENMAUSBOT_SOURCE.json +68 -0
  253. package/remote-computer-agent/dist/cli-stdio-bridge.mjs +215 -25
  254. package/remote-computer-agent/dist/cli-stdio-bridge.mjs.map +4 -4
  255. package/remote-computer-agent/dist/cli.mjs +1688 -168
  256. package/remote-computer-agent/dist/cli.mjs.map +4 -4
  257. package/remote-computer-agent/dist/index.mjs +1579 -122
  258. package/remote-computer-agent/dist/index.mjs.map +4 -4
  259. package/remote-computer-agent/dist/native-peer.ps1 +51 -3
  260. package/remote-computer-agent/package-lock.json +308 -2
  261. package/remote-computer-agent/package.json +2 -1
  262. package/remote-computer-agent/pair-device.ps1 +20 -0
  263. package/remote-computer-agent/setup.ps1 +38 -16
  264. package/remote-computer-agent/src/cli.ts +24 -3
  265. package/remote-computer-agent/src/desktop-session.ts +11 -3
  266. package/remote-computer-agent/src/e2e-server.ts +81 -0
  267. package/remote-computer-agent/src/pairing-page.ts +35 -0
  268. package/remote-computer-agent/src/protocol.ts +25 -25
  269. package/remote-computer-agent/src/secure-pairing.ts +105 -0
  270. package/remote-computer-agent/src/store.ts +20 -5
  271. package/remote-computer-agent/src/transport.ts +39 -15
@@ -2,7 +2,7 @@
2
2
  // the first Main Bot of a new team.
3
3
  import { mkdir } from 'node:fs/promises'
4
4
  import { MAX_AVATAR_IMAGE, ROOM_MAX_MEMBERS, ROOM_MODES, ROOM_NAME_MAX, ROOM_NOTICE_MAX } from './constants.js'
5
- import { currentColor, isColor, normalize, unquote } from './text.js'
5
+ import { currentColor, isColor, normalize, splitRef, unquote } from './text.js'
6
6
  import { nameProblem } from '../shared/mentions.js'
7
7
 
8
8
  const BROWSER_GRANT_KINDS = new Set(['post', 'upload', 'download', 'delete', '*'])
@@ -420,6 +420,114 @@ export function install(rt) {
420
420
  return bot
421
421
  }
422
422
 
423
+ // Trusted product-only profile edit. No notice is delivered, no model is selected,
424
+ // and no kickoff is released. The owner service supplies the final synchronous
425
+ // committed-state check; the live base is compared at the same mutation boundary.
426
+ async function updateBotProfile(id, patch, { base, check } = {}) {
427
+ const fail = (code, message) => { throw Object.assign(new Error(message), { code }) }
428
+ const fields = { name: 40, role: 24, instructions: 4000 }
429
+ const plain = value => value && typeof value === 'object' && !Array.isArray(value)
430
+ if (!plain(patch) || !Object.keys(patch).length || Object.keys(patch).some(key => !Object.hasOwn(fields, key))
431
+ || !plain(base) || typeof check !== 'function') fail('BOT_PROFILE_INVALID', 'Invalid profile mutation')
432
+ const bot = rt.botOf(id)
433
+ if (!bot || state.bots[id] !== bot) fail('BOT_PROFILE_UNKNOWN_BOT', 'Unknown Bot')
434
+ if (rt.readOnly() !== undefined) fail('PRODUCT_READ_ONLY', rt.readOnly())
435
+ const next = {}
436
+ for (const field of Object.keys(patch)) {
437
+ if (typeof patch[field] !== 'string' || patch[field].trim().length > fields[field]) fail('BOT_PROFILE_INVALID', 'Invalid profile field')
438
+ if (typeof base[field] !== 'string' || (bot[field] ?? '') !== base[field]) fail('BOT_PROFILE_CONFLICT', 'Bot profile changed before commit')
439
+ next[field] = patch[field].trim()
440
+ }
441
+ if (next.name !== undefined) {
442
+ const clash = rt.findBot(next.name)
443
+ const pending = isHeld('bot', next.name) && normalize(next.name) !== normalize(bot.name)
444
+ const problem = next.name === bot.name ? undefined : nameProblem(next.name)
445
+ if (!next.name || (clash && clash.id !== id) || pending || problem !== undefined) fail('BOT_PROFILE_NAME_INVALID', 'Bot name is not available')
446
+ }
447
+ const checked = check(bot)
448
+ if (checked && typeof checked.then === 'function') fail('BOT_PROFILE_INVALID', 'Profile commit check must be synchronous')
449
+ // No await between the check and all three field assignments.
450
+ const renamed = next.name !== undefined && next.name !== bot.name
451
+ Object.assign(bot, next)
452
+ const rooms = renamed ? Object.values(state.rooms).filter(room => !room.named && room.members.includes(id)) : []
453
+ for (const room of rooms) room.name = autoRoomName(room.members)
454
+ await rt.save()
455
+ // Session titles are presentation. A failure cannot undo an already persisted
456
+ // profile or turn the successful save into a request the UI might retry.
457
+ if (renamed) {
458
+ try {
459
+ const agent = await rt.resolveAgent(rt.chatOf(id))
460
+ const current = rt.botOf(id)
461
+ if (current) await ctx.sessionTitle.rename(agent.session, current.name)
462
+ } catch (error) { rt.warn('ds-bot: profile session title not updated: %s', error?.message ?? error) }
463
+ for (const room of rooms) await retitleRoom(room)
464
+ }
465
+ return bot
466
+ }
467
+
468
+ // Trusted product-only thread records. A thread Session is made by the caller
469
+ // through rt.createSession — the same factory every own chat uses — and
470
+ // recorded here in one save, so the committed state the Host lifecycle
471
+ // compiles and the directory the product keeps can never disagree about who
472
+ // owns a Session. Nothing here delivers a notice, picks a model beyond the
473
+ // Bot's own, or starts a turn.
474
+ const THREAD_LIMIT = 64
475
+ const THREAD_ID = /^[A-Za-z0-9_-]{1,128}$/
476
+ const threadFail = (code, message) => { throw Object.assign(new Error(message), { code }) }
477
+ async function addThreadSession(botId, threadId, sessionId, { name } = {}) {
478
+ const readOnly = rt.readOnly()
479
+ if (readOnly !== undefined) threadFail('PRODUCT_READ_ONLY', readOnly)
480
+ const bot = rt.botOf(botId)
481
+ if (!bot || state.bots[botId] !== bot) threadFail('BOT_THREAD_UNKNOWN_BOT', 'Unknown Bot')
482
+ if (!THREAD_ID.test(threadId ?? '') || !THREAD_ID.test(sessionId ?? '')) threadFail('BOT_THREAD_INVALID', 'Invalid thread identity')
483
+ const threads = bot.threads ?? []
484
+ if (threads.length >= THREAD_LIMIT) threadFail('BOT_THREAD_LIMIT', 'Too many threads on one Bot')
485
+ if (threads.some(thread => thread.threadId === threadId)) threadFail('BOT_THREAD_KNOWN', 'That thread already has a Session')
486
+ for (const other of Object.values(state.bots)) {
487
+ for (const thread of other.threads ?? []) if (thread.sessionId === sessionId) threadFail('BOT_THREAD_SESSION_TAKEN', 'That Session already belongs to a thread')
488
+ }
489
+ const record = { threadId, sessionId, createdAt: Date.now(), ...(typeof name === 'string' && name.trim() ? { name: name.trim().slice(0, 80) } : {}) }
490
+ ;(bot.threads ??= []).push(record)
491
+ await rt.save()
492
+ // A thread runs on its Bot's model and permission preset, as a fresh part does.
493
+ const ref = await rt.desiredModel(bot)
494
+ if (ref !== undefined) {
495
+ const effort = await rt.desiredEffort(bot, ref)
496
+ await rt.selectSessionModel(sessionId, { ...splitRef(ref), ...(effort === undefined ? {} : { reasoningEffort: effort }) })
497
+ .catch(error => rt.warn('ds-bot: model for a thread of %s not applied: %s', bot.name, error))
498
+ }
499
+ await rt.applyPermission(bot, sessionId)
500
+ .catch(error => rt.warn('ds-bot: permission for a thread of %s not applied: %s', bot.name, error))
501
+ return { ...record }
502
+ }
503
+
504
+ async function renameThread(botId, threadId, name) {
505
+ const readOnly = rt.readOnly()
506
+ if (readOnly !== undefined) threadFail('PRODUCT_READ_ONLY', readOnly)
507
+ const bot = rt.botOf(botId)
508
+ const thread = bot?.threads?.find(entry => entry.threadId === threadId)
509
+ if (!bot || !thread) threadFail('BOT_THREAD_UNKNOWN', 'Unknown thread')
510
+ if (typeof name !== 'string' || name.trim().length > 80) threadFail('BOT_THREAD_INVALID', 'Invalid thread name')
511
+ thread.name = name.trim()
512
+ await rt.save()
513
+ return { ...thread }
514
+ }
515
+
516
+ // Archive hides a thread and drops its authority in the same save: the
517
+ // Session and its history stay, and only an explicit restore brings it back.
518
+ async function setThreadArchived(botId, threadId, archivedAt) {
519
+ const readOnly = rt.readOnly()
520
+ if (readOnly !== undefined) threadFail('PRODUCT_READ_ONLY', readOnly)
521
+ const bot = rt.botOf(botId)
522
+ const thread = bot?.threads?.find(entry => entry.threadId === threadId)
523
+ if (!bot || !thread) threadFail('BOT_THREAD_UNKNOWN', 'Unknown thread')
524
+ if (archivedAt === undefined || archivedAt === null) delete thread.archivedAt
525
+ else if (Number.isFinite(archivedAt) && archivedAt >= 0) thread.archivedAt = archivedAt
526
+ else threadFail('BOT_THREAD_INVALID', 'Invalid archive stamp')
527
+ await rt.save()
528
+ return { ...thread }
529
+ }
530
+
423
531
  async function setFlags(id, { pinned, hidden }) {
424
532
  const entry = rt.botOf(id) ?? rt.roomOf(id) ?? rt.own(state.agents, id)
425
533
  if (entry === undefined) throw new Error('Unknown conversation')
@@ -694,7 +802,7 @@ export function install(rt) {
694
802
  const archived = rt.archiveBotMemory(id)
695
803
  const current = rt.chatOf(id)
696
804
  // Questions and exchange turns are kept per Session, so every part of the chat is cleared.
697
- const parts = new Set([id, current, ...(bot.segments ?? []).map(segment => segment.sessionId)])
805
+ const parts = new Set([id, current, ...(bot.segments ?? []).map(segment => segment.sessionId), ...(bot.threads ?? []).map(thread => thread.sessionId)])
698
806
  // `archiving` stands until the archive's path is known: the memory may already be
699
807
  // there, and no region of its own closes it yet.
700
808
  const closedArchive = bot.private === true ? {
@@ -769,7 +877,8 @@ export function install(rt) {
769
877
  }
770
878
 
771
879
  Object.assign(rt, {
772
- ensureWorkspace, keepTeamFolder, createSession, createBot, duplicateBot, updateBot, setFlags,
880
+ ensureWorkspace, keepTeamFolder, createSession, createBot, duplicateBot, updateBot, updateBotProfile,
881
+ addThreadSession, renameThread, setThreadArchived, setFlags,
773
882
  roomBusy, autoRoomName, retitleRoom, createRoom, updateRoom, deleteRoom,
774
883
  setMain, deleteBot, bootstrap, releaseKickoff, greetingOut, holdKickoffAgain, userWrote, userAnswered,
775
884
  })
@@ -430,6 +430,8 @@ export function install(rt) {
430
430
  const roomOwners = new Map()
431
431
  /** @type {Map<string, {botId?:string, roomId?:string}>} */
432
432
  const pastOwners = new Map()
433
+ /** @type {Map<string, string>} */
434
+ const threadOwners = new Map()
433
435
  const chatOf = botId => botOf(botId)?.sessionId ?? botId
434
436
  const sessionOfRoom = room => (room === undefined ? undefined : room.sessionId ?? room.id)
435
437
  const indexSessions = () => {
@@ -437,9 +439,23 @@ export function install(rt) {
437
439
  groupOwners.clear()
438
440
  roomOwners.clear()
439
441
  pastOwners.clear()
442
+ threadOwners.clear()
440
443
  for (const bot of Object.values(state.bots)) {
441
444
  for (const segment of bot.segments ?? []) pastOwners.set(segment.sessionId, { botId: bot.id })
442
445
  chatOwners.set(chatOf(bot.id), bot.id)
446
+ // A live thread Session is another own chat of the same Bot: it speaks
447
+ // with the Bot's identity, prompt, tools and money, so it sits in
448
+ // chatOwners beside the Bot's default chat. chatOf() stays the default
449
+ // chat, which is a different thing from any one thread. An archived
450
+ // thread keeps its history readable and gains no authority.
451
+ for (const thread of bot.threads ?? []) {
452
+ if (thread?.sessionId === undefined) continue
453
+ if (thread.archivedAt === undefined || thread.archivedAt === null) {
454
+ if (chatOwners.has(thread.sessionId)) { warn('ds-bot: thread Session %s is owned already; it keeps its first owner', thread.sessionId); continue }
455
+ chatOwners.set(thread.sessionId, bot.id)
456
+ threadOwners.set(thread.sessionId, bot.id)
457
+ } else pastOwners.set(thread.sessionId, { botId: bot.id, threadId: thread.threadId })
458
+ }
443
459
  }
444
460
  for (const room of Object.values(state.rooms)) {
445
461
  const sessionId = sessionOfRoom(room)
@@ -461,6 +477,22 @@ export function install(rt) {
461
477
  const selfOf = id => chatOwners.get(id) ?? groupOwners.get(id)?.botId ?? pastOwners.get(id)?.botId
462
478
  // A Session where the Bot speaks now, as opposed to an archived segment.
463
479
  const isBotSession = id => chatOwners.has(id) || groupOwners.has(id)
480
+ // Threads: several independent own chats under one Bot. threadsOf() is the
481
+ // read-only record list, threadOf() the live owner of one Session, and
482
+ // sessionsOfBot() every Session that speaks for a Bot.
483
+ const threadsOf = botId => (botOf(botId)?.threads ?? []).map(thread => ({ ...thread }))
484
+ const threadOf = sessionId => {
485
+ if (typeof sessionId !== 'string') return undefined
486
+ for (const bot of Object.values(state.bots)) {
487
+ for (const thread of bot.threads ?? []) {
488
+ if (thread.sessionId !== sessionId) continue
489
+ return thread.archivedAt === undefined || thread.archivedAt === null ? { botId: bot.id, threadId: thread.threadId } : undefined
490
+ }
491
+ }
492
+ return undefined
493
+ }
494
+ const threadSessionOf = (botId, threadId) => threadsOf(botId).find(thread => thread.threadId === threadId)?.sessionId
495
+ const sessionsOfBot = botId => [...new Set([botId, chatOf(botId), ...(botOf(botId)?.segments ?? []).map(segment => segment.sessionId), ...threadsOf(botId).map(thread => thread.sessionId), ...groupSessionsOf(botId)])]
464
496
  const groupSessionsOf = botId => Object.values(state.rooms).map(room => room.sessions?.[botId]).filter(Boolean)
465
497
  const dropGroupSession = (room, botId) => {
466
498
  if (room.memberDirs !== undefined) delete room.memberDirs[botId]
@@ -506,7 +538,8 @@ export function install(rt) {
506
538
  Object.assign(rt, {
507
539
  state, load, save, refresh, readOnly, own,
508
540
  botOf, roomOf, isMain, chatOf, sessionOfRoom, selfOf, isBotSession,
509
- chatOwners, groupOwners, roomOwners, pastOwners,
541
+ chatOwners, groupOwners, roomOwners, pastOwners, threadOwners,
542
+ threadsOf, threadOf, threadSessionOf, sessionsOfBot,
510
543
  groupSessionsOf, dropGroupSession, retireGroupSession, findBot, findRooms, isAdminOf,
511
544
  })
512
545
  }
@@ -304,7 +304,7 @@ export function install(rt) {
304
304
  const owners = new Map()
305
305
  const botOwner = bot => ({ owner: bot.id, name: bot.name })
306
306
  for (const bot of Object.values(state.bots)) {
307
- for (const sessionId of [bot.id, rt.chatOf(bot.id), ...(bot.segments ?? []).map(segment => segment.sessionId)]) owners.set(sessionId, botOwner(bot))
307
+ for (const sessionId of rt.sessionsOfBot(bot.id)) owners.set(sessionId, botOwner(bot))
308
308
  }
309
309
  for (const room of Object.values(state.rooms)) {
310
310
  for (const sessionId of [rt.sessionOfRoom(room), ...(room.parts ?? []).map(segment => segment.sessionId)].filter(Boolean)) owners.set(sessionId, { owner: `room:${room.id}`, name: room.name })
@@ -1,62 +1,152 @@
1
- import { createRemoteTools } from './remote-tools.mjs'
2
- /**
3
- * Mount the independent Bot runtime as this plugin's own path.
4
- *
5
- * This plugin does not have to borrow the Harness delivery pipeline: it owns a session store, a
6
- * loop and a dispatch point, so a turn is just "input in, answer out" through the Host's model
7
- * service. The provider below turns that service's stream into the small reply shape the loop
8
- * expects, and the assembly module keeps the two runtimes from ever serving one Bot.
9
- *
10
- * Everything is opt-in: without `enabled` the runtime is never constructed, and nothing about the
11
- * existing Bot integration changes.
12
- */
13
-
14
- import { join } from 'node:path'
15
- import { createWorkspaceTools } from './workspace-tools.mjs'
16
- import { createIndependentRuntime } from '../../packages/orchestration/src/bot-runtime-assembly.mjs'
17
-
18
- const fail = code => { const error = new Error(code); error.code = code; throw error }
19
-
20
- /** Collect one streamed answer into `{ text }`. Tool calls are not inferred from text. */
21
- export async function collectAnswer(stream) {
22
- let text = ''
23
- for await (const chunk of stream) {
24
- if (typeof chunk === 'string') { text += chunk; continue }
25
- if (chunk?.type === 'text' && typeof chunk.text === 'string') { text += chunk.text; continue }
26
- if (chunk?.type === 'delta' && typeof chunk.text === 'string') { text += chunk.text }
27
- }
28
- return { text }
29
- }
30
-
31
- /**
32
- * @param {{
33
- * ctx: { get: (name: string) => unknown, llm?: object, logger?: object },
34
- * settings?: { enabled?: boolean, dir?: string, provider?: string, model?: string, maxSteps?: number },
35
- * stateDir: string,
36
- * integration?: (request: object) => Promise<object>,
37
- * now?: () => number,
38
- * }} options
39
- */
40
- export function createPluginRuntime({ ctx, settings = {}, stateDir, integration, now = Date.now } = {}) {
41
- if (settings.enabled !== true) return null
42
- if (!ctx || typeof ctx.get !== 'function') throw new TypeError('PLUGIN_RUNTIME_REQUIRES_CONTEXT')
43
- const llm = ctx.get('llm') ?? ctx.llm
44
- if (!llm || typeof llm.stream !== 'function') throw new TypeError('PLUGIN_RUNTIME_REQUIRES_LLM')
45
- const provider = settings.provider
46
- const model = settings.model
47
- if (typeof provider !== 'string' || !provider || typeof model !== 'string' || !model) fail('PLUGIN_RUNTIME_REQUIRES_MODEL')
48
-
49
- const providerFor = () => async ({ messages }) => collectAnswer(llm.stream({ provider, model, messages }))
50
- // Tools are per Bot: the Host invoker is bound to this Bot, so one Bot cannot borrow another grant.
51
- const toolsFor = botId => typeof settings.remoteInvoke === 'function'
52
- ? createRemoteTools({ invoke: (tool, args) => settings.remoteInvoke(botId, tool, args), ...(Array.isArray(settings.remoteTools) ? { names: settings.remoteTools } : {}) })
53
- : typeof settings.workspaceRoot === 'string' && settings.workspaceRoot ? createWorkspaceTools({ root: settings.workspaceRoot }) : null
54
- return createIndependentRuntime({
55
- dir: settings.dir ? String(settings.dir) : join(stateDir, 'independent-runtime'),
56
- integration: typeof integration === 'function' ? integration : async () => fail('INDEPENDENT_RUNTIME_NO_INTEGRATION'),
57
- providerFor,
58
- toolsFor,
59
- now,
60
- ...(Number.isSafeInteger(settings.maxSteps) ? { maxSteps: settings.maxSteps } : {}),
61
- })
62
- }
1
+ import { createRemoteTools } from './remote-tools.mjs'
2
+ import { INDEPENDENT_TOOL_NAMES, independentToolEntry } from '../../packages/orchestration/src/independent-tool-catalog.mjs'
3
+ /**
4
+ * Mount the independent Bot runtime as this plugin's own path.
5
+ *
6
+ * This plugin does not have to borrow the Harness delivery pipeline: it owns a session store, a
7
+ * loop and a dispatch point, so a turn is just "input in, answer out" through the Host's model
8
+ * service. The provider below asks the Host's own trusted execution frame for that answer — the
9
+ * committed Bot, the model that Bot has saved, the Bot policy and the one money stream — and the
10
+ * assembly module keeps the two runtimes from ever serving one Bot.
11
+ *
12
+ * Everything is opt-in: without `enabled` the runtime is never constructed, and nothing about the
13
+ * existing Bot integration changes.
14
+ */
15
+
16
+ import { join } from 'node:path'
17
+ import { createIndependentRuntime } from '../../packages/orchestration/src/bot-runtime-assembly.mjs'
18
+ import { collectAnswer } from '../../packages/orchestration/src/bot-independent-stream.mjs'
19
+
20
+ const fail = code => { const error = new Error(code); error.code = code; throw error }
21
+
22
+ export { collectAnswer }
23
+
24
+ /**
25
+ * Where this Bot's runtime switch lives. The path is published even when the independent runtime
26
+ * is switched off, because a Bot that was moved before it was switched off still has a committed
27
+ * mode on disk: a Host that cannot read that back has to refuse, not assume integration.
28
+ */
29
+ export function runtimeSwitchPath({ settings = {}, stateDir } = {}) {
30
+ if (typeof stateDir !== 'string' || stateDir === '') throw new TypeError('RUNTIME_STATE_DIR_REQUIRED')
31
+ return settings.dir ? String(settings.dir) : join(stateDir, 'independent-runtime')
32
+ }
33
+
34
+ /**
35
+ * @param {{
36
+ * ctx: { get: (name: string) => unknown, llm?: object, logger?: object },
37
+ * settings?: { enabled?: boolean, dir?: string, maxSteps?: number,
38
+ * remoteInvoke?: (botId: string, tool: string, args: object) => Promise<unknown>,
39
+ * remoteTools?: string[], toolsFor?: (botId: string) => object | null },
40
+ * stateDir: string,
41
+ * integration?: (request: object) => Promise<object>,
42
+ * serving?: object | null,
43
+ * now?: () => number,
44
+ * }} options
45
+ */
46
+ export function createPluginRuntime({ ctx, settings = {}, stateDir, integration, serving = null, now = Date.now } = {}) {
47
+ if (settings.enabled !== true) return null
48
+ if (!ctx || typeof ctx.get !== 'function') throw new TypeError('PLUGIN_RUNTIME_REQUIRES_CONTEXT')
49
+ const llm = ctx.get('llm') ?? ctx.llm
50
+ if (!llm || typeof llm.stream !== 'function') throw new TypeError('PLUGIN_RUNTIME_REQUIRES_LLM')
51
+ // The Bot product publishes the only frame an independent turn may run in. It is resolved per
52
+ // call, never captured at construction: the plugin starts the product synchronously and composes
53
+ // it asynchronously, so `connect` may build this runtime before the frame exists. A turn that
54
+ // starts before it does is refused — a Host with no committed Bot, no Bot policy and no money
55
+ // path must not answer from a global provider/model setting.
56
+ const executionOf = () => {
57
+ const execution = (() => { try { return ctx.get('botIndependentExecution') ?? null } catch { return null } })()
58
+ if (!execution || typeof execution.complete !== 'function' || typeof execution.routeFor !== 'function' || typeof execution.assertTurn !== 'function') {
59
+ throw Object.assign(new Error('宿主尚未发布 Bot 产品的独立执行上下文,本次请求被拒绝。'), { code: 'INDEPENDENT_EXECUTION_UNAVAILABLE', outcome: 'refused' })
60
+ }
61
+ return execution
62
+ }
63
+ // Per Bot, per step: the route comes from that Bot's own committed record and the call runs in
64
+ // the Host's frame, so one Bot can neither pick another Bot's model nor answer without policy.
65
+ // The tool schemas travel with the request: they are the ones the Host just re-decided for this
66
+ // step, so the model is only ever told about tools this Bot holds right now.
67
+ const providerFor = botId => async ({ messages, signal, tools, onText, conversationId }) => {
68
+ const execution = executionOf()
69
+ return execution.complete({ botId, ...execution.routeFor(botId), messages, signal, ...(conversationId ? { conversationId } : {}), ...(tools ? { tools } : {}), ...(onText ? { onText } : {}) })
70
+ }
71
+ // Tools are per Bot and Host-bound. The order is the Host's own, decided per call:
72
+ //
73
+ // 1. The Host's published independent tool service (`createIndependentToolHost`), which owns the
74
+ // machine directory, the durable Task journal and the per-Bot catalog. This is the only path
75
+ // on which a write is a tracked Task.
76
+ // 2. An explicit per-Bot resolver the Host passes in (`toolsFor`), for a Host that composes its
77
+ // own surface.
78
+ // 3. The Host's own remote invoker, which carries this Bot's subject, live binding, root and a
79
+ // fresh task/attempt per call. Without a durable Task backend this path offers the model the
80
+ // read-only tools only, and names the rest as unavailable — a Host that cannot track a write
81
+ // must not pretend it can.
82
+ //
83
+ // A Host that offers none of the three offers nothing, and the loop then says so: no shared
84
+ // workspace root is handed to every Bot as a stand-in.
85
+ const hostFor = botId => {
86
+ const host = (() => { try { return ctx.get('botIndependentToolHost') ?? null } catch { return null } })()
87
+ if (host && typeof host.forBot === 'function') return host.forBot(botId) ?? null
88
+ if (typeof settings.toolsFor === 'function') return settings.toolsFor(botId) ?? null
89
+ if (typeof settings.remoteInvoke === 'function') return legacyToolsFor(botId)
90
+ return null
91
+ }
92
+ // The Host's own remote invoker, in the vocabulary the loop speaks. Reads are offered by their
93
+ // catalog names; the machine-tool spellings still dispatch, because a Host that wired `remoteInvoke`
94
+ // already applies its own binding, root and task checks there.
95
+ const legacyToolsFor = botId => {
96
+ const inner = createRemoteTools({
97
+ invoke: (tool, args, context) => settings.remoteInvoke(botId, tool, args, context),
98
+ ...(Array.isArray(settings.remoteTools) ? { names: settings.remoteTools } : {}),
99
+ })
100
+ const granted = new Set(inner.names)
101
+ const offered = INDEPENDENT_TOOL_NAMES.map(independentToolEntry)
102
+ .filter(entry => entry && entry.machineTool && entry.backend === null && granted.has(entry.machineTool))
103
+ const unavailable = INDEPENDENT_TOOL_NAMES.map(independentToolEntry)
104
+ .filter(entry => entry && (entry.backend !== null || !granted.has(entry.machineTool)))
105
+ .map(entry => ({ name: entry.name, code: entry.backend ? 'TASK_BACKEND_UNAVAILABLE' : 'MACHINE_TOOL_NOT_GRANTED' }))
106
+ return {
107
+ names: offered.map(entry => entry.name),
108
+ origin: inner.origin,
109
+ isReadonly: name => offered.some(entry => entry.name === name && entry.effect === 'read'),
110
+ schemas: () => offered.map(entry => ({ name: entry.name, description: entry.description, parameters: entry.parameters })),
111
+ unavailable: () => unavailable,
112
+ async call(name, args = {}, context = {}) {
113
+ const entry = offered.find(item => item.name === name)
114
+ return inner.call(entry ? entry.machineTool : name, args, context)
115
+ },
116
+ }
117
+ }
118
+ const runtime = createIndependentRuntime({
119
+ dir: runtimeSwitchPath({ settings, stateDir }),
120
+ integration: typeof integration === 'function' ? integration : async () => fail('INDEPENDENT_RUNTIME_NO_INTEGRATION'),
121
+ providerFor,
122
+ toolsFor: hostFor,
123
+ // The Bot product's own frame re-decides the turn before every transcript write, every
124
+ // provider step and every tool call, so a move, a deletion, a revocation or an emptied grant
125
+ // stops the turn at the next checkpoint instead of finishing on a state that no longer stands.
126
+ assertTurn: botId => executionOf().assertTurn(botId),
127
+ // Published by the Host composition; a model/owner payload can never select
128
+ // the callback or turn a stop request into confirmed native-tree retirement.
129
+ retireConversation: async (botId, details) => {
130
+ const retirement = (() => { try { return ctx.get('botIndependentConversationRetirement') } catch { return null } })()
131
+ if (typeof retirement !== 'function') throw Object.assign(new Error('INDEPENDENT_CONTEXT_RETIREMENT_UNAVAILABLE'), { code: 'INDEPENDENT_CONTEXT_RETIREMENT_UNAVAILABLE', outcome: 'refused' })
132
+ return retirement(botId, details)
133
+ },
134
+ now,
135
+ // The Host's own serving admission: the same lease the integrated stream holds, so a Bot is
136
+ // never written by both paths and a move sees a turn this path is running.
137
+ ...(serving ? { serving } : {}),
138
+ ...(Number.isSafeInteger(settings.maxSteps) ? { maxSteps: settings.maxSteps } : {}),
139
+ })
140
+ // A Host that unloads must not leave this runtime's loops behind: a turn that never finishes would
141
+ // otherwise sit on its lease until the lease expires. The effect is the Host's own disposal seam and
142
+ // closing twice is a no-op. The Host's tool service is asked to close as well - this runtime is its
143
+ // only consumer - but a Host that owns its lifecycle may close it first, which is also a no-op.
144
+ if (typeof ctx.effect === 'function') {
145
+ ctx.effect(() => async () => {
146
+ const host = (() => { try { return ctx.get('botIndependentToolHost') ?? null } catch { return null } })()
147
+ try { host?.close?.() } catch { /* the Host owns its own lifecycle */ }
148
+ runtime.close()
149
+ }, 'model-router: independent Bot runtime')
150
+ }
151
+ return runtime
152
+ }
@@ -6,8 +6,9 @@
6
6
  * grant, and exposes the same tool names the model-facing path uses. A name outside the allowed set
7
7
  * never reaches `invoke`, so a reply cannot invent a tool.
8
8
  *
9
- * `invoke(tool, args)` must already bind machine, root, Bot, task and operation identity. Nothing
10
- * here passes an endpoint, a path outside the root, or an operation id.
9
+ * `invoke(tool, args, context)` must already bind machine, root, Bot, task and operation identity,
10
+ * and it receives the turn's cancel signal so a stop reaches the machine layer. Nothing here passes
11
+ * an endpoint, a path outside the root, or an operation id.
11
12
  */
12
13
 
13
14
  export const REMOTE_TOOL_NAMES = Object.freeze([
@@ -21,7 +22,7 @@ const fail = (code, message) => { const error = new Error(message ?? code); erro
21
22
  export const READ_ONLY_TOOLS = Object.freeze(['machine_ping', 'fs_list', 'fs_read', 'operation_status', 'artifact_stat', 'artifact_read'])
22
23
 
23
24
  /**
24
- * @param {{ invoke: (tool: string, args: object) => Promise<unknown>, names?: string[], origin?: string }} options
25
+ * @param {{ invoke: (tool: string, args: object, context?: { signal?: AbortSignal, botId?: string }) => Promise<unknown>, names?: string[], origin?: string }} options
25
26
  */
26
27
  export function createRemoteTools({ invoke, names = REMOTE_TOOL_NAMES, machines = [], origin = '远程电脑' } = {}) {
27
28
  if (typeof invoke !== 'function') throw new TypeError('REMOTE_TOOLS_REQUIRE_INVOKE')
@@ -37,7 +38,7 @@ export function createRemoteTools({ invoke, names = REMOTE_TOOL_NAMES, machines
37
38
  /** Whether a name may run beside another call in the same step. */
38
39
  isReadonly: name => allowed.includes(name) && READ_ONLY_TOOLS.includes(name),
39
40
  origin,
40
- async call(name, args = {}) {
41
+ async call(name, args = {}, context = {}) {
41
42
  if (typeof name !== 'string' || !allowed.includes(name)) fail('TOOL_NOT_GRANTED')
42
43
  if (!args || typeof args !== 'object' || Array.isArray(args)) fail('INVALID_TOOL_ARGS')
43
44
  if (Object.hasOwn(args, 'machineId') && !machinesAllowed.includes(args.machineId)) fail('MACHINE_NOT_ALLOWED')
@@ -45,7 +46,9 @@ export function createRemoteTools({ invoke, names = REMOTE_TOOL_NAMES, machines
45
46
  for (const key of ['rootId', 'botId', 'taskId', 'attemptId', 'operationId', 'endpoint', 'secretRef']) {
46
47
  if (Object.hasOwn(args, key)) fail(`TOOL_ARG_FORBIDDEN:${key}`)
47
48
  }
48
- return invoke(name, args)
49
+ // The turn's cancel travels with the call, so a stop reaches the machine layer instead of
50
+ // being discovered only after the whole round finished.
51
+ return invoke(name, args, { ...(context?.signal ? { signal: context.signal } : {}), ...(context?.botId ? { botId: context.botId } : {}) })
49
52
  },
50
53
  })
51
54
  }
@@ -0,0 +1,10 @@
1
+ OpenMausBot protocol components
2
+ Upstream: https://github.com/milind-soni/OpenMausBot
3
+ Pinned commit: 1bc4749aa8307d7d6998bf86de4ef6478460dd93
4
+ Author: Milind Soni and OpenMausBot contributors
5
+ License: Apache License 2.0; see vendor/LICENSE.
6
+
7
+ The companion end-to-end encryption protocol, descriptor, credentials, yamux
8
+ codec and interoperability vectors are copied without modification. Product
9
+ adapters and their permission policy are separate from the frozen upstream
10
+ files. SOURCE.json records each source path and byte hash.
@@ -0,0 +1,15 @@
1
+ # OpenMausBot 端到端协议
2
+
3
+ 冻结复用 `companion/src/e2e` 的 Noise、凭据派生、设备描述符、yamux、流会话与 Node 客户端。来源、固定提交、完整许可证和每文件 SHA-256 位于 `SOURCE.json`、`NOTICE` 与 `vendor/LICENSE`。
4
+
5
+ 这是全功能迁移的协议底座,导入协议本身不授予任何 Bot、电脑或模型权限。产品适配器负责一次性配对、设备生命周期、Host 授权与原任务身份;不改冻结源文件。`scripts/openmaus-protocol-entry.mjs` 聚合冻结源码,经固定 esbuild 构建为 `dist/protocol.mjs`;生产 `index.mjs` 只导出 JavaScript。Node 不允许在 node_modules 中直接擦除 TypeScript,因此不能把开发目录中直接导入 `.ts` 的成功当作安装后可用。
6
+
7
+ ```sh
8
+ node scripts/sync-openmaus-protocol.mjs --check
9
+ node scripts/sync-openmaus-protocol.mjs --source ../OpenMausBot
10
+ node scripts/build-openmaus-protocol.mjs
11
+ node scripts/build-openmaus-protocol.mjs --check
12
+ node --test tests/pairing-protocol.test.mjs
13
+ ```
14
+
15
+ 导入只接受固定提交的可信 checkout;允许 Windows 检出换行差异,写入 vendor 的始终是固定 Git blob 原始字节。更换提交是显式的源升级,不能自动取远端最新版本。
@@ -0,0 +1,68 @@
1
+ {
2
+ "schemaVersion": 1,
3
+ "upstream": "https://github.com/milind-soni/OpenMausBot",
4
+ "commit": "1bc4749aa8307d7d6998bf86de4ef6478460dd93",
5
+ "license": "Apache-2.0",
6
+ "files": [
7
+ {
8
+ "source": "companion/src/e2e/credentials.ts",
9
+ "target": "vendor/companion/src/e2e/credentials.ts",
10
+ "sha256": "1b576030c9e795eb64f5bee9fdc5918d6cfb92f2481b70259f7636991d6f5f6f",
11
+ "bytes": 6328
12
+ },
13
+ {
14
+ "source": "companion/src/e2e/noise.ts",
15
+ "target": "vendor/companion/src/e2e/noise.ts",
16
+ "sha256": "9269403237b43246142d5dd69bee0269d3b41f81c6bfe95520693320b6469250",
17
+ "bytes": 15871
18
+ },
19
+ {
20
+ "source": "companion/src/e2e/descriptor.ts",
21
+ "target": "vendor/companion/src/e2e/descriptor.ts",
22
+ "sha256": "5a7b39e4571e8b83fd85a3b6141cc09501918326afcc58c6ccb4a61344bbb2c8",
23
+ "bytes": 4570
24
+ },
25
+ {
26
+ "source": "companion/src/e2e/yamux.ts",
27
+ "target": "vendor/companion/src/e2e/yamux.ts",
28
+ "sha256": "1292b278d229d3ed8b92a32aec4137e04329d6fb2497d90ffee6f6d4bef20683",
29
+ "bytes": 13359
30
+ },
31
+ {
32
+ "source": "companion/src/e2e/session.ts",
33
+ "target": "vendor/companion/src/e2e/session.ts",
34
+ "sha256": "1ff7d26fe674ccab9f75026fec15f85bd791f99d75b888eaf24b7be744c4da0e",
35
+ "bytes": 14336
36
+ },
37
+ {
38
+ "source": "companion/src/e2e/phone.ts",
39
+ "target": "vendor/companion/src/e2e/phone.ts",
40
+ "sha256": "9fd5a069eecdea29afe5fb00ea2bfc5d42d8aae448927cd9ce3770a8cd2de5d0",
41
+ "bytes": 4083
42
+ },
43
+ {
44
+ "source": "companion/src/e2e/vectors/v1.json",
45
+ "target": "vendor/companion/src/e2e/vectors/v1.json",
46
+ "sha256": "5471a0070ddbb69dca34dee6da550de4581e8e1a40e22e0f97873d539793bf4e",
47
+ "bytes": 204553
48
+ },
49
+ {
50
+ "source": "companion/src/e2e/vectors/v1.sha256",
51
+ "target": "vendor/companion/src/e2e/vectors/v1.sha256",
52
+ "sha256": "99de21e9eef973a92e651517d88e977a1ef91fb441183648115b8a06b24796ee",
53
+ "bytes": 74
54
+ },
55
+ {
56
+ "source": "companion/src/e2e/README.md",
57
+ "target": "vendor/companion/src/e2e/README.md",
58
+ "sha256": "b752ee693e97c9674a207561be536105a34f90e99eaaead49b1f644f2b69589d",
59
+ "bytes": 29207
60
+ },
61
+ {
62
+ "source": "LICENSE",
63
+ "target": "vendor/LICENSE",
64
+ "sha256": "cfc7749b96f63bd31c3c42b5c471bf756814053e847c10f3eb003417bc523d30",
65
+ "bytes": 11358
66
+ }
67
+ ]
68
+ }