@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,322 @@
1
+ /**
2
+ * Unsent composer drafts: the text a person typed and did not send.
3
+ *
4
+ * What this is and nothing more. No attachment bytes, no OS paths, no Blob URLs,
5
+ * no credentials, and no send: restoring a draft only fills the Host composer's
6
+ * input, it never submits, starts a turn, calls a model or answers an approval.
7
+ *
8
+ * Why a store of its own: the Host composer keeps its draft only while its
9
+ * session stays mounted, so switching conversations or reloading the window loses
10
+ * what was typed. The client surface the product injects (sessions,
11
+ * uiConversation, slots, theme, layout) exposes no draft state of its own, so the
12
+ * product keeps the text here instead of reading the composer's DOM — a hashed
13
+ * class selector or a global contenteditable query would be a guess about the
14
+ * Host's markup, and this store only uses identities the Host already publishes.
15
+ *
16
+ * Host scope: several Hosts can share one origin (dsh-app://app), so a plain
17
+ * origin store would let one Host read another's drafts. Every blob carries the
18
+ * Host namespace that wrote it, and a store refuses to read, write or adopt a
19
+ * blob from a different namespace or from an older replay of its own. A session
20
+ * is only reachable once the Host surface has put it in the saved directory, and
21
+ * the identity always comes from the mounted conversation's own roster row, never
22
+ * from an argument a model could supply.
23
+ *
24
+ * The namespace itself is the Host's: it arrives from the Host's own
25
+ * `ui.namespace` read (see composer-draft-host.jsx). `resolveHostId` below is a
26
+ * fixture helper for tests and the offline browser preview; the production
27
+ * wiring never imports it.
28
+ */
29
+
30
+ export const COMPOSER_DRAFT_KEY = 'bot-product:composer-drafts'
31
+ export const COMPOSER_DRAFT_HOST_KEY = 'bot-product:composer-draft-host'
32
+
33
+ /**
34
+ * One Host's blob lives under its own key. Two Hosts in one origin then cannot
35
+ * overwrite each other, whatever their epochs say; the namespace inside the blob
36
+ * is the second lock on the same door.
37
+ */
38
+ export const composerDraftKey = hostId => `${COMPOSER_DRAFT_KEY}:${hostId}`
39
+ /** Per-conversation text and per-Host totals. A refusal names its reason. */
40
+ export const COMPOSER_DRAFT_LIMITS = Object.freeze({
41
+ maxSessions: 48,
42
+ maxTextChars: 20000,
43
+ maxTextBytes: 65536,
44
+ maxTotalBytes: 262144,
45
+ maxDirectory: 512,
46
+ saveDelayMs: 500,
47
+ })
48
+
49
+ const KINDS = new Set(['bot', 'room'])
50
+ const HOST_PATTERN = /^host-[0-9a-f]{16}$/
51
+ /** The threadId shape the Host's own thread directory accepts. */
52
+ const THREAD_PATTERN = /^[A-Za-z0-9_-]{1,128}$/
53
+ const encoder = typeof TextEncoder === 'function' ? new TextEncoder() : undefined
54
+
55
+ /** UTF-8 length, so a Chinese draft is budgeted by what it costs to store. */
56
+ export const textBytes = text => (encoder ? encoder.encode(text).length : String(text).length)
57
+
58
+ /**
59
+ * The storage identity of one conversation: a Bot or a group, never a part. A
60
+ * Bot's chat spans numbered parts, and an unsent line survives a part rotation
61
+ * because it belongs to the conversation, not to the session that was open.
62
+ *
63
+ * A thread is a separate conversation under the same Bot: each `threadId` gets
64
+ * its own draft, so two threads of one Bot never share text. The threadId comes
65
+ * from the Host's own thread directory mapped through the trusted roster, never
66
+ * from a model argument.
67
+ */
68
+ export function conversationKey({ kind, ownerId, threadId } = {}) {
69
+ if (!KINDS.has(kind)) return ''
70
+ const owner = typeof ownerId === 'string' ? ownerId.trim() : ''
71
+ if (!owner || /[\s:/]/.test(owner)) return ''
72
+ const thread = typeof threadId === 'string' ? threadId.trim() : ''
73
+ if (!thread) return `${kind}:${owner}`
74
+ if (!THREAD_PATTERN.test(thread)) return ''
75
+ return `${kind}:${owner}:thread:${thread}`
76
+ }
77
+
78
+ /** The key for a mounted session, read from the Host roster it came with. */
79
+ export function ownerKeyFromRoster(sessionId, roster) {
80
+ const id = typeof sessionId === 'string' ? sessionId : ''
81
+ if (!id || !roster) return ''
82
+ const thread = roster.threadsById?.[id]
83
+ if (thread) return conversationKey({ kind: 'bot', ownerId: thread.botId, threadId: thread.threadId })
84
+ const bot = roster.byId?.[id]
85
+ if (bot) return conversationKey({ kind: 'bot', ownerId: bot.id ?? id })
86
+ const room = roster.roomsById?.[id]
87
+ if (room) return conversationKey({ kind: 'room', ownerId: room.id ?? id })
88
+ return ''
89
+ }
90
+
91
+ /**
92
+ * The key for a mounted session inside one thread of a Bot. `threadOf` maps the
93
+ * mounted session to the stable threadId the Host directory gave it; a session
94
+ * the mapping does not name is the Bot's default conversation, which every part
95
+ * of that Bot shares.
96
+ */
97
+ export function threadKeyFromRoster(sessionId, roster, threadOf) {
98
+ const id = typeof sessionId === 'string' ? sessionId : ''
99
+ if (!id || !roster) return ''
100
+ const thread = roster.threadsById?.[id]
101
+ if (thread) return conversationKey({ kind: 'bot', ownerId: thread.botId, threadId: thread.threadId })
102
+ const bot = roster.byId?.[id]
103
+ if (bot) return conversationKey({ kind: 'bot', ownerId: bot.id ?? id, threadId: threadOf?.(id) })
104
+ const room = roster.roomsById?.[id]
105
+ if (room) return conversationKey({ kind: 'room', ownerId: room.id ?? id })
106
+ return ''
107
+ }
108
+
109
+ const mintHostId = random => {
110
+ const raw = typeof random === 'function' ? String(random() ?? '') : ''
111
+ const hex = raw.replace(/[^0-9a-f]/gi, '').slice(0, 16)
112
+ // A namespace that silently collides would hand one Host another's drafts.
113
+ if (hex.length !== 16) throw new Error('composer-draft: the Host namespace needs 16 hex characters')
114
+ return `host-${hex.toLowerCase()}`
115
+ }
116
+
117
+ /**
118
+ * The persisted Host namespace. Generated once and kept, so a reload reads the
119
+ * same drafts; a blocked storage keeps it for this window only.
120
+ *
121
+ * FIXTURE HELPER. This is a browser-origin id, not a Host identity: two Hosts
122
+ * sharing the `dsh-app://app` origin would mint and read the same value, which
123
+ * is exactly the boundary the production path must not have. The production
124
+ * namespace comes from the Host's own `ui.namespace` read through the product
125
+ * API (composer-draft-host.jsx); nothing in the production wiring imports this.
126
+ */
127
+ export function resolveHostId({ storage, key = COMPOSER_DRAFT_HOST_KEY, random } = {}) {
128
+ const read = () => {
129
+ try { return storage?.getItem?.(key) ?? '' } catch { return '' }
130
+ }
131
+ const found = read()
132
+ if (HOST_PATTERN.test(found)) return found
133
+ const minted = mintHostId(random)
134
+ try { storage?.setItem?.(key, minted) } catch { /* a blocked store keeps this window's namespace */ }
135
+ return minted
136
+ }
137
+
138
+ const readBlob = (storage, key) => {
139
+ try {
140
+ const raw = storage?.getItem?.(key)
141
+ const parsed = raw ? JSON.parse(raw) : null
142
+ return parsed && typeof parsed === 'object' && !Array.isArray(parsed) ? parsed : null
143
+ } catch { return null }
144
+ }
145
+
146
+ const writeBlob = (storage, key, blob) => {
147
+ try { storage?.setItem?.(key, JSON.stringify(blob)); return true } catch { return false }
148
+ }
149
+
150
+ const cleanId = value => (typeof value === 'string' && value.length <= 200 ? value : '')
151
+
152
+ const bytesOf = sessions => Object.values(sessions).reduce((sum, row) => sum + (row?.bytes ?? 0), 0)
153
+
154
+ /**
155
+ * One Host's draft store. Every entry point refuses instead of guessing: an
156
+ * unknown conversation, a foreign or replayed blob, an over-budget text and a
157
+ * stale revision all return `{ ok: false, reason }` and change nothing.
158
+ */
159
+ export function createComposerDraftStore({ storage, hostId, limits = {}, now, schedule } = {}) {
160
+ const bound = { ...COMPOSER_DRAFT_LIMITS, ...limits }
161
+ const clock = typeof now === 'function' ? now : () => Date.now()
162
+ const later = typeof schedule === 'function' ? schedule : run => setTimeout(run, bound.saveDelayMs)
163
+ const namespace = typeof hostId === 'string' && HOST_PATTERN.test(hostId) ? hostId : ''
164
+ if (!namespace) throw new Error('composer-draft: a store needs a persisted Host namespace')
165
+ const blobKey = composerDraftKey(namespace)
166
+
167
+ let state = { epoch: 0, directory: {}, sessions: {} }
168
+ let written = 0
169
+ let loaded = false
170
+ let scheduled = false
171
+ let dirty = false
172
+
173
+ const refuse = reason => ({ ok: false, reason })
174
+
175
+ const load = () => {
176
+ if (loaded) return
177
+ loaded = true
178
+ const blob = readBlob(storage, blobKey)
179
+ if (!blob) return
180
+ // Another Host's blob, or an older replay of this one, is not adopted.
181
+ if (blob.hostId !== namespace) return
182
+ if (!Number.isInteger(blob.epoch) || blob.epoch < written) return
183
+ state = {
184
+ epoch: blob.epoch,
185
+ directory: blob.directory && typeof blob.directory === 'object' ? blob.directory : {},
186
+ sessions: blob.sessions && typeof blob.sessions === 'object' ? blob.sessions : {},
187
+ }
188
+ written = blob.epoch
189
+ }
190
+
191
+ const commit = () => {
192
+ if (!dirty) return
193
+ state.epoch += 1
194
+ if (writeBlob(storage, blobKey, { version: 1, hostId: namespace, epoch: state.epoch, directory: state.directory, sessions: state.sessions })) written = state.epoch
195
+ dirty = false
196
+ }
197
+
198
+ const soon = () => {
199
+ dirty = true
200
+ if (scheduled) return
201
+ scheduled = true
202
+ later(() => { scheduled = false; load(); commit() })
203
+ }
204
+
205
+ /** Drops drafts the Host no longer serves, then the oldest over budget. */
206
+ const prune = keep => {
207
+ if (keep !== undefined) {
208
+ const live = keep instanceof Set ? keep : new Set(keep ?? [])
209
+ for (const key of Object.keys(state.directory)) if (!live.has(key)) delete state.directory[key]
210
+ for (const key of Object.keys(state.sessions)) if (!live.has(key)) delete state.sessions[key]
211
+ }
212
+ const order = Object.entries(state.sessions).sort((left, right) => (left[1]?.updatedAt ?? 0) - (right[1]?.updatedAt ?? 0))
213
+ while (order.length > bound.maxSessions) {
214
+ const [key] = order.shift()
215
+ delete state.sessions[key]
216
+ }
217
+ for (const [key, row] of order) {
218
+ if (bytesOf(state.sessions) <= bound.maxTotalBytes) break
219
+ delete state.sessions[key]
220
+ }
221
+ const seen = Object.entries(state.directory).sort((left, right) => (left[1]?.seenAt ?? 0) - (right[1]?.seenAt ?? 0))
222
+ while (seen.length > bound.maxDirectory) delete state.directory[seen.shift()[0]]
223
+ }
224
+
225
+ /**
226
+ * The key of a conversation the saved Host directory knows, or null. A
227
+ * conversation the Host is not serving has no draft to read or write.
228
+ */
229
+ const served = key => {
230
+ load()
231
+ if (!key || !state.directory[key]) return null
232
+ return key
233
+ }
234
+
235
+ return {
236
+ hostId: namespace,
237
+ limits: bound,
238
+
239
+ /** Records that the Host surface is serving this conversation right now. */
240
+ see({ kind, ownerId, threadId, sessionId } = {}) {
241
+ const key = conversationKey({ kind, ownerId, threadId })
242
+ if (!key) return false
243
+ load()
244
+ const previous = state.directory[key] ?? {}
245
+ state.directory[key] = { sessionId: cleanId(sessionId) || previous.sessionId || '', seenAt: clock() }
246
+ prune()
247
+ soon()
248
+ return true
249
+ },
250
+
251
+ /** Keeps only the conversations the Host still lists. */
252
+ reconcile(keys) {
253
+ load()
254
+ prune(keys)
255
+ soon()
256
+ return Object.keys(state.sessions).length
257
+ },
258
+
259
+ /** The saved draft, or null. Never a draft from another Host. */
260
+ read({ kind, ownerId, threadId } = {}) {
261
+ const key = served(conversationKey({ kind, ownerId, threadId }))
262
+ if (key === null) return null
263
+ const row = state.sessions[key]
264
+ return row ? { ...row, key } : null
265
+ },
266
+
267
+ /** Stores one draft. A stale revision is dropped, never written over. */
268
+ save({ kind, ownerId, threadId, sessionId, text, revision, pendingAt, pendingRequestId } = {}) {
269
+ const key = served(conversationKey({ kind, ownerId, threadId }))
270
+ if (key === null) return refuse('unknown-conversation')
271
+ const body = typeof text === 'string' ? text : ''
272
+ if (!body) return refuse('empty-draft')
273
+ if (body.length > bound.maxTextChars) return refuse('too-long')
274
+ const bytes = textBytes(body)
275
+ if (bytes > bound.maxTextBytes) return refuse('too-large')
276
+ const previous = state.sessions[key]
277
+ if (!Number.isInteger(revision) || revision <= (previous?.revision ?? 0)) return refuse('stale-revision')
278
+ const others = bytesOf(state.sessions) - (previous?.bytes ?? 0)
279
+ if (others + bytes > bound.maxTotalBytes) return refuse('over-budget')
280
+ state.sessions[key] = {
281
+ text: body, bytes, revision, sessionId: cleanId(sessionId), updatedAt: clock(),
282
+ // A send that took this text and has not been answered yet, so the next
283
+ // mount asks the witness before showing it back.
284
+ pendingAt: Number.isFinite(pendingAt) && pendingAt > 0 ? Math.floor(pendingAt) : 0,
285
+ pendingRequestId: typeof pendingRequestId === 'string' && pendingRequestId.length <= 256 ? pendingRequestId : '',
286
+ }
287
+ prune()
288
+ soon()
289
+ return { ok: true }
290
+ },
291
+
292
+ /** Drops one draft. Only a revision at least as new as the saved one may. */
293
+ clear({ kind, ownerId, threadId, revision } = {}) {
294
+ const key = served(conversationKey({ kind, ownerId, threadId }))
295
+ if (key === null) return refuse('unknown-conversation')
296
+ const previous = state.sessions[key]
297
+ if (!previous) return { ok: true }
298
+ if (!Number.isInteger(revision) || revision <= previous.revision) return refuse('stale-revision')
299
+ delete state.sessions[key]
300
+ soon()
301
+ return { ok: true }
302
+ },
303
+
304
+ /** Writes the pending change now, before a window goes away. */
305
+ flush() {
306
+ scheduled = false
307
+ load()
308
+ commit()
309
+ return { ok: true }
310
+ },
311
+
312
+ snapshot() {
313
+ load()
314
+ return {
315
+ hostId: namespace,
316
+ epoch: state.epoch,
317
+ sessions: Object.fromEntries(Object.entries(state.sessions).map(([key, row]) => [key, { ...row }])),
318
+ directory: Object.fromEntries(Object.entries(state.directory).map(([key, row]) => [key, { ...row }])),
319
+ }
320
+ },
321
+ }
322
+ }
@@ -0,0 +1,54 @@
1
+ import React, { useEffect, useState } from 'react'
2
+ import { ContactNavigation } from './chat-ui.jsx'
3
+ import { taskProgress } from './task-progress.mjs'
4
+
5
+ /** The contact footer reads the same committed task projection as the inbox. */
6
+ export function contactAttention(snapshot) {
7
+ if (snapshot?.schemaVersion !== 1 || snapshot.tasks?.status !== 'available') return { known: false, count: 0, partial: false }
8
+ return { known: true, count: taskProgress(snapshot).attention.length, partial: snapshot.tasks.truncated === true }
9
+ }
10
+
11
+ export function notificationAttention(snapshot) {
12
+ const value = snapshot?.schemaVersion === 1 ? snapshot.notifications : null
13
+ if (value?.status !== 'available' || !Number.isSafeInteger(value.unreadCount) || value.unreadCount < 0) return { known: false, count: 0, partial: false }
14
+ return { known: true, count: value.unreadCount, partial: value.truncated === true }
15
+ }
16
+
17
+ /** Read-only polling stops while hidden and retires every result from a replaced Host. */
18
+ export function ContactAttentionNavigation({ productApi, ...props }) {
19
+ const [state, setState] = useState(null)
20
+ useEffect(() => {
21
+ if (typeof productApi?.attention !== 'function') return undefined
22
+ let live = true, epoch = 0, timer, request
23
+ const publish = value => { if (live) setState({ scope: productApi, ...value }) }
24
+ const pause = () => { epoch++; clearTimeout(timer); request?.abort(); request = undefined; publish({ value: null }) }
25
+ const load = async () => {
26
+ clearTimeout(timer)
27
+ if (!live || document.hidden) return
28
+ const current = ++epoch, controller = new AbortController()
29
+ request?.abort(); request = controller
30
+ const deadline = setTimeout(() => { controller.abort(); if (live && current === epoch) publish({ value: null }) }, 8000)
31
+ const valid = () => live && current === epoch && !controller.signal.aborted
32
+ let failed = false
33
+ try {
34
+ const value = await productApi.attention(controller.signal)
35
+ if (valid()) publish({ value })
36
+ } catch {
37
+ failed = true
38
+ if (live && current === epoch) publish({ value: null })
39
+ } finally {
40
+ clearTimeout(deadline)
41
+ if (request === controller) request = undefined
42
+ if (live && current === epoch && !document.hidden) timer = setTimeout(load, failed ? 15000 : 5000)
43
+ }
44
+ }
45
+ const visible = () => { if (document.hidden) pause(); else void load() }
46
+ document.addEventListener('visibilitychange', visible)
47
+ void load()
48
+ return () => { live = false; epoch++; clearTimeout(timer); request?.abort(); document.removeEventListener('visibilitychange', visible) }
49
+ }, [productApi])
50
+ const attention = contactAttention(state?.scope === productApi ? state.value : null)
51
+ const notifications = notificationAttention(state?.scope === productApi ? state.value : null)
52
+ return <ContactNavigation {...props} attentionCount={attention.count} attentionPartial={attention.partial} attentionKnown={attention.known}
53
+ notificationCount={notifications.count} notificationPartial={notifications.partial} notificationKnown={notifications.known}/>
54
+ }