@ljwei-stak/dsh-model-route-bot 0.66.3 → 0.67.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (272) hide show
  1. package/.dsh-plugin/bot-product.mjs +81 -5
  2. package/.dsh-plugin/client.js +17490 -1791
  3. package/README.i18n.yaml +4 -4
  4. package/README.md +409 -364
  5. package/README.zh.md +409 -364
  6. package/apps/integration-client/README.md +31 -7
  7. package/apps/integration-client/api.mjs +14 -3
  8. package/apps/integration-client/binding-editor.jsx +308 -109
  9. package/apps/integration-client/binding-model.mjs +262 -0
  10. package/apps/integration-client/binding.css +144 -0
  11. package/apps/integration-client/bot-model-status.mjs +18 -0
  12. package/apps/integration-client/bot-profile-identity.jsx +197 -0
  13. package/apps/integration-client/bot-profile.css +57 -0
  14. package/apps/integration-client/bot-profile.mjs +97 -0
  15. package/apps/integration-client/bot-settings-dialog.css +18 -0
  16. package/apps/integration-client/bot-settings-dialog.jsx +111 -0
  17. package/apps/integration-client/bot-thread-header-bridge.css +20 -0
  18. package/apps/integration-client/bot-thread-header.jsx +278 -0
  19. package/apps/integration-client/bot-threads-panel.jsx +378 -0
  20. package/apps/integration-client/bot-threads.css +169 -0
  21. package/apps/integration-client/chat-design.mjs +250 -0
  22. package/apps/integration-client/chat-ui.jsx +175 -0
  23. package/apps/integration-client/chat.css +477 -0
  24. package/apps/integration-client/composer-draft-binding.mjs +50 -0
  25. package/apps/integration-client/composer-draft-bound.jsx +14 -0
  26. package/apps/integration-client/composer-draft-host.jsx +142 -0
  27. package/apps/integration-client/composer-draft-session.jsx +287 -0
  28. package/apps/integration-client/composer-draft-witness.mjs +115 -0
  29. package/apps/integration-client/composer-draft.css +4 -0
  30. package/apps/integration-client/composer-draft.mjs +322 -0
  31. package/apps/integration-client/contact-attention.jsx +54 -0
  32. package/apps/integration-client/data-panel.jsx +439 -0
  33. package/apps/integration-client/data.css +69 -0
  34. package/apps/integration-client/desktop-bridge.mjs +1 -0
  35. package/apps/integration-client/desktop-http-socket.mjs +49 -0
  36. package/apps/integration-client/desktop-keyboard.mjs +32 -0
  37. package/apps/integration-client/desktop-pane.jsx +99 -30
  38. package/apps/integration-client/desktop-stream.mjs +13 -3
  39. package/apps/integration-client/desktop-tools.jsx +19 -0
  40. package/apps/integration-client/desktop-touch.mjs +27 -0
  41. package/apps/integration-client/desktop.css +41 -0
  42. package/apps/integration-client/independent-chat-controller.mjs +224 -0
  43. package/apps/integration-client/independent-chat-entry.jsx +61 -0
  44. package/apps/integration-client/independent-chat-navigation.mjs +21 -0
  45. package/apps/integration-client/independent-chat-page.jsx +148 -0
  46. package/apps/integration-client/independent-chat-rich.jsx +64 -0
  47. package/apps/integration-client/independent-chat-rich.mjs +30 -0
  48. package/apps/integration-client/independent-chat.css +73 -0
  49. package/apps/integration-client/index.jsx +24 -16
  50. package/apps/integration-client/library-panel.jsx +4 -1
  51. package/apps/integration-client/licenses/jsqr-1.4.0-LICENSE.txt +202 -0
  52. package/apps/integration-client/main-route-editor.jsx +94 -0
  53. package/apps/integration-client/main-route-model.mjs +100 -0
  54. package/apps/integration-client/model-picker.css +37 -0
  55. package/apps/integration-client/model-picker.jsx +83 -0
  56. package/apps/integration-client/native-activity.css +141 -0
  57. package/apps/integration-client/native-activity.jsx +276 -0
  58. package/apps/integration-client/native-activity.mjs +384 -0
  59. package/apps/integration-client/notifications-panel.jsx +186 -0
  60. package/apps/integration-client/notifications.css +126 -0
  61. package/apps/integration-client/pairing-qr.jsx +79 -0
  62. package/apps/integration-client/panels.jsx +126 -115
  63. package/apps/integration-client/product-slots.jsx +46 -0
  64. package/apps/integration-client/routine-recorder.mjs +23 -2
  65. package/apps/integration-client/secure-pairing.jsx +84 -0
  66. package/apps/integration-client/tasks-panel.jsx +32 -10
  67. package/apps/integration-client/ui-icons.jsx +17 -0
  68. package/apps/integration-client/usage-explorer.jsx +43 -0
  69. package/apps/integration-client/usage-report.mjs +71 -0
  70. package/apps/integration-client/usage.css +34 -0
  71. package/apps/integration-client/web-panel-model.mjs +140 -0
  72. package/apps/integration-client/web-panel-page.jsx +221 -0
  73. package/apps/integration-client/web-panel-pins-live.jsx +28 -0
  74. package/apps/integration-client/web-panel.css +139 -0
  75. package/apps/integration-client/workspace-model.mjs +67 -0
  76. package/apps/integration-client/workspace-navigation.mjs +56 -0
  77. package/apps/integration-client/workspace-ui.jsx +173 -0
  78. package/apps/integration-client/workspace.css +332 -0
  79. package/modules/bot-runtime/SOURCE.json +176 -50
  80. package/modules/bot-runtime/index.mjs +169 -13
  81. package/modules/bot-runtime/product-overlay/OVERLAY.json +128 -2
  82. package/modules/bot-runtime/product-overlay/base/src/client/header.js +244 -0
  83. package/modules/bot-runtime/product-overlay/base/src/client/learning-card.js +202 -0
  84. package/modules/bot-runtime/product-overlay/base/src/client/model-menu.js +371 -0
  85. package/modules/bot-runtime/product-overlay/base/src/client/secret-card.js +363 -0
  86. package/modules/bot-runtime/product-overlay/base/src/client/sources.js +68 -0
  87. package/modules/bot-runtime/product-overlay/base/src/client/turn-error.js +260 -0
  88. package/modules/bot-runtime/product-overlay/base/src/host/channel.js +647 -0
  89. package/modules/bot-runtime/product-overlay/base/src/host/memory.js +2050 -0
  90. package/modules/bot-runtime/product-overlay/base/src/host/models.js +314 -0
  91. package/modules/bot-runtime/product-overlay/base/src/host/parts.js +1473 -0
  92. package/modules/bot-runtime/product-overlay/base/src/host/privacy.js +368 -0
  93. package/modules/bot-runtime/product-overlay/base/src/host/roster.js +776 -0
  94. package/modules/bot-runtime/product-overlay/base/src/host/store.js +512 -0
  95. package/modules/bot-runtime/product-overlay/base/src/host/usage.js +653 -0
  96. package/modules/bot-runtime/product-overlay/patches/src/client/details-drawer.js.patch +48 -2
  97. package/modules/bot-runtime/product-overlay/patches/src/client/header.js.patch +41 -0
  98. package/modules/bot-runtime/product-overlay/patches/src/client/index.js.patch +33 -1
  99. package/modules/bot-runtime/product-overlay/patches/src/client/learning-card.js.patch +20 -0
  100. package/modules/bot-runtime/product-overlay/patches/src/client/model-menu.js.patch +24 -0
  101. package/modules/bot-runtime/product-overlay/patches/src/client/secret-card.js.patch +25 -0
  102. package/modules/bot-runtime/product-overlay/patches/src/client/sources.js.patch +52 -0
  103. package/modules/bot-runtime/product-overlay/patches/src/client/turn-error.js.patch +23 -0
  104. package/modules/bot-runtime/product-overlay/patches/src/host/channel.js.patch +46 -0
  105. package/modules/bot-runtime/product-overlay/patches/src/host/memory.js.patch +11 -0
  106. package/modules/bot-runtime/product-overlay/patches/src/host/models.js.patch +16 -0
  107. package/modules/bot-runtime/product-overlay/patches/src/host/parts.js.patch +95 -0
  108. package/modules/bot-runtime/product-overlay/patches/src/host/privacy.js.patch +11 -0
  109. package/modules/bot-runtime/product-overlay/patches/src/host/roster.js.patch +145 -0
  110. package/modules/bot-runtime/product-overlay/patches/src/host/store.js.patch +68 -0
  111. package/modules/bot-runtime/product-overlay/patches/src/host/usage.js.patch +11 -0
  112. package/modules/bot-runtime/read-model.mjs +19 -4
  113. package/modules/bot-runtime/vendor/ds-bot/src/client/details-drawer.js +15 -5
  114. package/modules/bot-runtime/vendor/ds-bot/src/client/header.js +9 -2
  115. package/modules/bot-runtime/vendor/ds-bot/src/client/index.js +9 -2
  116. package/modules/bot-runtime/vendor/ds-bot/src/client/learning-card.js +2 -2
  117. package/modules/bot-runtime/vendor/ds-bot/src/client/model-menu.js +5 -3
  118. package/modules/bot-runtime/vendor/ds-bot/src/client/secret-card.js +3 -3
  119. package/modules/bot-runtime/vendor/ds-bot/src/client/sources.js +25 -2
  120. package/modules/bot-runtime/vendor/ds-bot/src/client/turn-error.js +3 -2
  121. package/modules/bot-runtime/vendor/ds-bot/src/host/channel.js +14 -2
  122. package/modules/bot-runtime/vendor/ds-bot/src/host/memory.js +1 -1
  123. package/modules/bot-runtime/vendor/ds-bot/src/host/models.js +7 -0
  124. package/modules/bot-runtime/vendor/ds-bot/src/host/parts.js +26 -9
  125. package/modules/bot-runtime/vendor/ds-bot/src/host/privacy.js +1 -1
  126. package/modules/bot-runtime/vendor/ds-bot/src/host/roster.js +112 -3
  127. package/modules/bot-runtime/vendor/ds-bot/src/host/store.js +34 -1
  128. package/modules/bot-runtime/vendor/ds-bot/src/host/usage.js +1 -1
  129. package/modules/independent-runtime/index.mjs +152 -62
  130. package/modules/independent-runtime/remote-tools.mjs +8 -5
  131. package/modules/openmaus-protocol/NOTICE +10 -0
  132. package/modules/openmaus-protocol/README.md +15 -0
  133. package/modules/openmaus-protocol/SOURCE.json +68 -0
  134. package/modules/openmaus-protocol/dist/protocol.mjs +1203 -0
  135. package/modules/openmaus-protocol/index.mjs +1 -0
  136. package/modules/openmaus-protocol/transport.mjs +77 -0
  137. package/modules/openmaus-protocol/vendor/LICENSE +202 -0
  138. package/modules/openmaus-protocol/vendor/companion/src/e2e/README.md +405 -0
  139. package/modules/openmaus-protocol/vendor/companion/src/e2e/credentials.ts +128 -0
  140. package/modules/openmaus-protocol/vendor/companion/src/e2e/descriptor.ts +84 -0
  141. package/modules/openmaus-protocol/vendor/companion/src/e2e/noise.ts +456 -0
  142. package/modules/openmaus-protocol/vendor/companion/src/e2e/phone.ts +104 -0
  143. package/modules/openmaus-protocol/vendor/companion/src/e2e/session.ts +417 -0
  144. package/modules/openmaus-protocol/vendor/companion/src/e2e/vectors/v1.json +2565 -0
  145. package/modules/openmaus-protocol/vendor/companion/src/e2e/vectors/v1.sha256 +1 -0
  146. package/modules/openmaus-protocol/vendor/companion/src/e2e/yamux.ts +335 -0
  147. package/package.json +20 -6
  148. package/packages/bot-acp/README.md +156 -0
  149. package/packages/bot-acp/licenses/LICENSE-OpenMausBot +202 -0
  150. package/packages/bot-acp/licenses/NOTICE.md +27 -0
  151. package/packages/bot-acp/src/acp-adapter.mjs +795 -0
  152. package/packages/bot-acp/src/acp-host.mjs +148 -0
  153. package/packages/bot-acp/src/native-launch.mjs +121 -0
  154. package/packages/bot-acp/src/permission-policy.mjs +283 -0
  155. package/packages/bot-acp/src/rpc-codec.mjs +265 -0
  156. package/packages/bot-acp/src/session-core.mjs +581 -0
  157. package/packages/bot-acp/src/stdio-rpc.mjs +329 -0
  158. package/packages/bot-engines/README.md +281 -0
  159. package/packages/bot-engines/licenses/LICENSE-OpenMausBot +202 -0
  160. package/packages/bot-engines/licenses/NOTICE.md +32 -0
  161. package/packages/bot-engines/src/claude-engine.mjs +772 -0
  162. package/packages/bot-engines/src/claude-llm-adapter.mjs +797 -0
  163. package/packages/bot-engines/src/claude-llm-turn.mjs +292 -0
  164. package/packages/bot-engines/src/claude-permission-broker.mjs +130 -0
  165. package/packages/bot-engines/src/claude-permission-proxy.mjs +87 -0
  166. package/packages/bot-engines/src/claude-protocol.mjs +177 -0
  167. package/packages/bot-engines/src/driver-catalog.mjs +334 -0
  168. package/packages/bot-engines/src/host-loop-request.mjs +95 -0
  169. package/packages/bot-engines/src/host-registrations.mjs +105 -0
  170. package/packages/capability-broker/src/cli-bootstrap.mjs +86 -16
  171. package/packages/capability-broker/src/cli-bridge-response.mjs +80 -0
  172. package/packages/capability-broker/src/cli-stdio-bridge.mjs +21 -8
  173. package/packages/cli-runtime/src/codex-session-core.mjs +4 -0
  174. package/packages/cli-runtime/src/native-cli-output.mjs +57 -0
  175. package/packages/cli-runtime/src/native-peer.ps1 +51 -3
  176. package/packages/cli-runtime/src/native-process.mjs +77 -14
  177. package/packages/model-router-plugin/README.md +3 -3
  178. package/packages/model-router-plugin/package.json +1 -1
  179. package/packages/orchestration/src/binding-product.mjs +19 -0
  180. package/packages/orchestration/src/bot-data-actions.mjs +45 -0
  181. package/packages/orchestration/src/bot-data-chart.mjs +109 -0
  182. package/packages/orchestration/src/bot-data-context.mjs +44 -0
  183. package/packages/orchestration/src/bot-data-engine.mjs +809 -0
  184. package/packages/orchestration/src/bot-data-io.mjs +186 -0
  185. package/packages/orchestration/src/bot-data-sql.mjs +195 -0
  186. package/packages/orchestration/src/bot-data-surface.mjs +144 -0
  187. package/packages/orchestration/src/bot-data-tools.mjs +68 -0
  188. package/packages/orchestration/src/bot-data-worker.mjs +228 -0
  189. package/packages/orchestration/src/bot-independent-execution.mjs +195 -0
  190. package/packages/orchestration/src/bot-independent-stream.mjs +74 -0
  191. package/packages/orchestration/src/bot-loop.mjs +152 -19
  192. package/packages/orchestration/src/bot-notifications.mjs +159 -0
  193. package/packages/orchestration/src/bot-policy.mjs +43 -4
  194. package/packages/orchestration/src/bot-profile.mjs +171 -0
  195. package/packages/orchestration/src/bot-routine.mjs +4 -4
  196. package/packages/orchestration/src/bot-runtime-assembly.mjs +255 -8
  197. package/packages/orchestration/src/bot-runtime-switch.mjs +13 -2
  198. package/packages/orchestration/src/bot-serving.mjs +164 -0
  199. package/packages/orchestration/src/bot-session-store.mjs +105 -9
  200. package/packages/orchestration/src/bot-settings.mjs +50 -11
  201. package/packages/orchestration/src/bot-thread-admission.mjs +84 -0
  202. package/packages/orchestration/src/bot-thread-host.mjs +187 -0
  203. package/packages/orchestration/src/bot-threads.mjs +790 -0
  204. package/packages/orchestration/src/data-product-access.mjs +37 -0
  205. package/packages/orchestration/src/data-tool-contract.mjs +291 -0
  206. package/packages/orchestration/src/desktop-http-carrier.mjs +120 -0
  207. package/packages/orchestration/src/desktop-proxy.mjs +311 -0
  208. package/packages/orchestration/src/desktop-sessions.mjs +43 -9
  209. package/packages/orchestration/src/ds-lifecycle.mjs +31 -4
  210. package/packages/orchestration/src/grant-refresh-queue.mjs +17 -0
  211. package/packages/orchestration/src/independent-chat-page.mjs +39 -0
  212. package/packages/orchestration/src/independent-chat-service.mjs +308 -0
  213. package/packages/orchestration/src/independent-context-state.mjs +44 -0
  214. package/packages/orchestration/src/independent-message-wire.mjs +84 -0
  215. package/packages/orchestration/src/independent-tool-catalog.mjs +175 -0
  216. package/packages/orchestration/src/independent-tool-families.mjs +73 -0
  217. package/packages/orchestration/src/independent-tool-host.mjs +344 -0
  218. package/packages/orchestration/src/main-bot-grant.mjs +15 -0
  219. package/packages/orchestration/src/main-model-routes.mjs +208 -0
  220. package/packages/orchestration/src/model-accounting.mjs +99 -35
  221. package/packages/orchestration/src/model-auxiliary-dispatch.mjs +187 -0
  222. package/packages/orchestration/src/model-loop-frame.mjs +179 -0
  223. package/packages/orchestration/src/model-prepared-generation.mjs +175 -0
  224. package/packages/orchestration/src/model-request-bounds.mjs +111 -0
  225. package/packages/orchestration/src/native-activity-contract.mjs +491 -0
  226. package/packages/orchestration/src/native-activity-service.mjs +102 -0
  227. package/packages/orchestration/src/native-activity-store.mjs +249 -0
  228. package/packages/orchestration/src/native-conversation-tools.mjs +78 -0
  229. package/packages/orchestration/src/native-data-tools.mjs +100 -0
  230. package/packages/orchestration/src/native-remote-actions.mjs +742 -0
  231. package/packages/orchestration/src/native-team-tools.mjs +73 -0
  232. package/packages/orchestration/src/pairing-invite.mjs +37 -0
  233. package/packages/orchestration/src/product-composition.mjs +1559 -1001
  234. package/packages/orchestration/src/remote-machines.mjs +82 -14
  235. package/packages/orchestration/src/routine-execution.mjs +5 -3
  236. package/packages/orchestration/src/routine-session.mjs +19 -13
  237. package/packages/orchestration/src/secure-pairing-host.mjs +112 -0
  238. package/packages/orchestration/src/task-journal.mjs +29 -0
  239. package/packages/orchestration/src/task-product.mjs +17 -0
  240. package/packages/orchestration/src/team-assignment-store.mjs +120 -0
  241. package/packages/orchestration/src/team-bridge.mjs +282 -0
  242. package/packages/orchestration/src/team-scheduler.mjs +83 -0
  243. package/packages/orchestration/src/team-sdk-tools.mjs +84 -0
  244. package/packages/orchestration/src/team-tool-contract.mjs +284 -0
  245. package/packages/orchestration/src/ui-host-namespace.mjs +23 -0
  246. package/packages/orchestration/src/web-panel-actions.mjs +103 -0
  247. package/packages/orchestration/src/web-panel-framing.mjs +227 -0
  248. package/packages/orchestration/src/web-panel-store.mjs +206 -0
  249. package/remote-computer-agent/README.md +39 -29
  250. package/remote-computer-agent/build.mjs +3 -0
  251. package/remote-computer-agent/dist/OPENMAUSBOT_LICENSE +202 -0
  252. package/remote-computer-agent/dist/OPENMAUSBOT_NOTICE +10 -0
  253. package/remote-computer-agent/dist/OPENMAUSBOT_SOURCE.json +68 -0
  254. package/remote-computer-agent/dist/cli-stdio-bridge.mjs +215 -25
  255. package/remote-computer-agent/dist/cli-stdio-bridge.mjs.map +4 -4
  256. package/remote-computer-agent/dist/cli.mjs +1688 -168
  257. package/remote-computer-agent/dist/cli.mjs.map +4 -4
  258. package/remote-computer-agent/dist/index.mjs +1579 -122
  259. package/remote-computer-agent/dist/index.mjs.map +4 -4
  260. package/remote-computer-agent/dist/native-peer.ps1 +51 -3
  261. package/remote-computer-agent/package-lock.json +308 -2
  262. package/remote-computer-agent/package.json +2 -1
  263. package/remote-computer-agent/pair-device.ps1 +20 -0
  264. package/remote-computer-agent/setup.ps1 +38 -16
  265. package/remote-computer-agent/src/cli.ts +24 -3
  266. package/remote-computer-agent/src/desktop-session.ts +11 -3
  267. package/remote-computer-agent/src/e2e-server.ts +81 -0
  268. package/remote-computer-agent/src/pairing-page.ts +35 -0
  269. package/remote-computer-agent/src/protocol.ts +25 -25
  270. package/remote-computer-agent/src/secure-pairing.ts +105 -0
  271. package/remote-computer-agent/src/store.ts +20 -5
  272. package/remote-computer-agent/src/transport.ts +39 -15
@@ -0,0 +1,491 @@
1
+ // Display-only contract for the native tool-activity surface.
2
+ //
3
+ // Production module. Nothing here requires SQLite, opens a file, loads a native
4
+ // binding or executes anything: it is pure validation, pure projection and pure
5
+ // causal record rules. The durable store (native-activity-store.mjs) and the
6
+ // owner-facing service (native-activity-service.mjs) build on it.
7
+ //
8
+ // What the ACTUAL engine publishes today (packages/bot-engines/src/claude-engine.mjs):
9
+ //
10
+ // events.emit('tool', { phase: 'started', id, name }) // line 447
11
+ // events.emit('tool', { phase: 'completed', id, ok }) // line 451
12
+ //
13
+ // That is the whole event. There is no `input`, no result body, no text, no
14
+ // screenshot and no error text on it, so this contract never claims any of
15
+ // those. `ok` is the CLI's own `is_error !== true`: it reports what the CLI
16
+ // said about the call, and nothing about whether a remote operation finished,
17
+ // was stopped, or was even started.
18
+ //
19
+ // Four rules this file exists to enforce:
20
+ //
21
+ // 1. Identity is Host-selected, never derived from the event or from model
22
+ // arguments. The Host mints an opaque, restart-safe `processGeneration`
23
+ // string per native engine instance plus launch; a tool id may be reused
24
+ // by the CLI across turns and process generations, so it is never a key
25
+ // on its own.
26
+ // 2. The projection is display-only. It emits no SDK executable tool-call
27
+ // chunk, never touches model messages, receipts, funding or tool
28
+ // dispatch, and carries no raw parameters, results, pixels or Host paths.
29
+ // 3. Uncertainty stays uncertainty. A call with no CLI return is not green, a
30
+ // cancel request is not a confirmed stop, and a duplicate or late event is
31
+ // counted rather than silently rewritten.
32
+ // 4. A read is a read. Every function here either validates input or derives
33
+ // output from the record it was given; none of them writes, prunes,
34
+ // recovers, executes or resends.
35
+
36
+ import { createHash } from 'node:crypto'
37
+
38
+ export const ACTIVITY_SCHEMA_VERSION = 1
39
+
40
+ /** The two chat runtimes that may report activity. */
41
+ export const ACTIVITY_MODES = Object.freeze(['integration', 'independent'])
42
+
43
+ /** The only two phases the real engine emits today. Nothing else is invented. */
44
+ export const ACTIVITY_EVENT_PHASES = Object.freeze(['started', 'completed'])
45
+
46
+ /** Display states. `turn-unconfirmed` is the honest terminal state of an unreturned call. */
47
+ export const ACTIVITY_STATES = Object.freeze(['running', 'cli-ok', 'cli-error', 'turn-unconfirmed'])
48
+
49
+ /** Turn outcomes the Host may report. The store never derives one. */
50
+ export const ACTIVITY_TURN_OUTCOMES = Object.freeze(['succeeded', 'failed', 'cancelled', 'unknown', 'refused', 'host-closed', 'host-lost'])
51
+
52
+ /** Sentinel for a fact the event did not carry. Never rendered as a success. */
53
+ export const ACTIVITY_UNRECORDED = 'unrecorded'
54
+
55
+ export const ACTIVITY_LIMITS = Object.freeze({
56
+ maxToolNameChars: 128,
57
+ maxCallIdChars: 256,
58
+ maxIdentityChars: 128,
59
+ maxProcessGenerationChars: 128,
60
+ maxSessionIdChars: 256,
61
+ maxRecentVisible: 4,
62
+ defaultPageSize: 20,
63
+ maxPageSize: 50,
64
+ maxCallsPerTurn: 64,
65
+ maxTurnsPerConversation: 512,
66
+ maxRecords: 20_000,
67
+ maxUnprojectedNames: 8,
68
+ })
69
+
70
+ /**
71
+ * Fields a tool event may carry that this contract refuses to project. A richer future
72
+ * engine event is projected by whitelist, and anything on this list is reported as
73
+ * `unprojected` instead of being smuggled into a display row.
74
+ */
75
+ export const ACTIVITY_UNPROJECTED_FIELDS = Object.freeze([
76
+ 'input', 'inputs', 'args', 'arguments', 'params', 'result', 'results', 'content', 'output',
77
+ 'text', 'body', 'stdout', 'stderr', 'diff', 'error', 'errorText', 'errors', 'screenshot',
78
+ 'image', 'images', 'pixels', 'base64', 'dataUri', 'path', 'paths', 'command', 'cmd', 'url',
79
+ 'endpoint', 'token', 'credential', 'secret', 'env', 'usage', 'cost', 'costUsd', 'session',
80
+ 'messageId', 'message', 'messages', 'toolCall', 'toolCalls',
81
+ ])
82
+
83
+ /** Closed key set of a projected event. `assertDisplayOnly` enforces exactly this. */
84
+ const EVENT_KEYS = Object.freeze([
85
+ 'schemaVersion', 'phase', 'callId', 'callIdRecorded', 'toolName', 'toolNameRecorded',
86
+ 'toolNameTruncated', 'redacted', 'ok', 'unprojected', 'unprojectedCount',
87
+ ])
88
+
89
+ /** Closed key set of a projected display record. */
90
+ const RECORD_KEYS = Object.freeze([
91
+ 'schemaVersion', 'callKey', 'botId', 'mode', 'conversationId', 'contextId',
92
+ 'processGeneration', 'turnId', 'callId', 'callIdRecorded', 'toolName',
93
+ 'toolNameRecorded', 'toolNameTruncated', 'redacted', 'state', 'cliOk', 'startedAt',
94
+ 'endedAt', 'updatedAt', 'turnSettled', 'turnOutcome', 'cancelRequested',
95
+ 'cancelRequestedAt', 'stopConfirmed', 'stopConfirmedAt', 'lateCliReturn', 'lateCliOk',
96
+ 'duplicateStarts', 'duplicateCompletions',
97
+ ])
98
+
99
+ /** Closed key set of a turn summary. */
100
+ const SUMMARY_KEYS = Object.freeze(['schemaVersion', 'turnKey', 'turnOpen', 'calls', 'byState', 'current', 'recent', 'recentTruncated'])
101
+
102
+ const IDENTIFIER = /^[A-Za-z0-9_.:-]{1,128}$/
103
+ const BOT_ID = /^[A-Za-z0-9_.-]{1,128}$/
104
+ const PROCESS_GENERATION = /^[A-Za-z0-9_.:-]{1,128}$/
105
+ const SESSION_ID = /^[^\s\u0000-\u001f\u007f]{1,256}$/
106
+ const HOST_CONVERSATION = /^host-[a-f0-9]{64}$/
107
+ const CONTROL = /[\u0000-\u001f\u007f]/
108
+ const IDENTITY_FIELDS = Object.freeze(['botId', 'mode', 'conversationId', 'contextId', 'processGeneration', 'turnId'])
109
+
110
+ const coded = (code, detail) => Object.assign(new TypeError(detail === undefined ? code : `${code}: ${detail}`), { code })
111
+ const fail = code => { throw coded(code) }
112
+ const boundedString = (value, pattern, code) => {
113
+ if (typeof value !== 'string' || !pattern.test(value)) fail(code)
114
+ return value
115
+ }
116
+ const digest = (...parts) => createHash('sha256').update(parts.join('\u001f')).digest('hex')
117
+
118
+ /** What each display state proves and what it explicitly does not prove. */
119
+ export const ACTIVITY_STATE_LABELS = Object.freeze({
120
+ running: Object.freeze({
121
+ zh: '进行中', en: 'running',
122
+ proves: 'CLI 已开始这个调用', provesEn: 'the CLI started this call',
123
+ notProves: '没有结果,也不代表远端操作已开始', notProvesEn: 'no result yet, and no proof a remote operation started',
124
+ }),
125
+ 'cli-ok': Object.freeze({
126
+ zh: 'CLI 报告成功', en: 'CLI reported ok',
127
+ proves: 'CLI 的 tool_result 未标记错误', provesEn: 'the CLI tool_result was not an error',
128
+ notProves: '不证明远端操作完成或已停止', notProvesEn: 'no proof the remote operation finished or stopped',
129
+ }),
130
+ 'cli-error': Object.freeze({
131
+ zh: 'CLI 报告失败', en: 'CLI reported error',
132
+ proves: 'CLI 的 tool_result 标记了错误', provesEn: 'the CLI tool_result was an error',
133
+ notProves: '不证明远端操作已停止或未开始', notProvesEn: 'no proof the remote operation stopped or never started',
134
+ }),
135
+ 'turn-unconfirmed': Object.freeze({
136
+ zh: '本回合未确认', en: 'turn unconfirmed',
137
+ proves: '回合已结束而该调用没有 CLI 返回', provesEn: 'the turn ended with no CLI return for this call',
138
+ notProves: '不推断成功或失败', notProvesEn: 'no success or failure is inferred',
139
+ }),
140
+ })
141
+
142
+ /** The honest statements every reader of this contract must keep. */
143
+ export const ACTIVITY_DISCLAIMERS = Object.freeze({
144
+ noDetail: '当前引擎工具事件只有 id/name/phase/ok;没有参数、结果正文、错误文本或截图,本契约不展示任何这些内容。',
145
+ noDetailEn: 'the engine tool event carries only id/name/phase/ok; no parameters, result body, error text or screenshot is projected',
146
+ cancelIsNotStop: '取消请求不是已停止确认;停止只由宿主真实的停止证据确认。',
147
+ cancelIsNotStopEn: 'a cancel request is not a confirmed stop; only real Host stop evidence confirms one',
148
+ duplicates: '重复的开始/完成事件不重复行,只计数;先到的 CLI 返回保留,不声称哪一个更权威。',
149
+ duplicatesEn: 'duplicate start/complete events are counted, never duplicated; the first CLI return is kept and neither is claimed authoritative',
150
+ lateEvents: '回合结束后的迟到事件只作为迟到证据记录,不改变该回合已确定的终态。',
151
+ lateEventsEn: 'an event arriving after the turn ended is recorded as late evidence and does not change that turn terminal state',
152
+ displayOnly: '只供展示;不产生 SDK 可执行工具调用块,不改写模型消息、收据、资金或工具派发。',
153
+ displayOnlyEn: 'display only; no SDK executable tool-call chunk, no change to model messages, receipts, funding or tool dispatch',
154
+ generation: 'processGeneration 是宿主为每次引擎实例随机铸造的不透明串;重启后旧代次的待确认行不会被新事件回答。',
155
+ generationEn: 'processGeneration is an opaque per-engine-instance string the Host mints at random; after a restart a pending row from an older generation is never answered by a newer one',
156
+ })
157
+
158
+ /**
159
+ * Validate a Host-selected activity identity. The field set is closed: an identity that
160
+ * arrives with an extra field is refused rather than partially trusted. A caller can only
161
+ * reach this function with values the Host itself chose.
162
+ *
163
+ * `processGeneration` is an OPAQUE Host-minted string, never a counter: the parent mints a
164
+ * random engine id plus launch identity, so a restart or a second engine instance can never
165
+ * reuse a generation, and a resettable integer could silently merge two engines' turns.
166
+ *
167
+ * @param {{botId:string, mode:string, conversationId:string, contextId?:string|null,
168
+ * processGeneration:string, turnId:string}} value
169
+ */
170
+ export function activityIdentity(value) {
171
+ if (!value || typeof value !== 'object' || Array.isArray(value)) fail('ACTIVITY_IDENTITY_REQUIRED')
172
+ const extra = Object.keys(value).filter(key => !IDENTITY_FIELDS.includes(key))
173
+ if (extra.length) fail('ACTIVITY_IDENTITY_UNKNOWN_FIELD')
174
+ const botId = boundedString(value.botId, BOT_ID, 'ACTIVITY_BOT_ID_INVALID')
175
+ if (!ACTIVITY_MODES.includes(value.mode)) fail('ACTIVITY_MODE_INVALID')
176
+ const conversationId = boundedString(value.conversationId, IDENTIFIER, 'ACTIVITY_CONVERSATION_ID_INVALID')
177
+ const contextId = value.contextId === undefined || value.contextId === null
178
+ ? null
179
+ : boundedString(value.contextId, IDENTIFIER, 'ACTIVITY_CONTEXT_ID_INVALID')
180
+ // Integration mode has no independent context, and its conversation is the Host's own
181
+ // hash of the owner-selected sessionId. Independent mode's conversation IS the context.
182
+ if (value.mode === 'integration') {
183
+ if (contextId !== null) fail('ACTIVITY_INTEGRATION_CONTEXT_REFUSED')
184
+ if (!HOST_CONVERSATION.test(conversationId)) fail('ACTIVITY_CONVERSATION_ID_INVALID')
185
+ }
186
+ if (value.mode === 'independent' && (contextId === null || contextId !== conversationId)) fail('ACTIVITY_INDEPENDENT_CONTEXT_INVALID')
187
+ const processGeneration = boundedString(value.processGeneration, PROCESS_GENERATION, 'ACTIVITY_PROCESS_GENERATION_INVALID')
188
+ const turnId = boundedString(value.turnId, IDENTIFIER, 'ACTIVITY_TURN_ID_INVALID')
189
+ return Object.freeze({ botId, mode: value.mode, conversationId, contextId, processGeneration, turnId })
190
+ }
191
+
192
+ /** The durable turn key: the same closed tuple as the call key, minus the call id. */
193
+ export function activityTurnKey(identity) {
194
+ return `turn1-${digest('dsh-native-activity-turn-v1', identity.mode, identity.botId, identity.conversationId, identity.contextId ?? '-', identity.processGeneration, identity.turnId)}`
195
+ }
196
+
197
+ /**
198
+ * The durable per-call key. The CLI call id is only ever one component: the same id in
199
+ * another turn, another conversation or another native process generation hashes to a
200
+ * different key, so it cannot collide with or overwrite this one.
201
+ */
202
+ export function activityCallKey(identity, callId) {
203
+ const id = boundedString(callId, /^[^\s\u0000-\u001f\u007f]{1,256}$/, 'ACTIVITY_CALL_ID_INVALID')
204
+ return `call1-${digest('dsh-native-activity-call-v1', identity.mode, identity.botId, identity.conversationId, identity.contextId ?? '-', identity.processGeneration, identity.turnId, id)}`
205
+ }
206
+
207
+ /** The integration-mode conversation id: the Host's own hash of the owner-selected sessionId. */
208
+ export const hostConversationId = sessionId => {
209
+ boundedString(sessionId, SESSION_ID, 'ACTIVITY_SESSION_ID_INVALID')
210
+ return `host-${createHash('sha256').update(sessionId).digest('hex')}`
211
+ }
212
+
213
+ /**
214
+ * Bound a CLI tool name for display. A name the event did not carry becomes the
215
+ * `unrecorded` sentinel with `recorded:false`; it is never replaced by a guess.
216
+ */
217
+ export function boundedToolName(value) {
218
+ if (typeof value !== 'string') return { name: ACTIVITY_UNRECORDED, recorded: false, truncated: false, redacted: false }
219
+ const cleaned = value.replace(/[\u0000-\u001f\u007f]/g, '').trim()
220
+ if (!cleaned) return { name: ACTIVITY_UNRECORDED, recorded: false, truncated: false, redacted: value.length > 0 }
221
+ const truncated = cleaned.length > ACTIVITY_LIMITS.maxToolNameChars
222
+ const name = truncated ? cleaned.slice(0, ACTIVITY_LIMITS.maxToolNameChars) : cleaned
223
+ if (!/^[A-Za-z0-9_][A-Za-z0-9_.:-]*$/.test(name)) return { name: ACTIVITY_UNRECORDED, recorded: false, truncated, redacted: true }
224
+ return { name, recorded: true, truncated, redacted: name !== value }
225
+ }
226
+
227
+ /**
228
+ * Project one raw engine tool event. Whitelist-only: nothing outside `phase`, `id`,
229
+ * `name` and `ok` survives, and every refused field is reported back to the caller so a
230
+ * richer future event cannot quietly widen what a display row claims.
231
+ *
232
+ * @param {{phase:string, id?:unknown, name?:unknown, ok?:unknown}} event
233
+ * @param {{redactToolName?:(name:string)=>string|null}} [policy] trusted Host redaction
234
+ */
235
+ export function toolEventProjection(event, policy = null) {
236
+ if (!event || typeof event !== 'object' || Array.isArray(event)) fail('ACTIVITY_EVENT_REQUIRED')
237
+ if (!ACTIVITY_EVENT_PHASES.includes(event.phase)) fail('ACTIVITY_EVENT_PHASE_UNKNOWN')
238
+ const unprojected = Object.keys(event).filter(key => !['phase', 'id', 'name', 'ok'].includes(key) && ACTIVITY_UNPROJECTED_FIELDS.includes(key))
239
+ const unprojectedCount = unprojected.length
240
+ const listed = unprojected.slice(0, ACTIVITY_LIMITS.maxUnprojectedNames)
241
+ if (event.phase === 'completed' && typeof event.ok !== 'boolean') fail('ACTIVITY_EVENT_OK_REQUIRED')
242
+ const callIdRecorded = typeof event.id === 'string' && event.id.length > 0
243
+ const callId = callIdRecorded && /^[^\s\u0000-\u001f\u007f]{1,256}$/.test(event.id) ? event.id : null
244
+ const bounded = boundedToolName(event.phase === 'started' ? event.name : undefined)
245
+ let toolName = bounded.name
246
+ let redacted = bounded.redacted
247
+ if (bounded.recorded && typeof policy?.redactToolName === 'function') {
248
+ const replacement = policy.redactToolName(bounded.name)
249
+ if (typeof replacement === 'string') {
250
+ const rebounded = boundedToolName(replacement)
251
+ toolName = rebounded.name
252
+ redacted = redacted || replacement !== bounded.name || rebounded.redacted
253
+ } else if (replacement === null) {
254
+ toolName = ACTIVITY_UNRECORDED
255
+ redacted = true
256
+ } else fail('ACTIVITY_REDACTION_INVALID')
257
+ }
258
+ return Object.freeze({
259
+ schemaVersion: ACTIVITY_SCHEMA_VERSION,
260
+ phase: event.phase,
261
+ callId,
262
+ callIdRecorded,
263
+ toolName,
264
+ toolNameRecorded: bounded.recorded && toolName !== ACTIVITY_UNRECORDED,
265
+ toolNameTruncated: bounded.truncated,
266
+ redacted,
267
+ ok: event.phase === 'completed' ? event.ok : null,
268
+ unprojected: Object.freeze(listed),
269
+ unprojectedCount,
270
+ })
271
+ }
272
+
273
+ /** The initial record of an observed call. Always `running`: a start is not a result. */
274
+ export function activityRecordFromEvent(identity, projection, at) {
275
+ if (!Number.isSafeInteger(at)) fail('ACTIVITY_TIME_REQUIRED')
276
+ if (projection.callId === null) fail('ACTIVITY_CALL_ID_MISSING')
277
+ return Object.freeze({
278
+ schemaVersion: ACTIVITY_SCHEMA_VERSION,
279
+ callKey: activityCallKey(identity, projection.callId),
280
+ botId: identity.botId, mode: identity.mode, conversationId: identity.conversationId,
281
+ contextId: identity.contextId, processGeneration: identity.processGeneration, turnId: identity.turnId,
282
+ callId: projection.callId, callIdRecorded: projection.callIdRecorded,
283
+ toolName: projection.toolName, toolNameRecorded: projection.toolNameRecorded,
284
+ toolNameTruncated: projection.toolNameTruncated, redacted: projection.redacted,
285
+ state: 'running', cliOk: null, startedAt: at, endedAt: null, updatedAt: at,
286
+ turnSettled: false, turnOutcome: null, cancelRequested: false, cancelRequestedAt: null,
287
+ stopConfirmed: false, stopConfirmedAt: null, lateCliReturn: false, lateCliOk: null,
288
+ duplicateStarts: 0, duplicateCompletions: 0,
289
+ })
290
+ }
291
+
292
+ const withRecord = (record, patch) => Object.freeze({ ...record, ...patch, updatedAt: patch.updatedAt ?? record.updatedAt })
293
+
294
+ /**
295
+ * Apply one projected event to one record. Pure: the input record is never mutated.
296
+ *
297
+ * Causal rules, each of which a caller can assert on:
298
+ * - a second `started` for the same call is counted, never a second row;
299
+ * - the first CLI return wins and later returns are counted, because this contract
300
+ * cannot tell which of two returns the CLI meant;
301
+ * - a return that arrives after the turn already ended is late evidence: it is recorded
302
+ * beside the call and the turn's terminal state is left alone;
303
+ * - nothing here ever turns `running` into a success.
304
+ *
305
+ * @param {object} record
306
+ * @param {object} projection from {@link toolEventProjection}
307
+ * @param {{turnOpen:boolean, at:number}} context
308
+ * @returns {{record:object, changed:boolean, reason:string}}
309
+ */
310
+ export function applyToolEvent(record, projection, { turnOpen, at }) {
311
+ if (!record || typeof record !== 'object') fail('ACTIVITY_RECORD_REQUIRED')
312
+ if (!Number.isSafeInteger(at)) fail('ACTIVITY_TIME_REQUIRED')
313
+ if (projection.phase === 'started') {
314
+ if (record.state !== 'running') {
315
+ return { record, changed: false, reason: 'late-start-after-terminal' }
316
+ }
317
+ return { record: withRecord(record, { duplicateStarts: record.duplicateStarts + 1 }), changed: false, reason: 'duplicate-start' }
318
+ }
319
+ if (record.cliOk !== null) {
320
+ return { record: withRecord(record, { duplicateCompletions: record.duplicateCompletions + 1 }), changed: false, reason: 'duplicate-completion' }
321
+ }
322
+ if (turnOpen !== true || record.turnSettled === true) {
323
+ return {
324
+ record: withRecord(record, { lateCliReturn: true, lateCliOk: projection.ok, updatedAt: at }),
325
+ changed: true,
326
+ reason: 'late-completion-after-turn-end',
327
+ }
328
+ }
329
+ return {
330
+ record: withRecord(record, {
331
+ cliOk: projection.ok,
332
+ state: projection.ok ? 'cli-ok' : 'cli-error',
333
+ endedAt: at,
334
+ updatedAt: at,
335
+ }),
336
+ changed: true,
337
+ reason: projection.ok ? 'cli-completed-ok' : 'cli-completed-error',
338
+ }
339
+ }
340
+
341
+ /**
342
+ * Close a turn. A call that never received a CLI return becomes `turn-unconfirmed`; it is
343
+ * never painted green by the turn ending. `cancelRequested` and `stopConfirmed` are only
344
+ * ever set from Host facts, and neither is derived from the other.
345
+ */
346
+ export function closeTurnRecord(record, { outcome, cancelRequested = false, stopConfirmed = false, at }) {
347
+ if (!record || typeof record !== 'object') fail('ACTIVITY_RECORD_REQUIRED')
348
+ if (!ACTIVITY_TURN_OUTCOMES.includes(outcome)) fail('ACTIVITY_TURN_OUTCOME_INVALID')
349
+ if (!Number.isSafeInteger(at)) fail('ACTIVITY_TIME_REQUIRED')
350
+ const patch = {
351
+ turnSettled: true, turnOutcome: outcome,
352
+ cancelRequested: record.cancelRequested || cancelRequested === true,
353
+ stopConfirmed: record.stopConfirmed || stopConfirmed === true,
354
+ updatedAt: at,
355
+ }
356
+ if (record.state === 'running') {
357
+ patch.state = 'turn-unconfirmed'
358
+ patch.endedAt = at
359
+ }
360
+ return withRecord(record, patch)
361
+ }
362
+
363
+ /** A cancel request. Deliberately does not touch `stopConfirmed`. */
364
+ export function requestCancelRecord(record, at) {
365
+ if (!record || typeof record !== 'object') fail('ACTIVITY_RECORD_REQUIRED')
366
+ if (!Number.isSafeInteger(at)) fail('ACTIVITY_TIME_REQUIRED')
367
+ if (record.cancelRequested) return { record, changed: false, reason: 'cancel-already-requested' }
368
+ return { record: withRecord(record, { cancelRequested: true, cancelRequestedAt: at, updatedAt: at }), changed: true, reason: 'cancel-requested' }
369
+ }
370
+
371
+ /** A confirmed process stop, from real Host stop evidence only. */
372
+ export function confirmStopRecord(record, at) {
373
+ if (!record || typeof record !== 'object') fail('ACTIVITY_RECORD_REQUIRED')
374
+ if (!Number.isSafeInteger(at)) fail('ACTIVITY_TIME_REQUIRED')
375
+ if (record.stopConfirmed) return { record, changed: false, reason: 'stop-already-confirmed' }
376
+ return { record: withRecord(record, { stopConfirmed: true, stopConfirmedAt: at, updatedAt: at }), changed: true, reason: 'stop-confirmed' }
377
+ }
378
+
379
+ /**
380
+ * The closed display projection of a stored record. Exactly {@link RECORD_KEYS}, every
381
+ * value a primitive, no nested object, no parameters, no results, no pixels.
382
+ */
383
+ export function projectActivityRecord(record) {
384
+ if (!record || typeof record !== 'object') fail('ACTIVITY_RECORD_REQUIRED')
385
+ const projected = {
386
+ schemaVersion: ACTIVITY_SCHEMA_VERSION,
387
+ callKey: record.callKey,
388
+ botId: record.botId, mode: record.mode, conversationId: record.conversationId,
389
+ contextId: record.contextId ?? null,
390
+ processGeneration: record.processGeneration, turnId: record.turnId,
391
+ callId: record.callId, callIdRecorded: record.callIdRecorded === true,
392
+ toolName: record.toolName, toolNameRecorded: record.toolNameRecorded === true,
393
+ toolNameTruncated: record.toolNameTruncated === true, redacted: record.redacted === true,
394
+ state: record.state,
395
+ cliOk: record.cliOk === undefined ? null : record.cliOk,
396
+ startedAt: record.startedAt, endedAt: record.endedAt ?? null, updatedAt: record.updatedAt,
397
+ turnSettled: record.turnSettled === true, turnOutcome: record.turnOutcome ?? null,
398
+ cancelRequested: record.cancelRequested === true, cancelRequestedAt: record.cancelRequestedAt ?? null,
399
+ stopConfirmed: record.stopConfirmed === true, stopConfirmedAt: record.stopConfirmedAt ?? null,
400
+ lateCliReturn: record.lateCliReturn === true, lateCliOk: record.lateCliOk ?? null,
401
+ duplicateStarts: Number.isSafeInteger(record.duplicateStarts) ? record.duplicateStarts : 0,
402
+ duplicateCompletions: Number.isSafeInteger(record.duplicateCompletions) ? record.duplicateCompletions : 0,
403
+ }
404
+ assertDisplayOnly(projected)
405
+ return Object.freeze(projected)
406
+ }
407
+
408
+ /**
409
+ * Fail closed if a value is not a closed display projection. Used by the store on every
410
+ * read path and by the tests, so a widened row is a loud failure instead of a leak.
411
+ */
412
+ export function assertDisplayOnly(value) {
413
+ if (!value || typeof value !== 'object' || Array.isArray(value)) fail('ACTIVITY_DISPLAY_SCHEMA_UNKNOWN')
414
+ const keys = Object.keys(value)
415
+ const allowed = value.schemaVersion === ACTIVITY_SCHEMA_VERSION ? (Object.hasOwn(value, 'callKey') ? RECORD_KEYS : Object.hasOwn(value, 'phase') ? EVENT_KEYS : null) : null
416
+ if (!allowed) fail('ACTIVITY_DISPLAY_SCHEMA_UNKNOWN')
417
+ const extra = keys.filter(key => !allowed.includes(key))
418
+ if (extra.length || keys.length !== allowed.length) fail('ACTIVITY_DISPLAY_FIELD_REFUSED')
419
+ for (const key of keys) {
420
+ const item = value[key]
421
+ if (item === null) continue
422
+ if (Array.isArray(item)) {
423
+ if (item.some(entry => typeof entry !== 'string')) fail('ACTIVITY_DISPLAY_FIELD_REFUSED')
424
+ continue
425
+ }
426
+ if (!['string', 'number', 'boolean'].includes(typeof item) || typeof item === 'number' && !Number.isSafeInteger(item)) fail('ACTIVITY_DISPLAY_FIELD_REFUSED')
427
+ if (typeof item === 'string' && (CONTROL.test(item) || item.length > 4096)) fail('ACTIVITY_DISPLAY_FIELD_REFUSED')
428
+ }
429
+ if (allowed === RECORD_KEYS) {
430
+ const identity = activityIdentity({ botId: value.botId, mode: value.mode, conversationId: value.conversationId,
431
+ contextId: value.contextId, processGeneration: value.processGeneration, turnId: value.turnId })
432
+ if (activityCallKey(identity, value.callId) !== value.callKey || !ACTIVITY_STATES.includes(value.state)) fail('ACTIVITY_DISPLAY_RECORD_INVALID')
433
+ for (const key of ['callIdRecorded', 'toolNameRecorded', 'toolNameTruncated', 'redacted', 'turnSettled', 'cancelRequested', 'stopConfirmed', 'lateCliReturn']) if (typeof value[key] !== 'boolean') fail('ACTIVITY_DISPLAY_RECORD_INVALID')
434
+ for (const key of ['startedAt', 'updatedAt', 'duplicateStarts', 'duplicateCompletions']) if (!Number.isSafeInteger(value[key]) || value[key] < 0) fail('ACTIVITY_DISPLAY_RECORD_INVALID')
435
+ for (const key of ['endedAt', 'cancelRequestedAt', 'stopConfirmedAt']) if (value[key] !== null && (!Number.isSafeInteger(value[key]) || value[key] < 0)) fail('ACTIVITY_DISPLAY_RECORD_INVALID')
436
+ if (typeof value.toolName !== 'string' || !value.toolName || value.toolName.length > ACTIVITY_LIMITS.maxToolNameChars) fail('ACTIVITY_DISPLAY_RECORD_INVALID')
437
+ if (value.cliOk !== null && typeof value.cliOk !== 'boolean' || value.lateCliOk !== null && typeof value.lateCliOk !== 'boolean') fail('ACTIVITY_DISPLAY_RECORD_INVALID')
438
+ if (value.state === 'cli-ok' && value.cliOk !== true || value.state === 'cli-error' && value.cliOk !== false || ['running', 'turn-unconfirmed'].includes(value.state) && value.cliOk !== null) fail('ACTIVITY_DISPLAY_RECORD_INVALID')
439
+ if (value.turnSettled ? !ACTIVITY_TURN_OUTCOMES.includes(value.turnOutcome) : value.turnOutcome !== null) fail('ACTIVITY_DISPLAY_RECORD_INVALID')
440
+ if (value.cancelRequested !== (value.cancelRequestedAt !== null) || value.stopConfirmed !== (value.stopConfirmedAt !== null)) fail('ACTIVITY_DISPLAY_RECORD_INVALID')
441
+ } else {
442
+ if (!ACTIVITY_EVENT_PHASES.includes(value.phase) || !Array.isArray(value.unprojected) || value.unprojected.length > ACTIVITY_LIMITS.maxUnprojectedNames || value.unprojected.some(key => !ACTIVITY_UNPROJECTED_FIELDS.includes(key))) fail('ACTIVITY_DISPLAY_EVENT_INVALID')
443
+ }
444
+ return true
445
+ }
446
+
447
+ /** Stable order for paging and for the recent-items crop: start time, then key. */
448
+ export function compareActivityRecords(a, b) {
449
+ if (a.startedAt !== b.startedAt) return a.startedAt - b.startedAt
450
+ return a.callKey < b.callKey ? -1 : a.callKey > b.callKey ? 1 : 0
451
+ }
452
+
453
+ /** Bounded, display-only summary of one turn or one page of calls. */
454
+ export function summarizeActivity(records, { turnKey = null, turnOpen = false } = {}) {
455
+ const list = (Array.isArray(records) ? records : []).map(record => projectActivityRecord(record))
456
+ list.sort(compareActivityRecords)
457
+ const byState = {}
458
+ for (const state of ACTIVITY_STATES) byState[state] = 0
459
+ for (const record of list) byState[record.state] += 1
460
+ const current = list.find(record => record.state === 'running') ?? null
461
+ const recent = list.slice(-ACTIVITY_LIMITS.maxRecentVisible).reverse()
462
+ const summary = {
463
+ schemaVersion: ACTIVITY_SCHEMA_VERSION,
464
+ turnKey,
465
+ turnOpen: turnOpen === true,
466
+ calls: list.length,
467
+ byState: Object.freeze(byState),
468
+ current,
469
+ recent: Object.freeze(recent),
470
+ recentTruncated: list.length > ACTIVITY_LIMITS.maxRecentVisible,
471
+ }
472
+ return Object.freeze(summary)
473
+ }
474
+
475
+ /** Opaque cursor: the boundary of the last returned row. */
476
+ export function activityCursor(at, callKey, direction = 'desc') {
477
+ if (!Number.isSafeInteger(at) || typeof callKey !== 'string' || !['asc', 'desc'].includes(direction)) fail('ACTIVITY_CURSOR_INVALID')
478
+ return Buffer.from(JSON.stringify({ v: 1, at, k: callKey, d: direction }), 'utf8').toString('base64url')
479
+ }
480
+
481
+ export function parseActivityCursor(cursor) {
482
+ if (typeof cursor !== 'string' || cursor.length > 4096) fail('ACTIVITY_CURSOR_INVALID')
483
+ let parsed
484
+ try { parsed = JSON.parse(Buffer.from(cursor, 'base64url').toString('utf8')) } catch { fail('ACTIVITY_CURSOR_INVALID') }
485
+ if (!parsed || parsed.v !== 1 || !Number.isSafeInteger(parsed.at) || typeof parsed.k !== 'string' || !['asc', 'desc'].includes(parsed.d)) fail('ACTIVITY_CURSOR_INVALID')
486
+ return Object.freeze({ at: parsed.at, callKey: parsed.k, direction: parsed.d })
487
+ }
488
+
489
+ export const ACTIVITY_EVENT_KEYS = EVENT_KEYS
490
+ export const ACTIVITY_RECORD_KEYS = RECORD_KEYS
491
+ export const ACTIVITY_SUMMARY_KEYS = SUMMARY_KEYS
@@ -0,0 +1,102 @@
1
+ /** Host-only writer and authenticated-owner read projection for native activity. */
2
+ import { createActivityStore } from './native-activity-store.mjs'
3
+ import { activityIdentity, hostConversationId, ACTIVITY_LIMITS, ACTIVITY_TURN_OUTCOMES } from './native-activity-contract.mjs'
4
+
5
+ const plain = value => value && typeof value === 'object' && !Array.isArray(value)
6
+ const fail = code => { throw Object.assign(new Error(code), { code }) }
7
+ const boundedId = value => typeof value === 'string' && /^[A-Za-z0-9_.:-]{1,128}$/.test(value)
8
+ const botIdOk = value => typeof value === 'string' && /^[A-Za-z0-9_.-]{1,128}$/.test(value)
9
+ const safeCode = (error, fallback) => /^ACTIVITY_[A-Z0-9_]{1,80}$/.test(error?.code ?? '') ? error.code : fallback
10
+
11
+ export function createNativeActivityService(options = {}) {
12
+ const allowed = ['Database', 'file', 'namespace', 'committedBot', 'nativeRouteForBot', 'redactToolName', 'now', 'ownerId', 'ownerPid', 'isProcessAlive', 'limits']
13
+ if (!plain(options) || Object.keys(options).some(key => !allowed.includes(key))) fail('ACTIVITY_SERVICE_OPTIONS_INVALID')
14
+ const { committedBot, nativeRouteForBot = () => false, ...storeOptions } = options
15
+ if (typeof committedBot !== 'function' || typeof nativeRouteForBot !== 'function') fail('ACTIVITY_SERVICE_LIFECYCLE_REQUIRED')
16
+ let store = null, closed = false, failures = 0, lastCode = null
17
+ const note = code => { failures++; lastCode = code; return { ok: false, code } }
18
+ try { store = createActivityStore(storeOptions) }
19
+ catch (error) { note(safeCode(error, 'ACTIVITY_STORE_UNAVAILABLE')) }
20
+ const assertOpen = () => { if (closed) fail('ACTIVITY_SERVICE_CLOSED') }
21
+ const lookup = botId => {
22
+ let bot
23
+ try { bot = committedBot(botId) } catch { fail('ACTIVITY_BOT_LOOKUP_FAILED') }
24
+ if (!bot || bot.id !== botId) fail('ACTIVITY_BOT_NOT_FOUND')
25
+ return bot
26
+ }
27
+ const recording = () => {
28
+ const health = store?.health()
29
+ const count = Math.max(failures, health?.failures ?? 0)
30
+ const code = lastCode ?? health?.lastCode
31
+ return Object.freeze({ status: count || !store ? 'degraded' : 'ready', failures: count,
32
+ ...(code ? { code } : {}) })
33
+ }
34
+ const observe = event => {
35
+ assertOpen()
36
+ try {
37
+ if (!plain(event) || !['turn-open', 'tool', 'turn-close', 'cancel-requested', 'stop-confirmed'].includes(event.kind)) fail('ACTIVITY_OBSERVATION_INVALID')
38
+ const keys = event.kind === 'tool' ? ['kind', 'identity', 'tool'] : event.kind === 'turn-close' ? ['kind', 'identity', 'outcome'] : ['kind', 'identity']
39
+ if (Object.keys(event).some(key => !keys.includes(key))) fail('ACTIVITY_OBSERVATION_UNKNOWN_FIELD')
40
+ const identity = activityIdentity(event.identity)
41
+ // A deleted Bot may still finish an already-observed turn. It cannot open
42
+ // new work or contribute new tool calls after leaving committed state.
43
+ if (event.kind === 'turn-open' || event.kind === 'tool') lookup(identity.botId)
44
+ if (!store) fail('ACTIVITY_STORE_UNAVAILABLE')
45
+ let result
46
+ switch (event.kind) {
47
+ case 'turn-open': result = store.openTurn(identity); break
48
+ case 'tool': result = store.recordToolEvent(identity, event.tool); break
49
+ case 'turn-close':
50
+ if (!ACTIVITY_TURN_OUTCOMES.includes(event.outcome)) fail('ACTIVITY_TURN_OUTCOME_INVALID')
51
+ result = store.closeTurn(identity, { outcome: event.outcome }); break
52
+ case 'cancel-requested': result = store.requestCancel(identity); break
53
+ case 'stop-confirmed': result = store.confirmStop(identity); break
54
+ }
55
+ return result?.ok === true ? result : note(safeCode(result, 'ACTIVITY_OBSERVATION_FAILED'))
56
+ } catch (error) { return note(safeCode(error, 'ACTIVITY_OBSERVATION_FAILED')) }
57
+ }
58
+ const list = (payload, context) => {
59
+ assertOpen()
60
+ if (context?.kind !== 'owner') fail('REMOTE_OWNER_REQUIRED')
61
+ context.signal?.throwIfAborted()
62
+ if (!plain(payload) || Object.keys(payload).some(key => !['botId', 'mode', 'sessionId', 'contextId', 'cursor', 'limit'].includes(key))) fail('ACTIVITY_LIST_PAYLOAD_INVALID')
63
+ if (!botIdOk(payload.botId)) fail('ACTIVITY_BOT_ID_INVALID')
64
+ if (!['integration', 'independent'].includes(payload.mode)) fail('ACTIVITY_MODE_INVALID')
65
+ const limit = payload.limit ?? ACTIVITY_LIMITS.defaultPageSize
66
+ if (!Number.isSafeInteger(limit) || limit < 1 || limit > ACTIVITY_LIMITS.maxPageSize) fail('ACTIVITY_QUERY_LIMIT_INVALID')
67
+ let conversationId, contextId
68
+ if (payload.mode === 'integration') {
69
+ if (Object.hasOwn(payload, 'contextId')) fail('ACTIVITY_INTEGRATION_CONTEXT_REFUSED')
70
+ conversationId = hostConversationId(payload.sessionId)
71
+ contextId = null
72
+ } else {
73
+ if (Object.hasOwn(payload, 'sessionId') || !boundedId(payload.contextId)) fail('ACTIVITY_INDEPENDENT_CONTEXT_INVALID')
74
+ conversationId = contextId = payload.contextId
75
+ }
76
+ lookup(payload.botId)
77
+ const scope = Object.freeze({ botId: payload.botId, mode: payload.mode, conversationId, contextId,
78
+ sessionId: payload.mode === 'integration' ? payload.sessionId : null })
79
+ try {
80
+ if (!store) fail('ACTIVITY_STORE_UNAVAILABLE')
81
+ const nativeRoute = nativeRouteForBot(payload.botId)
82
+ if (typeof nativeRoute !== 'boolean') fail('ACTIVITY_ROUTE_STATE_INVALID')
83
+ const page = store.page({ botId: scope.botId, mode: scope.mode, conversationId, contextId, cursor: payload.cursor ?? null,
84
+ limit: Math.min(limit, options.limits?.maxPageSize ?? ACTIVITY_LIMITS.maxPageSize) })
85
+ if (page.items.some(row => row.botId !== scope.botId || row.mode !== scope.mode || row.conversationId !== conversationId || row.contextId !== contextId)) fail('ACTIVITY_PAGE_SCOPE_MISMATCH')
86
+ context.signal?.throwIfAborted()
87
+ return Object.freeze({ schemaVersion: 1, scope, items: page.items, nextCursor: page.nextCursor, nativeRoute, recording: recording() })
88
+ } catch (error) {
89
+ if (error?.name === 'AbortError') throw error
90
+ const code = safeCode(error, 'ACTIVITY_LIST_FAILED')
91
+ fail(code)
92
+ }
93
+ }
94
+ const close = () => {
95
+ if (closed) return { closed: true, alreadyClosed: true }
96
+ closed = true
97
+ const result = store?.close()
98
+ if (result?.failures) note('ACTIVITY_STORE_CLOSE_FAILED')
99
+ return { closed: true, recording: recording() }
100
+ }
101
+ return Object.freeze({ observe, list, close, health: recording })
102
+ }