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
package/src/memory/db.ts CHANGED
@@ -3,6 +3,9 @@ import { drizzle } from 'drizzle-orm/bun-sqlite';
3
3
  import { computeMemoryFingerprint } from './fingerprint.js';
4
4
  import * as schema from './schema.js';
5
5
  import { getDbPath, ensureDataDir, migrateToDataLayout, migrateToWorkspaceLayout } from '../util/platform.js';
6
+ import { getLogger } from '../util/logger.js';
7
+
8
+ const log = getLogger('memory-db');
6
9
 
7
10
  let db: ReturnType<typeof drizzle<typeof schema>> | null = null;
8
11
 
@@ -271,6 +274,7 @@ export function initializeDb(): void {
271
274
  name TEXT NOT NULL,
272
275
  enabled INTEGER NOT NULL DEFAULT 1,
273
276
  cron_expression TEXT NOT NULL,
277
+ schedule_syntax TEXT NOT NULL DEFAULT 'cron',
274
278
  timezone TEXT,
275
279
  message TEXT NOT NULL,
276
280
  next_run_at INTEGER NOT NULL,
@@ -337,8 +341,8 @@ export function initializeDb(): void {
337
341
  )
338
342
  `);
339
343
 
340
- try { database.run(/*sql*/ `ALTER TABLE published_pages ADD COLUMN app_id TEXT`); } catch {}
341
- try { database.run(/*sql*/ `ALTER TABLE published_pages ADD COLUMN project_slug TEXT`); } catch {}
344
+ try { database.run(/*sql*/ `ALTER TABLE published_pages ADD COLUMN app_id TEXT`); } catch (e) { log.debug({ err: e }, 'ALTER TABLE published_pages ADD COLUMN app_id (likely already exists)'); }
345
+ try { database.run(/*sql*/ `ALTER TABLE published_pages ADD COLUMN project_slug TEXT`); } catch (e) { log.debug({ err: e }, 'ALTER TABLE published_pages ADD COLUMN project_slug (likely already exists)'); }
342
346
 
343
347
  database.run(/*sql*/ `
344
348
  CREATE TABLE IF NOT EXISTS shared_app_links (
@@ -538,6 +542,9 @@ export function initializeDb(): void {
538
542
  try { database.run(/*sql*/ `ALTER TABLE channel_inbound_events ADD COLUMN raw_payload TEXT`); } catch { /* already exists */ }
539
543
  try { database.run(/*sql*/ `ALTER TABLE conversations ADD COLUMN thread_type TEXT NOT NULL DEFAULT 'standard'`); } catch { /* already exists */ }
540
544
  try { database.run(/*sql*/ `ALTER TABLE conversations ADD COLUMN memory_scope_id TEXT NOT NULL DEFAULT 'default'`); } catch { /* already exists */ }
545
+ try { database.run(/*sql*/ `ALTER TABLE attachments ADD COLUMN thumbnail_base64 TEXT`); } catch { /* already exists */ }
546
+ try { database.run(/*sql*/ `ALTER TABLE cron_jobs ADD COLUMN schedule_syntax TEXT NOT NULL DEFAULT 'cron'`); } catch { /* already exists */ }
547
+ try { database.run(/*sql*/ `ALTER TABLE messages ADD COLUMN metadata TEXT`); } catch { /* already exists */ }
541
548
 
542
549
  migrateJobDeferrals(database);
543
550
  migrateToolInvocationsFk(database);
@@ -601,6 +608,7 @@ export function initializeDb(): void {
601
608
  database.run(/*sql*/ `CREATE INDEX IF NOT EXISTS idx_reminders_status_fire_at ON reminders(status, fire_at)`);
602
609
 
603
610
  database.run(/*sql*/ `CREATE INDEX IF NOT EXISTS idx_cron_jobs_enabled_next_run ON cron_jobs(enabled, next_run_at)`);
611
+ database.run(/*sql*/ `CREATE INDEX IF NOT EXISTS idx_cron_jobs_syntax_enabled_next_run ON cron_jobs(schedule_syntax, enabled, next_run_at)`);
604
612
  database.run(/*sql*/ `CREATE INDEX IF NOT EXISTS idx_cron_runs_job_id ON cron_runs(job_id)`);
605
613
 
606
614
  database.run(/*sql*/ `CREATE INDEX IF NOT EXISTS idx_accounts_service ON accounts(service)`);
@@ -728,12 +736,32 @@ export function initializeDb(): void {
728
736
  )
729
737
  `);
730
738
 
739
+ database.run(/*sql*/ `
740
+ CREATE TABLE IF NOT EXISTS processed_callbacks (
741
+ id TEXT PRIMARY KEY,
742
+ dedupe_key TEXT NOT NULL UNIQUE,
743
+ call_session_id TEXT NOT NULL REFERENCES call_sessions(id) ON DELETE CASCADE,
744
+ created_at INTEGER NOT NULL
745
+ )
746
+ `);
747
+
731
748
  database.run(/*sql*/ `CREATE INDEX IF NOT EXISTS idx_call_sessions_conversation_id ON call_sessions(conversation_id)`);
732
749
  database.run(/*sql*/ `CREATE INDEX IF NOT EXISTS idx_call_sessions_provider_call_sid ON call_sessions(provider_call_sid)`);
733
750
  database.run(/*sql*/ `CREATE INDEX IF NOT EXISTS idx_call_sessions_status ON call_sessions(status)`);
734
751
  database.run(/*sql*/ `CREATE INDEX IF NOT EXISTS idx_call_events_call_session_id ON call_events(call_session_id)`);
735
752
  database.run(/*sql*/ `CREATE INDEX IF NOT EXISTS idx_call_pending_questions_call_session_id ON call_pending_questions(call_session_id)`);
736
753
  database.run(/*sql*/ `CREATE INDEX IF NOT EXISTS idx_call_pending_questions_status ON call_pending_questions(status)`);
754
+ database.run(/*sql*/ `CREATE INDEX IF NOT EXISTS idx_processed_callbacks_dedupe_key ON processed_callbacks(dedupe_key)`);
755
+ database.run(/*sql*/ `CREATE INDEX IF NOT EXISTS idx_processed_callbacks_call_session_id ON processed_callbacks(call_session_id)`);
756
+
757
+ // Add claim ownership token to prevent cross-handler claim interference
758
+ try { database.run(/*sql*/ `ALTER TABLE processed_callbacks ADD COLUMN claim_id TEXT`); } catch { /* already exists */ }
759
+
760
+ // Unique constraint: at most one non-null provider_call_sid per (provider, provider_call_sid).
761
+ // On upgraded databases that pre-date this constraint, duplicate rows may exist; deduplicate
762
+ // them first to avoid a UNIQUE constraint failure that would prevent startup.
763
+ migrateCallSessionsProviderSidDedup(database);
764
+ database.run(/*sql*/ `CREATE UNIQUE INDEX IF NOT EXISTS idx_call_sessions_provider_sid_unique ON call_sessions(provider, provider_call_sid) WHERE provider_call_sid IS NOT NULL`);
737
765
 
738
766
  // ── Follow-ups ─────────────────────────────────────────────────────
739
767
 
@@ -823,6 +851,13 @@ export function initializeDb(): void {
823
851
  )
824
852
  `);
825
853
 
854
+ // Work item run contract snapshot
855
+ try { database.run(/*sql*/ `ALTER TABLE work_items ADD COLUMN required_tools TEXT`); } catch (e) { log.debug({ err: e }, 'ALTER TABLE work_items ADD COLUMN required_tools (likely already exists)'); }
856
+
857
+ // Work item permission preflight columns
858
+ try { database.run(/*sql*/ `ALTER TABLE work_items ADD COLUMN approved_tools TEXT`); } catch (e) { log.debug({ err: e }, 'ALTER TABLE work_items ADD COLUMN approved_tools (likely already exists)'); }
859
+ try { database.run(/*sql*/ `ALTER TABLE work_items ADD COLUMN approval_status TEXT DEFAULT 'none'`); } catch (e) { log.debug({ err: e }, 'ALTER TABLE work_items ADD COLUMN approval_status (likely already exists)'); }
860
+
826
861
  database.run(/*sql*/ `CREATE INDEX IF NOT EXISTS idx_work_items_status ON work_items(status)`);
827
862
  database.run(/*sql*/ `CREATE INDEX IF NOT EXISTS idx_work_items_task_id ON work_items(task_id)`);
828
863
  database.run(/*sql*/ `CREATE INDEX IF NOT EXISTS idx_work_items_priority_sort ON work_items(priority_tier, sort_index)`);
@@ -1369,3 +1404,62 @@ function migrateAssistantIdToSelf(database: ReturnType<typeof drizzle<typeof sch
1369
1404
  throw e;
1370
1405
  }
1371
1406
  }
1407
+
1408
+ /**
1409
+ * One-shot migration: remove duplicate (provider, provider_call_sid) rows from
1410
+ * call_sessions so that the unique index can be created safely on upgraded databases
1411
+ * that pre-date the constraint.
1412
+ *
1413
+ * For each set of duplicates, the most recently updated row is kept.
1414
+ */
1415
+ function migrateCallSessionsProviderSidDedup(database: ReturnType<typeof drizzle<typeof schema>>): void {
1416
+ const raw = (database as unknown as { $client: Database }).$client;
1417
+
1418
+ // Quick check: if the unique index already exists, no dedup is needed.
1419
+ const idxExists = raw.query(
1420
+ `SELECT 1 FROM sqlite_master WHERE type = 'index' AND name = 'idx_call_sessions_provider_sid_unique'`,
1421
+ ).get();
1422
+ if (idxExists) return;
1423
+
1424
+ // Check if the table even exists yet (first boot).
1425
+ const tableExists = raw.query(
1426
+ `SELECT 1 FROM sqlite_master WHERE type = 'table' AND name = 'call_sessions'`,
1427
+ ).get();
1428
+ if (!tableExists) return;
1429
+
1430
+ // Count duplicates before doing any work.
1431
+ const dupCount = raw.query(/*sql*/ `
1432
+ SELECT COUNT(*) AS c FROM (
1433
+ SELECT provider, provider_call_sid
1434
+ FROM call_sessions
1435
+ WHERE provider_call_sid IS NOT NULL
1436
+ GROUP BY provider, provider_call_sid
1437
+ HAVING COUNT(*) > 1
1438
+ )
1439
+ `).get() as { c: number } | null;
1440
+
1441
+ if (!dupCount || dupCount.c === 0) return;
1442
+
1443
+ log.warn({ duplicateGroups: dupCount.c }, 'Deduplicating call_sessions with duplicate provider_call_sid before creating unique index');
1444
+
1445
+ try {
1446
+ raw.exec('BEGIN');
1447
+
1448
+ // Keep the most recently updated row per (provider, provider_call_sid);
1449
+ // delete the rest.
1450
+ raw.exec(/*sql*/ `
1451
+ DELETE FROM call_sessions
1452
+ WHERE provider_call_sid IS NOT NULL
1453
+ AND rowid NOT IN (
1454
+ SELECT MAX(rowid) FROM call_sessions
1455
+ WHERE provider_call_sid IS NOT NULL
1456
+ GROUP BY provider, provider_call_sid
1457
+ )
1458
+ `);
1459
+
1460
+ raw.exec('COMMIT');
1461
+ } catch (e) {
1462
+ try { raw.exec('ROLLBACK'); } catch { /* no active transaction */ }
1463
+ throw e;
1464
+ }
1465
+ }
@@ -3,11 +3,14 @@ import { eq, sql } from 'drizzle-orm';
3
3
  import { getConfig } from '../config/loader.js';
4
4
  import type { MemoryEntityConfig } from '../config/types.js';
5
5
  import { getLogger } from '../util/logger.js';
6
+ import { truncate } from '../util/truncate.js';
6
7
  import { getDb } from './db.js';
7
8
  import { memoryEntities, memoryEntityRelations, memoryItemEntities } from './schema.js';
8
9
 
9
10
  const log = getLogger('memory-entity-extractor');
10
11
 
12
+ const ENTITY_EXTRACTION_TIMEOUT_MS = 15_000;
13
+
11
14
  export type EntityType =
12
15
  | 'person'
13
16
  | 'project'
@@ -145,7 +148,7 @@ export async function extractEntitiesWithLLM(
145
148
  messages: [{ role: 'user' as const, content: text }],
146
149
  }) as Promise<Anthropic.Message>,
147
150
  new Promise<never>((_, reject) =>
148
- setTimeout(() => reject(new Error('Entity extraction LLM timeout')), 15000),
151
+ setTimeout(() => reject(new Error('Entity extraction LLM timeout')), ENTITY_EXTRACTION_TIMEOUT_MS),
149
152
  ),
150
153
  ]) as Anthropic.Message;
151
154
 
@@ -460,12 +463,12 @@ function dedupeAliasList(rawAliases: string[], canonicalName: string): string[]
460
463
 
461
464
  function normalizeEntityName(value: string | null | undefined): string | null {
462
465
  if (!value) return null;
463
- const normalized = String(value).trim().slice(0, 200);
466
+ const normalized = truncate(String(value).trim(), 200, '');
464
467
  return normalized.length > 0 ? normalized : null;
465
468
  }
466
469
 
467
470
  function normalizeEvidence(value: string | null | undefined): string | null {
468
471
  if (!value) return null;
469
- const normalized = String(value).trim().slice(0, 500);
472
+ const normalized = truncate(String(value).trim(), 500, '');
470
473
  return normalized.length > 0 ? normalized : null;
471
474
  }
@@ -4,6 +4,7 @@ import { v4 as uuid } from 'uuid';
4
4
  import { getConfig } from '../config/loader.js';
5
5
  import type { MemoryExtractionConfig } from '../config/types.js';
6
6
  import { getLogger } from '../util/logger.js';
7
+ import { truncate } from '../util/truncate.js';
7
8
  import { computeMemoryFingerprint } from './fingerprint.js';
8
9
  import { enqueueMemoryJob } from './jobs-store.js';
9
10
  import { extractTextFromStoredMessageContent } from './message-content.js';
@@ -199,8 +200,8 @@ async function extractItemsWithLLM(
199
200
  for (const raw of input.items) {
200
201
  if (!VALID_KINDS.has(raw.kind)) continue;
201
202
  if (!raw.subject || !raw.statement) continue;
202
- const subject = String(raw.subject).slice(0, 80);
203
- const statement = String(raw.statement).slice(0, 500);
203
+ const subject = truncate(String(raw.subject), 80, '');
204
+ const statement = truncate(String(raw.statement), 500, '');
204
205
  const confidence = clamp(parseScore(raw.confidence, 0.5), 0, 1);
205
206
  const importance = clamp(parseScore(raw.importance, 0.5), 0, 1);
206
207
  const fingerprint = computeMemoryFingerprint(scopeId, raw.kind, subject, statement);
@@ -333,7 +334,7 @@ export async function extractAndUpsertMemoryItemsForMessage(messageId: string, s
333
334
  db.insert(memoryItemSources).values({
334
335
  memoryItemId,
335
336
  messageId,
336
- evidence: item.statement.slice(0, 500),
337
+ evidence: truncate(item.statement, 500, ''),
337
338
  createdAt: now,
338
339
  }).onConflictDoNothing().run();
339
340
 
@@ -410,7 +411,7 @@ function inferSubject(sentence: string, kind: MemoryItemKind): string {
410
411
  if (match) return match[1];
411
412
  }
412
413
  const words = trimmed.split(/\s+/).slice(0, 6).join(' ');
413
- return words.slice(0, 80);
414
+ return truncate(words, 80, '');
414
415
  }
415
416
 
416
417
  function includesAny(text: string, needles: string[]): boolean {
@@ -2,6 +2,7 @@ import { and, asc, eq, lte, notInArray, inArray } from 'drizzle-orm';
2
2
  import { v4 as uuid } from 'uuid';
3
3
  import { getDb } from './db.js';
4
4
  import { memoryJobs } from './schema.js';
5
+ import { truncate } from '../util/truncate.js';
5
6
 
6
7
  export type MemoryJobType =
7
8
  | 'embed_segment'
@@ -331,7 +332,7 @@ export function failMemoryJob(
331
332
  status: 'failed',
332
333
  attempts,
333
334
  updatedAt: now,
334
- lastError: error.slice(0, 2000),
335
+ lastError: truncate(error, 2000, ''),
335
336
  })
336
337
  .where(eq(memoryJobs.id, id))
337
338
  .run();
@@ -343,7 +344,7 @@ export function failMemoryJob(
343
344
  attempts,
344
345
  runAfter: now + retryDelayMs,
345
346
  updatedAt: now,
346
- lastError: error.slice(0, 2000),
347
+ lastError: truncate(error, 2000, ''),
347
348
  })
348
349
  .where(eq(memoryJobs.id, id))
349
350
  .run();
@@ -16,7 +16,7 @@ export function recordUsageEvent(input: UsageEventInput, pricing: PricingResult)
16
16
  db.insert(llmUsageEvents).values({
17
17
  id: event.id,
18
18
  createdAt: event.createdAt,
19
- assistantId: event.assistantId,
19
+ assistantId: 'self',
20
20
  conversationId: event.conversationId,
21
21
  runId: event.runId,
22
22
  requestId: event.requestId,
@@ -45,7 +45,6 @@ export function listUsageEvents(options?: { limit?: number }): UsageEvent[] {
45
45
  return rows.map(row => ({
46
46
  id: row.id,
47
47
  createdAt: row.createdAt,
48
- assistantId: row.assistantId,
49
48
  conversationId: row.conversationId,
50
49
  runId: row.runId,
51
50
  requestId: row.requestId,
@@ -85,7 +85,6 @@ function rowToRun(row: typeof messageRuns.$inferSelect): Run {
85
85
  // ---------------------------------------------------------------------------
86
86
 
87
87
  export function createRun(
88
- assistantId: string,
89
88
  conversationId: string,
90
89
  messageId?: string,
91
90
  ): Run {
@@ -95,7 +94,7 @@ export function createRun(
95
94
 
96
95
  const row = {
97
96
  id,
98
- assistantId,
97
+ assistantId: 'self',
99
98
  conversationId,
100
99
  messageId: messageId ?? null,
101
100
  status: 'running' as const,
@@ -23,6 +23,7 @@ export const messages = sqliteTable('messages', {
23
23
  role: text('role').notNull(),
24
24
  content: text('content').notNull(),
25
25
  createdAt: integer('created_at').notNull(),
26
+ metadata: text('metadata'),
26
27
  });
27
28
 
28
29
  export const toolInvocations = sqliteTable('tool_invocations', {
@@ -164,6 +165,7 @@ export const attachments = sqliteTable('attachments', {
164
165
  kind: text('kind').notNull(),
165
166
  dataBase64: text('data_base64').notNull(),
166
167
  contentHash: text('content_hash'),
168
+ thumbnailBase64: text('thumbnail_base64'),
167
169
  createdAt: integer('created_at').notNull(),
168
170
  });
169
171
 
@@ -242,13 +244,14 @@ export const reminders = sqliteTable('reminders', {
242
244
  updatedAt: integer('updated_at').notNull(),
243
245
  });
244
246
 
245
- // ── Cron / Deferred Tasks ────────────────────────────────────────────
247
+ // ── Recurrence Schedules ─────────────────────────────────────────────
246
248
 
247
249
  export const cronJobs = sqliteTable('cron_jobs', {
248
250
  id: text('id').primaryKey(),
249
251
  name: text('name').notNull(),
250
252
  enabled: integer('enabled', { mode: 'boolean' }).notNull().default(true),
251
253
  cronExpression: text('cron_expression').notNull(), // e.g. '0 9 * * 1-5'
254
+ scheduleSyntax: text('schedule_syntax').notNull().default('cron'), // 'cron' | 'rrule'
252
255
  timezone: text('timezone'), // e.g. 'America/Los_Angeles'
253
256
  message: text('message').notNull(),
254
257
  nextRunAt: integer('next_run_at').notNull(),
@@ -288,6 +291,11 @@ export const cronRuns = sqliteTable('cron_runs', {
288
291
  createdAt: integer('created_at').notNull(),
289
292
  });
290
293
 
294
+ // Recurrence-centric aliases — prefer these in new code.
295
+ // Physical table names remain `cron_jobs` / `cron_runs` for migration compatibility.
296
+ export const scheduleJobs = cronJobs;
297
+ export const scheduleRuns = cronRuns;
298
+
291
299
  // ── LLM Usage Events (cost tracking ledger) ─────────────────────────
292
300
 
293
301
  // ── Entity Graph ─────────────────────────────────────────────────────
@@ -432,7 +440,7 @@ export const workItems = sqliteTable('work_items', {
432
440
  taskId: text('task_id').notNull().references(() => tasks.id),
433
441
  title: text('title').notNull(),
434
442
  notes: text('notes'),
435
- status: text('status').notNull().default('queued'), // queued | running | awaiting_review | failed | done | archived
443
+ status: text('status').notNull().default('queued'), // queued | running | awaiting_review | failed | cancelled | done | archived
436
444
  priorityTier: integer('priority_tier').notNull().default(1), // 0=high, 1=medium, 2=low
437
445
  sortIndex: integer('sort_index'), // manual ordering within same priority tier; null = fall back to updated_at
438
446
  lastRunId: text('last_run_id'),
@@ -440,6 +448,9 @@ export const workItems = sqliteTable('work_items', {
440
448
  lastRunStatus: text('last_run_status'), // 'completed' | 'failed' | null
441
449
  sourceType: text('source_type'), // reserved for future bridge (e.g. 'followup', 'triage')
442
450
  sourceId: text('source_id'), // reserved for future bridge
451
+ requiredTools: text('required_tools'), // JSON array snapshot of tools needed for this run (null=unknown, []=none, ["bash",...]=specific)
452
+ approvedTools: text('approved_tools'), // JSON array of pre-approved tool names
453
+ approvalStatus: text('approval_status').default('none'), // 'none' | 'approved' | 'denied'
443
454
  createdAt: integer('created_at').notNull(),
444
455
  updatedAt: integer('updated_at').notNull(),
445
456
  });
@@ -569,3 +580,13 @@ export const callPendingQuestions = sqliteTable('call_pending_questions', {
569
580
  answeredAt: integer('answered_at'),
570
581
  answerText: text('answer_text'),
571
582
  });
583
+
584
+ export const processedCallbacks = sqliteTable('processed_callbacks', {
585
+ id: text('id').primaryKey(),
586
+ dedupeKey: text('dedupe_key').notNull().unique(),
587
+ callSessionId: text('call_session_id')
588
+ .notNull()
589
+ .references(() => callSessions.id, { onDelete: 'cascade' }),
590
+ claimId: text('claim_id'),
591
+ createdAt: integer('created_at').notNull(),
592
+ });
@@ -8,6 +8,7 @@
8
8
 
9
9
  import type { Message as ProviderMessage } from './provider-types.js';
10
10
  import type { Message, ToolDefinition } from '../providers/types.js';
11
+ import { truncate } from '../util/truncate.js';
11
12
  import { getProvider } from '../providers/registry.js';
12
13
  import { getConfig } from '../config/loader.js';
13
14
 
@@ -97,7 +98,7 @@ function buildCorpus(messages: ProviderMessage[]): string[] {
97
98
  for (const msg of messages) {
98
99
  if (!msg.text.trim()) continue;
99
100
  const to = msg.conversationId;
100
- const truncatedBody = msg.text.slice(0, 500);
101
+ const truncatedBody = truncate(msg.text, 500, '');
101
102
  entries.push(`To: ${to}\n\n${truncatedBody}`);
102
103
  }
103
104
  return entries;
@@ -143,7 +144,7 @@ export async function extractStylePatterns(
143
144
 
144
145
  const stylePatterns: StylePattern[] = (result.style_patterns ?? []).map((p) => ({
145
146
  aspect: p.aspect,
146
- summary: p.summary.slice(0, 500),
147
+ summary: truncate(p.summary, 500, ''),
147
148
  importance: p.importance,
148
149
  examples: p.examples,
149
150
  }));
@@ -1,11 +1,13 @@
1
1
  import Anthropic from '@anthropic-ai/sdk';
2
2
  import { getConfig } from '../config/loader.js';
3
3
  import { getLogger } from '../util/logger.js';
4
+ import { truncate } from '../util/truncate.js';
4
5
  import type { ThreadMessage, ThreadSummary } from './types.js';
5
6
 
6
7
  const log = getLogger('thread-summarizer');
7
8
 
8
9
  const SUMMARIZATION_MODEL = 'claude-haiku-4-5-20251001';
10
+ const SUMMARIZATION_TIMEOUT_MS = 20_000;
9
11
  const DEFAULT_MAX_TOKENS = 4000;
10
12
  const CHARS_PER_TOKEN = 4;
11
13
 
@@ -172,14 +174,10 @@ function extractParticipants(messages: ThreadMessage[]): Array<{ name: string }>
172
174
 
173
175
  function summarizeSingleMessage(message: ThreadMessage): ThreadSummary {
174
176
  return {
175
- summary: message.body.length > 200
176
- ? message.body.slice(0, 197) + '...'
177
- : message.body,
177
+ summary: truncate(message.body, 200),
178
178
  participants: [{ name: message.sender }],
179
179
  openQuestions: [],
180
- lastAction: message.body.length > 200
181
- ? message.body.slice(0, 197) + '...'
182
- : message.body,
180
+ lastAction: truncate(message.body, 200),
183
181
  sentiment: 'neutral',
184
182
  messageCount: 1,
185
183
  };
@@ -225,7 +223,7 @@ async function summarizeWithLLM(
225
223
  timer = setTimeout(() => {
226
224
  abortController.abort();
227
225
  reject(new Error('Thread summarization LLM timeout'));
228
- }, 20000);
226
+ }, SUMMARIZATION_TIMEOUT_MS);
229
227
  }),
230
228
  ]);
231
229
 
@@ -249,7 +247,7 @@ async function summarizeWithLLM(
249
247
  : 'neutral';
250
248
 
251
249
  return {
252
- summary: String(input.summary ?? '').slice(0, 2000),
250
+ summary: truncate(String(input.summary ?? ''), 2000, ''),
253
251
  participants: Array.isArray(input.participants)
254
252
  ? input.participants.map((p) => ({
255
253
  name: String(p.name),
@@ -259,7 +257,7 @@ async function summarizeWithLLM(
259
257
  openQuestions: Array.isArray(input.openQuestions)
260
258
  ? input.openQuestions.map((q) => String(q))
261
259
  : [],
262
- lastAction: String(input.lastAction ?? '').slice(0, 500),
260
+ lastAction: truncate(String(input.lastAction ?? ''), 500, ''),
263
261
  sentiment,
264
262
  messageCount: messages.length,
265
263
  };
@@ -280,9 +278,7 @@ function buildFallbackSummary(messages: ThreadMessage[]): ThreadSummary {
280
278
  summary: `Thread with ${messages.length} message(s) from ${extractParticipants(messages).map((p) => p.name).join(', ')}.`,
281
279
  participants: extractParticipants(messages),
282
280
  openQuestions: [],
283
- lastAction: lastMsg.body.length > 200
284
- ? lastMsg.body.slice(0, 197) + '...'
285
- : lastMsg.body,
281
+ lastAction: truncate(lastMsg.body, 200),
286
282
  sentiment: 'neutral',
287
283
  messageCount: messages.length,
288
284
  };
@@ -8,6 +8,7 @@
8
8
  */
9
9
 
10
10
  import Anthropic from '@anthropic-ai/sdk';
11
+ import { truncate } from '../util/truncate.js';
11
12
  import { v4 as uuid } from 'uuid';
12
13
  import { and, eq, isNull, desc } from 'drizzle-orm';
13
14
  import { getConfig } from '../config/loader.js';
@@ -24,6 +25,7 @@ import { DEFAULT_TRIAGE_CATEGORIES } from './types.js';
24
25
  const log = getLogger('triage-engine');
25
26
 
26
27
  const TRIAGE_MODEL = 'claude-haiku-4-5-20251001';
28
+ const TRIAGE_CLASSIFICATION_TIMEOUT_MS = 15_000;
27
29
 
28
30
  // ── Playbook fetching ────────────────────────────────────────────────
29
31
 
@@ -244,7 +246,7 @@ async function classifyWithLLM(
244
246
  timer = setTimeout(() => {
245
247
  abortController.abort();
246
248
  reject(new Error('Triage classification LLM timeout'));
247
- }, 15000);
249
+ }, TRIAGE_CLASSIFICATION_TIMEOUT_MS);
248
250
  }),
249
251
  ]);
250
252
 
@@ -282,7 +284,7 @@ async function classifyWithLLM(
282
284
  category: typeof input.category === 'string' ? input.category : 'needs_response',
283
285
  confidence,
284
286
  suggestedAction: typeof input.suggestedAction === 'string'
285
- ? input.suggestedAction.slice(0, 500)
287
+ ? truncate(input.suggestedAction, 500, '')
286
288
  : 'Review manually',
287
289
  matchedPlaybooks,
288
290
  };
@@ -0,0 +1,20 @@
1
+ import { OpenAIProvider } from '../openai/client.js';
2
+
3
+ export interface OpenRouterProviderOptions {
4
+ apiKey?: string;
5
+ baseURL?: string;
6
+ streamTimeoutMs?: number;
7
+ }
8
+
9
+ const DEFAULT_OPENROUTER_BASE_URL = 'https://openrouter.ai/api/v1';
10
+
11
+ export class OpenRouterProvider extends OpenAIProvider {
12
+ constructor(apiKey: string, model: string, options: OpenRouterProviderOptions = {}) {
13
+ super(apiKey, model, {
14
+ baseURL: options.baseURL?.trim() || DEFAULT_OPENROUTER_BASE_URL,
15
+ providerName: 'openrouter',
16
+ providerLabel: 'OpenRouter',
17
+ streamTimeoutMs: options.streamTimeoutMs,
18
+ });
19
+ }
20
+ }
@@ -4,6 +4,7 @@ import { OpenAIProvider } from "./openai/client.js";
4
4
  import { GeminiProvider } from "./gemini/client.js";
5
5
  import { OllamaProvider } from "./ollama/client.js";
6
6
  import { FireworksProvider } from "./fireworks/client.js";
7
+ import { OpenRouterProvider } from "./openrouter/client.js";
7
8
  import { RetryProvider } from "./retry.js";
8
9
  import { FailoverProvider } from "./failover.js";
9
10
  import { wrapWithLogfire } from "../logfire.js";
@@ -15,6 +16,7 @@ const DEFAULT_MODELS: Record<string, string> = {
15
16
  gemini: 'gemini-3-flash',
16
17
  ollama: 'llama3.2',
17
18
  fireworks: 'accounts/fireworks/models/kimi-k2p5',
19
+ openrouter: 'x-ai/grok-4',
18
20
  };
19
21
 
20
22
  const providers = new Map<string, Provider>();
@@ -135,4 +137,10 @@ export function initializeProviders(config: ProvidersConfig): void {
135
137
  wrapWithLogfire(new FireworksProvider(config.apiKeys.fireworks, model, { streamTimeoutMs })),
136
138
  ));
137
139
  }
140
+ if (config.apiKeys.openrouter) {
141
+ const model = resolveModel(config, 'openrouter');
142
+ registerProvider('openrouter', new RetryProvider(
143
+ wrapWithLogfire(new OpenRouterProvider(config.apiKeys.openrouter, model, { streamTimeoutMs })),
144
+ ));
145
+ }
138
146
  }