@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
@@ -17,7 +17,39 @@
17
17
 
18
18
  const hostOf = (url) => { try { return new URL(url).hostname.replace(/^www\./, '') } catch { return '' } }
19
19
  const shortUrl = url => String(url ?? '').replace(/^https?:\/\//i, '').replace(/^www\./i, '').replace(/\/$/, '')
20
- @@ -268,7 +269,7 @@
20
+ @@ -194,13 +195,20 @@
21
+
22
+ export function DetailsDrawer({ sessionId, startRenaming, startTab, hasStrip, roster, actions, useSessions, useSessionStatus, useActivity, useWork, useBrowserTabs, useBrowserPages }) {
23
+ const owner = roster.byId[sessionId]
24
+ - const part = owner ? currentPart(owner) : sessionId
25
+ + // A thread Session is its own part: the drawer shows that thread's
26
+ + // transcript, stats, questions and usage, while everything Bot-scoped
27
+ + // (settings, model, folder, memory, computer) stays the Bot's — a thread has
28
+ + // no authority of its own.
29
+ + const thread = roster.threadsById?.[sessionId]
30
+ + const threadName = typeof thread?.name === 'string' ? thread.name.trim() : ''
31
+ + const part = thread ? (thread.sessionId ?? sessionId) : (owner ? currentPart(owner) : sessionId)
32
+ const isMainBot = isMainOf(roster, owner?.id)
33
+ const [tab, setTab] = useState('details')
34
+ const room = roster.roomsById[sessionId]
35
+ // The info pane swaps its overview for the Bot or group settings view in place.
36
+ const [view, setView] = useState(startTab === 'settings' && (owner || room) ? 'settings' : 'overview')
37
+ - const [editing, setEditing] = useState(Boolean(startRenaming && (roster.byId[sessionId] || roster.roomsById[sessionId])))
38
+ + // A thread is named in the thread directory, never by renaming its Bot here.
39
+ + const [editing, setEditing] = useState(Boolean(startRenaming && thread === undefined && (roster.byId[sessionId] || roster.roomsById[sessionId])))
40
+ const [draftName, setDraftName] = useState(roster.byId[sessionId]?.name ?? (roster.roomsById[sessionId]?.named ? roster.roomsById[sessionId].name : ''))
41
+ const [copied, setCopied] = useState(false)
42
+ const [error, setError] = useState('')
43
+ @@ -218,7 +226,7 @@
44
+ const running = useSessionStatus(map => map.get(part)?.running === true)
45
+ const doing = useActivity(map => map[bot?.id ?? sessionId])
46
+ const transcript = useSessions(list => transcriptOf(list, part, bot?.name ?? room?.name ?? t('Bot'), roster.exchangeTurns[part]))
47
+ - const usage = useSessions(list => usageOf(list, [part, ...(bot?.parts ?? room?.parts ?? [])]))
48
+ + const usage = useSessions(list => usageOf(list, [part, ...(thread === undefined ? (bot?.parts ?? room?.parts ?? []) : [])]))
49
+ const run = promise => Promise.resolve(promise)
50
+ .then(() => { setError(''); return true })
51
+ .catch(failure => { setError(failure?.message ?? String(failure)); return false })
52
+ @@ -268,7 +276,7 @@
21
53
  }
22
54
  const subtitle = bot ? bot.role : roomSubtitle(room, roster)
23
55
  // Tab sets: a 1:1 Bot has Details and Browser; a group has only Details, so no tab row.
@@ -26,7 +58,21 @@
26
58
  return [h(Fragment, { key: 'top' }, top),
27
59
  h('div', { key: 'id', className: 'bt-drawer-id', 'data-move': side },
28
60
  bot ? h(BotAvatar, { bot, size: 64, state: botState(running, doing, roster.questions?.[part] !== undefined), main: isMainBot, badge: false, gaze: true, pokeable: true })
29
- @@ -306,6 +307,7 @@
61
+ @@ -280,8 +288,11 @@
62
+ onChange: event => setDraftName(event.target.value), onBlur: saveName,
63
+ onKeyDown: (event) => { if (event.key === 'Enter') void saveName(); if (event.key === 'Escape') { event.stopPropagation(); setEditing(false) } },
64
+ })
65
+ - : 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),
66
+ - subtitle ? h('span', { className: 'bt-drawer-role' }, subtitle) : null),
67
+ + : thread !== undefined
68
+ + ? h('span', { className: 'bt-drawer-name', 'data-thread': '' }, current)
69
+ + : 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),
70
+ + subtitle ? h('span', { className: 'bt-drawer-role' }, subtitle) : null,
71
+ + threadName ? h('span', { className: 'bt-drawer-role', 'data-thread': threadName }, threadName) : null),
72
+ tabs.length > 1 ? h('div', { key: 'tabs', ref: glide.box, className: 'bt-tabs', role: 'tablist', 'data-move': side },
73
+ h('span', { ref: glide.thumb, className: 'bt-thumb', 'aria-hidden': true }),
74
+ 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,
75
+ @@ -306,6 +317,7 @@
30
76
  h('div', { className: 'bt-kv' }, h('span', null, bot.name), h('span', null, usage))) : null,
31
77
  h(ScheduleSummary, { key: 'routines', bot, actions }),
32
78
  ] : null,
@@ -0,0 +1,41 @@
1
+ --- a/src/client/header.js
2
+ +++ b/src/client/header.js
3
+ @@ -72,6 +72,11 @@
4
+ const doing = useActivity(map => map[owner])
5
+ const bot = roster.byId[sessionId]
6
+ const room = roster.roomsById[sessionId]
7
+ + // A thread is another own chat of this Bot: the header keeps naming the Bot,
8
+ + // because that is who is speaking, and adds the thread so the owner can tell
9
+ + // two contexts of the same Bot apart at a glance.
10
+ + const thread = roster.threadsById?.[sessionId]
11
+ + const threadName = typeof thread?.name === 'string' ? thread.name.trim() : ''
12
+ const goal = room?.goal
13
+ const goalCompose = useUi(state => state.goalCompose === sessionId)
14
+ const goalOpen = useUi(state => state.goalOpen === sessionId)
15
+ @@ -116,7 +121,7 @@
16
+ }, [showGoal])
17
+ const pill = h('button', {
18
+ type: 'button', className: 'bt-pill', 'aria-expanded': away ? undefined : detailsOpen, 'data-memory': memoryNotice ? 'updated' : undefined,
19
+ - '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'),
20
+ + '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'),
21
+ title: away ? name : undefined,
22
+ onClick: () => (away ? selectView('chat') : memoryNotice ? actions.openMemory(changed) : actions.toggleDetails(sessionId)),
23
+ },
24
+ @@ -127,6 +132,7 @@
25
+ 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)),
26
+ h('span', { className: 'bt-pill-name' }, name))
27
+ : h('span', { className: 'bt-pill-name' }, away ? t('Back to chat') : bot?.name ?? room?.name ?? (title || t('New chat'))),
28
+ + threadName ? h('span', { className: 'bt-swap' }, threadName) : null,
29
+ goalShown && view?.number !== undefined ? h('span', { className: 'bt-pill-count' }, `${view.number}/${view.count}`) : null,
30
+ h(MemoryNotice, { shown: memoryNotice }),
31
+ h('span', { className: 'bt-pill-go', 'aria-hidden': true }, h(GoArrow)))
32
+ @@ -134,7 +140,8 @@
33
+ showGoal ? h(GoalIsland, { room, goal, view, open: goalOpen, composing: goalCompose, roomWork, roster, actions, pill }) : pill,
34
+ h('span', { className: 'bt-sr', role: 'status' }, memoryNotice ? t('Memory updated') : ''),
35
+ h(NewMessagePills, { key: sessionId, sessionId, useSessions, useUi, useTurnClock }),
36
+ - h(ActivityIndicator, { key: `activity:${sessionId}`, sessionId, useRoster, useSessionStatus, useSession, useLive, useWork, actions }))
37
+ + h(ActivityIndicator, { key: `activity:${sessionId}`, sessionId, useRoster, useSessionStatus, useSession, useLive, useWork, actions }),
38
+ + typeof actions.conversationAddon === 'function' ? h(actions.conversationAddon, { sessionId, bot, room, roster, actions, running }) : null)
39
+ }
40
+
41
+ // New-message pills. "N new messages ↓" takes Back to bottom's place when
@@ -1,6 +1,38 @@
1
1
  --- a/src/client/index.js
2
2
  +++ b/src/client/index.js
3
- @@ -1082,7 +1082,7 @@
3
+ @@ -199,7 +199,7 @@
4
+ for (const id of pendingAdopt.keys()) {
5
+ if (roster.getSnapshot().agentOf[id] !== undefined || listed[id] === undefined) pendingAdopt.delete(id)
6
+ }
7
+ - for (const id of [...value.bots.map(currentPart), ...value.rooms.map(room => room.sessionId ?? room.id)]) {
8
+ + 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))]) {
9
+ if (refreshed.has(id)) continue
10
+ refreshed.add(id)
11
+ void Promise.resolve(ctx.sessions.refreshProjections?.(id)).catch(quietly('refresh projections'))
12
+ @@ -407,6 +407,13 @@
13
+ // A Bot id or a group chat id, or any part of either's chat, opens the current part.
14
+ const partFor = (id) => {
15
+ const snapshot = roster.getSnapshot()
16
+ + // A thread Session, or a thread id, opens its own Session: it is not the
17
+ + // Bot's default chat, and opening it is never redirected there. The thread
18
+ + // is resolved before the Bot, because a thread Session also resolves to its
19
+ + // Bot in `byId` — every other Bot surface needs that, and this one line
20
+ + // keeps the two from being confused.
21
+ + const thread = snapshot.threadsById?.[id]
22
+ + if (thread !== undefined) return thread.sessionId ?? id
23
+ const bot = snapshot.byId[id]
24
+ if (bot !== undefined) return currentPart(bot)
25
+ const room = snapshot.roomsById[id]
26
+ @@ -437,7 +444,7 @@
27
+ const snapshot = roster.getSnapshot()
28
+ if (!snapshot.ready || !snapshot.mainBotId) return
29
+ const main = mainView()
30
+ - if (main !== undefined && snapshot.byId[main] === undefined && snapshot.roomsById[main] === undefined) {
31
+ + if (main !== undefined && snapshot.byId[main] === undefined && snapshot.roomsById[main] === undefined && snapshot.threadsById?.[main] === undefined) {
32
+ actions.openSession(snapshot.mainBotId)
33
+ }
34
+ }
35
+ @@ -1082,7 +1089,7 @@
4
36
  let landed = false
5
37
  let declared = true
6
38
  try {
@@ -0,0 +1,20 @@
1
+ --- a/src/client/learning-card.js
2
+ +++ b/src/client/learning-card.js
3
+ @@ -1,7 +1,7 @@
4
+ import { createElement as h, useId, useMemo, useRef, useState } from 'react'
5
+ import { useTurnFacts } from './btw.js'
6
+ import { editLines, lineDiff, proposalPlacement } from './learning-card-model.js'
7
+ -import { currentPart } from './text.js'
8
+ +import { isOwnChatOf } from './sources.js'
9
+ import { t } from './i18n.js'
10
+ import { CheckIcon, MemoryIcon, PencilIcon } from './icons.js'
11
+ import { spring, SPRING_TAP } from './motion.js'
12
+ @@ -106,7 +106,7 @@
13
+ const facts = useTurnFacts(sessionId, actions)
14
+ const [failed, setFailed] = useState({})
15
+ const bot = roster.byId?.[sessionId]
16
+ - if (!bot || currentPart(bot) !== sessionId) return null
17
+ + if (!bot || !isOwnChatOf(bot, sessionId)) return null
18
+ const proposals = roster.learning?.[bot.id] ?? []
19
+ const visible = [...proposals]
20
+ for (const entry of Object.values(failed)) {
@@ -0,0 +1,24 @@
1
+ --- a/src/client/model-menu.js
2
+ +++ b/src/client/model-menu.js
3
+ @@ -1,6 +1,6 @@
4
+ import { createElement as h, Fragment, useEffect, useLayoutEffect, useRef, useState, useSyncExternalStore } from 'react'
5
+ import { createPortal } from 'react-dom'
6
+ -import { currentPart } from './text.js'
7
+ +import { isOwnChatOf } from './sources.js'
8
+ import { CheckIcon, SlidersIcon } from './icons.js'
9
+ import { BotMark } from './mark.js'
10
+ import { KeyIcon, MaskedInput } from './secret-card.js'
11
+ @@ -202,9 +202,11 @@
12
+ const bot = roster.byId[sessionId]
13
+ const [busy, setBusy] = useState('')
14
+ const [error, setError] = useState('')
15
+ - const shown = Boolean(bot) && currentPart(bot) === sessionId && roster.ready
16
+ + // A thread is another own chat of the same Bot, so the Bot's model choice
17
+ + // belongs to it exactly as it belongs to the default chat.
18
+ + const shown = Boolean(bot) && isOwnChatOf(bot, sessionId) && roster.ready
19
+ useEffect(() => { if (shown) void actions.refreshModels?.() }, [shown, sessionId])
20
+ - if (!bot || currentPart(bot) !== sessionId || !roster.ready) return null
21
+ + if (!bot || !isOwnChatOf(bot, sessionId) || !roster.ready) return null
22
+ const models = roster.models ?? []
23
+ // A model whose provider has no API key fails every turn, so it is no choice.
24
+ const usable = models.filter(model => !model.needsKey)
@@ -0,0 +1,25 @@
1
+ --- a/src/client/secret-card.js
2
+ +++ b/src/client/secret-card.js
3
+ @@ -5,11 +5,11 @@
4
+ import { createElement as h, useEffect, useId, useRef, useState } from 'react'
5
+ import { CheckIcon, CloseIcon, TrashIcon } from './icons.js'
6
+ import { BotAvatar, BotMark } from './mark.js'
7
+ -import { contentText, currentPart } from './text.js'
8
+ +import { contentText } from './text.js'
9
+ import { dateLocale, t } from './i18n.js'
10
+ import { SPRING_POP, SPRING_SHAPE, spring } from './motion.js'
11
+ import { useDockHold } from './card-hold.js'
12
+ -import { heldCardKey } from './sources.js'
13
+ +import { heldCardKey, isOwnChatOf } from './sources.js'
14
+ import { CellEmpty } from './cell-empty.js'
15
+
16
+ const MIN_VALUE = 4
17
+ @@ -178,7 +178,7 @@
18
+ const shown = useCards?.(state => state[sessionId]) ?? NONE
19
+ const held = useHeldCards?.(state => state[sessionId]) ?? NONE
20
+ const bot = roster.byId[sessionId]
21
+ - if (!bot || currentPart(bot) !== sessionId) return null
22
+ + if (!bot || !isOwnChatOf(bot, sessionId)) return null
23
+ const drawn = entry => entry.callId && entry.sessionId === sessionId && shown[entry.callId] > 0 && !(held[heldCardKey('secret', entry.id)] > 0)
24
+ const waiting = (roster.secretRequests?.[bot.id] ?? []).filter(entry => entry.status === 'pending' && !drawn(entry))
25
+ if (waiting.length === 0) return null
@@ -0,0 +1,52 @@
1
+ --- a/src/client/sources.js
2
+ +++ b/src/client/sources.js
3
+ @@ -16,7 +16,7 @@
4
+ }
5
+ }
6
+
7
+ -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 }
8
+ +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 }
9
+ // Any Main Bot holds the Main Bot powers; `mainBotId`, the first one, is the sidebar tile.
10
+ export const isMainOf = (roster, id) => id !== undefined && (roster.mainBotIds ?? []).includes(id)
11
+
12
+ @@ -28,6 +28,21 @@
13
+ for (const bot of value.bots) {
14
+ for (const id of [...(bot.parts ?? []), bot.sessionId ?? bot.id, bot.id]) byId[id] = bot
15
+ }
16
+ + // A thread is another own chat of its Bot, and it is indexed twice on
17
+ + // purpose. `threadsById` holds the thread itself — its own Session, its name,
18
+ + // its archive — so opening one opens the thread and never the Bot's default
19
+ + // chat. Its Session also resolves to its Bot in `byId`, so every surface that
20
+ + // asks "whose chat is this?" (the header, the composer's extra menu, the
21
+ + // details drawer, and with it the model, memory, computer, voice and
22
+ + // attachment controls) keeps the Bot instead of going blank inside a thread.
23
+ + // A thread never takes a Bot's own chat identity: a Session the Bot already
24
+ + // owns stays the Bot's.
25
+ + const threadsById = {}
26
+ + for (const bot of value.bots) for (const thread of bot.threads ?? []) {
27
+ + if (thread.sessionId !== undefined) threadsById[thread.sessionId] = { botId: bot.id, ...thread }
28
+ + if (thread.threadId !== undefined) threadsById[thread.threadId] = { botId: bot.id, ...thread }
29
+ + if (thread.sessionId !== undefined && byId[thread.sessionId] === undefined) byId[thread.sessionId] = bot
30
+ + }
31
+ const roomsById = {}
32
+ // A group's own Session is a chain of parts like a Bot's chat: `room.id` is its
33
+ // first part's id and `sessionId` its current one; every part id finds the room.
34
+ @@ -41,9 +56,17 @@
35
+ agentsById[agent.id] = agent
36
+ for (const sessionId of agent.sessions ?? []) agentOf[sessionId] = agent.id
37
+ }
38
+ - return { ...value, agents: value.agents ?? [], byId, roomsById, agentsById, agentOf, ready: true }
39
+ + return { ...value, agents: value.agents ?? [], byId, roomsById, agentsById, agentOf, threadsById, ready: true }
40
+ }
41
+
42
+ +// Whether a Session is one of this Bot's own chats: its current part, or one of
43
+ +// its threads' Sessions. A thread is another own chat of the same Bot, so the
44
+ +// Bot-scoped surfaces (model choice, memory learning, secret requests, turn
45
+ +// errors) belong to it exactly as they belong to the default chat. Earlier
46
+ +// parts are deliberately not included: they keep the behaviour they had.
47
+ +export const isOwnChatOf = (bot, sessionId) => bot !== undefined && bot !== null && typeof sessionId === 'string'
48
+ + && ((bot.sessionId ?? bot.id) === sessionId || (bot.threads ?? []).some(thread => thread.sessionId === sessionId))
49
+ +
50
+ // Whether a main-view Session id belongs to the team's DSH Agent: the conversation
51
+ // group then stays withdrawn while the shell keeps the sidebar. Before the roster
52
+ // arrives, the ids cached from the last pull answer.
@@ -0,0 +1,23 @@
1
+ --- a/src/client/turn-error.js
2
+ +++ b/src/client/turn-error.js
3
+ @@ -1,9 +1,10 @@
4
+ import { createElement as h, useEffect, useMemo, useState, useSyncExternalStore } from 'react'
5
+ -import { currentPart, isHiddenTurn } from './text.js'
6
+ +import { isHiddenTurn } from './text.js'
7
+ import { CheckIcon, CopyIcon } from './icons.js'
8
+ import { ModelKeyCard, manageModels } from './model-menu.js'
9
+ import { knownFailure } from './diagnostics.js'
10
+ import { hostText, t } from './i18n.js'
11
+ +import { isOwnChatOf } from './sources.js'
12
+
13
+ // -------------------------------------------------------------------------
14
+ // A failed turn in a Bot's chat: what went wrong and what the user can do about it,
15
+ @@ -157,7 +158,7 @@
16
+ const provider = blocked?.provider ?? providerOf(bot, roster)
17
+ const text = failureText(data.code, { name: bot?.name ?? t('This Bot'), provider: providerName(roster, provider), status: blocked?.status })
18
+ const facts = failureFacts(data, blocked)
19
+ - const current = bot !== undefined && currentPart(bot) === sessionId && !later
20
+ + const current = bot !== undefined && isOwnChatOf(bot, sessionId) && !later
21
+ // The key route never takes the account's credential, so a sign-in alone leaves the Bot
22
+ // failing; it moves onto the account route. With no account model listed yet, the
23
+ // user picks one.
@@ -0,0 +1,46 @@
1
+ --- a/src/host/channel.js
2
+ +++ b/src/host/channel.js
3
+ @@ -43,8 +43,16 @@
4
+ // `sessionId` is the Bot's current own-chat part; `parts` its earlier ones.
5
+ // `waiting`: the Bot holds its greeting until the user picks its model or fixes
6
+ // what failed it. `blocked`: the facts of a failure only the user can fix (turns.js).
7
+ - bots: Object.values(state.bots).map(({ segments, kickoff, ...bot }) => ({
8
+ - ...bot, sessionId: rt.chatOf(bot.id), parts: (segments ?? []).map(segment => segment.sessionId), ...(kickoff ? { waiting: true } : {}),
9
+ + bots: Object.values(state.bots).map(({ segments, threads, kickoff, ...bot }) => ({
10
+ + ...bot, sessionId: rt.chatOf(bot.id), parts: (segments ?? []).map(segment => segment.sessionId),
11
+ + // `sessionId` stays the default chat; `threads` are the Bot's other own
12
+ + // chats, each with its own Session. An archived thread keeps its id and
13
+ + // its history and opens read-only.
14
+ + threads: (threads ?? []).map(thread => ({ threadId: thread.threadId, sessionId: thread.sessionId,
15
+ + ...(thread.name === undefined ? {} : { name: thread.name }),
16
+ + ...(thread.createdAt === undefined ? {} : { createdAt: thread.createdAt }),
17
+ + ...(thread.archivedAt === undefined || thread.archivedAt === null ? {} : { archivedAt: thread.archivedAt }) })),
18
+ + ...(kickoff ? { waiting: true } : {}),
19
+ })),
20
+ // `sessionId` is the group's own Session's current part, `ownParts` its earlier
21
+ // ones; `parts` the members' hidden group Sessions and their earlier segments,
22
+ @@ -435,6 +443,7 @@
23
+ await rt.load()
24
+ const past = rt.pastOwners.get(body.sessionId)
25
+ const sessionId = rt.botOf(body.sessionId) ? rt.chatOf(body.sessionId) : past && past.roomId === undefined ? rt.chatOf(past.botId) : body.sessionId
26
+ + if (past?.threadId !== undefined) return fail('This thread is archived; open it again to write in it', 'bot/archived')
27
+ const question = rt.own(state.questions, sessionId)
28
+ if (question === undefined || question.id !== body.questionId) return fail('This question is no longer open', 'bot/stale')
29
+ const reply = questionReply(question, body.answer)
30
+ @@ -458,6 +467,7 @@
31
+ await rt.load()
32
+ const past = rt.pastOwners.get(body.sessionId)
33
+ const sessionId = rt.botOf(body.sessionId) ? rt.chatOf(body.sessionId) : past && past.roomId === undefined ? rt.chatOf(past.botId) : body.sessionId
34
+ + if (past?.threadId !== undefined) return fail('This thread is archived; open it again to write in it', 'bot/archived')
35
+ const pending = rt.own(state.views, sessionId)
36
+ if (pending === undefined || pending.id !== body.viewId) return fail('This view is no longer open', 'bot/stale')
37
+ const checked = validateViewAnswer(pending.view, body.action, body.values)
38
+ @@ -485,6 +495,8 @@
39
+ // A Bot id, a group chat's id, or an earlier part of either means the
40
+ // current part.
41
+ const past = rt.pastOwners.get(body.sessionId)
42
+ + // An archived thread keeps its history but takes no new input.
43
+ + if (past?.threadId !== undefined) return fail('This thread is archived; open it again to write in it', 'bot/archived')
44
+ const room = rt.roomOf(body.sessionId)
45
+ const sessionId = rt.botOf(body.sessionId) ? rt.chatOf(body.sessionId)
46
+ : room !== undefined ? rt.sessionOfRoom(room)
@@ -0,0 +1,11 @@
1
+ --- a/src/host/memory.js
2
+ +++ b/src/host/memory.js
3
+ @@ -866,7 +866,7 @@
4
+ // A deleted Bot's memory moves to the archive, whole.
5
+ function archiveBotMemory(botId) {
6
+ const bot = rt.botOf(botId)
7
+ - for (const sessionId of [botId, rt.chatOf(botId), ...(bot?.segments ?? []).map(segment => segment.sessionId), ...rt.groupSessionsOf(botId)]) renewMemory(sessionId)
8
+ + for (const sessionId of rt.sessionsOfBot(botId)) renewMemory(sessionId)
9
+ summaries.delete(botId)
10
+ news.delete(botId)
11
+ return queued(async () => {
@@ -0,0 +1,16 @@
1
+ --- a/src/host/models.js
2
+ +++ b/src/host/models.js
3
+ @@ -244,6 +244,13 @@
4
+ await selectSessionModel(sessionId, route)
5
+ .catch(error => rt.warn('ds-bot: model for %s in a group not applied: %s', bot.name, error))
6
+ }
7
+ + // A thread follows its Bot's model, as a group Session does.
8
+ + for (const sessionId of rt.threadsOf(bot.id).map(thread => thread.sessionId)) {
9
+ + const applied = sessionSelections.get(sessionId)
10
+ + if (!force && applied !== undefined && sameSelection(applied, route)) continue
11
+ + await selectSessionModel(sessionId, route)
12
+ + .catch(error => rt.warn('ds-bot: model for a thread of %s not applied: %s', bot.name, error))
13
+ + }
14
+ if (!ownChanged) return ref
15
+ bot.appliedModel = ref
16
+ if (effort === undefined) delete bot.appliedEffort
@@ -0,0 +1,95 @@
1
+ --- a/src/host/parts.js
2
+ +++ b/src/host/parts.js
3
+ @@ -446,6 +446,11 @@
4
+ const session = agent.session
5
+ const header = session.requestHeader()
6
+ const route = header?.config
7
+ if (!route?.provider || !route?.model) throw new Error('the Session has not called a model yet')
8
+ + // The Host's prepared auxiliary stream binds one adapter generation for this note and
9
+ + // admits the money against that same generation. A Host that has not wired it refuses
10
+ + // the note here, rather than re-resolving the route at dispatch the way a bare stream
11
+ + // does.
12
+ + const auxiliary = ctx.get('botAuxiliaryModelStream')
13
+ + if (typeof auxiliary !== 'function') throw new Error('the Host has no prepared auxiliary model stream')
14
+ const messages = session.deriveMessages()
15
+ const base = folder ? FOLDER_PROMPT : HANDOFF_PROMPT
16
+ let retry
17
+ @@ -453,7 +458,7 @@
18
+ for (let attempt = 1; attempt <= NOTE_ATTEMPTS; attempt += 1) {
19
+ const prompt = retry === undefined ? base : `${base}\n\n${retry}`
20
+ const assembler = new BlockAssembler()
21
+ - for await (const chunk of ctx.llm.stream({
22
+ + for await (const chunk of auxiliary.stream({
23
+ provider: route.provider,
24
+ model: route.model,
25
+ ...(route.reasoningEffort ? { reasoningEffort: route.reasoningEffort } : {}),
26
+ @@ -515,6 +520,12 @@
27
+ async function writeCheckpoint(input, agent, signal) {
28
+ const session = agent.session
29
+ const route = session.requestHeader()?.config
30
+ if (!route?.provider || !route?.model) throw new Error('the Session has not called a model yet')
31
+ + // The Host's prepared auxiliary stream binds one adapter generation for this checkpoint,
32
+ + // so the summary is written by the generation the accounting admitted. A Host that has
33
+ + // not wired it refuses the checkpoint instead of pricing one generation and streaming
34
+ + // another.
35
+ + const auxiliary = ctx.get('botAuxiliaryModelStream')
36
+ + if (typeof auxiliary !== 'function') throw new Error('the Host has no prepared auxiliary model stream')
37
+ // The results list grows with the work, so the checkpoint may write as much as one
38
+ // of the Bot's own replies.
39
+ const maxTokens = route.maxTokens
40
+ @@ -522,7 +531,7 @@
41
+ const tail = checkpointTail(head, session.deriveMessages())
42
+ const prompt = tail.length === 0 ? CHECKPOINT_PROMPT : `${CHECKPOINT_PROMPT}\n\n${checkpointTailNote(tail)}`
43
+ const assembler = new BlockAssembler()
44
+ - for await (const chunk of ctx.llm.stream({
45
+ + for await (const chunk of auxiliary.stream({
46
+ provider: route.provider,
47
+ model: route.model,
48
+ ...(route.reasoningEffort ? { reasoningEffort: route.reasoningEffort } : {}),
49
+ @@ -571,18 +582,19 @@
50
+ const lines = []
51
+ for await (const line of partLines(agent.id, AbortSignal.timeout(HANDOFF_READ_MS), { group })) {
52
+ lines.push({ who: line.who, text: line.who === 'you' || line.who === 'user' ? line.text.replace(/^(?:You|User): /, '') : line.text })
53
+ if (lines.length >= STEWARD_LINES) break
54
+ }
55
+ - const info = await ctx.llm.resolveModelInfo(route.provider, route.model).catch(() => undefined)
56
+ - // The decision needs no long thinking.
57
+ - const off = info?.reasoning?.efforts?.some(effort => effort.id === 'off')
58
+ + // The decision needs no long thinking. Whether this model can be asked to think less
59
+ + // is decided from the generation this very call prepares, not from a read of whatever
60
+ + // registration happens to be current now.
61
+ + const auxiliary = ctx.get('botAuxiliaryModelStream')
62
+ + if (typeof auxiliary !== 'function') throw new Error('the Host has no prepared auxiliary model stream')
63
+ const assembler = new BlockAssembler()
64
+ - for await (const chunk of ctx.llm.stream({
65
+ + for await (const chunk of auxiliary.stream({
66
+ provider: route.provider,
67
+ model: route.model,
68
+ - ...(off ? { reasoningEffort: 'off' } : {}),
69
+ system: STEWARD_SYSTEM,
70
+ messages: rt.tagUsage([{ role: 'user', content: [{ type: 'text', text: stewardRequest({ lines, midTurn, checkpoints: checkpointCount(session.deriveMessages()), group }) }] }], { sessionId: agent.id, kind: 'compaction' }),
71
+ maxTokens: STEWARD_MAX_TOKENS,
72
+ sessionId: session.id,
73
+ purpose: 'compaction',
74
+ signal: AbortSignal.timeout(STEWARD_TIMEOUT_MS),
75
+ - })) assembler.push(chunk)
76
+ + }, { controls: auxiliary.reasoningOff })) assembler.push(chunk)
77
+ @@ -732,7 +743,10 @@
78
+
79
+ const placeOf = (sessionId) => {
80
+ const botId = rt.chatOwners.get(sessionId)
81
+ - if (botId !== undefined) return { botId }
82
+ + // A thread is not a part chain: it keeps its own Session for as long as it
83
+ + // lives, so the steward neither condenses it nor hands its input on to the
84
+ + // Bot's default chat.
85
+ + if (botId !== undefined) return rt.threadOf(sessionId) === undefined ? { botId } : undefined
86
+ const owner = rt.groupOwners.get(sessionId)
87
+ if (owner !== undefined) return { botId: owner.botId, roomId: owner.roomId }
88
+ @@ -1234,6 +1245,7 @@
89
+ // past it.
90
+ async function checkContext(agent) {
91
+ if (switches.has(agent.id) || freshParts.has(agent.id) || checking.has(agent.id) || readOnly()) return
92
+ + if (placeOf(agent.id) === undefined) return
93
+ if (lastTurnEnd(agent)?.data?.reason?.kind === 'error') return
94
+ const lines = await contextLines(agent.session)
95
+ const tokens = tokensOf(agent.session)
@@ -0,0 +1,11 @@
1
+ --- a/src/host/privacy.js
2
+ +++ b/src/host/privacy.js
3
+ @@ -30,7 +30,7 @@
4
+ const privateBots = () => Object.values(state.bots).filter(bot => bot.private === true)
5
+ // Deleting a private Bot does not make the files and memory it leaves behind public.
6
+ const protectedBots = () => [...new Map([...(state.privateArchives ?? []), ...privateBots()].map(bot => [bot.id, bot])).values()]
7
+ - const sessionsOf = bot => [...new Set([bot.id, ...(bot.sessionIds ?? [rt.chatOf(bot.id), ...(bot.segments ?? []).map(segment => segment.sessionId), ...rt.groupSessionsOf(bot.id)])])]
8
+ + const sessionsOf = bot => [...new Set([bot.id, ...(bot.sessionIds ?? [rt.sessionsOfBot(bot.id)])])]
9
+ const dataRoots = () => [rt.dshHome, rt.home, process.env.DS_BOT_USER_DSH_HOME?.trim()].filter(Boolean)
10
+
11
+ // A path as it is and as the file system resolves it, folded for comparing. realBeyond
@@ -0,0 +1,145 @@
1
+ --- a/src/host/roster.js
2
+ +++ b/src/host/roster.js
3
+ @@ -2,7 +2,7 @@
4
+ // the first Main Bot of a new team.
5
+ import { mkdir } from 'node:fs/promises'
6
+ import { MAX_AVATAR_IMAGE, ROOM_MAX_MEMBERS, ROOM_MODES, ROOM_NAME_MAX, ROOM_NOTICE_MAX } from './constants.js'
7
+ -import { currentColor, isColor, normalize, unquote } from './text.js'
8
+ +import { currentColor, isColor, normalize, splitRef, unquote } from './text.js'
9
+ import { nameProblem } from '../shared/mentions.js'
10
+
11
+ const BROWSER_GRANT_KINDS = new Set(['post', 'upload', 'download', 'delete', '*'])
12
+ @@ -420,6 +420,114 @@
13
+ return bot
14
+ }
15
+
16
+ + // Trusted product-only profile edit. No notice is delivered, no model is selected,
17
+ + // and no kickoff is released. The owner service supplies the final synchronous
18
+ + // committed-state check; the live base is compared at the same mutation boundary.
19
+ + async function updateBotProfile(id, patch, { base, check } = {}) {
20
+ + const fail = (code, message) => { throw Object.assign(new Error(message), { code }) }
21
+ + const fields = { name: 40, role: 24, instructions: 4000 }
22
+ + const plain = value => value && typeof value === 'object' && !Array.isArray(value)
23
+ + if (!plain(patch) || !Object.keys(patch).length || Object.keys(patch).some(key => !Object.hasOwn(fields, key))
24
+ + || !plain(base) || typeof check !== 'function') fail('BOT_PROFILE_INVALID', 'Invalid profile mutation')
25
+ + const bot = rt.botOf(id)
26
+ + if (!bot || state.bots[id] !== bot) fail('BOT_PROFILE_UNKNOWN_BOT', 'Unknown Bot')
27
+ + if (rt.readOnly() !== undefined) fail('PRODUCT_READ_ONLY', rt.readOnly())
28
+ + const next = {}
29
+ + for (const field of Object.keys(patch)) {
30
+ + if (typeof patch[field] !== 'string' || patch[field].trim().length > fields[field]) fail('BOT_PROFILE_INVALID', 'Invalid profile field')
31
+ + if (typeof base[field] !== 'string' || (bot[field] ?? '') !== base[field]) fail('BOT_PROFILE_CONFLICT', 'Bot profile changed before commit')
32
+ + next[field] = patch[field].trim()
33
+ + }
34
+ + if (next.name !== undefined) {
35
+ + const clash = rt.findBot(next.name)
36
+ + const pending = isHeld('bot', next.name) && normalize(next.name) !== normalize(bot.name)
37
+ + const problem = next.name === bot.name ? undefined : nameProblem(next.name)
38
+ + if (!next.name || (clash && clash.id !== id) || pending || problem !== undefined) fail('BOT_PROFILE_NAME_INVALID', 'Bot name is not available')
39
+ + }
40
+ + const checked = check(bot)
41
+ + if (checked && typeof checked.then === 'function') fail('BOT_PROFILE_INVALID', 'Profile commit check must be synchronous')
42
+ + // No await between the check and all three field assignments.
43
+ + const renamed = next.name !== undefined && next.name !== bot.name
44
+ + Object.assign(bot, next)
45
+ + const rooms = renamed ? Object.values(state.rooms).filter(room => !room.named && room.members.includes(id)) : []
46
+ + for (const room of rooms) room.name = autoRoomName(room.members)
47
+ + await rt.save()
48
+ + // Session titles are presentation. A failure cannot undo an already persisted
49
+ + // profile or turn the successful save into a request the UI might retry.
50
+ + if (renamed) {
51
+ + try {
52
+ + const agent = await rt.resolveAgent(rt.chatOf(id))
53
+ + const current = rt.botOf(id)
54
+ + if (current) await ctx.sessionTitle.rename(agent.session, current.name)
55
+ + } catch (error) { rt.warn('ds-bot: profile session title not updated: %s', error?.message ?? error) }
56
+ + for (const room of rooms) await retitleRoom(room)
57
+ + }
58
+ + return bot
59
+ + }
60
+ +
61
+ + // Trusted product-only thread records. A thread Session is made by the caller
62
+ + // through rt.createSession — the same factory every own chat uses — and
63
+ + // recorded here in one save, so the committed state the Host lifecycle
64
+ + // compiles and the directory the product keeps can never disagree about who
65
+ + // owns a Session. Nothing here delivers a notice, picks a model beyond the
66
+ + // Bot's own, or starts a turn.
67
+ + const THREAD_LIMIT = 64
68
+ + const THREAD_ID = /^[A-Za-z0-9_-]{1,128}$/
69
+ + const threadFail = (code, message) => { throw Object.assign(new Error(message), { code }) }
70
+ + async function addThreadSession(botId, threadId, sessionId, { name } = {}) {
71
+ + const readOnly = rt.readOnly()
72
+ + if (readOnly !== undefined) threadFail('PRODUCT_READ_ONLY', readOnly)
73
+ + const bot = rt.botOf(botId)
74
+ + if (!bot || state.bots[botId] !== bot) threadFail('BOT_THREAD_UNKNOWN_BOT', 'Unknown Bot')
75
+ + if (!THREAD_ID.test(threadId ?? '') || !THREAD_ID.test(sessionId ?? '')) threadFail('BOT_THREAD_INVALID', 'Invalid thread identity')
76
+ + const threads = bot.threads ?? []
77
+ + if (threads.length >= THREAD_LIMIT) threadFail('BOT_THREAD_LIMIT', 'Too many threads on one Bot')
78
+ + if (threads.some(thread => thread.threadId === threadId)) threadFail('BOT_THREAD_KNOWN', 'That thread already has a Session')
79
+ + for (const other of Object.values(state.bots)) {
80
+ + for (const thread of other.threads ?? []) if (thread.sessionId === sessionId) threadFail('BOT_THREAD_SESSION_TAKEN', 'That Session already belongs to a thread')
81
+ + }
82
+ + const record = { threadId, sessionId, createdAt: Date.now(), ...(typeof name === 'string' && name.trim() ? { name: name.trim().slice(0, 80) } : {}) }
83
+ + ;(bot.threads ??= []).push(record)
84
+ + await rt.save()
85
+ + // A thread runs on its Bot's model and permission preset, as a fresh part does.
86
+ + const ref = await rt.desiredModel(bot)
87
+ + if (ref !== undefined) {
88
+ + const effort = await rt.desiredEffort(bot, ref)
89
+ + await rt.selectSessionModel(sessionId, { ...splitRef(ref), ...(effort === undefined ? {} : { reasoningEffort: effort }) })
90
+ + .catch(error => rt.warn('ds-bot: model for a thread of %s not applied: %s', bot.name, error))
91
+ + }
92
+ + await rt.applyPermission(bot, sessionId)
93
+ + .catch(error => rt.warn('ds-bot: permission for a thread of %s not applied: %s', bot.name, error))
94
+ + return { ...record }
95
+ + }
96
+ +
97
+ + async function renameThread(botId, threadId, name) {
98
+ + const readOnly = rt.readOnly()
99
+ + if (readOnly !== undefined) threadFail('PRODUCT_READ_ONLY', readOnly)
100
+ + const bot = rt.botOf(botId)
101
+ + const thread = bot?.threads?.find(entry => entry.threadId === threadId)
102
+ + if (!bot || !thread) threadFail('BOT_THREAD_UNKNOWN', 'Unknown thread')
103
+ + if (typeof name !== 'string' || name.trim().length > 80) threadFail('BOT_THREAD_INVALID', 'Invalid thread name')
104
+ + thread.name = name.trim()
105
+ + await rt.save()
106
+ + return { ...thread }
107
+ + }
108
+ +
109
+ + // Archive hides a thread and drops its authority in the same save: the
110
+ + // Session and its history stay, and only an explicit restore brings it back.
111
+ + async function setThreadArchived(botId, threadId, archivedAt) {
112
+ + const readOnly = rt.readOnly()
113
+ + if (readOnly !== undefined) threadFail('PRODUCT_READ_ONLY', readOnly)
114
+ + const bot = rt.botOf(botId)
115
+ + const thread = bot?.threads?.find(entry => entry.threadId === threadId)
116
+ + if (!bot || !thread) threadFail('BOT_THREAD_UNKNOWN', 'Unknown thread')
117
+ + if (archivedAt === undefined || archivedAt === null) delete thread.archivedAt
118
+ + else if (Number.isFinite(archivedAt) && archivedAt >= 0) thread.archivedAt = archivedAt
119
+ + else threadFail('BOT_THREAD_INVALID', 'Invalid archive stamp')
120
+ + await rt.save()
121
+ + return { ...thread }
122
+ + }
123
+ +
124
+ async function setFlags(id, { pinned, hidden }) {
125
+ const entry = rt.botOf(id) ?? rt.roomOf(id) ?? rt.own(state.agents, id)
126
+ if (entry === undefined) throw new Error('Unknown conversation')
127
+ @@ -694,7 +802,7 @@
128
+ const archived = rt.archiveBotMemory(id)
129
+ const current = rt.chatOf(id)
130
+ // Questions and exchange turns are kept per Session, so every part of the chat is cleared.
131
+ - const parts = new Set([id, current, ...(bot.segments ?? []).map(segment => segment.sessionId)])
132
+ + const parts = new Set([id, current, ...(bot.segments ?? []).map(segment => segment.sessionId), ...(bot.threads ?? []).map(thread => thread.sessionId)])
133
+ // `archiving` stands until the archive's path is known: the memory may already be
134
+ // there, and no region of its own closes it yet.
135
+ const closedArchive = bot.private === true ? {
136
+ @@ -769,7 +877,8 @@
137
+ }
138
+
139
+ Object.assign(rt, {
140
+ - ensureWorkspace, keepTeamFolder, createSession, createBot, duplicateBot, updateBot, setFlags,
141
+ + ensureWorkspace, keepTeamFolder, createSession, createBot, duplicateBot, updateBot, updateBotProfile,
142
+ + addThreadSession, renameThread, setThreadArchived, setFlags,
143
+ roomBusy, autoRoomName, retitleRoom, createRoom, updateRoom, deleteRoom,
144
+ setMain, deleteBot, bootstrap, releaseKickoff, greetingOut, holdKickoffAgain, userWrote, userAnswered,
145
+ })