@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
@@ -0,0 +1,68 @@
1
+ --- a/src/host/store.js
2
+ +++ b/src/host/store.js
3
+ @@ -430,6 +430,8 @@
4
+ const roomOwners = new Map()
5
+ /** @type {Map<string, {botId?:string, roomId?:string}>} */
6
+ const pastOwners = new Map()
7
+ + /** @type {Map<string, string>} */
8
+ + const threadOwners = new Map()
9
+ const chatOf = botId => botOf(botId)?.sessionId ?? botId
10
+ const sessionOfRoom = room => (room === undefined ? undefined : room.sessionId ?? room.id)
11
+ const indexSessions = () => {
12
+ @@ -437,9 +439,23 @@
13
+ groupOwners.clear()
14
+ roomOwners.clear()
15
+ pastOwners.clear()
16
+ + threadOwners.clear()
17
+ for (const bot of Object.values(state.bots)) {
18
+ for (const segment of bot.segments ?? []) pastOwners.set(segment.sessionId, { botId: bot.id })
19
+ chatOwners.set(chatOf(bot.id), bot.id)
20
+ + // A live thread Session is another own chat of the same Bot: it speaks
21
+ + // with the Bot's identity, prompt, tools and money, so it sits in
22
+ + // chatOwners beside the Bot's default chat. chatOf() stays the default
23
+ + // chat, which is a different thing from any one thread. An archived
24
+ + // thread keeps its history readable and gains no authority.
25
+ + for (const thread of bot.threads ?? []) {
26
+ + if (thread?.sessionId === undefined) continue
27
+ + if (thread.archivedAt === undefined || thread.archivedAt === null) {
28
+ + if (chatOwners.has(thread.sessionId)) { warn('ds-bot: thread Session %s is owned already; it keeps its first owner', thread.sessionId); continue }
29
+ + chatOwners.set(thread.sessionId, bot.id)
30
+ + threadOwners.set(thread.sessionId, bot.id)
31
+ + } else pastOwners.set(thread.sessionId, { botId: bot.id, threadId: thread.threadId })
32
+ + }
33
+ }
34
+ for (const room of Object.values(state.rooms)) {
35
+ const sessionId = sessionOfRoom(room)
36
+ @@ -461,6 +477,22 @@
37
+ const selfOf = id => chatOwners.get(id) ?? groupOwners.get(id)?.botId ?? pastOwners.get(id)?.botId
38
+ // A Session where the Bot speaks now, as opposed to an archived segment.
39
+ const isBotSession = id => chatOwners.has(id) || groupOwners.has(id)
40
+ + // Threads: several independent own chats under one Bot. threadsOf() is the
41
+ + // read-only record list, threadOf() the live owner of one Session, and
42
+ + // sessionsOfBot() every Session that speaks for a Bot.
43
+ + const threadsOf = botId => (botOf(botId)?.threads ?? []).map(thread => ({ ...thread }))
44
+ + const threadOf = sessionId => {
45
+ + if (typeof sessionId !== 'string') return undefined
46
+ + for (const bot of Object.values(state.bots)) {
47
+ + for (const thread of bot.threads ?? []) {
48
+ + if (thread.sessionId !== sessionId) continue
49
+ + return thread.archivedAt === undefined || thread.archivedAt === null ? { botId: bot.id, threadId: thread.threadId } : undefined
50
+ + }
51
+ + }
52
+ + return undefined
53
+ + }
54
+ + const threadSessionOf = (botId, threadId) => threadsOf(botId).find(thread => thread.threadId === threadId)?.sessionId
55
+ + const sessionsOfBot = botId => [...new Set([botId, chatOf(botId), ...(botOf(botId)?.segments ?? []).map(segment => segment.sessionId), ...threadsOf(botId).map(thread => thread.sessionId), ...groupSessionsOf(botId)])]
56
+ const groupSessionsOf = botId => Object.values(state.rooms).map(room => room.sessions?.[botId]).filter(Boolean)
57
+ const dropGroupSession = (room, botId) => {
58
+ if (room.memberDirs !== undefined) delete room.memberDirs[botId]
59
+ @@ -506,7 +538,8 @@
60
+ Object.assign(rt, {
61
+ state, load, save, refresh, readOnly, own,
62
+ botOf, roomOf, isMain, chatOf, sessionOfRoom, selfOf, isBotSession,
63
+ - chatOwners, groupOwners, roomOwners, pastOwners,
64
+ + chatOwners, groupOwners, roomOwners, pastOwners, threadOwners,
65
+ + threadsOf, threadOf, threadSessionOf, sessionsOfBot,
66
+ groupSessionsOf, dropGroupSession, retireGroupSession, findBot, findRooms, isAdminOf,
67
+ })
68
+ }
@@ -0,0 +1,11 @@
1
+ --- a/src/host/usage.js
2
+ +++ b/src/host/usage.js
3
+ @@ -304,7 +304,7 @@
4
+ const owners = new Map()
5
+ const botOwner = bot => ({ owner: bot.id, name: bot.name })
6
+ for (const bot of Object.values(state.bots)) {
7
+ - for (const sessionId of [bot.id, rt.chatOf(bot.id), ...(bot.segments ?? []).map(segment => segment.sessionId)]) owners.set(sessionId, botOwner(bot))
8
+ + for (const sessionId of rt.sessionsOfBot(bot.id)) owners.set(sessionId, botOwner(bot))
9
+ }
10
+ for (const room of Object.values(state.rooms)) {
11
+ 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 })
@@ -7,19 +7,32 @@ const missing = {
7
7
  botBindings: blocked('宿主尚未提供 Bot 执行绑定投影。', '接入 Bot 设置服务后,可在这里查看和配置每个 Bot 的模型、工作区与资金范围。'),
8
8
  fundingAccounts: blocked('宿主尚未提供资金账户目录投影。', '接入资金账户服务后,可在这里按 provider 配置本地限额策略。'),
9
9
  tasks: blocked('宿主尚未提供统一任务服务的只读投影。', '接入 Task Service 后,可在这里跟踪、取消或核对任务。'),
10
+ threads: blocked('宿主尚未提供 Bot 线程目录投影。', '接入 Bot 线程服务后,可在这里为同一个 Bot 建立多条独立会话。'),
10
11
  machines: blocked('尚未接入远程电脑目录。', '安装并配对同版本 remote-computer-agent,然后在机器目录中登记。'),
12
+ webPanels: blocked('宿主尚未提供固定网页面板目录。', '接入固定网页面板存储后,可在这里把常用网页固定在侧栏。'),
11
13
  usage: blocked('宿主尚未提供统一费用与预占投影。', '接入唯一 MR 资金库的只读投影;已有 Token 用量仍可在 DS 用量页查看。'),
12
14
  }
13
15
  const ACTIONS = new Set([
14
- 'task.inspect', 'task.cancel', 'task.retry', 'task.reconcile', 'task.plan.submit', 'task.plan.cancel', 'usage.resolve',
15
- 'binding.save', 'funding.account.save', 'machine.secret', 'machine.pair', 'machine.bind', 'machine.probe',
16
+ 'ui.namespace',
17
+ 'nativeActivity.list',
18
+ 'botProfile.inspect', 'botProfile.save',
19
+ 'independentChat.send', 'independentChat.inspect', 'independentChat.cancel', 'independentChat.view', 'independentChat.messages',
20
+ 'independentChat.newConversation',
21
+ 'task.inspect', 'task.attention', 'task.cancel', 'task.retry', 'task.reconcile', 'task.plan.submit', 'task.plan.cancel', 'usage.resolve',
22
+ 'threads.readModel', 'threads.list', 'threads.create', 'threads.rename', 'threads.switch', 'threads.open', 'threads.archive', 'threads.unarchive', 'threads.ensure', 'threads.recover', 'threads.reconcile',
23
+ 'webPanels.list', 'webPanels.probe', 'webPanels.save', 'webPanels.remove', 'webPanels.order',
24
+ 'notifications.readModel', 'notifications.list', 'notifications.read', 'notifications.markAll',
25
+ 'data.catalog', 'data.load', 'data.describe', 'data.sql', 'data.show', 'data.page', 'data.export', 'data.remove', 'data.cancel',
26
+ 'binding.inspect', 'binding.save', 'funding.account.save', 'machine.secret', 'machine.pair', 'machine.pair.invite', 'machine.bind', 'machine.probe',
16
27
  'machine.revoke', 'machine.reconcile', 'machine.artifact', 'machine.desktop.view', 'machine.desktop.act',
17
28
  'machine.desktop.open', 'machine.desktop.renew', 'machine.desktop.close',
29
+ 'machine.desktop.poll', 'machine.desktop.send',
18
30
  'library.list', 'library.read', 'library.write', 'library.upload', 'library.download', 'library.remove', 'library.mkdir', 'library.search',
19
31
  'routine.list', 'routine.run', 'routine.save', 'routine.remove', 'routine.history', 'routine.cancel', 'routine.resolve', 'roles.list',
20
32
  'botRoutingInspect', 'botRoutingDecide', 'botRouting.dispatch', 'botRouting.save', 'botRouting.status',
21
33
  'botRuntimeStatus', 'botRuntimeMove', 'botRuntimeHistory', 'botRuntimeMessage',
22
34
  ])
35
+ const READ_ONLY_ACTIONS = new Set(['independentChat.inspect', 'independentChat.view', 'independentChat.messages', 'nativeActivity.list'])
23
36
  const plain = value => value && typeof value === 'object' && !Array.isArray(value)
24
37
  const error = (code, message) => Object.assign(new Error(message), { code })
25
38
  const desktopClientOrigin = request => {
@@ -63,6 +76,7 @@ export function createProductReadModel({ runtime, lifecycle, providers = {}, act
63
76
  lifecycle: lifecycle.status(),
64
77
  readOnly: runtime.readOnly() ?? null,
65
78
  bots: Object.values(state.bots).map(bot => ({ id: bot.id, name: bot.name, role: bot.role ?? '', model: bot.model ?? null,
79
+ appliedModel: typeof bot.appliedModel === 'string' ? bot.appliedModel : null,
66
80
  sessionId: runtime.chatOf(bot.id), workDir: bot.workDir ?? null, main: state.mainBotIds?.includes(bot.id) ?? false })),
67
81
  mainBotId: state.mainBotIds?.[0] ?? null,
68
82
  ...Object.fromEntries(values),
@@ -76,9 +90,10 @@ export function createProductReadModel({ runtime, lifecycle, providers = {}, act
76
90
  throw error('INVALID_PRODUCT_ACTION', '未知或格式无效的操作。')
77
91
  }
78
92
  if (typeof actions[request.action] !== 'function') throw error('PRODUCT_ACTION_UNAVAILABLE', '此宿主尚未接入该操作,请查看页面中的接入说明。')
79
- if (runtime.readOnly() !== undefined) throw error('PRODUCT_READ_ONLY', runtime.readOnly())
93
+ const readOnlyReason = runtime.readOnly()
94
+ if (readOnlyReason !== undefined && !READ_ONLY_ACTIONS.has(request.action)) throw error('PRODUCT_READ_ONLY', readOnlyReason)
80
95
  // context belongs to authorizeRequest(), never to the JSON request.
81
- return actions[request.action](request.payload, context)
96
+ return actions[request.action](request.payload, readOnlyReason !== undefined ? { ...context, readOnly: true } : context)
82
97
  },
83
98
  })
84
99
  }
@@ -195,13 +195,20 @@ function GroupOverview({ room, roster, actions, usage, useWork, openSettings })
195
195
 
196
196
  export function DetailsDrawer({ sessionId, startRenaming, startTab, hasStrip, roster, actions, useSessions, useSessionStatus, useActivity, useWork, useBrowserTabs, useBrowserPages }) {
197
197
  const owner = roster.byId[sessionId]
198
- const part = owner ? currentPart(owner) : sessionId
198
+ // A thread Session is its own part: the drawer shows that thread's
199
+ // transcript, stats, questions and usage, while everything Bot-scoped
200
+ // (settings, model, folder, memory, computer) stays the Bot's — a thread has
201
+ // no authority of its own.
202
+ const thread = roster.threadsById?.[sessionId]
203
+ const threadName = typeof thread?.name === 'string' ? thread.name.trim() : ''
204
+ const part = thread ? (thread.sessionId ?? sessionId) : (owner ? currentPart(owner) : sessionId)
199
205
  const isMainBot = isMainOf(roster, owner?.id)
200
206
  const [tab, setTab] = useState('details')
201
207
  const room = roster.roomsById[sessionId]
202
208
  // The info pane swaps its overview for the Bot or group settings view in place.
203
209
  const [view, setView] = useState(startTab === 'settings' && (owner || room) ? 'settings' : 'overview')
204
- const [editing, setEditing] = useState(Boolean(startRenaming && (roster.byId[sessionId] || roster.roomsById[sessionId])))
210
+ // A thread is named in the thread directory, never by renaming its Bot here.
211
+ const [editing, setEditing] = useState(Boolean(startRenaming && thread === undefined && (roster.byId[sessionId] || roster.roomsById[sessionId])))
205
212
  const [draftName, setDraftName] = useState(roster.byId[sessionId]?.name ?? (roster.roomsById[sessionId]?.named ? roster.roomsById[sessionId].name : ''))
206
213
  const [copied, setCopied] = useState(false)
207
214
  const [error, setError] = useState('')
@@ -219,7 +226,7 @@ export function DetailsDrawer({ sessionId, startRenaming, startTab, hasStrip, ro
219
226
  const running = useSessionStatus(map => map.get(part)?.running === true)
220
227
  const doing = useActivity(map => map[bot?.id ?? sessionId])
221
228
  const transcript = useSessions(list => transcriptOf(list, part, bot?.name ?? room?.name ?? t('Bot'), roster.exchangeTurns[part]))
222
- const usage = useSessions(list => usageOf(list, [part, ...(bot?.parts ?? room?.parts ?? [])]))
229
+ const usage = useSessions(list => usageOf(list, [part, ...(thread === undefined ? (bot?.parts ?? room?.parts ?? []) : [])]))
223
230
  const run = promise => Promise.resolve(promise)
224
231
  .then(() => { setError(''); return true })
225
232
  .catch(failure => { setError(failure?.message ?? String(failure)); return false })
@@ -281,8 +288,11 @@ export function DetailsDrawer({ sessionId, startRenaming, startTab, hasStrip, ro
281
288
  onChange: event => setDraftName(event.target.value), onBlur: saveName,
282
289
  onKeyDown: (event) => { if (event.key === 'Enter') void saveName(); if (event.key === 'Escape') { event.stopPropagation(); setEditing(false) } },
283
290
  })
284
- : h('button', { type: 'button', className: 'bt-drawer-name', 'aria-label': t('Rename {name}', { name: current }), title: t('Rename'), onClick: () => { setDraftName(room && !room.named ? '' : current); setEditing(true) } }, current),
285
- subtitle ? h('span', { className: 'bt-drawer-role' }, subtitle) : null),
291
+ : thread !== undefined
292
+ ? h('span', { className: 'bt-drawer-name', 'data-thread': '' }, current)
293
+ : h('button', { type: 'button', className: 'bt-drawer-name', 'aria-label': t('Rename {name}', { name: current }), title: t('Rename'), onClick: () => { setDraftName(room && !room.named ? '' : current); setEditing(true) } }, current),
294
+ subtitle ? h('span', { className: 'bt-drawer-role' }, subtitle) : null,
295
+ threadName ? h('span', { className: 'bt-drawer-role', 'data-thread': threadName }, threadName) : null),
286
296
  tabs.length > 1 ? h('div', { key: 'tabs', ref: glide.box, className: 'bt-tabs', role: 'tablist', 'data-move': side },
287
297
  h('span', { ref: glide.thumb, className: 'bt-thumb', 'aria-hidden': true }),
288
298
  tabs.map(key => h('button', { key, type: 'button', role: 'tab', className: 'bt-tab', 'aria-selected': tab === key, onClick: () => setTab(key) }, t(TAB_LABELS[key])))) : null,
@@ -72,6 +72,11 @@ export function HeaderPill({ sessionId, useRoster, useSessions, useSessionStatus
72
72
  const doing = useActivity(map => map[owner])
73
73
  const bot = roster.byId[sessionId]
74
74
  const room = roster.roomsById[sessionId]
75
+ // A thread is another own chat of this Bot: the header keeps naming the Bot,
76
+ // because that is who is speaking, and adds the thread so the owner can tell
77
+ // two contexts of the same Bot apart at a glance.
78
+ const thread = roster.threadsById?.[sessionId]
79
+ const threadName = typeof thread?.name === 'string' ? thread.name.trim() : ''
75
80
  const goal = room?.goal
76
81
  const goalCompose = useUi(state => state.goalCompose === sessionId)
77
82
  const goalOpen = useUi(state => state.goalOpen === sessionId)
@@ -116,7 +121,7 @@ export function HeaderPill({ sessionId, useRoster, useSessions, useSessionStatus
116
121
  }, [showGoal])
117
122
  const pill = h('button', {
118
123
  type: 'button', className: 'bt-pill', 'aria-expanded': away ? undefined : detailsOpen, 'data-memory': memoryNotice ? 'updated' : undefined,
119
- 'aria-label': away ? t('Back to chat') : memoryNotice ? t('Memory updated. Open {name}\'s memory', { name: roster.byId[changed]?.name ?? name }) : t('View conversation details'),
124
+ 'aria-label': away ? t('Back to chat') : memoryNotice ? t('Memory updated. Open {name}\'s memory', { name: roster.byId[changed]?.name ?? name }) : threadName ? `${t('View conversation details')} · ${threadName}` : t('View conversation details'),
120
125
  title: away ? name : undefined,
121
126
  onClick: () => (away ? selectView('chat') : memoryNotice ? actions.openMemory(changed) : actions.toggleDetails(sessionId)),
122
127
  },
@@ -127,6 +132,7 @@ export function HeaderPill({ sessionId, useRoster, useSessions, useSessionStatus
127
132
  h('span', { className: 'bt-pill-goal' }, h('span', { className: 'bt-pill-ico', 'aria-hidden': true }, h(GoalIcon)), h('span', { className: 'bt-pill-title' }, view.title)),
128
133
  h('span', { className: 'bt-pill-name' }, name))
129
134
  : h('span', { className: 'bt-pill-name' }, away ? t('Back to chat') : bot?.name ?? room?.name ?? (title || t('New chat'))),
135
+ threadName ? h('span', { className: 'bt-swap' }, threadName) : null,
130
136
  goalShown && view?.number !== undefined ? h('span', { className: 'bt-pill-count' }, `${view.number}/${view.count}`) : null,
131
137
  h(MemoryNotice, { shown: memoryNotice }),
132
138
  h('span', { className: 'bt-pill-go', 'aria-hidden': true }, h(GoArrow)))
@@ -134,7 +140,8 @@ export function HeaderPill({ sessionId, useRoster, useSessions, useSessionStatus
134
140
  showGoal ? h(GoalIsland, { room, goal, view, open: goalOpen, composing: goalCompose, roomWork, roster, actions, pill }) : pill,
135
141
  h('span', { className: 'bt-sr', role: 'status' }, memoryNotice ? t('Memory updated') : ''),
136
142
  h(NewMessagePills, { key: sessionId, sessionId, useSessions, useUi, useTurnClock }),
137
- h(ActivityIndicator, { key: `activity:${sessionId}`, sessionId, useRoster, useSessionStatus, useSession, useLive, useWork, actions }))
143
+ h(ActivityIndicator, { key: `activity:${sessionId}`, sessionId, useRoster, useSessionStatus, useSession, useLive, useWork, actions }),
144
+ typeof actions.conversationAddon === 'function' ? h(actions.conversationAddon, { sessionId, bot, room, roster, actions, running }) : null)
138
145
  }
139
146
 
140
147
  // New-message pills. "N new messages ↓" takes Back to bottom's place when
@@ -199,7 +199,7 @@ export function apply(ctx) {
199
199
  for (const id of pendingAdopt.keys()) {
200
200
  if (roster.getSnapshot().agentOf[id] !== undefined || listed[id] === undefined) pendingAdopt.delete(id)
201
201
  }
202
- for (const id of [...value.bots.map(currentPart), ...value.rooms.map(room => room.sessionId ?? room.id)]) {
202
+ for (const id of [...value.bots.map(currentPart), ...value.rooms.map(room => room.sessionId ?? room.id), ...(value.bots ?? []).flatMap(bot => (bot.threads ?? []).map(thread => thread.sessionId))]) {
203
203
  if (refreshed.has(id)) continue
204
204
  refreshed.add(id)
205
205
  void Promise.resolve(ctx.sessions.refreshProjections?.(id)).catch(quietly('refresh projections'))
@@ -407,6 +407,13 @@ export function apply(ctx) {
407
407
  // A Bot id or a group chat id, or any part of either's chat, opens the current part.
408
408
  const partFor = (id) => {
409
409
  const snapshot = roster.getSnapshot()
410
+ // A thread Session, or a thread id, opens its own Session: it is not the
411
+ // Bot's default chat, and opening it is never redirected there. The thread
412
+ // is resolved before the Bot, because a thread Session also resolves to its
413
+ // Bot in `byId` — every other Bot surface needs that, and this one line
414
+ // keeps the two from being confused.
415
+ const thread = snapshot.threadsById?.[id]
416
+ if (thread !== undefined) return thread.sessionId ?? id
410
417
  const bot = snapshot.byId[id]
411
418
  if (bot !== undefined) return currentPart(bot)
412
419
  const room = snapshot.roomsById[id]
@@ -437,7 +444,7 @@ export function apply(ctx) {
437
444
  const snapshot = roster.getSnapshot()
438
445
  if (!snapshot.ready || !snapshot.mainBotId) return
439
446
  const main = mainView()
440
- if (main !== undefined && snapshot.byId[main] === undefined && snapshot.roomsById[main] === undefined) {
447
+ if (main !== undefined && snapshot.byId[main] === undefined && snapshot.roomsById[main] === undefined && snapshot.threadsById?.[main] === undefined) {
441
448
  actions.openSession(snapshot.mainBotId)
442
449
  }
443
450
  }
@@ -1,7 +1,7 @@
1
1
  import { createElement as h, useId, useMemo, useRef, useState } from 'react'
2
2
  import { useTurnFacts } from './btw.js'
3
3
  import { editLines, lineDiff, proposalPlacement } from './learning-card-model.js'
4
- import { currentPart } from './text.js'
4
+ import { isOwnChatOf } from './sources.js'
5
5
  import { t } from './i18n.js'
6
6
  import { CheckIcon, MemoryIcon, PencilIcon } from './icons.js'
7
7
  import { spring, SPRING_TAP } from './motion.js'
@@ -106,7 +106,7 @@ function LearningProposals({ turn, dock, sessionId, useRoster, actions }) {
106
106
  const facts = useTurnFacts(sessionId, actions)
107
107
  const [failed, setFailed] = useState({})
108
108
  const bot = roster.byId?.[sessionId]
109
- if (!bot || currentPart(bot) !== sessionId) return null
109
+ if (!bot || !isOwnChatOf(bot, sessionId)) return null
110
110
  const proposals = roster.learning?.[bot.id] ?? []
111
111
  const visible = [...proposals]
112
112
  for (const entry of Object.values(failed)) {
@@ -1,6 +1,6 @@
1
1
  import { createElement as h, Fragment, useEffect, useLayoutEffect, useRef, useState, useSyncExternalStore } from 'react'
2
2
  import { createPortal } from 'react-dom'
3
- import { currentPart } from './text.js'
3
+ import { isOwnChatOf } from './sources.js'
4
4
  import { CheckIcon, SlidersIcon } from './icons.js'
5
5
  import { BotMark } from './mark.js'
6
6
  import { KeyIcon, MaskedInput } from './secret-card.js'
@@ -202,9 +202,11 @@ export function ModelChoiceDock({ sessionId, useRoster, actions }) {
202
202
  const bot = roster.byId[sessionId]
203
203
  const [busy, setBusy] = useState('')
204
204
  const [error, setError] = useState('')
205
- const shown = Boolean(bot) && currentPart(bot) === sessionId && roster.ready
205
+ // A thread is another own chat of the same Bot, so the Bot's model choice
206
+ // belongs to it exactly as it belongs to the default chat.
207
+ const shown = Boolean(bot) && isOwnChatOf(bot, sessionId) && roster.ready
206
208
  useEffect(() => { if (shown) void actions.refreshModels?.() }, [shown, sessionId])
207
- if (!bot || currentPart(bot) !== sessionId || !roster.ready) return null
209
+ if (!bot || !isOwnChatOf(bot, sessionId) || !roster.ready) return null
208
210
  const models = roster.models ?? []
209
211
  // A model whose provider has no API key fails every turn, so it is no choice.
210
212
  const usable = models.filter(model => !model.needsKey)
@@ -5,11 +5,11 @@
5
5
  import { createElement as h, useEffect, useId, useRef, useState } from 'react'
6
6
  import { CheckIcon, CloseIcon, TrashIcon } from './icons.js'
7
7
  import { BotAvatar, BotMark } from './mark.js'
8
- import { contentText, currentPart } from './text.js'
8
+ import { contentText } from './text.js'
9
9
  import { dateLocale, t } from './i18n.js'
10
10
  import { SPRING_POP, SPRING_SHAPE, spring } from './motion.js'
11
11
  import { useDockHold } from './card-hold.js'
12
- import { heldCardKey } from './sources.js'
12
+ import { heldCardKey, isOwnChatOf } from './sources.js'
13
13
  import { CellEmpty } from './cell-empty.js'
14
14
 
15
15
  const MIN_VALUE = 4
@@ -178,7 +178,7 @@ export function SecretDock({ sessionId, useRoster, useCards, useHeldCards, actio
178
178
  const shown = useCards?.(state => state[sessionId]) ?? NONE
179
179
  const held = useHeldCards?.(state => state[sessionId]) ?? NONE
180
180
  const bot = roster.byId[sessionId]
181
- if (!bot || currentPart(bot) !== sessionId) return null
181
+ if (!bot || !isOwnChatOf(bot, sessionId)) return null
182
182
  const drawn = entry => entry.callId && entry.sessionId === sessionId && shown[entry.callId] > 0 && !(held[heldCardKey('secret', entry.id)] > 0)
183
183
  const waiting = (roster.secretRequests?.[bot.id] ?? []).filter(entry => entry.status === 'pending' && !drawn(entry))
184
184
  if (waiting.length === 0) return null
@@ -16,7 +16,7 @@ export function createSource(initial) {
16
16
  }
17
17
  }
18
18
 
19
- export const EMPTY_ROSTER = { revision: -1, mainBotId: null, mainBotIds: [], bots: [], rooms: [], agents: [], exchangeTurns: {}, questions: {}, btwCards: {}, sideChats: {}, learning: {}, secrets: [], capabilities: [], capabilityStatuses: {}, secretRequests: {}, models: [], defaultModel: null, modelsKey: null, prefs: {}, byId: {}, roomsById: {}, agentsById: {}, agentOf: {}, ready: false }
19
+ export const EMPTY_ROSTER = { revision: -1, mainBotId: null, mainBotIds: [], bots: [], rooms: [], agents: [], exchangeTurns: {}, questions: {}, btwCards: {}, sideChats: {}, learning: {}, secrets: [], capabilities: [], capabilityStatuses: {}, secretRequests: {}, models: [], defaultModel: null, modelsKey: null, prefs: {}, byId: {}, roomsById: {}, agentsById: {}, agentOf: {}, threadsById: {}, ready: false }
20
20
  // Any Main Bot holds the Main Bot powers; `mainBotId`, the first one, is the sidebar tile.
21
21
  export const isMainOf = (roster, id) => id !== undefined && (roster.mainBotIds ?? []).includes(id)
22
22
 
@@ -28,6 +28,21 @@ export function indexRoster(value) {
28
28
  for (const bot of value.bots) {
29
29
  for (const id of [...(bot.parts ?? []), bot.sessionId ?? bot.id, bot.id]) byId[id] = bot
30
30
  }
31
+ // A thread is another own chat of its Bot, and it is indexed twice on
32
+ // purpose. `threadsById` holds the thread itself — its own Session, its name,
33
+ // its archive — so opening one opens the thread and never the Bot's default
34
+ // chat. Its Session also resolves to its Bot in `byId`, so every surface that
35
+ // asks "whose chat is this?" (the header, the composer's extra menu, the
36
+ // details drawer, and with it the model, memory, computer, voice and
37
+ // attachment controls) keeps the Bot instead of going blank inside a thread.
38
+ // A thread never takes a Bot's own chat identity: a Session the Bot already
39
+ // owns stays the Bot's.
40
+ const threadsById = {}
41
+ for (const bot of value.bots) for (const thread of bot.threads ?? []) {
42
+ if (thread.sessionId !== undefined) threadsById[thread.sessionId] = { botId: bot.id, ...thread }
43
+ if (thread.threadId !== undefined) threadsById[thread.threadId] = { botId: bot.id, ...thread }
44
+ if (thread.sessionId !== undefined && byId[thread.sessionId] === undefined) byId[thread.sessionId] = bot
45
+ }
31
46
  const roomsById = {}
32
47
  // A group's own Session is a chain of parts like a Bot's chat: `room.id` is its
33
48
  // first part's id and `sessionId` its current one; every part id finds the room.
@@ -41,9 +56,17 @@ export function indexRoster(value) {
41
56
  agentsById[agent.id] = agent
42
57
  for (const sessionId of agent.sessions ?? []) agentOf[sessionId] = agent.id
43
58
  }
44
- return { ...value, agents: value.agents ?? [], byId, roomsById, agentsById, agentOf, ready: true }
59
+ return { ...value, agents: value.agents ?? [], byId, roomsById, agentsById, agentOf, threadsById, ready: true }
45
60
  }
46
61
 
62
+ // Whether a Session is one of this Bot's own chats: its current part, or one of
63
+ // its threads' Sessions. A thread is another own chat of the same Bot, so the
64
+ // Bot-scoped surfaces (model choice, memory learning, secret requests, turn
65
+ // errors) belong to it exactly as they belong to the default chat. Earlier
66
+ // parts are deliberately not included: they keep the behaviour they had.
67
+ export const isOwnChatOf = (bot, sessionId) => bot !== undefined && bot !== null && typeof sessionId === 'string'
68
+ && ((bot.sessionId ?? bot.id) === sessionId || (bot.threads ?? []).some(thread => thread.sessionId === sessionId))
69
+
47
70
  // Whether a main-view Session id belongs to the team's DSH Agent: the conversation
48
71
  // group then stays withdrawn while the shell keeps the sidebar. Before the roster
49
72
  // arrives, the ids cached from the last pull answer.
@@ -1,9 +1,10 @@
1
1
  import { createElement as h, useEffect, useMemo, useState, useSyncExternalStore } from 'react'
2
- import { currentPart, isHiddenTurn } from './text.js'
2
+ import { isHiddenTurn } from './text.js'
3
3
  import { CheckIcon, CopyIcon } from './icons.js'
4
4
  import { ModelKeyCard, manageModels } from './model-menu.js'
5
5
  import { knownFailure } from './diagnostics.js'
6
6
  import { hostText, t } from './i18n.js'
7
+ import { isOwnChatOf } from './sources.js'
7
8
 
8
9
  // -------------------------------------------------------------------------
9
10
  // A failed turn in a Bot's chat: what went wrong and what the user can do about it,
@@ -157,7 +158,7 @@ export function FailureCell({ node, sessionId, useRoster, useTurnClock, actions
157
158
  const provider = blocked?.provider ?? providerOf(bot, roster)
158
159
  const text = failureText(data.code, { name: bot?.name ?? t('This Bot'), provider: providerName(roster, provider), status: blocked?.status })
159
160
  const facts = failureFacts(data, blocked)
160
- const current = bot !== undefined && currentPart(bot) === sessionId && !later
161
+ const current = bot !== undefined && isOwnChatOf(bot, sessionId) && !later
161
162
  // The key route never takes the account's credential, so a sign-in alone leaves the Bot
162
163
  // failing; it moves onto the account route. With no account model listed yet, the
163
164
  // user picks one.
@@ -43,8 +43,16 @@ export function install(rt) {
43
43
  // `sessionId` is the Bot's current own-chat part; `parts` its earlier ones.
44
44
  // `waiting`: the Bot holds its greeting until the user picks its model or fixes
45
45
  // what failed it. `blocked`: the facts of a failure only the user can fix (turns.js).
46
- bots: Object.values(state.bots).map(({ segments, kickoff, ...bot }) => ({
47
- ...bot, sessionId: rt.chatOf(bot.id), parts: (segments ?? []).map(segment => segment.sessionId), ...(kickoff ? { waiting: true } : {}),
46
+ bots: Object.values(state.bots).map(({ segments, threads, kickoff, ...bot }) => ({
47
+ ...bot, sessionId: rt.chatOf(bot.id), parts: (segments ?? []).map(segment => segment.sessionId),
48
+ // `sessionId` stays the default chat; `threads` are the Bot's other own
49
+ // chats, each with its own Session. An archived thread keeps its id and
50
+ // its history and opens read-only.
51
+ threads: (threads ?? []).map(thread => ({ threadId: thread.threadId, sessionId: thread.sessionId,
52
+ ...(thread.name === undefined ? {} : { name: thread.name }),
53
+ ...(thread.createdAt === undefined ? {} : { createdAt: thread.createdAt }),
54
+ ...(thread.archivedAt === undefined || thread.archivedAt === null ? {} : { archivedAt: thread.archivedAt }) })),
55
+ ...(kickoff ? { waiting: true } : {}),
48
56
  })),
49
57
  // `sessionId` is the group's own Session's current part, `ownParts` its earlier
50
58
  // ones; `parts` the members' hidden group Sessions and their earlier segments,
@@ -435,6 +443,7 @@ export function install(rt) {
435
443
  await rt.load()
436
444
  const past = rt.pastOwners.get(body.sessionId)
437
445
  const sessionId = rt.botOf(body.sessionId) ? rt.chatOf(body.sessionId) : past && past.roomId === undefined ? rt.chatOf(past.botId) : body.sessionId
446
+ if (past?.threadId !== undefined) return fail('This thread is archived; open it again to write in it', 'bot/archived')
438
447
  const question = rt.own(state.questions, sessionId)
439
448
  if (question === undefined || question.id !== body.questionId) return fail('This question is no longer open', 'bot/stale')
440
449
  const reply = questionReply(question, body.answer)
@@ -458,6 +467,7 @@ export function install(rt) {
458
467
  await rt.load()
459
468
  const past = rt.pastOwners.get(body.sessionId)
460
469
  const sessionId = rt.botOf(body.sessionId) ? rt.chatOf(body.sessionId) : past && past.roomId === undefined ? rt.chatOf(past.botId) : body.sessionId
470
+ if (past?.threadId !== undefined) return fail('This thread is archived; open it again to write in it', 'bot/archived')
461
471
  const pending = rt.own(state.views, sessionId)
462
472
  if (pending === undefined || pending.id !== body.viewId) return fail('This view is no longer open', 'bot/stale')
463
473
  const checked = validateViewAnswer(pending.view, body.action, body.values)
@@ -485,6 +495,8 @@ export function install(rt) {
485
495
  // A Bot id, a group chat's id, or an earlier part of either means the
486
496
  // current part.
487
497
  const past = rt.pastOwners.get(body.sessionId)
498
+ // An archived thread keeps its history but takes no new input.
499
+ if (past?.threadId !== undefined) return fail('This thread is archived; open it again to write in it', 'bot/archived')
488
500
  const room = rt.roomOf(body.sessionId)
489
501
  const sessionId = rt.botOf(body.sessionId) ? rt.chatOf(body.sessionId)
490
502
  : room !== undefined ? rt.sessionOfRoom(room)
@@ -866,7 +866,7 @@ export function install(rt) {
866
866
  // A deleted Bot's memory moves to the archive, whole.
867
867
  function archiveBotMemory(botId) {
868
868
  const bot = rt.botOf(botId)
869
- for (const sessionId of [botId, rt.chatOf(botId), ...(bot?.segments ?? []).map(segment => segment.sessionId), ...rt.groupSessionsOf(botId)]) renewMemory(sessionId)
869
+ for (const sessionId of rt.sessionsOfBot(botId)) renewMemory(sessionId)
870
870
  summaries.delete(botId)
871
871
  news.delete(botId)
872
872
  return queued(async () => {
@@ -244,6 +244,13 @@ export function install(rt) {
244
244
  await selectSessionModel(sessionId, route)
245
245
  .catch(error => rt.warn('ds-bot: model for %s in a group not applied: %s', bot.name, error))
246
246
  }
247
+ // A thread follows its Bot's model, as a group Session does.
248
+ for (const sessionId of rt.threadsOf(bot.id).map(thread => thread.sessionId)) {
249
+ const applied = sessionSelections.get(sessionId)
250
+ if (!force && applied !== undefined && sameSelection(applied, route)) continue
251
+ await selectSessionModel(sessionId, route)
252
+ .catch(error => rt.warn('ds-bot: model for a thread of %s not applied: %s', bot.name, error))
253
+ }
247
254
  if (!ownChanged) return ref
248
255
  bot.appliedModel = ref
249
256
  if (effort === undefined) delete bot.appliedEffort
@@ -446,6 +446,12 @@ export function install(rt) {
446
446
  const header = session.requestHeader()
447
447
  const route = header?.config
448
448
  if (!route?.provider || !route?.model) throw new Error('the Session has not called a model yet')
449
+ // The Host's prepared auxiliary stream binds one adapter generation for this note and
450
+ // admits the money against that same generation. A Host that has not wired it refuses
451
+ // the note here, rather than re-resolving the route at dispatch the way a bare stream
452
+ // does.
453
+ const auxiliary = ctx.get('botAuxiliaryModelStream')
454
+ if (typeof auxiliary !== 'function') throw new Error('the Host has no prepared auxiliary model stream')
449
455
  const messages = session.deriveMessages()
450
456
  const base = folder ? FOLDER_PROMPT : HANDOFF_PROMPT
451
457
  let retry
@@ -453,7 +459,7 @@ export function install(rt) {
453
459
  for (let attempt = 1; attempt <= NOTE_ATTEMPTS; attempt += 1) {
454
460
  const prompt = retry === undefined ? base : `${base}\n\n${retry}`
455
461
  const assembler = new BlockAssembler()
456
- for await (const chunk of ctx.llm.stream({
462
+ for await (const chunk of auxiliary.stream({
457
463
  provider: route.provider,
458
464
  model: route.model,
459
465
  ...(route.reasoningEffort ? { reasoningEffort: route.reasoningEffort } : {}),
@@ -515,6 +521,12 @@ export function install(rt) {
515
521
  const session = agent.session
516
522
  const route = session.requestHeader()?.config
517
523
  if (!route?.provider || !route?.model) throw new Error('the Session has not called a model yet')
524
+ // The Host's prepared auxiliary stream binds one adapter generation for this checkpoint,
525
+ // so the summary is written by the generation the accounting admitted. A Host that has
526
+ // not wired it refuses the checkpoint instead of pricing one generation and streaming
527
+ // another.
528
+ const auxiliary = ctx.get('botAuxiliaryModelStream')
529
+ if (typeof auxiliary !== 'function') throw new Error('the Host has no prepared auxiliary model stream')
518
530
  // The results list grows with the work, so the checkpoint may write as much as one
519
531
  // of the Bot's own replies.
520
532
  const maxTokens = route.maxTokens
@@ -522,7 +534,7 @@ export function install(rt) {
522
534
  const tail = checkpointTail(head, session.deriveMessages())
523
535
  const prompt = tail.length === 0 ? CHECKPOINT_PROMPT : `${CHECKPOINT_PROMPT}\n\n${checkpointTailNote(tail)}`
524
536
  const assembler = new BlockAssembler()
525
- for await (const chunk of ctx.llm.stream({
537
+ for await (const chunk of auxiliary.stream({
526
538
  provider: route.provider,
527
539
  model: route.model,
528
540
  ...(route.reasoningEffort ? { reasoningEffort: route.reasoningEffort } : {}),
@@ -571,21 +583,22 @@ export function install(rt) {
571
583
  lines.push({ who: line.who, text: line.who === 'you' || line.who === 'user' ? line.text.replace(/^(?:You|User): /, '') : line.text })
572
584
  if (lines.length >= STEWARD_LINES) break
573
585
  }
574
- const info = await ctx.llm.resolveModelInfo(route.provider, route.model).catch(() => undefined)
575
- // The decision needs no long thinking.
576
- const off = info?.reasoning?.efforts?.some(effort => effort.id === 'off')
586
+ // The decision needs no long thinking. Whether this model can be asked to think less
587
+ // is decided from the generation this very call prepares, not from a read of whatever
588
+ // registration happens to be current now.
589
+ const auxiliary = ctx.get('botAuxiliaryModelStream')
590
+ if (typeof auxiliary !== 'function') throw new Error('the Host has no prepared auxiliary model stream')
577
591
  const assembler = new BlockAssembler()
578
- for await (const chunk of ctx.llm.stream({
592
+ for await (const chunk of auxiliary.stream({
579
593
  provider: route.provider,
580
594
  model: route.model,
581
- ...(off ? { reasoningEffort: 'off' } : {}),
582
595
  system: STEWARD_SYSTEM,
583
596
  messages: rt.tagUsage([{ role: 'user', content: [{ type: 'text', text: stewardRequest({ lines, midTurn, checkpoints: checkpointCount(session.deriveMessages()), group }) }] }], { sessionId: agent.id, kind: 'compaction' }),
584
597
  maxTokens: STEWARD_MAX_TOKENS,
585
598
  sessionId: session.id,
586
599
  purpose: 'compaction',
587
600
  signal: AbortSignal.timeout(STEWARD_TIMEOUT_MS),
588
- })) assembler.push(chunk)
601
+ }, { controls: auxiliary.reasoningOff })) assembler.push(chunk)
589
602
  const finish = assembler.finish
590
603
  if (finish.kind === 'error' || finish.kind === 'aborted') throw new Error(finish.failure?.message ?? finish.kind)
591
604
  return parseDecision(assembler.blocks().filter(block => block.type === 'text').map(block => block.text).join('\n'))
@@ -732,7 +745,10 @@ export function install(rt) {
732
745
 
733
746
  const placeOf = (sessionId) => {
734
747
  const botId = rt.chatOwners.get(sessionId)
735
- if (botId !== undefined) return { botId }
748
+ // A thread is not a part chain: it keeps its own Session for as long as it
749
+ // lives, so the steward neither condenses it nor hands its input on to the
750
+ // Bot's default chat.
751
+ if (botId !== undefined) return rt.threadOf(sessionId) === undefined ? { botId } : undefined
736
752
  const owner = rt.groupOwners.get(sessionId)
737
753
  if (owner !== undefined) return { botId: owner.botId, roomId: owner.roomId }
738
754
  const roomId = rt.roomOwners.get(sessionId)
@@ -1234,6 +1250,7 @@ export function install(rt) {
1234
1250
  // past it.
1235
1251
  async function checkContext(agent) {
1236
1252
  if (switches.has(agent.id) || freshParts.has(agent.id) || checking.has(agent.id) || readOnly()) return
1253
+ if (placeOf(agent.id) === undefined) return
1237
1254
  if (lastTurnEnd(agent)?.data?.reason?.kind === 'error') return
1238
1255
  const lines = await contextLines(agent.session)
1239
1256
  const tokens = tokensOf(agent.session)
@@ -30,7 +30,7 @@ export function install(rt) {
30
30
  const privateBots = () => Object.values(state.bots).filter(bot => bot.private === true)
31
31
  // Deleting a private Bot does not make the files and memory it leaves behind public.
32
32
  const protectedBots = () => [...new Map([...(state.privateArchives ?? []), ...privateBots()].map(bot => [bot.id, bot])).values()]
33
- const sessionsOf = bot => [...new Set([bot.id, ...(bot.sessionIds ?? [rt.chatOf(bot.id), ...(bot.segments ?? []).map(segment => segment.sessionId), ...rt.groupSessionsOf(bot.id)])])]
33
+ const sessionsOf = bot => [...new Set([bot.id, ...(bot.sessionIds ?? [rt.sessionsOfBot(bot.id)])])]
34
34
  const dataRoots = () => [rt.dshHome, rt.home, process.env.DS_BOT_USER_DSH_HOME?.trim()].filter(Boolean)
35
35
 
36
36
  // A path as it is and as the file system resolves it, folded for comparing. realBeyond