vellum 0.2.1 → 0.2.7

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 (361) hide show
  1. package/README.md +15 -2
  2. package/bun.lock +71 -100
  3. package/package.json +5 -3
  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 +305 -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-twilio-config.test.ts +221 -0
  36. package/src/__tests__/handlers-twitter-config.test.ts +718 -0
  37. package/src/__tests__/intent-routing.test.ts +64 -57
  38. package/src/__tests__/ipc-roundtrip.benchmark.test.ts +237 -0
  39. package/src/__tests__/ipc-snapshot.test.ts +71 -28
  40. package/src/__tests__/llm-usage-store.test.ts +3 -8
  41. package/src/__tests__/media-generate-image.test.ts +1 -1
  42. package/src/__tests__/media-reuse-story.e2e.test.ts +7 -7
  43. package/src/__tests__/memory-regressions.test.ts +100 -2
  44. package/src/__tests__/memory-retrieval.benchmark.test.ts +430 -0
  45. package/src/__tests__/parallel-tool.benchmark.test.ts +294 -0
  46. package/src/__tests__/playbook-tools.test.ts +342 -0
  47. package/src/__tests__/profile-compiler.test.ts +2 -1
  48. package/src/__tests__/provider-commit-message-generator.test.ts +303 -0
  49. package/src/__tests__/provider-streaming.benchmark.test.ts +773 -0
  50. package/src/__tests__/recurrence-engine-rruleset.test.ts +78 -0
  51. package/src/__tests__/recurrence-engine.test.ts +69 -0
  52. package/src/__tests__/recurrence-types.test.ts +71 -0
  53. package/src/__tests__/registry.test.ts +5 -3
  54. package/src/__tests__/relay-server.test.ts +633 -0
  55. package/src/__tests__/reminder-store.test.ts +6 -3
  56. package/src/__tests__/reminder.test.ts +43 -77
  57. package/src/__tests__/run-orchestrator-assistant-events.test.ts +8 -4
  58. package/src/__tests__/run-orchestrator.test.ts +4 -4
  59. package/src/__tests__/runtime-attachment-metadata.test.ts +7 -6
  60. package/src/__tests__/runtime-runs-http.test.ts +4 -4
  61. package/src/__tests__/runtime-runs.test.ts +4 -4
  62. package/src/__tests__/schedule-store.test.ts +482 -0
  63. package/src/__tests__/schedule-tools.test.ts +700 -0
  64. package/src/__tests__/scheduler-recurrence.test.ts +329 -0
  65. package/src/__tests__/server-history-render.test.ts +14 -13
  66. package/src/__tests__/session-conflict-gate.test.ts +28 -25
  67. package/src/__tests__/session-error.test.ts +28 -0
  68. package/src/__tests__/session-init.benchmark.test.ts +462 -0
  69. package/src/__tests__/session-queue.test.ts +71 -48
  70. package/src/__tests__/session-runtime-assembly.test.ts +161 -0
  71. package/src/__tests__/session-surfaces-task-progress.test.ts +104 -0
  72. package/src/__tests__/signup-e2e.test.ts +2 -1
  73. package/src/__tests__/skill-projection.benchmark.test.ts +328 -0
  74. package/src/__tests__/skill-script-runner.test.ts +159 -0
  75. package/src/__tests__/speaker-identification.test.ts +52 -0
  76. package/src/__tests__/subagent-manager-notify.test.ts +42 -10
  77. package/src/__tests__/subagent-tools.test.ts +141 -41
  78. package/src/__tests__/task-compiler.test.ts +2 -1
  79. package/src/__tests__/task-runner.test.ts +2 -1
  80. package/src/__tests__/task-scheduler.test.ts +2 -1
  81. package/src/__tests__/task-tools.test.ts +49 -56
  82. package/src/__tests__/tool-audit-listener.test.ts +1 -0
  83. package/src/__tests__/tool-domain-event-publisher.test.ts +2 -0
  84. package/src/__tests__/tool-execution-pipeline.benchmark.test.ts +500 -0
  85. package/src/__tests__/tool-executor.test.ts +13 -17
  86. package/src/__tests__/turn-commit.test.ts +218 -3
  87. package/src/__tests__/twilio-provider.test.ts +143 -0
  88. package/src/__tests__/twilio-routes.test.ts +789 -0
  89. package/src/__tests__/twitter-auth-handler.test.ts +581 -0
  90. package/src/__tests__/view-image-tool.test.ts +217 -0
  91. package/src/__tests__/workspace-git-service.test.ts +186 -0
  92. package/src/__tests__/workspace-heartbeat-service.test.ts +13 -3
  93. package/src/agent-heartbeat/agent-heartbeat-service.ts +155 -0
  94. package/src/bundler/app-bundler.ts +12 -8
  95. package/src/calls/__tests__/twilio-webhook-urls.test.ts +162 -0
  96. package/src/calls/call-bridge.ts +95 -0
  97. package/src/calls/call-constants.ts +43 -5
  98. package/src/calls/call-domain.ts +276 -0
  99. package/src/calls/call-orchestrator.ts +43 -17
  100. package/src/calls/call-recovery.ts +207 -0
  101. package/src/calls/call-state-machine.ts +68 -0
  102. package/src/calls/call-store.ts +192 -5
  103. package/src/calls/relay-server.ts +41 -4
  104. package/src/calls/speaker-identification.ts +213 -0
  105. package/src/calls/twilio-config.ts +8 -8
  106. package/src/calls/twilio-provider.ts +13 -9
  107. package/src/calls/twilio-routes.ts +90 -76
  108. package/src/calls/twilio-webhook-urls.ts +50 -0
  109. package/src/calls/types.ts +1 -1
  110. package/src/cli/config-commands.ts +334 -0
  111. package/src/cli/core-commands.ts +776 -0
  112. package/src/cli/doordash.ts +251 -1
  113. package/src/cli/ipc-client.ts +82 -0
  114. package/src/cli/map.ts +270 -0
  115. package/src/cli/twitter.ts +575 -0
  116. package/src/cli.ts +7 -5
  117. package/src/commands/__tests__/cc-command-registry.test.ts +319 -0
  118. package/src/commands/cc-command-registry.ts +209 -0
  119. package/src/config/bundled-skills/contacts/SKILL.md +39 -0
  120. package/src/config/bundled-skills/contacts/TOOLS.json +122 -0
  121. package/src/config/bundled-skills/contacts/tools/contact-merge.ts +9 -0
  122. package/src/config/bundled-skills/contacts/tools/contact-search.ts +9 -0
  123. package/src/config/bundled-skills/contacts/tools/contact-upsert.ts +9 -0
  124. package/src/config/bundled-skills/document/SKILL.md +18 -0
  125. package/src/config/bundled-skills/document/TOOLS.json +53 -0
  126. package/src/config/bundled-skills/document/tools/document-create.ts +9 -0
  127. package/src/config/bundled-skills/document/tools/document-update.ts +9 -0
  128. package/src/config/bundled-skills/doordash/SKILL.md +82 -23
  129. package/src/config/bundled-skills/followups/SKILL.md +32 -0
  130. package/src/config/bundled-skills/followups/TOOLS.json +100 -0
  131. package/src/config/bundled-skills/followups/tools/followup-create.ts +9 -0
  132. package/src/config/bundled-skills/followups/tools/followup-list.ts +9 -0
  133. package/src/config/bundled-skills/followups/tools/followup-resolve.ts +9 -0
  134. package/src/config/bundled-skills/image-studio/tools/media-generate-image.ts +1 -23
  135. package/src/config/bundled-skills/messaging/tools/messaging-analyze-style.ts +2 -1
  136. package/src/config/bundled-skills/playbooks/SKILL.md +31 -0
  137. package/src/config/bundled-skills/playbooks/TOOLS.json +126 -0
  138. package/src/config/bundled-skills/playbooks/tools/playbook-create.ts +9 -0
  139. package/src/config/bundled-skills/playbooks/tools/playbook-delete.ts +9 -0
  140. package/src/config/bundled-skills/playbooks/tools/playbook-list.ts +9 -0
  141. package/src/config/bundled-skills/playbooks/tools/playbook-update.ts +9 -0
  142. package/src/config/bundled-skills/reminder/SKILL.md +20 -0
  143. package/src/config/bundled-skills/reminder/TOOLS.json +67 -0
  144. package/src/config/bundled-skills/reminder/tools/reminder-cancel.ts +9 -0
  145. package/src/config/bundled-skills/reminder/tools/reminder-create.ts +9 -0
  146. package/src/config/bundled-skills/reminder/tools/reminder-list.ts +9 -0
  147. package/src/config/bundled-skills/schedule/SKILL.md +74 -0
  148. package/src/config/bundled-skills/schedule/TOOLS.json +135 -0
  149. package/src/config/bundled-skills/schedule/tools/schedule-create.ts +9 -0
  150. package/src/config/bundled-skills/schedule/tools/schedule-delete.ts +9 -0
  151. package/src/config/bundled-skills/schedule/tools/schedule-list.ts +9 -0
  152. package/src/config/bundled-skills/schedule/tools/schedule-update.ts +9 -0
  153. package/src/config/bundled-skills/subagent/SKILL.md +25 -0
  154. package/src/config/bundled-skills/subagent/TOOLS.json +107 -0
  155. package/src/config/bundled-skills/subagent/tools/subagent-abort.ts +9 -0
  156. package/src/config/bundled-skills/subagent/tools/subagent-message.ts +9 -0
  157. package/src/config/bundled-skills/subagent/tools/subagent-read.ts +9 -0
  158. package/src/config/bundled-skills/subagent/tools/subagent-spawn.ts +9 -0
  159. package/src/config/bundled-skills/subagent/tools/subagent-status.ts +9 -0
  160. package/src/config/bundled-skills/tasks/SKILL.md +28 -0
  161. package/src/config/bundled-skills/tasks/TOOLS.json +256 -0
  162. package/src/config/bundled-skills/tasks/tools/task-delete.ts +9 -0
  163. package/src/config/bundled-skills/tasks/tools/task-list-add.ts +9 -0
  164. package/src/config/bundled-skills/tasks/tools/task-list-remove.ts +9 -0
  165. package/src/config/bundled-skills/tasks/tools/task-list-show.ts +9 -0
  166. package/src/config/bundled-skills/tasks/tools/task-list-update.ts +9 -0
  167. package/src/config/bundled-skills/tasks/tools/task-list.ts +9 -0
  168. package/src/config/bundled-skills/tasks/tools/task-run.ts +9 -0
  169. package/src/config/bundled-skills/tasks/tools/task-save.ts +9 -0
  170. package/src/config/bundled-skills/twitter/SKILL.md +134 -0
  171. package/src/config/bundled-skills/watcher/SKILL.md +27 -0
  172. package/src/config/bundled-skills/watcher/TOOLS.json +147 -0
  173. package/src/config/bundled-skills/watcher/tools/watcher-create.ts +9 -0
  174. package/src/config/bundled-skills/watcher/tools/watcher-delete.ts +9 -0
  175. package/src/config/bundled-skills/watcher/tools/watcher-digest.ts +9 -0
  176. package/src/config/bundled-skills/watcher/tools/watcher-list.ts +9 -0
  177. package/src/config/bundled-skills/watcher/tools/watcher-update.ts +9 -0
  178. package/src/config/defaults.ts +34 -0
  179. package/src/config/loader.ts +4 -1
  180. package/src/config/schema.ts +165 -1
  181. package/src/config/system-prompt.ts +61 -16
  182. package/src/config/templates/IDENTITY.md +7 -0
  183. package/src/config/types.ts +4 -0
  184. package/src/config/vellum-skills/telegram-setup/SKILL.md +1 -5
  185. package/src/contacts/contact-store.ts +4 -4
  186. package/src/daemon/assistant-attachments.ts +10 -0
  187. package/src/daemon/classifier.ts +3 -1
  188. package/src/daemon/computer-use-session.ts +3 -1
  189. package/src/daemon/date-context.ts +136 -0
  190. package/src/daemon/handlers/apps.ts +16 -1
  191. package/src/daemon/handlers/browser.ts +54 -0
  192. package/src/daemon/handlers/computer-use.ts +7 -1
  193. package/src/daemon/handlers/config.ts +205 -5
  194. package/src/daemon/handlers/diagnostics.ts +5 -1
  195. package/src/daemon/handlers/documents.ts +18 -29
  196. package/src/daemon/handlers/home-base.ts +5 -1
  197. package/src/daemon/handlers/index.ts +40 -277
  198. package/src/daemon/handlers/misc.ts +9 -1
  199. package/src/daemon/handlers/publish.ts +6 -1
  200. package/src/daemon/handlers/sessions.ts +65 -12
  201. package/src/daemon/handlers/shared.ts +36 -1
  202. package/src/daemon/handlers/signing.ts +37 -0
  203. package/src/daemon/handlers/skills.ts +20 -6
  204. package/src/daemon/handlers/subagents.ts +8 -3
  205. package/src/daemon/handlers/twitter-auth.ts +169 -0
  206. package/src/daemon/handlers/work-items.ts +384 -68
  207. package/src/daemon/ipc-contract-inventory.json +32 -4
  208. package/src/daemon/ipc-contract.ts +156 -37
  209. package/src/daemon/ipc-protocol.ts +7 -2
  210. package/src/daemon/lifecycle.ts +21 -0
  211. package/src/daemon/main.ts +10 -4
  212. package/src/daemon/ride-shotgun-handler.ts +75 -10
  213. package/src/daemon/server.ts +143 -26
  214. package/src/daemon/session-agent-loop.ts +922 -0
  215. package/src/daemon/session-attachments.ts +28 -5
  216. package/src/daemon/session-conflict-gate.ts +18 -109
  217. package/src/daemon/session-error.ts +24 -3
  218. package/src/daemon/session-lifecycle.ts +147 -0
  219. package/src/daemon/session-media-retry.ts +147 -0
  220. package/src/daemon/session-messaging.ts +145 -0
  221. package/src/daemon/session-notifiers.ts +164 -0
  222. package/src/daemon/session-process.ts +2 -2
  223. package/src/daemon/session-queue-manager.ts +1 -0
  224. package/src/daemon/session-runtime-assembly.ts +52 -0
  225. package/src/daemon/session-skill-tools.ts +124 -5
  226. package/src/daemon/session-slash.ts +3 -0
  227. package/src/daemon/session-surfaces.ts +77 -2
  228. package/src/daemon/session-tool-setup.ts +216 -2
  229. package/src/daemon/session-usage.ts +0 -2
  230. package/src/daemon/session.ts +114 -1404
  231. package/src/daemon/video-thumbnail.ts +60 -0
  232. package/src/doordash/client.ts +121 -27
  233. package/src/doordash/queries.ts +1 -2
  234. package/src/export/formatter.ts +3 -1
  235. package/src/followups/followup-store.ts +4 -2
  236. package/src/followups/types.ts +6 -0
  237. package/src/hooks/templates.ts +1 -1
  238. package/src/index.ts +32 -1153
  239. package/src/memory/attachments-store.ts +28 -83
  240. package/src/memory/channel-delivery-store.ts +7 -21
  241. package/src/memory/clarification-resolver.ts +6 -5
  242. package/src/memory/conflict-intent.ts +114 -0
  243. package/src/memory/contradiction-checker.ts +3 -2
  244. package/src/memory/conversation-key-store.ts +10 -29
  245. package/src/memory/conversation-store.ts +2 -1
  246. package/src/memory/db.ts +96 -2
  247. package/src/memory/entity-extractor.ts +6 -3
  248. package/src/memory/items-extractor.ts +5 -4
  249. package/src/memory/job-handlers/conflict.ts +23 -1
  250. package/src/memory/jobs-store.ts +3 -2
  251. package/src/memory/llm-usage-store.ts +1 -2
  252. package/src/memory/runs-store.ts +1 -2
  253. package/src/memory/schema.ts +23 -2
  254. package/src/messaging/style-analyzer.ts +3 -2
  255. package/src/messaging/thread-summarizer.ts +8 -12
  256. package/src/messaging/triage-engine.ts +4 -2
  257. package/src/providers/openrouter/client.ts +20 -0
  258. package/src/providers/registry.ts +8 -0
  259. package/src/runtime/gateway-client.ts +36 -0
  260. package/src/runtime/http-server.ts +166 -22
  261. package/src/runtime/routes/attachment-routes.ts +2 -3
  262. package/src/runtime/routes/call-routes.ts +140 -0
  263. package/src/runtime/routes/channel-routes.ts +125 -88
  264. package/src/runtime/routes/conversation-routes.ts +5 -5
  265. package/src/runtime/routes/run-routes.ts +2 -2
  266. package/src/runtime/run-orchestrator.ts +9 -3
  267. package/src/schedule/recurrence-engine.ts +138 -0
  268. package/src/schedule/recurrence-types.ts +67 -0
  269. package/src/schedule/schedule-store.ts +102 -57
  270. package/src/schedule/scheduler.ts +9 -6
  271. package/src/security/oauth2.ts +29 -4
  272. package/src/security/secret-allowlist.ts +46 -0
  273. package/src/skills/clawhub.ts +1 -1
  274. package/src/subagent/manager.ts +40 -8
  275. package/src/swarm/backend-claude-code.ts +64 -9
  276. package/src/swarm/worker-prompts.ts +2 -1
  277. package/src/tasks/SPEC.md +34 -28
  278. package/src/tasks/ephemeral-permissions.ts +16 -7
  279. package/src/tasks/task-compiler.ts +5 -4
  280. package/src/tasks/task-runner.ts +10 -5
  281. package/src/tasks/task-scheduler.ts +1 -1
  282. package/src/tasks/tool-sanitizer.ts +36 -0
  283. package/src/tools/assets/search.ts +4 -4
  284. package/src/tools/browser/api-map.ts +293 -0
  285. package/src/tools/browser/auto-navigate.ts +270 -0
  286. package/src/tools/browser/browser-execution.ts +2 -1
  287. package/src/tools/browser/browser-manager.ts +2 -2
  288. package/src/tools/browser/network-recorder.ts +5 -4
  289. package/src/tools/browser/x-auto-navigate.ts +207 -0
  290. package/src/tools/calls/call-end.ts +17 -67
  291. package/src/tools/calls/call-start.ts +24 -85
  292. package/src/tools/calls/call-status.ts +35 -51
  293. package/src/tools/claude-code/claude-code.ts +207 -11
  294. package/src/tools/contacts/contact-merge.ts +46 -78
  295. package/src/tools/contacts/contact-search.ts +35 -79
  296. package/src/tools/contacts/contact-upsert.ts +35 -108
  297. package/src/tools/credentials/vault.ts +20 -4
  298. package/src/tools/document/document-tool.ts +71 -144
  299. package/src/tools/executor.ts +129 -10
  300. package/src/tools/followups/followup_create.ts +46 -88
  301. package/src/tools/followups/followup_list.ts +34 -74
  302. package/src/tools/followups/followup_resolve.ts +31 -66
  303. package/src/tools/host-terminal/cli-discover.ts +2 -1
  304. package/src/tools/host-terminal/host-shell.ts +10 -0
  305. package/src/tools/memory/handlers.ts +5 -4
  306. package/src/tools/network/__tests__/web-search.test.ts +427 -0
  307. package/src/tools/network/script-proxy/__tests__/logging.test.ts +248 -0
  308. package/src/tools/network/script-proxy/__tests__/policy.test.ts +234 -0
  309. package/src/tools/network/script-proxy/__tests__/router.test.ts +76 -0
  310. package/src/tools/network/web-fetch.ts +18 -6
  311. package/src/tools/playbooks/index.ts +4 -5
  312. package/src/tools/playbooks/playbook-create.ts +3 -47
  313. package/src/tools/playbooks/playbook-delete.ts +1 -25
  314. package/src/tools/playbooks/playbook-list.ts +1 -28
  315. package/src/tools/playbooks/playbook-update.ts +3 -51
  316. package/src/tools/reminder/reminder.ts +5 -78
  317. package/src/tools/schedule/create.ts +69 -74
  318. package/src/tools/schedule/delete.ts +21 -47
  319. package/src/tools/schedule/list.ts +55 -74
  320. package/src/tools/schedule/update.ts +77 -84
  321. package/src/tools/subagent/abort.ts +29 -58
  322. package/src/tools/subagent/message.ts +30 -63
  323. package/src/tools/subagent/read.ts +53 -84
  324. package/src/tools/subagent/spawn.ts +43 -82
  325. package/src/tools/subagent/status.ts +42 -71
  326. package/src/tools/swarm/delegate.ts +2 -1
  327. package/src/tools/tasks/index.ts +8 -8
  328. package/src/tools/tasks/task-delete.ts +60 -88
  329. package/src/tools/tasks/task-list.ts +31 -52
  330. package/src/tools/tasks/task-run.ts +72 -108
  331. package/src/tools/tasks/task-save.ts +33 -65
  332. package/src/tools/tasks/work-item-enqueue.ts +183 -215
  333. package/src/tools/tasks/work-item-list.ts +33 -63
  334. package/src/tools/tasks/work-item-remove.ts +45 -97
  335. package/src/tools/tasks/work-item-update.ts +91 -163
  336. package/src/tools/terminal/backends/native.ts +3 -1
  337. package/src/tools/tool-manifest.ts +0 -62
  338. package/src/tools/types.ts +6 -0
  339. package/src/tools/ui-surface/definitions.ts +3 -1
  340. package/src/tools/watch/screen-watch.ts +3 -1
  341. package/src/tools/watcher/create.ts +52 -98
  342. package/src/tools/watcher/delete.ts +20 -46
  343. package/src/tools/watcher/digest.ts +36 -70
  344. package/src/tools/watcher/list.ts +49 -79
  345. package/src/tools/watcher/update.ts +45 -91
  346. package/src/twitter/client.ts +690 -0
  347. package/src/twitter/session.ts +91 -0
  348. package/src/usage/types.ts +0 -1
  349. package/src/util/truncate.ts +6 -0
  350. package/src/watcher/providers/slack.ts +2 -1
  351. package/src/watcher/watcher-store.ts +3 -2
  352. package/src/work-items/work-item-store.ts +27 -2
  353. package/src/workspace/commit-message-enrichment-service.ts +31 -7
  354. package/src/workspace/git-service.ts +87 -22
  355. package/src/workspace/provider-commit-message-generator.ts +269 -0
  356. package/src/workspace/turn-commit.ts +62 -3
  357. package/src/tools/contacts/index.ts +0 -4
  358. package/src/tools/document/index.ts +0 -5
  359. package/src/tools/followups/index.ts +0 -3
  360. package/src/tools/subagent/index.ts +0 -5
  361. /package/src/__tests__/{memory-context-benchmark.test.ts → memory-context-benchmark.benchmark.test.ts} +0 -0
@@ -0,0 +1,145 @@
1
+ /**
2
+ * Session messaging methods: enqueue, persistUserMessage,
3
+ * redirectToSecurePrompt, and queue/confirmation helpers.
4
+ *
5
+ * Extracted from Session to keep the class focused on coordination.
6
+ */
7
+
8
+ import { v4 as uuid } from 'uuid';
9
+ import type { Message } from '../providers/types.js';
10
+ import type { ServerMessage, UserMessageAttachment } from './ipc-protocol.js';
11
+ import { createUserMessage } from '../agent/message-types.js';
12
+ import * as conversationStore from '../memory/conversation-store.js';
13
+ import type { SecretPrompter } from '../permissions/secret-prompter.js';
14
+ import type { MessageQueue } from './session-queue-manager.js';
15
+ import { getLogger } from '../util/logger.js';
16
+
17
+ const log = getLogger('session-messaging');
18
+
19
+ // ── Context Interface ────────────────────────────────────────────────
20
+
21
+ export interface MessagingSessionContext {
22
+ readonly conversationId: string;
23
+ messages: Message[];
24
+ processing: boolean;
25
+ abortController: AbortController | null;
26
+ currentRequestId?: string;
27
+ readonly queue: MessageQueue;
28
+ }
29
+
30
+ // ── enqueueMessage ───────────────────────────────────────────────────
31
+
32
+ export function enqueueMessage(
33
+ ctx: MessagingSessionContext,
34
+ content: string,
35
+ attachments: UserMessageAttachment[],
36
+ onEvent: (msg: ServerMessage) => void,
37
+ requestId: string,
38
+ activeSurfaceId?: string,
39
+ currentPage?: string,
40
+ metadata?: Record<string, unknown>,
41
+ ): { queued: boolean; rejected?: boolean; requestId: string } {
42
+ if (!ctx.processing) {
43
+ return { queued: false, requestId };
44
+ }
45
+
46
+ const pushed = ctx.queue.push({ content, attachments, requestId, onEvent, activeSurfaceId, currentPage, metadata });
47
+ if (!pushed) {
48
+ return { queued: false, rejected: true, requestId };
49
+ }
50
+ return { queued: true, requestId };
51
+ }
52
+
53
+ // ── persistUserMessage ───────────────────────────────────────────────
54
+
55
+ export function persistUserMessage(
56
+ ctx: MessagingSessionContext,
57
+ content: string,
58
+ attachments: UserMessageAttachment[],
59
+ requestId?: string,
60
+ metadata?: Record<string, unknown>,
61
+ ): string {
62
+ if (ctx.processing) {
63
+ throw new Error('Session is already processing a message');
64
+ }
65
+
66
+ if (!content.trim() && attachments.length === 0) {
67
+ throw new Error('Message content or attachments are required');
68
+ }
69
+
70
+ const reqId = requestId ?? uuid();
71
+ ctx.currentRequestId = reqId;
72
+ ctx.processing = true;
73
+ ctx.abortController = new AbortController();
74
+
75
+ const userMessage = createUserMessage(content, attachments.map((attachment) => ({
76
+ id: attachment.id,
77
+ filename: attachment.filename,
78
+ mimeType: attachment.mimeType,
79
+ data: attachment.data,
80
+ extractedText: attachment.extractedText,
81
+ })));
82
+ ctx.messages.push(userMessage);
83
+
84
+ try {
85
+ const persistedUserMessage = conversationStore.addMessage(
86
+ ctx.conversationId,
87
+ 'user',
88
+ JSON.stringify(userMessage.content),
89
+ metadata,
90
+ );
91
+
92
+ if (!persistedUserMessage.id) {
93
+ throw new Error('Failed to persist user message');
94
+ }
95
+
96
+ return persistedUserMessage.id;
97
+ } catch (err) {
98
+ ctx.messages.pop();
99
+ ctx.processing = false;
100
+ ctx.abortController = null;
101
+ ctx.currentRequestId = undefined;
102
+ throw err;
103
+ }
104
+ }
105
+
106
+ // ── redirectToSecurePrompt ───────────────────────────────────────────
107
+
108
+ export function redirectToSecurePrompt(
109
+ conversationId: string,
110
+ secretPrompter: SecretPrompter,
111
+ detectedTypes: string[],
112
+ onComplete?: () => void,
113
+ ): void {
114
+ const service = 'detected';
115
+ const field = detectedTypes.join(',');
116
+ secretPrompter.prompt(
117
+ service, field,
118
+ 'Secure Credential Entry',
119
+ 'Your message contained a secret. Please enter it here instead — it will be stored securely and never sent to the AI.',
120
+ undefined, conversationId,
121
+ ).then(async (result) => {
122
+ if (!result.value) return;
123
+
124
+ const { setSecureKey } = await import('../security/secure-keys.js');
125
+ const { upsertCredentialMetadata } = await import('../tools/credentials/metadata-store.js');
126
+
127
+ if (result.delivery === 'transient_send') {
128
+ const { credentialBroker } = await import('../tools/credentials/broker.js');
129
+ credentialBroker.injectTransient(service, field, result.value);
130
+ try { upsertCredentialMetadata(service, field, {}); } catch (e) { log.debug({ err: e, service, field }, 'Non-critical credential metadata upsert failed'); }
131
+ log.info({ service, field, delivery: 'transient_send' }, 'Ingress redirect: transient credential injected');
132
+ } else {
133
+ const key = `credential:${service}:${field}`;
134
+ const stored = setSecureKey(key, result.value);
135
+ if (stored) {
136
+ try { upsertCredentialMetadata(service, field, {}); } catch (e) { log.debug({ err: e, service, field }, 'Non-critical credential metadata upsert failed'); }
137
+ log.info({ service, field }, 'Ingress redirect: credential stored');
138
+ } else {
139
+ log.warn({ service, field }, 'Ingress redirect: secure storage write failed');
140
+ }
141
+ }
142
+ }).catch(() => { /* prompt timeout or cancel is fine */ }).finally(() => {
143
+ onComplete?.();
144
+ });
145
+ }
@@ -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. */