vellum 0.2.1 → 0.2.2

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 (349) hide show
  1. package/README.md +15 -2
  2. package/bun.lock +5 -2
  3. package/package.json +4 -2
  4. package/scripts/capture-x-graphql.ts +562 -0
  5. package/scripts/ipc/check-swift-decoder-drift.ts +2 -1
  6. package/scripts/test.sh +5 -0
  7. package/src/__tests__/__snapshots__/ipc-snapshot.test.ts.snap +133 -34
  8. package/src/__tests__/account-registry.test.ts +2 -1
  9. package/src/__tests__/agent-heartbeat-service.test.ts +250 -0
  10. package/src/__tests__/asset-materialize-tool.test.ts +16 -15
  11. package/src/__tests__/asset-search-tool.test.ts +23 -22
  12. package/src/__tests__/attachments-store.test.ts +56 -127
  13. package/src/__tests__/browser-skill-baseline-tool-payload.test.ts +5 -4
  14. package/src/__tests__/browser-skill-endstate.test.ts +4 -3
  15. package/src/__tests__/call-bridge.test.ts +385 -0
  16. package/src/__tests__/call-constants.test.ts +40 -0
  17. package/src/__tests__/call-orchestrator.test.ts +130 -4
  18. package/src/__tests__/call-recovery.test.ts +518 -0
  19. package/src/__tests__/call-routes-http.test.ts +459 -0
  20. package/src/__tests__/call-state-machine.test.ts +143 -0
  21. package/src/__tests__/call-store.test.ts +216 -1
  22. package/src/__tests__/cli-discover.test.ts +1 -1
  23. package/src/__tests__/commit-message-enrichment-service.test.ts +148 -7
  24. package/src/__tests__/compaction.benchmark.test.ts +176 -0
  25. package/src/__tests__/computer-use-tools.test.ts +250 -0
  26. package/src/__tests__/config-schema.test.ts +299 -3
  27. package/src/__tests__/conflict-store.test.ts +2 -1
  28. package/src/__tests__/contacts-tools.test.ts +331 -0
  29. package/src/__tests__/conversation-store.test.ts +30 -32
  30. package/src/__tests__/credential-security-invariants.test.ts +4 -0
  31. package/src/__tests__/date-context.test.ts +373 -0
  32. package/src/__tests__/db-schedule-syntax-migration.test.ts +129 -0
  33. package/src/__tests__/fixtures/media-reuse-fixtures.ts +3 -3
  34. package/src/__tests__/followup-tools.test.ts +303 -0
  35. package/src/__tests__/handlers-twitter-config.test.ts +718 -0
  36. package/src/__tests__/intent-routing.test.ts +64 -57
  37. package/src/__tests__/ipc-roundtrip.benchmark.test.ts +237 -0
  38. package/src/__tests__/ipc-snapshot.test.ts +62 -28
  39. package/src/__tests__/llm-usage-store.test.ts +3 -8
  40. package/src/__tests__/media-generate-image.test.ts +1 -1
  41. package/src/__tests__/media-reuse-story.e2e.test.ts +7 -7
  42. package/src/__tests__/memory-retrieval.benchmark.test.ts +430 -0
  43. package/src/__tests__/parallel-tool.benchmark.test.ts +294 -0
  44. package/src/__tests__/playbook-tools.test.ts +342 -0
  45. package/src/__tests__/profile-compiler.test.ts +2 -1
  46. package/src/__tests__/provider-streaming.benchmark.test.ts +773 -0
  47. package/src/__tests__/recurrence-engine-rruleset.test.ts +78 -0
  48. package/src/__tests__/recurrence-engine.test.ts +69 -0
  49. package/src/__tests__/recurrence-types.test.ts +71 -0
  50. package/src/__tests__/registry.test.ts +5 -3
  51. package/src/__tests__/relay-server.test.ts +633 -0
  52. package/src/__tests__/reminder-store.test.ts +6 -3
  53. package/src/__tests__/reminder.test.ts +43 -77
  54. package/src/__tests__/run-orchestrator-assistant-events.test.ts +8 -4
  55. package/src/__tests__/run-orchestrator.test.ts +4 -4
  56. package/src/__tests__/runtime-attachment-metadata.test.ts +7 -6
  57. package/src/__tests__/runtime-runs-http.test.ts +4 -4
  58. package/src/__tests__/runtime-runs.test.ts +4 -4
  59. package/src/__tests__/schedule-store.test.ts +482 -0
  60. package/src/__tests__/schedule-tools.test.ts +700 -0
  61. package/src/__tests__/scheduler-recurrence.test.ts +329 -0
  62. package/src/__tests__/server-history-render.test.ts +14 -13
  63. package/src/__tests__/session-error.test.ts +28 -0
  64. package/src/__tests__/session-init.benchmark.test.ts +462 -0
  65. package/src/__tests__/session-queue.test.ts +71 -48
  66. package/src/__tests__/session-runtime-assembly.test.ts +161 -0
  67. package/src/__tests__/session-surfaces-task-progress.test.ts +104 -0
  68. package/src/__tests__/signup-e2e.test.ts +2 -1
  69. package/src/__tests__/skill-projection.benchmark.test.ts +328 -0
  70. package/src/__tests__/skill-script-runner.test.ts +159 -0
  71. package/src/__tests__/speaker-identification.test.ts +52 -0
  72. package/src/__tests__/subagent-manager-notify.test.ts +42 -10
  73. package/src/__tests__/subagent-tools.test.ts +141 -41
  74. package/src/__tests__/task-compiler.test.ts +2 -1
  75. package/src/__tests__/task-runner.test.ts +2 -1
  76. package/src/__tests__/task-scheduler.test.ts +2 -1
  77. package/src/__tests__/task-tools.test.ts +49 -56
  78. package/src/__tests__/tool-audit-listener.test.ts +1 -0
  79. package/src/__tests__/tool-domain-event-publisher.test.ts +2 -0
  80. package/src/__tests__/tool-execution-pipeline.benchmark.test.ts +500 -0
  81. package/src/__tests__/tool-executor.test.ts +13 -17
  82. package/src/__tests__/turn-commit.test.ts +218 -3
  83. package/src/__tests__/twilio-provider.test.ts +143 -0
  84. package/src/__tests__/twilio-routes.test.ts +789 -0
  85. package/src/__tests__/twitter-auth-handler.test.ts +581 -0
  86. package/src/__tests__/view-image-tool.test.ts +217 -0
  87. package/src/__tests__/workspace-git-service.test.ts +186 -0
  88. package/src/__tests__/workspace-heartbeat-service.test.ts +13 -3
  89. package/src/agent-heartbeat/agent-heartbeat-service.ts +155 -0
  90. package/src/bundler/app-bundler.ts +12 -8
  91. package/src/calls/call-bridge.ts +95 -0
  92. package/src/calls/call-constants.ts +43 -5
  93. package/src/calls/call-domain.ts +276 -0
  94. package/src/calls/call-orchestrator.ts +43 -17
  95. package/src/calls/call-recovery.ts +207 -0
  96. package/src/calls/call-state-machine.ts +68 -0
  97. package/src/calls/call-store.ts +192 -5
  98. package/src/calls/relay-server.ts +41 -4
  99. package/src/calls/speaker-identification.ts +213 -0
  100. package/src/calls/twilio-provider.ts +10 -6
  101. package/src/calls/twilio-routes.ts +90 -76
  102. package/src/calls/types.ts +1 -1
  103. package/src/cli/config-commands.ts +334 -0
  104. package/src/cli/core-commands.ts +776 -0
  105. package/src/cli/doordash.ts +251 -1
  106. package/src/cli/ipc-client.ts +82 -0
  107. package/src/cli/map.ts +246 -0
  108. package/src/cli/twitter.ts +575 -0
  109. package/src/cli.ts +7 -5
  110. package/src/commands/__tests__/cc-command-registry.test.ts +319 -0
  111. package/src/commands/cc-command-registry.ts +209 -0
  112. package/src/config/bundled-skills/contacts/SKILL.md +39 -0
  113. package/src/config/bundled-skills/contacts/TOOLS.json +122 -0
  114. package/src/config/bundled-skills/contacts/tools/contact-merge.ts +9 -0
  115. package/src/config/bundled-skills/contacts/tools/contact-search.ts +9 -0
  116. package/src/config/bundled-skills/contacts/tools/contact-upsert.ts +9 -0
  117. package/src/config/bundled-skills/document/SKILL.md +18 -0
  118. package/src/config/bundled-skills/document/TOOLS.json +53 -0
  119. package/src/config/bundled-skills/document/tools/document-create.ts +9 -0
  120. package/src/config/bundled-skills/document/tools/document-update.ts +9 -0
  121. package/src/config/bundled-skills/doordash/SKILL.md +82 -23
  122. package/src/config/bundled-skills/followups/SKILL.md +32 -0
  123. package/src/config/bundled-skills/followups/TOOLS.json +100 -0
  124. package/src/config/bundled-skills/followups/tools/followup-create.ts +9 -0
  125. package/src/config/bundled-skills/followups/tools/followup-list.ts +9 -0
  126. package/src/config/bundled-skills/followups/tools/followup-resolve.ts +9 -0
  127. package/src/config/bundled-skills/image-studio/tools/media-generate-image.ts +1 -23
  128. package/src/config/bundled-skills/messaging/tools/messaging-analyze-style.ts +2 -1
  129. package/src/config/bundled-skills/playbooks/SKILL.md +31 -0
  130. package/src/config/bundled-skills/playbooks/TOOLS.json +126 -0
  131. package/src/config/bundled-skills/playbooks/tools/playbook-create.ts +9 -0
  132. package/src/config/bundled-skills/playbooks/tools/playbook-delete.ts +9 -0
  133. package/src/config/bundled-skills/playbooks/tools/playbook-list.ts +9 -0
  134. package/src/config/bundled-skills/playbooks/tools/playbook-update.ts +9 -0
  135. package/src/config/bundled-skills/reminder/SKILL.md +20 -0
  136. package/src/config/bundled-skills/reminder/TOOLS.json +67 -0
  137. package/src/config/bundled-skills/reminder/tools/reminder-cancel.ts +9 -0
  138. package/src/config/bundled-skills/reminder/tools/reminder-create.ts +9 -0
  139. package/src/config/bundled-skills/reminder/tools/reminder-list.ts +9 -0
  140. package/src/config/bundled-skills/schedule/SKILL.md +74 -0
  141. package/src/config/bundled-skills/schedule/TOOLS.json +135 -0
  142. package/src/config/bundled-skills/schedule/tools/schedule-create.ts +9 -0
  143. package/src/config/bundled-skills/schedule/tools/schedule-delete.ts +9 -0
  144. package/src/config/bundled-skills/schedule/tools/schedule-list.ts +9 -0
  145. package/src/config/bundled-skills/schedule/tools/schedule-update.ts +9 -0
  146. package/src/config/bundled-skills/subagent/SKILL.md +25 -0
  147. package/src/config/bundled-skills/subagent/TOOLS.json +107 -0
  148. package/src/config/bundled-skills/subagent/tools/subagent-abort.ts +9 -0
  149. package/src/config/bundled-skills/subagent/tools/subagent-message.ts +9 -0
  150. package/src/config/bundled-skills/subagent/tools/subagent-read.ts +9 -0
  151. package/src/config/bundled-skills/subagent/tools/subagent-spawn.ts +9 -0
  152. package/src/config/bundled-skills/subagent/tools/subagent-status.ts +9 -0
  153. package/src/config/bundled-skills/tasks/SKILL.md +28 -0
  154. package/src/config/bundled-skills/tasks/TOOLS.json +256 -0
  155. package/src/config/bundled-skills/tasks/tools/task-delete.ts +9 -0
  156. package/src/config/bundled-skills/tasks/tools/task-list-add.ts +9 -0
  157. package/src/config/bundled-skills/tasks/tools/task-list-remove.ts +9 -0
  158. package/src/config/bundled-skills/tasks/tools/task-list-show.ts +9 -0
  159. package/src/config/bundled-skills/tasks/tools/task-list-update.ts +9 -0
  160. package/src/config/bundled-skills/tasks/tools/task-list.ts +9 -0
  161. package/src/config/bundled-skills/tasks/tools/task-run.ts +9 -0
  162. package/src/config/bundled-skills/tasks/tools/task-save.ts +9 -0
  163. package/src/config/bundled-skills/twitter/SKILL.md +134 -0
  164. package/src/config/bundled-skills/watcher/SKILL.md +27 -0
  165. package/src/config/bundled-skills/watcher/TOOLS.json +147 -0
  166. package/src/config/bundled-skills/watcher/tools/watcher-create.ts +9 -0
  167. package/src/config/bundled-skills/watcher/tools/watcher-delete.ts +9 -0
  168. package/src/config/bundled-skills/watcher/tools/watcher-digest.ts +9 -0
  169. package/src/config/bundled-skills/watcher/tools/watcher-list.ts +9 -0
  170. package/src/config/bundled-skills/watcher/tools/watcher-update.ts +9 -0
  171. package/src/config/defaults.ts +33 -0
  172. package/src/config/loader.ts +4 -1
  173. package/src/config/schema.ts +161 -1
  174. package/src/config/system-prompt.ts +61 -16
  175. package/src/config/templates/IDENTITY.md +7 -0
  176. package/src/config/types.ts +4 -0
  177. package/src/contacts/contact-store.ts +4 -4
  178. package/src/daemon/assistant-attachments.ts +10 -0
  179. package/src/daemon/classifier.ts +3 -1
  180. package/src/daemon/computer-use-session.ts +3 -1
  181. package/src/daemon/date-context.ts +136 -0
  182. package/src/daemon/handlers/apps.ts +16 -1
  183. package/src/daemon/handlers/browser.ts +54 -0
  184. package/src/daemon/handlers/computer-use.ts +7 -1
  185. package/src/daemon/handlers/config.ts +163 -5
  186. package/src/daemon/handlers/diagnostics.ts +5 -1
  187. package/src/daemon/handlers/documents.ts +18 -29
  188. package/src/daemon/handlers/home-base.ts +5 -1
  189. package/src/daemon/handlers/index.ts +40 -277
  190. package/src/daemon/handlers/misc.ts +9 -1
  191. package/src/daemon/handlers/publish.ts +6 -1
  192. package/src/daemon/handlers/sessions.ts +65 -12
  193. package/src/daemon/handlers/shared.ts +36 -1
  194. package/src/daemon/handlers/signing.ts +37 -0
  195. package/src/daemon/handlers/skills.ts +20 -6
  196. package/src/daemon/handlers/subagents.ts +8 -3
  197. package/src/daemon/handlers/twitter-auth.ts +169 -0
  198. package/src/daemon/handlers/work-items.ts +384 -68
  199. package/src/daemon/ipc-contract-inventory.json +28 -4
  200. package/src/daemon/ipc-contract.ts +133 -37
  201. package/src/daemon/ipc-protocol.ts +7 -2
  202. package/src/daemon/lifecycle.ts +21 -0
  203. package/src/daemon/main.ts +10 -4
  204. package/src/daemon/ride-shotgun-handler.ts +74 -10
  205. package/src/daemon/server.ts +143 -26
  206. package/src/daemon/session-agent-loop.ts +887 -0
  207. package/src/daemon/session-attachments.ts +28 -5
  208. package/src/daemon/session-error.ts +24 -3
  209. package/src/daemon/session-lifecycle.ts +147 -0
  210. package/src/daemon/session-media-retry.ts +147 -0
  211. package/src/daemon/session-messaging.ts +145 -0
  212. package/src/daemon/session-notifiers.ts +164 -0
  213. package/src/daemon/session-process.ts +2 -2
  214. package/src/daemon/session-queue-manager.ts +1 -0
  215. package/src/daemon/session-runtime-assembly.ts +52 -0
  216. package/src/daemon/session-skill-tools.ts +124 -5
  217. package/src/daemon/session-slash.ts +3 -0
  218. package/src/daemon/session-surfaces.ts +77 -2
  219. package/src/daemon/session-tool-setup.ts +216 -2
  220. package/src/daemon/session-usage.ts +0 -2
  221. package/src/daemon/session.ts +114 -1404
  222. package/src/daemon/video-thumbnail.ts +60 -0
  223. package/src/doordash/client.ts +121 -27
  224. package/src/doordash/queries.ts +1 -2
  225. package/src/export/formatter.ts +3 -1
  226. package/src/followups/followup-store.ts +4 -2
  227. package/src/followups/types.ts +6 -0
  228. package/src/hooks/templates.ts +1 -1
  229. package/src/index.ts +32 -1153
  230. package/src/memory/attachments-store.ts +28 -83
  231. package/src/memory/channel-delivery-store.ts +7 -21
  232. package/src/memory/clarification-resolver.ts +6 -5
  233. package/src/memory/contradiction-checker.ts +3 -2
  234. package/src/memory/conversation-key-store.ts +10 -29
  235. package/src/memory/conversation-store.ts +2 -1
  236. package/src/memory/db.ts +96 -2
  237. package/src/memory/entity-extractor.ts +6 -3
  238. package/src/memory/items-extractor.ts +5 -4
  239. package/src/memory/jobs-store.ts +3 -2
  240. package/src/memory/llm-usage-store.ts +1 -2
  241. package/src/memory/runs-store.ts +1 -2
  242. package/src/memory/schema.ts +23 -2
  243. package/src/messaging/style-analyzer.ts +3 -2
  244. package/src/messaging/thread-summarizer.ts +8 -12
  245. package/src/messaging/triage-engine.ts +4 -2
  246. package/src/providers/openrouter/client.ts +20 -0
  247. package/src/providers/registry.ts +8 -0
  248. package/src/runtime/http-server.ts +108 -20
  249. package/src/runtime/routes/attachment-routes.ts +2 -3
  250. package/src/runtime/routes/call-routes.ts +140 -0
  251. package/src/runtime/routes/channel-routes.ts +5 -10
  252. package/src/runtime/routes/conversation-routes.ts +5 -5
  253. package/src/runtime/routes/run-routes.ts +2 -2
  254. package/src/runtime/run-orchestrator.ts +9 -3
  255. package/src/schedule/recurrence-engine.ts +138 -0
  256. package/src/schedule/recurrence-types.ts +67 -0
  257. package/src/schedule/schedule-store.ts +102 -57
  258. package/src/schedule/scheduler.ts +9 -6
  259. package/src/security/oauth2.ts +29 -4
  260. package/src/security/secret-allowlist.ts +46 -0
  261. package/src/skills/clawhub.ts +1 -1
  262. package/src/subagent/manager.ts +40 -8
  263. package/src/swarm/backend-claude-code.ts +64 -9
  264. package/src/swarm/worker-prompts.ts +2 -1
  265. package/src/tasks/SPEC.md +34 -28
  266. package/src/tasks/ephemeral-permissions.ts +16 -7
  267. package/src/tasks/task-compiler.ts +5 -4
  268. package/src/tasks/task-runner.ts +10 -5
  269. package/src/tasks/task-scheduler.ts +1 -1
  270. package/src/tasks/tool-sanitizer.ts +36 -0
  271. package/src/tools/assets/search.ts +4 -4
  272. package/src/tools/browser/api-map.ts +220 -0
  273. package/src/tools/browser/auto-navigate.ts +270 -0
  274. package/src/tools/browser/browser-execution.ts +2 -1
  275. package/src/tools/browser/browser-manager.ts +2 -2
  276. package/src/tools/browser/network-recorder.ts +5 -4
  277. package/src/tools/browser/x-auto-navigate.ts +207 -0
  278. package/src/tools/calls/call-end.ts +17 -67
  279. package/src/tools/calls/call-start.ts +24 -85
  280. package/src/tools/calls/call-status.ts +35 -51
  281. package/src/tools/claude-code/claude-code.ts +77 -11
  282. package/src/tools/contacts/contact-merge.ts +46 -78
  283. package/src/tools/contacts/contact-search.ts +35 -79
  284. package/src/tools/contacts/contact-upsert.ts +35 -108
  285. package/src/tools/credentials/vault.ts +20 -4
  286. package/src/tools/document/document-tool.ts +71 -144
  287. package/src/tools/executor.ts +129 -10
  288. package/src/tools/followups/followup_create.ts +46 -88
  289. package/src/tools/followups/followup_list.ts +34 -74
  290. package/src/tools/followups/followup_resolve.ts +31 -66
  291. package/src/tools/host-terminal/cli-discover.ts +2 -1
  292. package/src/tools/host-terminal/host-shell.ts +10 -0
  293. package/src/tools/memory/handlers.ts +5 -4
  294. package/src/tools/network/__tests__/web-search.test.ts +427 -0
  295. package/src/tools/network/script-proxy/__tests__/logging.test.ts +248 -0
  296. package/src/tools/network/script-proxy/__tests__/policy.test.ts +234 -0
  297. package/src/tools/network/script-proxy/__tests__/router.test.ts +76 -0
  298. package/src/tools/network/web-fetch.ts +18 -6
  299. package/src/tools/playbooks/index.ts +4 -5
  300. package/src/tools/playbooks/playbook-create.ts +3 -47
  301. package/src/tools/playbooks/playbook-delete.ts +1 -25
  302. package/src/tools/playbooks/playbook-list.ts +1 -28
  303. package/src/tools/playbooks/playbook-update.ts +3 -51
  304. package/src/tools/reminder/reminder.ts +5 -78
  305. package/src/tools/schedule/create.ts +69 -74
  306. package/src/tools/schedule/delete.ts +21 -47
  307. package/src/tools/schedule/list.ts +55 -74
  308. package/src/tools/schedule/update.ts +77 -84
  309. package/src/tools/subagent/abort.ts +29 -58
  310. package/src/tools/subagent/message.ts +30 -63
  311. package/src/tools/subagent/read.ts +53 -84
  312. package/src/tools/subagent/spawn.ts +43 -82
  313. package/src/tools/subagent/status.ts +42 -71
  314. package/src/tools/swarm/delegate.ts +2 -1
  315. package/src/tools/tasks/index.ts +8 -8
  316. package/src/tools/tasks/task-delete.ts +60 -88
  317. package/src/tools/tasks/task-list.ts +31 -52
  318. package/src/tools/tasks/task-run.ts +72 -108
  319. package/src/tools/tasks/task-save.ts +33 -65
  320. package/src/tools/tasks/work-item-enqueue.ts +183 -215
  321. package/src/tools/tasks/work-item-list.ts +33 -63
  322. package/src/tools/tasks/work-item-remove.ts +45 -97
  323. package/src/tools/tasks/work-item-update.ts +91 -163
  324. package/src/tools/terminal/backends/native.ts +3 -1
  325. package/src/tools/tool-manifest.ts +0 -62
  326. package/src/tools/types.ts +6 -0
  327. package/src/tools/ui-surface/definitions.ts +3 -1
  328. package/src/tools/watch/screen-watch.ts +3 -1
  329. package/src/tools/watcher/create.ts +52 -98
  330. package/src/tools/watcher/delete.ts +20 -46
  331. package/src/tools/watcher/digest.ts +36 -70
  332. package/src/tools/watcher/list.ts +49 -79
  333. package/src/tools/watcher/update.ts +45 -91
  334. package/src/twitter/client.ts +690 -0
  335. package/src/twitter/session.ts +91 -0
  336. package/src/usage/types.ts +0 -1
  337. package/src/util/truncate.ts +6 -0
  338. package/src/watcher/providers/slack.ts +2 -1
  339. package/src/watcher/watcher-store.ts +3 -2
  340. package/src/work-items/work-item-store.ts +27 -2
  341. package/src/workspace/commit-message-enrichment-service.ts +31 -7
  342. package/src/workspace/git-service.ts +87 -22
  343. package/src/workspace/provider-commit-message-generator.ts +242 -0
  344. package/src/workspace/turn-commit.ts +62 -3
  345. package/src/tools/contacts/index.ts +0 -4
  346. package/src/tools/document/index.ts +0 -5
  347. package/src/tools/followups/index.ts +0 -3
  348. package/src/tools/subagent/index.ts +0 -5
  349. /package/src/__tests__/{memory-context-benchmark.test.ts → memory-context-benchmark.benchmark.test.ts} +0 -0
@@ -0,0 +1,164 @@
1
+ /**
2
+ * Watch and call notifier registration/unregistration, extracted from
3
+ * the Session constructor and dispose/abort methods.
4
+ *
5
+ * Notifier callbacks read from the provided context object at invocation
6
+ * time (not registration time), so they always see the latest sendToClient
7
+ * and messages references even after updateClient().
8
+ */
9
+
10
+ import type { Message } from '../providers/types.js';
11
+ import type { ServerMessage } from './ipc-protocol.js';
12
+ import { createAssistantMessage } from '../agent/message-types.js';
13
+ import * as conversationStore from '../memory/conversation-store.js';
14
+ import {
15
+ registerWatchStartNotifier,
16
+ unregisterWatchStartNotifier,
17
+ registerWatchCommentaryNotifier,
18
+ unregisterWatchCommentaryNotifier,
19
+ registerWatchCompletionNotifier,
20
+ unregisterWatchCompletionNotifier,
21
+ pruneWatchSessions,
22
+ } from '../tools/watch/watch-state.js';
23
+ import type { WatchSession } from '../tools/watch/watch-state.js';
24
+ import { lastCommentaryBySession, lastSummaryBySession } from './watch-handler.js';
25
+ import {
26
+ registerCallQuestionNotifier,
27
+ unregisterCallQuestionNotifier,
28
+ registerCallCompletionNotifier,
29
+ unregisterCallCompletionNotifier,
30
+ } from '../calls/call-state.js';
31
+ import { getCallSession, getCallEvents } from '../calls/call-store.js';
32
+
33
+ /**
34
+ * Subset of Session state that notifier callbacks need to read at
35
+ * invocation time. Properties are read lazily from this reference.
36
+ */
37
+ export interface NotifierSessionContext {
38
+ sendToClient: (msg: ServerMessage) => void;
39
+ messages: Message[];
40
+ }
41
+
42
+ /**
43
+ * Register watch and call notifiers for a session. Call once during
44
+ * construction; the notifier callbacks close over `ctx` so they see
45
+ * live sendToClient/messages values.
46
+ */
47
+ export function registerSessionNotifiers(
48
+ conversationId: string,
49
+ ctx: NotifierSessionContext,
50
+ ): void {
51
+ registerWatchStartNotifier(conversationId, (session: WatchSession) => {
52
+ ctx.sendToClient({
53
+ type: 'watch_started',
54
+ sessionId: conversationId,
55
+ watchId: session.watchId,
56
+ durationSeconds: session.durationSeconds,
57
+ intervalSeconds: session.intervalSeconds,
58
+ });
59
+ });
60
+
61
+ registerWatchCommentaryNotifier(conversationId, (_session: WatchSession) => {
62
+ const commentary = lastCommentaryBySession.get(conversationId);
63
+ if (commentary) {
64
+ lastCommentaryBySession.delete(conversationId);
65
+ ctx.sendToClient({
66
+ type: 'assistant_text_delta',
67
+ text: commentary,
68
+ sessionId: conversationId,
69
+ });
70
+ ctx.sendToClient({
71
+ type: 'message_complete',
72
+ sessionId: conversationId,
73
+ });
74
+ }
75
+ });
76
+
77
+ registerWatchCompletionNotifier(conversationId, (_session: WatchSession) => {
78
+ const summary = lastSummaryBySession.get(conversationId);
79
+ if (summary) {
80
+ lastSummaryBySession.delete(conversationId);
81
+ ctx.sendToClient({
82
+ type: 'assistant_text_delta',
83
+ text: summary,
84
+ sessionId: conversationId,
85
+ });
86
+ ctx.sendToClient({
87
+ type: 'message_complete',
88
+ sessionId: conversationId,
89
+ });
90
+ }
91
+ });
92
+
93
+ registerCallQuestionNotifier(conversationId, (callSessionId: string, question: string) => {
94
+ const callSession = getCallSession(callSessionId);
95
+ const callee = callSession?.toNumber ?? 'the caller';
96
+ const questionText = `**Live call question** (to ${callee}):\n\n${question}\n\n_Reply in this thread to answer._`;
97
+
98
+ conversationStore.addMessage(
99
+ conversationId,
100
+ 'assistant',
101
+ JSON.stringify([{ type: 'text', text: questionText }]),
102
+ );
103
+
104
+ ctx.messages.push(createAssistantMessage(questionText));
105
+
106
+ ctx.sendToClient({
107
+ type: 'assistant_text_delta',
108
+ text: questionText,
109
+ sessionId: conversationId,
110
+ });
111
+ ctx.sendToClient({
112
+ type: 'message_complete',
113
+ sessionId: conversationId,
114
+ });
115
+ });
116
+
117
+ registerCallCompletionNotifier(conversationId, (callSessionId: string) => {
118
+ const callSession = getCallSession(callSessionId);
119
+ const events = getCallEvents(callSessionId);
120
+ const duration = callSession?.endedAt && callSession?.startedAt
121
+ ? Math.round((callSession.endedAt - callSession.startedAt) / 1000)
122
+ : null;
123
+ const durationStr = duration !== null ? ` (${duration}s)` : '';
124
+ const summaryText = `**Call completed**${durationStr}. ${events.length} event(s) recorded.`;
125
+
126
+ conversationStore.addMessage(
127
+ conversationId,
128
+ 'assistant',
129
+ JSON.stringify([{ type: 'text', text: summaryText }]),
130
+ );
131
+
132
+ ctx.messages.push(createAssistantMessage(summaryText));
133
+
134
+ ctx.sendToClient({
135
+ type: 'assistant_text_delta',
136
+ text: summaryText,
137
+ sessionId: conversationId,
138
+ });
139
+ ctx.sendToClient({
140
+ type: 'message_complete',
141
+ sessionId: conversationId,
142
+ });
143
+ });
144
+ }
145
+
146
+ /**
147
+ * Unregister watch notifiers and prune watch sessions. Called during
148
+ * abort when the session is actively processing.
149
+ */
150
+ export function unregisterWatchNotifiers(conversationId: string): void {
151
+ unregisterWatchStartNotifier(conversationId);
152
+ unregisterWatchCommentaryNotifier(conversationId);
153
+ unregisterWatchCompletionNotifier(conversationId);
154
+ pruneWatchSessions(conversationId);
155
+ }
156
+
157
+ /**
158
+ * Unregister call notifiers. Called during dispose regardless of
159
+ * processing state (notifiers are registered in the constructor).
160
+ */
161
+ export function unregisterCallNotifiers(conversationId: string): void {
162
+ unregisterCallQuestionNotifier(conversationId);
163
+ unregisterCallCompletionNotifier(conversationId);
164
+ }
@@ -55,7 +55,7 @@ export interface ProcessSessionContext {
55
55
  currentPage?: string;
56
56
  /** Request-scoped skill IDs preactivated via slash resolution. */
57
57
  preactivatedSkillIds?: string[];
58
- persistUserMessage(content: string, attachments: UserMessageAttachment[], requestId?: string): string;
58
+ persistUserMessage(content: string, attachments: UserMessageAttachment[], requestId?: string, metadata?: Record<string, unknown>): string;
59
59
  runAgentLoop(
60
60
  content: string,
61
61
  userMessageId: string,
@@ -155,7 +155,7 @@ export function drainQueue(session: ProcessSessionContext, reason: QueueDrainRea
155
155
  // resolves early (no runAgentLoop call), so we must continue draining.
156
156
  let userMessageId: string;
157
157
  try {
158
- userMessageId = session.persistUserMessage(resolvedContent, next.attachments, next.requestId);
158
+ userMessageId = session.persistUserMessage(resolvedContent, next.attachments, next.requestId, next.metadata);
159
159
  } catch (err) {
160
160
  const message = err instanceof Error ? err.message : String(err);
161
161
  log.error({ err, conversationId: session.conversationId, requestId: next.requestId }, 'Failed to persist queued message');
@@ -14,6 +14,7 @@ export interface QueuedMessage {
14
14
  onEvent: (msg: ServerMessage) => void;
15
15
  activeSurfaceId?: string;
16
16
  currentPage?: string;
17
+ metadata?: Record<string, unknown>;
17
18
  }
18
19
 
19
20
  export const MAX_QUEUE_DEPTH = 10;
@@ -312,6 +312,44 @@ export function stripWorkspaceTopLevelContext(messages: Message[]): Message[] {
312
312
  }).filter((message): message is NonNullable<typeof message> => message !== null);
313
313
  }
314
314
 
315
+ /**
316
+ * Prepend temporal context to a user message so the model has
317
+ * authoritative date/time grounding each turn.
318
+ */
319
+ export function injectTemporalContext(message: Message, temporalContext: string): Message {
320
+ return {
321
+ ...message,
322
+ content: [
323
+ { type: 'text', text: temporalContext },
324
+ ...message.content,
325
+ ],
326
+ };
327
+ }
328
+
329
+ /**
330
+ * Strip `<temporal_context>` blocks injected by `injectTemporalContext`.
331
+ * Called after the agent run to prevent temporal context from persisting
332
+ * in session history.
333
+ *
334
+ * Uses a specific prefix (`<temporal_context>\nToday:`) so that
335
+ * user-authored text that happens to start with `<temporal_context>`
336
+ * is preserved.
337
+ */
338
+ const TEMPORAL_INJECTED_PREFIX = '<temporal_context>\nToday:';
339
+
340
+ export function stripTemporalContext(messages: Message[]): Message[] {
341
+ return messages.map((message) => {
342
+ if (message.role !== 'user') return message;
343
+ const nextContent = message.content.filter((block) => {
344
+ if (block.type !== 'text') return true;
345
+ return !block.text.startsWith(TEMPORAL_INJECTED_PREFIX);
346
+ });
347
+ if (nextContent.length === message.content.length) return message;
348
+ if (nextContent.length === 0) return null;
349
+ return { ...message, content: nextContent };
350
+ }).filter((message): message is NonNullable<typeof message> => message !== null);
351
+ }
352
+
315
353
  /**
316
354
  * Strip `<active_workspace>` (and legacy `<active_dynamic_page>`) blocks
317
355
  * injected by `injectActiveSurfaceContext`. Called after the agent run to
@@ -344,6 +382,7 @@ export function applyRuntimeInjections(
344
382
  activeSurface?: ActiveSurfaceContext | null;
345
383
  workspaceTopLevelContext?: string | null;
346
384
  channelCapabilities?: ChannelCapabilities | null;
385
+ temporalContext?: string | null;
347
386
  },
348
387
  ): Message[] {
349
388
  let result = runMessages;
@@ -378,6 +417,19 @@ export function applyRuntimeInjections(
378
417
  }
379
418
  }
380
419
 
420
+ // Temporal context is injected before workspace top-level so it
421
+ // appears after workspace context in the final message content
422
+ // (both are prepended, so later injections appear first).
423
+ if (options.temporalContext) {
424
+ const userTail = result[result.length - 1];
425
+ if (userTail && userTail.role === 'user') {
426
+ result = [
427
+ ...result.slice(0, -1),
428
+ injectTemporalContext(userTail, options.temporalContext),
429
+ ];
430
+ }
431
+ }
432
+
381
433
  // Workspace top-level context is injected last so it appears first
382
434
  // (prepended) in the user message content, keeping cache breakpoints
383
435
  // anchored to the trailing blocks.
@@ -10,6 +10,7 @@
10
10
 
11
11
  import type { Message, ToolDefinition } from '../providers/types.js';
12
12
  import type { SkillSummary, SkillToolManifest } from '../config/skills.js';
13
+ import type { ActiveSkillEntry } from '../skills/active-skill-tools.js';
13
14
  import { loadSkillCatalog } from '../config/skills.js';
14
15
  import { deriveActiveSkills } from '../skills/active-skill-tools.js';
15
16
 
@@ -38,6 +39,32 @@ export interface SkillToolProjection {
38
39
  allowedToolNames: Set<string>;
39
40
  }
40
41
 
42
+ /**
43
+ * Session-scoped cache for skill projection. Avoids re-scanning the entire
44
+ * conversation history and re-reading the filesystem on every agent turn.
45
+ *
46
+ * Each session should own its own cache instance to prevent cross-session
47
+ * state bleed.
48
+ */
49
+ export interface SkillProjectionCache {
50
+ /** Cached deriveActiveSkills result. */
51
+ derived?: {
52
+ /** Number of messages in history when this cache was last computed. */
53
+ messageCount: number;
54
+ /** Reference to the first message when cache was computed. Compaction
55
+ * replaces the first message with a new summary object, so a reference
56
+ * mismatch signals that history was rewritten even if the count matches. */
57
+ firstMessage: Message | undefined;
58
+ /** IDs already seen — used for deduplication during incremental scans. */
59
+ seenIds: Set<string>;
60
+ /** The accumulated active skill entries. */
61
+ entries: ActiveSkillEntry[];
62
+ };
63
+ /** Cached skill catalog. Invalidated when the session is marked stale
64
+ * (e.g. skill directories changed on disk while a run is in progress). */
65
+ catalog?: SkillSummary[];
66
+ }
67
+
41
68
  export interface ProjectSkillToolsOptions {
42
69
  /** Skill IDs that should be treated as active regardless of history markers. */
43
70
  preactivatedSkillIds?: string[];
@@ -49,6 +76,12 @@ export interface ProjectSkillToolsOptions {
49
76
  * unregistered and re-registered with the updated definitions.
50
77
  */
51
78
  previouslyActiveSkillIds?: Map<string, string>;
79
+ /**
80
+ * Session-scoped projection cache. When provided, projectSkillTools will
81
+ * avoid redundant deriveActiveSkills scans and loadSkillCatalog filesystem
82
+ * reads across agent turns.
83
+ */
84
+ cache?: SkillProjectionCache;
52
85
  }
53
86
 
54
87
  // ---------------------------------------------------------------------------
@@ -73,6 +106,82 @@ function loadManifestForSkill(skill: SkillSummary): SkillToolManifest | null {
73
106
  }
74
107
  }
75
108
 
109
+ // ---------------------------------------------------------------------------
110
+ // Cache helpers
111
+ // ---------------------------------------------------------------------------
112
+
113
+ /**
114
+ * Return active skill entries, using the projection cache when available.
115
+ *
116
+ * History is append-only within a session (messages are only added, never
117
+ * mutated in place). If history.length hasn't changed since the last scan,
118
+ * the cached result is returned immediately. If new messages were appended,
119
+ * only the delta is scanned and merged. If history shrank (e.g. compression
120
+ * replaced earlier messages), the cache is invalidated and a full rescan
121
+ * is performed.
122
+ */
123
+ function getCachedActiveSkills(
124
+ history: Message[],
125
+ cache?: SkillProjectionCache,
126
+ ): ActiveSkillEntry[] {
127
+ if (!cache) return deriveActiveSkills(history);
128
+
129
+ const cached = cache.derived;
130
+
131
+ // Fast path: history unchanged since last scan. Both the count and the
132
+ // first message reference must match — compaction can rewrite history
133
+ // without changing the total count.
134
+ if (cached && cached.messageCount === history.length && cached.firstMessage === history[0]) {
135
+ return cached.entries;
136
+ }
137
+
138
+ // History grew (and first message is unchanged) — scan only the new messages.
139
+ if (cached && cached.messageCount < history.length && cached.firstMessage === history[0]) {
140
+ const delta = history.slice(cached.messageCount);
141
+ const newEntries = deriveActiveSkills(delta);
142
+
143
+ // Merge: add any entries not already seen.
144
+ let changed = false;
145
+ for (const entry of newEntries) {
146
+ if (!cached.seenIds.has(entry.id)) {
147
+ cached.seenIds.add(entry.id);
148
+ cached.entries.push(entry);
149
+ changed = true;
150
+ }
151
+ }
152
+
153
+ cached.messageCount = history.length;
154
+ if (changed) {
155
+ log.debug(
156
+ { newEntries: newEntries.length, total: cached.entries.length },
157
+ 'Incremental skill derivation found new entries',
158
+ );
159
+ }
160
+ return cached.entries;
161
+ }
162
+
163
+ // History shrank, compaction rewrote it, or no cache yet — full rescan.
164
+ const entries = deriveActiveSkills(history);
165
+ const seenIds = new Set(entries.map((e) => e.id));
166
+ cache.derived = { messageCount: history.length, firstMessage: history[0], seenIds, entries };
167
+ return entries;
168
+ }
169
+
170
+ /**
171
+ * Return the skill catalog, caching it across agent turns.
172
+ *
173
+ * The cache is invalidated when the session is marked stale (e.g. skill
174
+ * directories changed on disk while the session is still processing).
175
+ */
176
+ function getCachedCatalog(cache?: SkillProjectionCache): SkillSummary[] {
177
+ if (!cache) return loadSkillCatalog();
178
+
179
+ if (!cache.catalog) {
180
+ cache.catalog = loadSkillCatalog();
181
+ }
182
+ return cache.catalog;
183
+ }
184
+
76
185
  // ---------------------------------------------------------------------------
77
186
  // Main export
78
187
  // ---------------------------------------------------------------------------
@@ -90,7 +199,7 @@ export function projectSkillTools(
90
199
  history: Message[],
91
200
  options?: ProjectSkillToolsOptions,
92
201
  ): SkillToolProjection {
93
- const contextEntries = deriveActiveSkills(history);
202
+ const contextEntries = getCachedActiveSkills(history, options?.cache);
94
203
  const preactivated = options?.preactivatedSkillIds ?? [];
95
204
  const prevActive = options?.previouslyActiveSkillIds ?? new Map<string, string>();
96
205
 
@@ -128,8 +237,8 @@ export function projectSkillTools(
128
237
  return { toolDefinitions: [], allowedToolNames: new Set() };
129
238
  }
130
239
 
131
- // Load the catalog once and index by ID for efficient lookup
132
- const catalog = loadSkillCatalog();
240
+ // Load the catalog (cached for session lifetime) and index by ID
241
+ const catalog = getCachedCatalog(options?.cache);
133
242
  const catalogById = new Map<string, SkillSummary>();
134
243
  for (const skill of catalog) {
135
244
  catalogById.set(skill.id, skill);
@@ -138,6 +247,9 @@ export function projectSkillTools(
138
247
  const allToolDefinitions: ToolDefinition[] = [];
139
248
  const allToolNames = new Set<string>();
140
249
  const successfulEntries = new Map<string, string>();
250
+ // Track skills already unregistered in the version-change branch so the
251
+ // transiently-failed cleanup loop doesn't double-decrement their refcount.
252
+ const alreadyUnregistered = new Set<string>();
141
253
 
142
254
  for (const skillId of activeIds) {
143
255
  const skill = catalogById.get(skillId);
@@ -178,7 +290,14 @@ export function projectSkillTools(
178
290
  // Hash changed — unregister stale tools, then re-register with new definitions
179
291
  log.info({ skillId, prevHash, currentHash }, 'Skill version changed, re-registering tools');
180
292
  unregisterSkillTools(skillId);
181
- registerSkillTools(tools);
293
+ alreadyUnregistered.add(skillId);
294
+ try {
295
+ registerSkillTools(tools);
296
+ } catch (err) {
297
+ log.error({ err, skillId }, 'Failed to re-register skill tools after version change');
298
+ // Don't add to successfulEntries — will be cleaned up as transiently-failed
299
+ continue;
300
+ }
182
301
  } else {
183
302
  // Hash unchanged — check if the bundled status drifted (e.g. a
184
303
  // managed skill override was added/removed with identical content).
@@ -204,7 +323,7 @@ export function projectSkillTools(
204
323
  // skill would be re-registered when it recovers next turn, inflating the
205
324
  // refcount since the prior registration was never decremented.
206
325
  for (const id of prevActive.keys()) {
207
- if (activeIds.has(id) && !successfulEntries.has(id)) {
326
+ if (activeIds.has(id) && !successfulEntries.has(id) && !alreadyUnregistered.has(id)) {
208
327
  log.info({ skillId: id }, 'Unregistering tools for transiently-failed skill');
209
328
  unregisterSkillTools(id);
210
329
  }
@@ -51,6 +51,9 @@ const PROVIDER_MODEL_SHORTCUTS: Record<string, { provider: string; model: string
51
51
 
52
52
  // Fireworks
53
53
  'fireworks': { provider: 'fireworks', model: 'accounts/fireworks/models/kimi-k2p5', displayName: 'Kimi K2.5' },
54
+
55
+ // OpenRouter
56
+ 'openrouter': { provider: 'openrouter', model: 'x-ai/grok-4', displayName: 'Grok 4 (OpenRouter)' },
54
57
  };
55
58
 
56
59
  /** Reverse lookup: model ID → provider, derived from PROVIDER_MODEL_SHORTCUTS. */
@@ -4,6 +4,7 @@ import type {
4
4
  ServerMessage,
5
5
  SurfaceType,
6
6
  SurfaceData,
7
+ CardSurfaceData,
7
8
  DynamicPageSurfaceData,
8
9
  FileUploadSurfaceData,
9
10
  UiSurfaceShow,
@@ -20,6 +21,74 @@ import {
20
21
  const log = getLogger('session-surfaces');
21
22
 
22
23
  const MAX_UNDO_DEPTH = 10;
24
+ const TASK_PROGRESS_TEMPLATE_FIELDS = ['title', 'status', 'steps'] as const;
25
+
26
+ function isPlainObject(value: unknown): value is Record<string, unknown> {
27
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
28
+ }
29
+
30
+ function normalizeCardShowData(input: Record<string, unknown>, rawData: Record<string, unknown>): CardSurfaceData {
31
+ const normalized: Record<string, unknown> = { ...rawData };
32
+
33
+ // Older prompt examples sent template/templateData at the top level.
34
+ if (typeof normalized.template !== 'string' && typeof input.template === 'string') {
35
+ normalized.template = input.template;
36
+ }
37
+ if (!isPlainObject(normalized.templateData) && isPlainObject(input.templateData)) {
38
+ normalized.templateData = input.templateData;
39
+ }
40
+
41
+ // task_progress cards need a title for Swift parsing; fall back when missing.
42
+ if (normalized.template === 'task_progress' && typeof normalized.title !== 'string') {
43
+ if (typeof input.title === 'string' && input.title.trim().length > 0) {
44
+ normalized.title = input.title;
45
+ } else if (isPlainObject(normalized.templateData) && typeof normalized.templateData.title === 'string') {
46
+ normalized.title = normalized.templateData.title;
47
+ } else {
48
+ normalized.title = 'Task Progress';
49
+ }
50
+ }
51
+
52
+ if (normalized.template === 'task_progress' && typeof normalized.body !== 'string') {
53
+ normalized.body = '';
54
+ }
55
+
56
+ return normalized as unknown as CardSurfaceData;
57
+ }
58
+
59
+ function normalizeTaskProgressCardPatch(existingCard: CardSurfaceData, patch: Record<string, unknown>): Record<string, unknown> {
60
+ if (existingCard.template !== 'task_progress') {
61
+ return patch;
62
+ }
63
+
64
+ const normalizedPatch: Record<string, unknown> = { ...patch };
65
+ const mergedTemplateData: Record<string, unknown> = isPlainObject(existingCard.templateData)
66
+ ? { ...existingCard.templateData }
67
+ : {};
68
+
69
+ let updatedTemplateData = false;
70
+
71
+ if (isPlainObject(normalizedPatch.templateData)) {
72
+ Object.assign(mergedTemplateData, normalizedPatch.templateData);
73
+ updatedTemplateData = true;
74
+ }
75
+
76
+ // Accept top-level task_progress fields from older prompt examples and
77
+ // move them into templateData where the Swift client expects them.
78
+ for (const key of TASK_PROGRESS_TEMPLATE_FIELDS) {
79
+ if (key in normalizedPatch) {
80
+ mergedTemplateData[key] = normalizedPatch[key];
81
+ delete normalizedPatch[key];
82
+ updatedTemplateData = true;
83
+ }
84
+ }
85
+
86
+ if (updatedTemplateData) {
87
+ normalizedPatch.templateData = mergedTemplateData;
88
+ }
89
+
90
+ return normalizedPatch;
91
+ }
23
92
 
24
93
  /**
25
94
  * Subset of Session state that surface helpers need access to.
@@ -447,7 +516,10 @@ export async function surfaceProxyResolver(
447
516
  const surfaceId = uuid();
448
517
  const surfaceType = input.surface_type as SurfaceType;
449
518
  const title = typeof input.title === 'string' ? input.title : undefined;
450
- const data = input.data as SurfaceData;
519
+ const rawData = isPlainObject(input.data) ? input.data : {};
520
+ const data = (surfaceType === 'card'
521
+ ? normalizeCardShowData(input, rawData)
522
+ : rawData) as SurfaceData;
451
523
  const actions = input.actions as Array<{ id: string; label: string; style?: string }> | undefined;
452
524
  // Interactive surfaces default to awaiting user action.
453
525
  const hasActions = Array.isArray(actions) && actions.length > 0;
@@ -502,12 +574,15 @@ export async function surfaceProxyResolver(
502
574
 
503
575
  if (toolName === 'ui_update') {
504
576
  const surfaceId = input.surface_id as string;
505
- const patch = input.data as Record<string, unknown>;
577
+ let patch = (isPlainObject(input.data) ? input.data : {}) as Record<string, unknown>;
506
578
 
507
579
  // Merge the partial patch into the stored full surface data
508
580
  const stored = ctx.surfaceState.get(surfaceId);
509
581
  let mergedData: SurfaceData;
510
582
  if (stored) {
583
+ if (stored.surfaceType === 'card') {
584
+ patch = normalizeTaskProgressCardPatch(stored.data as CardSurfaceData, patch);
585
+ }
511
586
  // Push current HTML to undo stack for dynamic pages
512
587
  if (stored.surfaceType === 'dynamic_page') {
513
588
  const currentHtml = (stored.data as DynamicPageSurfaceData).html;