@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,512 @@
1
+ // Store: the saved team (state.json), its migrations, the lock that keeps a second dsh
2
+ // host on the same data from writing it, and the indexes that map a Session to the Bot
3
+ // or group chat it belongs to.
4
+ import { copyFileSync, mkdirSync, readFileSync, statSync } from 'node:fs'
5
+ import { mkdir, unlink, writeFile } from 'node:fs/promises'
6
+ import { join } from 'node:path'
7
+ import { LEGACY_THEMES, MAX_EXCHANGES, ROOM_MODES, TEXT_KEPT } from './constants.js'
8
+ import { replaceFile } from './files.js'
9
+ import { homeLock } from './home-lock.js'
10
+ import { currentColor, headOf, normalize, unquote } from './text.js'
11
+
12
+ // A table read that sees only the keys the table holds itself. Ids that come from
13
+ // outside (a request body, a tool argument, a name a model gives) go through this, so
14
+ // "__proto__" and other inherited names read as "not there" instead of returning
15
+ // Object.prototype.
16
+ export const own = (table, key) => (table != null && Object.hasOwn(table, key) ? table[key] : undefined)
17
+
18
+ export function install(rt) {
19
+ const { ctx, home, statePath } = rt
20
+
21
+ /** @type {{version:1, workspaceId?:string, mainBotIds:string[], bots:Record<string,any>, privateArchives:any[], rooms:Record<string,any>, agents:Record<string,any>, capabilities:{items:Record<string,any>}, exchanges:any[], exchangeTurns:Record<string,number[]>, revision:number}} */
22
+ const state = { version: 1, mainBotIds: [], bots: {}, privateArchives: [], rooms: {}, agents: {}, capabilities: { items: {} }, exchanges: [], exchangeTurns: {}, works: {}, requests: {}, questions: {}, views: {}, prefs: {}, switching: {}, revision: 0 }
23
+ let loaded = null
24
+ let writing = Promise.resolve()
25
+ const { warn } = rt
26
+
27
+ const EMPTY = JSON.stringify(state)
28
+ // The other dsh host that holds state.lock while this one only reads (see home-lock.js);
29
+ // undefined while this host may write.
30
+ let holder
31
+ let lockBroken = false
32
+ let stuck
33
+ let warnedReadOnly = false
34
+ let stopped = false
35
+ // mtime and size of the state.json this host last read or wrote.
36
+ let seen = ''
37
+ // Bumped each time the in-memory state is replaced from the file, so a queued
38
+ // save cannot write a snapshot taken of the state it replaced.
39
+ let lockEpoch = 0
40
+ const stamp = () => {
41
+ try {
42
+ const { mtimeMs, size } = statSync(statePath)
43
+ return `${mtimeMs}:${size}`
44
+ } catch {
45
+ return ''
46
+ }
47
+ }
48
+ const lock = homeLock(join(home, 'state.lock'))
49
+ const claimLock = () => {
50
+ if (lockBroken) return
51
+ try {
52
+ mkdirSync(home, { recursive: true })
53
+ holder = lock.claim()
54
+ } catch (error) {
55
+ // An unusable lock (a read-only or odd file system) must not stop the team; a
56
+ // save that then cannot write still rejects.
57
+ lockBroken = true
58
+ holder = undefined
59
+ warn('ds-bot: cannot lock %s, so another dsh on the same data could undo changes: %s', home, error)
60
+ }
61
+ }
62
+ const readOnly = () => {
63
+ if (stuck !== undefined) return `The Bot team file ${statePath} could not be read (${stuck}). DS Bot does not change it; fix or move the file and reload.`
64
+ if (holder === undefined) return undefined
65
+ const where = holder.pid ? `pid ${holder.pid}${holder.host ? ` on ${holder.host}` : ''}` : 'unknown process'
66
+ return `The Bot team in ${home} is open in another dsh (Web or Desktop, ${where}). This window shows the team but cannot change it; close the other one and reload.`
67
+ }
68
+
69
+ // Synchronous on purpose: the saved team and the session indexes built from it
70
+ // exist before the first tool or shell env call can resolve a Session.
71
+ const readFileState = () => {
72
+ let contents
73
+ try {
74
+ contents = readFileSync(statePath, 'utf8')
75
+ } catch (error) {
76
+ seen = stamp()
77
+ if (error?.code === 'ENOENT') {
78
+ stuck = undefined
79
+ return undefined
80
+ }
81
+ stuck = error?.code ?? error?.message ?? String(error)
82
+ warn('ds-bot: unreadable state %s: %s', statePath, error)
83
+ return undefined
84
+ }
85
+ seen = stamp()
86
+ let parsed
87
+ try {
88
+ parsed = JSON.parse(contents)
89
+ } catch (error) {
90
+ const backup = `${statePath}.broken-${Date.now()}`
91
+ try {
92
+ copyFileSync(statePath, backup)
93
+ stuck = undefined
94
+ warn('ds-bot: unreadable state %s: %s; backed up as %s', statePath, error, backup)
95
+ } catch (copyError) {
96
+ stuck = copyError?.code ?? copyError?.message ?? error?.message ?? String(error)
97
+ warn('ds-bot: unreadable state %s: %s; backup failed: %s', statePath, error, copyError)
98
+ }
99
+ return undefined
100
+ }
101
+ if (parsed?.version === 1) {
102
+ stuck = undefined
103
+ return parsed
104
+ }
105
+ stuck = 'saved by a newer DS Bot; update DS Bot'
106
+ return undefined
107
+ }
108
+
109
+ // Fill `state` from the saved file and bring an older team up to date. Returns the
110
+ // groups whose Session titles still need the new name.
111
+ const adopt = (parsed) => {
112
+ lockEpoch += 1
113
+ const hadCapabilities = parsed !== null && parsed !== undefined && Object.hasOwn(parsed, 'capabilities')
114
+ for (const key of Object.keys(state)) delete state[key]
115
+ Object.assign(state, JSON.parse(EMPTY), parsed ?? {})
116
+ state.prefs ??= {}
117
+ // Teams saved before the DSH Agent existed carry no `agents`; corrupt values reset.
118
+ if (state.agents === null || typeof state.agents !== 'object' || Array.isArray(state.agents)) state.agents = {}
119
+ let migrated = !hadCapabilities
120
+ if (state.capabilities === null || typeof state.capabilities !== 'object' || Array.isArray(state.capabilities)) {
121
+ state.capabilities = { items: {} }
122
+ migrated = true
123
+ } else if (state.capabilities.items === null || typeof state.capabilities.items !== 'object' || Array.isArray(state.capabilities.items)) {
124
+ state.capabilities.items = {}
125
+ migrated = true
126
+ }
127
+ for (const bot of Object.values(state.bots)) {
128
+ if (!Array.isArray(bot.capabilities)) {
129
+ bot.capabilities = []
130
+ migrated = true
131
+ }
132
+ if (bot.color !== currentColor(bot.color)) {
133
+ bot.color = currentColor(bot.color)
134
+ migrated = true
135
+ }
136
+ // LEGACY: Bots saved with a lab ran on that lab's default model; the model they
137
+ // last ran on becomes their own, so dropping the lab changes nothing for them.
138
+ if ('lab' in bot) {
139
+ if (!bot.model && bot.appliedModel) bot.model = bot.appliedModel
140
+ delete bot.lab
141
+ migrated = true
142
+ }
143
+ }
144
+ // A team from before several Main Bots were allowed names a single one.
145
+ if (!Array.isArray(state.mainBotIds)) state.mainBotIds = []
146
+ if ('mainBotId' in state) {
147
+ if (state.mainBotId && !state.mainBotIds.includes(state.mainBotId)) state.mainBotIds.unshift(state.mainBotId)
148
+ delete state.mainBotId
149
+ migrated = true
150
+ }
151
+ state.mainBotIds = state.mainBotIds.filter(id => state.bots[id])
152
+ if (!Array.isArray(state.privateArchives)) state.privateArchives = []
153
+ // Teams from before the whale was stored at setup: their first Bot, while still a
154
+ // Main Bot, gets it once wherever it has no look of its own. After that the whale is
155
+ // an ordinary look the user can change, and it never passes to another Bot.
156
+ if (state.whaleStored !== true) {
157
+ const first = Object.values(state.bots).reduce((best, bot) => (best === undefined || (bot.createdAt ?? 0) < (best.createdAt ?? 0) ? bot : best), undefined)
158
+ if (first !== undefined && state.mainBotIds.includes(first.id)) {
159
+ if (!first.avatar?.shape && !first.avatar?.image) first.avatar = { ...first.avatar, shape: 'whale' }
160
+ if (!first.color) first.color = 'deepseek'
161
+ }
162
+ state.whaleStored = true
163
+ if (first !== undefined) migrated = true
164
+ }
165
+ if (Object.hasOwn(LEGACY_THEMES, state.prefs.theme ?? '')) {
166
+ state.prefs.theme = LEGACY_THEMES[state.prefs.theme]
167
+ migrated = true
168
+ }
169
+ if (state.works === null || typeof state.works !== 'object' || Array.isArray(state.works)) state.works = {}
170
+ if (state.requests === null || typeof state.requests !== 'object' || Array.isArray(state.requests)) state.requests = {}
171
+ if (state.views === null || typeof state.views !== 'object' || Array.isArray(state.views)) state.views = {}
172
+ // LEGACY: aside cards lived in this file only before the feature shipped;
173
+ // they are kept under btw/ per part now. A saved copy is dropped, not moved.
174
+ if ('btwCards' in state) {
175
+ delete state.btwCards
176
+ migrated = true
177
+ }
178
+ const writer = holder === undefined && stuck === undefined
179
+ // LEGACY: Bots no longer count the rounds they start in a group.
180
+ for (const room of Object.values(state.rooms)) {
181
+ if (!('botRounds' in room)) continue
182
+ delete room.botRounds
183
+ migrated = true
184
+ }
185
+ // LEGACY: the late-round limit went with the round caps, and the relay limit that
186
+ // followed it went too. Calls it parked stay until the group's next round runs them.
187
+ for (const room of Object.values(state.rooms)) {
188
+ if (['lateRounds', 'quietNoticed', 'quietShown', 'relays', 'relayLimit'].every(key => room[key] === undefined)) continue
189
+ delete room.lateRounds
190
+ delete room.quietNoticed
191
+ delete room.quietShown
192
+ delete room.relays
193
+ delete room.relayLimit
194
+ migrated = true
195
+ }
196
+ // Groups from before group settings existed reply in turn, led by a Main Bot member.
197
+ for (const room of Object.values(state.rooms)) {
198
+ if (room.admin !== undefined && ROOM_MODES.includes(room.mode)) continue
199
+ if (room.admin === undefined) room.admin = room.members.find(id => state.mainBotIds.includes(id)) ?? null
200
+ if (!ROOM_MODES.includes(room.mode)) room.mode = 'everyone'
201
+ room.notice ??= ''
202
+ migrated = true
203
+ }
204
+ // A Bot deleted while a group was made or changed could stay listed in it, even as its
205
+ // admin; it leaves the group now.
206
+ for (const room of Object.values(state.rooms)) {
207
+ const members = room.members.filter(id => state.bots[id])
208
+ const admin = members.includes(room.admin) ? room.admin : null
209
+ if (members.length === room.members.length && admin === room.admin) continue
210
+ room.members = members
211
+ room.admin = admin
212
+ migrated = true
213
+ }
214
+ // Teams from before #206 kept every long message whole in this file, written whole on
215
+ // every save. A message between Bots is whole in its recipient's Session; a group
216
+ // line's whole text moves to a file of its own, which only the writing host makes.
217
+ for (const entry of state.exchanges) {
218
+ if (typeof entry.text !== 'string' || entry.text.length <= TEXT_KEPT || entry.length !== undefined) continue
219
+ entry.length = entry.text.length
220
+ entry.text = headOf(entry.text, TEXT_KEPT)
221
+ migrated = true
222
+ }
223
+ for (const room of writer ? Object.values(state.rooms) : []) {
224
+ if (!Array.isArray(room.log)) continue
225
+ const log = room.log.map(line => (typeof line.text === 'string' && line.length === undefined && typeof line.seq === 'number' ? rt.keepGroupLine(room.id, line, line.text) : line))
226
+ if (log.some((line, index) => line !== room.log[index])) {
227
+ room.log = log
228
+ migrated = true
229
+ }
230
+ }
231
+ // Unnamed groups from before names followed their members may still list old names.
232
+ const stale = Object.values(state.rooms).filter(room => !room.named && room.name !== rt.autoRoomName(room.members))
233
+ for (const room of stale) room.name = rt.autoRoomName(room.members)
234
+ indexSessions()
235
+ return { migrated: migrated || stale.length > 0, stale }
236
+ }
237
+
238
+ const load = () => loaded ??= (async () => {
239
+ claimLock()
240
+ if (holder !== undefined) warn('ds-bot: %s', readOnly())
241
+ const { migrated, stale } = adopt(readFileState())
242
+ if (holder !== undefined) return
243
+ if (migrated && stuck === undefined) {
244
+ // A failed write must not poison `loaded`: the migrated state stays in
245
+ // memory and the next save() writes it.
246
+ try {
247
+ await save()
248
+ } catch (error) {
249
+ warn('ds-bot: saving the migrated Bot team failed: %s', error)
250
+ }
251
+ }
252
+ // Not awaited: renaming a session must not hold up the first roster read.
253
+ for (const room of stale) void rt.retitleRoom(room)
254
+ })()
255
+
256
+ // An outside edit of state.json while this host held the lock, found at a
257
+ // reclaim or by a queued write that saw the file change: this host's in-memory
258
+ // state goes aside first, then the file on disk is adopted. Queued behind
259
+ // `writing` — and behind any adopt already queued — so no save or second
260
+ // refresh slips between finding the change and the adopt.
261
+ let adopting
262
+ const adoptAfterReclaim = () => {
263
+ const run = (adopting ?? writing).then(async () => {
264
+ if (stamp() !== seen) {
265
+ const aside = `${statePath}.replaced-${Date.now()}`
266
+ try {
267
+ await writeFile(aside, JSON.stringify(state))
268
+ warn('ds-bot: %s changed while no host held the team; it is adopted and this host\'s state is kept as %s', statePath, aside)
269
+ } catch (error) {
270
+ warn('ds-bot: %s changed while no host held the team; it is adopted, and this host\'s state is lost because it could not be kept aside: %s', statePath, error)
271
+ }
272
+ }
273
+ adopt(readFileState())
274
+ })
275
+ writing = run.catch(() => {})
276
+ const tracked = run.finally(() => { if (adopting === tracked) adopting = undefined })
277
+ adopting = tracked
278
+ return tracked
279
+ }
280
+
281
+ // The lock was this host's and is its own again. An adopt still queued is
282
+ // awaited; otherwise the file is compared with what this host last saw:
283
+ // unchanged means nothing external happened and the in-memory state stays,
284
+ // changed means an outside edit is adopted the usual way.
285
+ const reclaimHeld = () => {
286
+ warnedReadOnly = false
287
+ if (adopting !== undefined) return adopting
288
+ return stamp() === seen ? undefined : adoptAfterReclaim()
289
+ }
290
+
291
+ // A reading host follows the file and takes over once the other host has gone.
292
+ const refresh = async () => {
293
+ await load()
294
+ const hadHolder = holder !== undefined
295
+ if (hadHolder) {
296
+ claimLock()
297
+ if (holder === undefined) {
298
+ warn('ds-bot: the other dsh has closed; this one now saves the Bot team')
299
+ warnedReadOnly = false
300
+ adopt(readFileState())
301
+ return
302
+ }
303
+ } else if (!lockBroken && !lock.owns()) {
304
+ // A cached "writer" is stale once the lock file no longer names this host:
305
+ // another dsh may have taken it over, or the file may just be gone. Re-claim
306
+ // and re-check instead of trusting the flag.
307
+ claimLock()
308
+ if (lockBroken) return
309
+ if (holder === undefined) {
310
+ await reclaimHeld()
311
+ return
312
+ }
313
+ warn('ds-bot: %s', readOnly())
314
+ }
315
+ if (adopting !== undefined) return adopting
316
+ if ((stuck !== undefined || holder !== undefined) && stamp() !== seen) adopt(readFileState())
317
+ }
318
+
319
+ const warnReadOnly = () => {
320
+ if (!warnedReadOnly) {
321
+ warnedReadOnly = true
322
+ warn('ds-bot: not saving the Bot team: %s', readOnly())
323
+ }
324
+ return false
325
+ }
326
+ const writable = () => {
327
+ if (stuck !== undefined) return warnReadOnly()
328
+ if (lockBroken) return true
329
+ if (holder === undefined && lock.owns()) return true
330
+ // Lost the lock: retake it when nobody else holds it (its file was deleted), else
331
+ // another host took it over and this one stops writing.
332
+ if (holder === undefined) {
333
+ claimLock()
334
+ if (holder === undefined) {
335
+ // A changed file is adopted first; the save queues behind that adopt.
336
+ const pending = reclaimHeld()
337
+ if (pending !== undefined) void pending.catch(() => {})
338
+ return true
339
+ }
340
+ }
341
+ return warnReadOnly()
342
+ }
343
+
344
+ const save = ({ required = false } = {}) => {
345
+ if (stopped) return required ? Promise.reject(new Error('The Bot team cannot be saved after this host stopped.')) : writing
346
+ indexSessions()
347
+ // A reading host keeps the file's revision, so its window does not skip the next change.
348
+ if (!writable()) return required ? Promise.reject(new Error(`This host cannot save the Bot team: ${readOnly()}`)) : writing
349
+ const run = writing.then(async () => {
350
+ // Re-check writability once the queued write runs: an adopt queued before
351
+ // it may have made this host read-only (e.g. a file a newer DS Bot wrote).
352
+ if (stuck !== undefined || holder !== undefined) {
353
+ if (required) throw new Error(`This host cannot save the Bot team: ${readOnly()}`)
354
+ warnReadOnly()
355
+ return
356
+ }
357
+ // The snapshot is taken here, so a save made while an adopt was pending
358
+ // writes the adopted state, not the stale one it had.
359
+ const epoch = lockEpoch
360
+ state.revision += 1
361
+ state.exchanges = state.exchanges.slice(-MAX_EXCHANGES)
362
+ const snapshot = JSON.stringify(state)
363
+ const temp = `${statePath}.${process.pid}.tmp`
364
+ try {
365
+ await mkdir(home, { recursive: true })
366
+ const ensureLock = () => {
367
+ if (lockBroken) return
368
+ const owned = lock.owns()
369
+ // An outside edit under this host's lock goes through the adopt step
370
+ // instead of being written over; a removed file is just written again.
371
+ const changed = owned && stamp() !== '' && stamp() !== seen
372
+ if (changed) void adoptAfterReclaim().catch(() => {})
373
+ // A different epoch means an adopt replaced the state this snapshot was
374
+ // taken from — another host's writes, or an outside edit it adopted.
375
+ if (!owned || changed || lockEpoch !== epoch) {
376
+ const error = new Error(changed ? 'ds-bot: the Bot team file changed under this host\'s lock' : 'ds-bot lost the state lock to another dsh')
377
+ error.code = 'ELOCKLOST'
378
+ throw error
379
+ }
380
+ }
381
+ ensureLock()
382
+ await writeFile(temp, snapshot, 'utf8')
383
+ ensureLock()
384
+ await replaceFile(temp, statePath, ensureLock)
385
+ seen = stamp()
386
+ rt.capabilitiesStateSaved?.()
387
+ rt.stateSaved?.()
388
+ } catch (error) {
389
+ await unlink(temp).catch(() => {})
390
+ throw error
391
+ }
392
+ })
393
+ writing = run.catch(() => {})
394
+ return run.catch(error => {
395
+ warn('ds-bot: state write failed: %s', error)
396
+ throw error
397
+ })
398
+ }
399
+
400
+ ctx.effect(() => () => {
401
+ stopped = true
402
+ return writing.finally(() => lock.release())
403
+ }, 'ds-bot: state lock')
404
+
405
+ const botOf = id => (id === undefined ? undefined : own(state.bots, id))
406
+ // A group's own Session moves on to a fresh part when the group's folder no longer
407
+ // matches the one it was made in (parts.js), like a Bot's own chat: `room.id` stays
408
+ // the first Session's id and `room.sessionId` the current one, so the group keeps
409
+ // one identity while its Session moves. `roomOwners` maps the current Session.
410
+ const roomOf = id => (id === undefined ? undefined : own(state.rooms, id) ?? own(state.rooms, roomOwners.get(id)))
411
+ const isMain = id => id !== undefined && state.mainBotIds.includes(id)
412
+
413
+ // Group Sessions. A member speaks in each group from a hidden Session of its own,
414
+ // `room.sessions[botId]`, that renders the same system prompt and tools as its own
415
+ // chat: the provider's prefix cache is shared, and a group turn never re-reads the
416
+ // member's private context. Created on the member's first turn in the group.
417
+ //
418
+ // Segments. A long conversation moves to a fresh Session at a context line (see
419
+ // parts.js), and a group's own Session to a fresh part when the group's folder
420
+ // changes. A Bot's id is the Session id of its first own-chat segment and never
421
+ // changes; `bot.sessionId` is the current one, and `room.sessionId` the group's.
422
+ // Earlier segments, of the own chat (`bot.segments`), of each group Session
423
+ // (`room.segments[botId]`) and of the group's own Session (`room.parts`), are
424
+ // archived and stay readable.
425
+ /** @type {Map<string, string>} */
426
+ const chatOwners = new Map()
427
+ /** @type {Map<string, {botId:string, roomId:string}>} */
428
+ const groupOwners = new Map()
429
+ /** @type {Map<string, string>} */
430
+ const roomOwners = new Map()
431
+ /** @type {Map<string, {botId?:string, roomId?:string}>} */
432
+ const pastOwners = new Map()
433
+ const chatOf = botId => botOf(botId)?.sessionId ?? botId
434
+ const sessionOfRoom = room => (room === undefined ? undefined : room.sessionId ?? room.id)
435
+ const indexSessions = () => {
436
+ chatOwners.clear()
437
+ groupOwners.clear()
438
+ roomOwners.clear()
439
+ pastOwners.clear()
440
+ for (const bot of Object.values(state.bots)) {
441
+ for (const segment of bot.segments ?? []) pastOwners.set(segment.sessionId, { botId: bot.id })
442
+ chatOwners.set(chatOf(bot.id), bot.id)
443
+ }
444
+ for (const room of Object.values(state.rooms)) {
445
+ const sessionId = sessionOfRoom(room)
446
+ if (sessionId !== undefined) roomOwners.set(sessionId, room.id)
447
+ for (const segment of room.parts ?? []) pastOwners.set(segment.sessionId, { roomId: room.id })
448
+ for (const [botId, segments] of Object.entries(room.segments ?? {})) {
449
+ for (const segment of segments) pastOwners.set(segment.sessionId, { botId, roomId: room.id })
450
+ }
451
+ for (const [botId, sessionId] of Object.entries(room.sessions ?? {})) groupOwners.set(sessionId, { botId, roomId: room.id })
452
+ }
453
+ // The agents module owns the DSH Agent session index, and tools.js re-syncs
454
+ // the scoped tools with this ownership (drops Sessions that left, scopes
455
+ // live Agents that joined); both install after this.
456
+ rt.indexAgents?.()
457
+ rt.syncScopedTools?.()
458
+ }
459
+ // The Bot a Session speaks for: its own chat, one of its group Sessions, or an
460
+ // earlier segment of either.
461
+ const selfOf = id => chatOwners.get(id) ?? groupOwners.get(id)?.botId ?? pastOwners.get(id)?.botId
462
+ // A Session where the Bot speaks now, as opposed to an archived segment.
463
+ const isBotSession = id => chatOwners.has(id) || groupOwners.has(id)
464
+ const groupSessionsOf = botId => Object.values(state.rooms).map(room => room.sessions?.[botId]).filter(Boolean)
465
+ const dropGroupSession = (room, botId) => {
466
+ if (room.memberDirs !== undefined) delete room.memberDirs[botId]
467
+ const sessionId = room.sessions?.[botId]
468
+ if (sessionId === undefined) return
469
+ delete room.sessions[botId]
470
+ for (const segment of room.segments?.[botId] ?? []) delete state.switching[segment.sessionId]
471
+ if (room.segments !== undefined) delete room.segments[botId]
472
+ delete state.switching[sessionId]
473
+ groupOwners.delete(sessionId)
474
+ rt.cancelApprovals?.(sessionId)
475
+ // A plain archive refuses a running Session, which would go on working for a group
476
+ // that no longer has it.
477
+ void ctx.workspaceRegistry.archiveSession(sessionId, { stopActivity: true }).catch(() => {})
478
+ }
479
+ // A group Session DSH can no longer open is set aside; the member's earlier parts in
480
+ // the group stay, and its next turn there starts a new Session.
481
+ const retireGroupSession = (room, botId) => {
482
+ const sessionId = room.sessions?.[botId]
483
+ if (sessionId === undefined) return
484
+ delete room.sessions[botId]
485
+ groupOwners.delete(sessionId)
486
+ rt.cancelApprovals?.(sessionId)
487
+ void ctx.workspaceRegistry.archiveSession(sessionId, { stopActivity: true }).catch(() => {})
488
+ }
489
+ const findBot = (ref) => {
490
+ if (ref === undefined) return undefined
491
+ const byId = own(state.bots, ref)
492
+ if (byId) return byId
493
+ const wanted = normalize(unquote(ref)).replace(/^@/, '')
494
+ return Object.values(state.bots).find(bot => normalize(bot.name) === wanted)
495
+ }
496
+ // Every group chat whose id or name matches; callers refuse an ambiguous name.
497
+ const findRooms = (ref) => {
498
+ if (ref === undefined || ref === null) return []
499
+ const byId = own(state.rooms, ref)
500
+ if (byId) return [byId]
501
+ const wanted = normalize(unquote(ref))
502
+ return Object.values(state.rooms).filter(room => normalize(room.name) === wanted)
503
+ }
504
+ const isAdminOf = (room, id) => id !== undefined && room.admin === id
505
+
506
+ Object.assign(rt, {
507
+ state, load, save, refresh, readOnly, own,
508
+ botOf, roomOf, isMain, chatOf, sessionOfRoom, selfOf, isBotSession,
509
+ chatOwners, groupOwners, roomOwners, pastOwners,
510
+ groupSessionsOf, dropGroupSession, retireGroupSession, findBot, findRooms, isAdminOf,
511
+ })
512
+ }