vellum 0.2.0 → 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 (361) 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 +161 -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__/app-bundler.test.ts +12 -33
  11. package/src/__tests__/asset-materialize-tool.test.ts +16 -15
  12. package/src/__tests__/asset-search-tool.test.ts +23 -22
  13. package/src/__tests__/attachments-store.test.ts +56 -127
  14. package/src/__tests__/browser-skill-baseline-tool-payload.test.ts +5 -4
  15. package/src/__tests__/browser-skill-endstate.test.ts +5 -8
  16. package/src/__tests__/call-bridge.test.ts +385 -0
  17. package/src/__tests__/call-constants.test.ts +40 -0
  18. package/src/__tests__/call-orchestrator.test.ts +454 -0
  19. package/src/__tests__/call-recovery.test.ts +518 -0
  20. package/src/__tests__/call-routes-http.test.ts +459 -0
  21. package/src/__tests__/call-state-machine.test.ts +143 -0
  22. package/src/__tests__/call-state.test.ts +133 -0
  23. package/src/__tests__/call-store.test.ts +691 -0
  24. package/src/__tests__/cli-discover.test.ts +1 -1
  25. package/src/__tests__/commit-message-enrichment-service.test.ts +550 -0
  26. package/src/__tests__/compaction.benchmark.test.ts +176 -0
  27. package/src/__tests__/computer-use-tools.test.ts +250 -0
  28. package/src/__tests__/config-schema.test.ts +348 -3
  29. package/src/__tests__/conflict-store.test.ts +2 -1
  30. package/src/__tests__/contacts-tools.test.ts +331 -0
  31. package/src/__tests__/conversation-store.test.ts +30 -32
  32. package/src/__tests__/credential-security-invariants.test.ts +4 -0
  33. package/src/__tests__/date-context.test.ts +373 -0
  34. package/src/__tests__/db-schedule-syntax-migration.test.ts +129 -0
  35. package/src/__tests__/doordash-session.test.ts +9 -0
  36. package/src/__tests__/fixtures/media-reuse-fixtures.ts +3 -3
  37. package/src/__tests__/followup-tools.test.ts +303 -0
  38. package/src/__tests__/handlers-twitter-config.test.ts +718 -0
  39. package/src/__tests__/intent-routing.test.ts +64 -57
  40. package/src/__tests__/ipc-roundtrip.benchmark.test.ts +237 -0
  41. package/src/__tests__/ipc-snapshot.test.ts +96 -28
  42. package/src/__tests__/llm-usage-store.test.ts +3 -8
  43. package/src/__tests__/media-generate-image.test.ts +1 -1
  44. package/src/__tests__/media-reuse-story.e2e.test.ts +7 -7
  45. package/src/__tests__/memory-retrieval.benchmark.test.ts +430 -0
  46. package/src/__tests__/parallel-tool.benchmark.test.ts +294 -0
  47. package/src/__tests__/playbook-tools.test.ts +342 -0
  48. package/src/__tests__/profile-compiler.test.ts +2 -1
  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 +17 -10
  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 +222 -0
  58. package/src/__tests__/run-orchestrator.test.ts +7 -7
  59. package/src/__tests__/runtime-attachment-metadata.test.ts +19 -20
  60. package/src/__tests__/runtime-runs-http.test.ts +5 -23
  61. package/src/__tests__/runtime-runs.test.ts +11 -11
  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-error.test.ts +28 -0
  67. package/src/__tests__/session-init.benchmark.test.ts +462 -0
  68. package/src/__tests__/session-queue.test.ts +89 -16
  69. package/src/__tests__/session-runtime-assembly.test.ts +161 -0
  70. package/src/__tests__/session-surfaces-task-progress.test.ts +104 -0
  71. package/src/__tests__/signup-e2e.test.ts +2 -1
  72. package/src/__tests__/skill-projection.benchmark.test.ts +328 -0
  73. package/src/__tests__/skill-script-runner.test.ts +159 -0
  74. package/src/__tests__/speaker-identification.test.ts +52 -0
  75. package/src/__tests__/subagent-manager-notify.test.ts +42 -10
  76. package/src/__tests__/subagent-tools.test.ts +141 -41
  77. package/src/__tests__/task-compiler.test.ts +2 -1
  78. package/src/__tests__/task-runner.test.ts +2 -1
  79. package/src/__tests__/task-scheduler.test.ts +2 -1
  80. package/src/__tests__/task-tools.test.ts +49 -56
  81. package/src/__tests__/tool-audit-listener.test.ts +1 -0
  82. package/src/__tests__/tool-domain-event-publisher.test.ts +2 -0
  83. package/src/__tests__/tool-execution-pipeline.benchmark.test.ts +500 -0
  84. package/src/__tests__/tool-executor.test.ts +13 -17
  85. package/src/__tests__/turn-commit.test.ts +273 -2
  86. package/src/__tests__/twilio-provider.test.ts +143 -0
  87. package/src/__tests__/twilio-routes.test.ts +789 -0
  88. package/src/__tests__/twitter-auth-handler.test.ts +581 -0
  89. package/src/__tests__/view-image-tool.test.ts +217 -0
  90. package/src/__tests__/workspace-git-service.test.ts +403 -0
  91. package/src/__tests__/workspace-heartbeat-service.test.ts +141 -2
  92. package/src/agent-heartbeat/agent-heartbeat-service.ts +155 -0
  93. package/src/bundler/app-bundler.ts +35 -14
  94. package/src/calls/call-bridge.ts +95 -0
  95. package/src/calls/call-constants.ts +48 -0
  96. package/src/calls/call-domain.ts +276 -0
  97. package/src/calls/call-orchestrator.ts +390 -0
  98. package/src/calls/call-recovery.ts +207 -0
  99. package/src/calls/call-state-machine.ts +68 -0
  100. package/src/calls/call-state.ts +64 -0
  101. package/src/calls/call-store.ts +416 -0
  102. package/src/calls/relay-server.ts +335 -0
  103. package/src/calls/speaker-identification.ts +213 -0
  104. package/src/calls/twilio-config.ts +34 -0
  105. package/src/calls/twilio-provider.ts +173 -0
  106. package/src/calls/twilio-routes.ts +250 -0
  107. package/src/calls/types.ts +37 -0
  108. package/src/calls/voice-provider.ts +14 -0
  109. package/src/cli/config-commands.ts +334 -0
  110. package/src/cli/core-commands.ts +776 -0
  111. package/src/cli/doordash.ts +256 -25
  112. package/src/cli/ipc-client.ts +82 -0
  113. package/src/cli/map.ts +246 -0
  114. package/src/cli/twitter.ts +575 -0
  115. package/src/cli.ts +7 -5
  116. package/src/commands/__tests__/cc-command-registry.test.ts +319 -0
  117. package/src/commands/cc-command-registry.ts +209 -0
  118. package/src/config/bundled-skills/contacts/SKILL.md +39 -0
  119. package/src/config/bundled-skills/contacts/TOOLS.json +122 -0
  120. package/src/config/bundled-skills/contacts/tools/contact-merge.ts +9 -0
  121. package/src/config/bundled-skills/contacts/tools/contact-search.ts +9 -0
  122. package/src/config/bundled-skills/contacts/tools/contact-upsert.ts +9 -0
  123. package/src/config/bundled-skills/document/SKILL.md +18 -0
  124. package/src/config/bundled-skills/document/TOOLS.json +53 -0
  125. package/src/config/bundled-skills/document/tools/document-create.ts +9 -0
  126. package/src/config/bundled-skills/document/tools/document-update.ts +9 -0
  127. package/src/config/bundled-skills/doordash/SKILL.md +163 -0
  128. package/src/config/bundled-skills/followups/SKILL.md +32 -0
  129. package/src/config/bundled-skills/followups/TOOLS.json +100 -0
  130. package/src/config/bundled-skills/followups/tools/followup-create.ts +9 -0
  131. package/src/config/bundled-skills/followups/tools/followup-list.ts +9 -0
  132. package/src/config/bundled-skills/followups/tools/followup-resolve.ts +9 -0
  133. package/src/config/bundled-skills/image-studio/TOOLS.json +2 -2
  134. package/src/config/bundled-skills/image-studio/tools/media-generate-image.ts +2 -24
  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 +44 -0
  179. package/src/config/loader.ts +4 -1
  180. package/src/config/schema.ts +218 -1
  181. package/src/config/system-prompt.ts +100 -6
  182. package/src/config/templates/IDENTITY.md +7 -0
  183. package/src/config/types.ts +5 -0
  184. package/src/contacts/contact-store.ts +4 -4
  185. package/src/daemon/assistant-attachments.ts +10 -0
  186. package/src/daemon/classifier.ts +3 -1
  187. package/src/daemon/computer-use-session.ts +3 -1
  188. package/src/daemon/date-context.ts +136 -0
  189. package/src/daemon/handlers/apps.ts +16 -1
  190. package/src/daemon/handlers/browser.ts +54 -0
  191. package/src/daemon/handlers/computer-use.ts +7 -1
  192. package/src/daemon/handlers/config.ts +192 -4
  193. package/src/daemon/handlers/diagnostics.ts +5 -1
  194. package/src/daemon/handlers/documents.ts +18 -29
  195. package/src/daemon/handlers/home-base.ts +5 -1
  196. package/src/daemon/handlers/index.ts +40 -271
  197. package/src/daemon/handlers/misc.ts +9 -1
  198. package/src/daemon/handlers/publish.ts +6 -1
  199. package/src/daemon/handlers/sessions.ts +65 -12
  200. package/src/daemon/handlers/shared.ts +36 -1
  201. package/src/daemon/handlers/signing.ts +37 -0
  202. package/src/daemon/handlers/skills.ts +20 -6
  203. package/src/daemon/handlers/subagents.ts +8 -3
  204. package/src/daemon/handlers/twitter-auth.ts +169 -0
  205. package/src/daemon/handlers/work-items.ts +495 -39
  206. package/src/daemon/ipc-contract-inventory.json +40 -4
  207. package/src/daemon/ipc-contract.ts +185 -37
  208. package/src/daemon/ipc-protocol.ts +7 -2
  209. package/src/daemon/lifecycle.ts +48 -5
  210. package/src/daemon/main.ts +10 -4
  211. package/src/daemon/ride-shotgun-handler.ts +74 -10
  212. package/src/daemon/server.ts +144 -29
  213. package/src/daemon/session-agent-loop.ts +887 -0
  214. package/src/daemon/session-attachments.ts +28 -5
  215. package/src/daemon/session-error.ts +24 -3
  216. package/src/daemon/session-lifecycle.ts +147 -0
  217. package/src/daemon/session-media-retry.ts +147 -0
  218. package/src/daemon/session-messaging.ts +145 -0
  219. package/src/daemon/session-notifiers.ts +164 -0
  220. package/src/daemon/session-process.ts +2 -2
  221. package/src/daemon/session-queue-manager.ts +1 -0
  222. package/src/daemon/session-runtime-assembly.ts +52 -0
  223. package/src/daemon/session-skill-tools.ts +124 -5
  224. package/src/daemon/session-slash.ts +3 -0
  225. package/src/daemon/session-surfaces.ts +77 -2
  226. package/src/daemon/session-tool-setup.ts +222 -2
  227. package/src/daemon/session-usage.ts +0 -2
  228. package/src/daemon/session.ts +114 -1365
  229. package/src/daemon/video-thumbnail.ts +60 -0
  230. package/src/doordash/client.ts +121 -27
  231. package/src/doordash/queries.ts +1 -2
  232. package/src/export/formatter.ts +3 -1
  233. package/src/followups/followup-store.ts +4 -2
  234. package/src/followups/types.ts +6 -0
  235. package/src/hooks/templates.ts +1 -1
  236. package/src/index.ts +32 -1151
  237. package/src/media/gemini-image-service.ts +1 -1
  238. package/src/memory/attachments-store.ts +28 -83
  239. package/src/memory/channel-delivery-store.ts +7 -21
  240. package/src/memory/clarification-resolver.ts +6 -5
  241. package/src/memory/contradiction-checker.ts +3 -2
  242. package/src/memory/conversation-key-store.ts +10 -29
  243. package/src/memory/conversation-store.ts +2 -1
  244. package/src/memory/db.ts +362 -2
  245. package/src/memory/entity-extractor.ts +6 -3
  246. package/src/memory/items-extractor.ts +5 -4
  247. package/src/memory/jobs-store.ts +3 -2
  248. package/src/memory/llm-usage-store.ts +1 -2
  249. package/src/memory/runs-store.ts +1 -2
  250. package/src/memory/schema.ts +65 -2
  251. package/src/messaging/style-analyzer.ts +3 -2
  252. package/src/messaging/thread-summarizer.ts +8 -12
  253. package/src/messaging/triage-engine.ts +4 -2
  254. package/src/providers/openrouter/client.ts +20 -0
  255. package/src/providers/registry.ts +8 -0
  256. package/src/runtime/http-server.ts +277 -25
  257. package/src/runtime/http-types.ts +0 -2
  258. package/src/runtime/routes/attachment-routes.ts +5 -6
  259. package/src/runtime/routes/call-routes.ts +140 -0
  260. package/src/runtime/routes/channel-routes.ts +12 -19
  261. package/src/runtime/routes/conversation-routes.ts +5 -9
  262. package/src/runtime/routes/run-routes.ts +4 -8
  263. package/src/runtime/run-orchestrator.ts +39 -6
  264. package/src/schedule/recurrence-engine.ts +138 -0
  265. package/src/schedule/recurrence-types.ts +67 -0
  266. package/src/schedule/schedule-store.ts +102 -57
  267. package/src/schedule/scheduler.ts +9 -6
  268. package/src/security/oauth2.ts +29 -4
  269. package/src/security/secret-allowlist.ts +46 -0
  270. package/src/skills/clawhub.ts +1 -1
  271. package/src/subagent/manager.ts +40 -8
  272. package/src/swarm/backend-claude-code.ts +64 -9
  273. package/src/swarm/worker-prompts.ts +2 -1
  274. package/src/tasks/SPEC.md +34 -28
  275. package/src/tasks/ephemeral-permissions.ts +16 -7
  276. package/src/tasks/task-compiler.ts +5 -4
  277. package/src/tasks/task-runner.ts +10 -5
  278. package/src/tasks/task-scheduler.ts +1 -1
  279. package/src/tasks/tool-sanitizer.ts +36 -0
  280. package/src/tools/assets/search.ts +4 -4
  281. package/src/tools/browser/api-map.ts +220 -0
  282. package/src/tools/browser/auto-navigate.ts +270 -0
  283. package/src/tools/browser/browser-execution.ts +2 -1
  284. package/src/tools/browser/browser-manager.ts +2 -2
  285. package/src/tools/browser/network-recorder.ts +5 -4
  286. package/src/tools/browser/x-auto-navigate.ts +207 -0
  287. package/src/tools/calls/call-end.ts +67 -0
  288. package/src/tools/calls/call-start.ts +73 -0
  289. package/src/tools/calls/call-status.ts +81 -0
  290. package/src/tools/claude-code/claude-code.ts +77 -11
  291. package/src/tools/contacts/contact-merge.ts +46 -78
  292. package/src/tools/contacts/contact-search.ts +35 -79
  293. package/src/tools/contacts/contact-upsert.ts +35 -108
  294. package/src/tools/credentials/vault.ts +21 -5
  295. package/src/tools/document/document-tool.ts +71 -144
  296. package/src/tools/executor.ts +129 -10
  297. package/src/tools/followups/followup_create.ts +46 -88
  298. package/src/tools/followups/followup_list.ts +34 -74
  299. package/src/tools/followups/followup_resolve.ts +31 -66
  300. package/src/tools/host-terminal/cli-discover.ts +2 -1
  301. package/src/tools/host-terminal/host-shell.ts +10 -0
  302. package/src/tools/memory/handlers.ts +5 -4
  303. package/src/tools/network/__tests__/web-search.test.ts +427 -0
  304. package/src/tools/network/script-proxy/__tests__/logging.test.ts +248 -0
  305. package/src/tools/network/script-proxy/__tests__/policy.test.ts +234 -0
  306. package/src/tools/network/script-proxy/__tests__/router.test.ts +76 -0
  307. package/src/tools/network/web-fetch.ts +18 -6
  308. package/src/tools/playbooks/index.ts +4 -5
  309. package/src/tools/playbooks/playbook-create.ts +3 -47
  310. package/src/tools/playbooks/playbook-delete.ts +1 -25
  311. package/src/tools/playbooks/playbook-list.ts +1 -28
  312. package/src/tools/playbooks/playbook-update.ts +3 -51
  313. package/src/tools/registry.ts +2 -4
  314. package/src/tools/reminder/reminder.ts +5 -78
  315. package/src/tools/schedule/create.ts +69 -74
  316. package/src/tools/schedule/delete.ts +21 -47
  317. package/src/tools/schedule/list.ts +55 -74
  318. package/src/tools/schedule/update.ts +77 -84
  319. package/src/tools/subagent/abort.ts +29 -58
  320. package/src/tools/subagent/message.ts +30 -63
  321. package/src/tools/subagent/read.ts +53 -84
  322. package/src/tools/subagent/spawn.ts +43 -82
  323. package/src/tools/subagent/status.ts +42 -71
  324. package/src/tools/swarm/delegate.ts +2 -1
  325. package/src/tools/tasks/index.ts +8 -6
  326. package/src/tools/tasks/task-delete.ts +69 -56
  327. package/src/tools/tasks/task-list.ts +31 -52
  328. package/src/tools/tasks/task-run.ts +74 -102
  329. package/src/tools/tasks/task-save.ts +33 -65
  330. package/src/tools/tasks/work-item-enqueue.ts +192 -134
  331. package/src/tools/tasks/work-item-list.ts +33 -78
  332. package/src/tools/tasks/work-item-remove.ts +60 -0
  333. package/src/tools/tasks/work-item-update.ts +114 -0
  334. package/src/tools/terminal/backends/native.ts +3 -1
  335. package/src/tools/tool-manifest.ts +20 -74
  336. package/src/tools/types.ts +6 -0
  337. package/src/tools/ui-surface/definitions.ts +6 -1
  338. package/src/tools/watch/screen-watch.ts +3 -1
  339. package/src/tools/watcher/create.ts +52 -98
  340. package/src/tools/watcher/delete.ts +20 -46
  341. package/src/tools/watcher/digest.ts +36 -70
  342. package/src/tools/watcher/list.ts +49 -79
  343. package/src/tools/watcher/update.ts +45 -91
  344. package/src/twitter/client.ts +690 -0
  345. package/src/twitter/session.ts +91 -0
  346. package/src/usage/types.ts +0 -1
  347. package/src/util/truncate.ts +6 -0
  348. package/src/watcher/providers/slack.ts +2 -1
  349. package/src/watcher/watcher-store.ts +3 -2
  350. package/src/work-items/work-item-store.ts +236 -2
  351. package/src/workspace/commit-message-enrichment-service.ts +284 -0
  352. package/src/workspace/commit-message-provider.ts +95 -0
  353. package/src/workspace/git-service.ts +272 -52
  354. package/src/workspace/heartbeat-service.ts +70 -13
  355. package/src/workspace/provider-commit-message-generator.ts +242 -0
  356. package/src/workspace/turn-commit.ts +100 -51
  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
@@ -1,8 +1,10 @@
1
- import { describe, test, expect, beforeEach, afterEach } from 'bun:test';
1
+ import { describe, test, expect, beforeEach, afterEach, afterAll } from 'bun:test';
2
2
  import { mkdirSync, rmSync, writeFileSync, existsSync } from 'node:fs';
3
3
  import { join } from 'node:path';
4
4
  import { tmpdir } from 'node:os';
5
5
  import { execFileSync } from 'node:child_process';
6
+ import type { CommitMessageProvider, CommitContext, CommitMessageResult } from '../workspace/commit-message-provider.js';
7
+
6
8
  import {
7
9
  WorkspaceGitService,
8
10
  _resetGitServiceRegistry,
@@ -11,6 +13,7 @@ import {
11
13
  HeartbeatService,
12
14
  _resetHeartbeatState,
13
15
  } from '../workspace/heartbeat-service.js';
16
+ import { _resetEnrichmentService, getEnrichmentService } from '../workspace/commit-message-enrichment-service.js';
14
17
 
15
18
  describe('HeartbeatService', () => {
16
19
  let testDir: string;
@@ -30,12 +33,20 @@ describe('HeartbeatService', () => {
30
33
  services.set(testDir, service);
31
34
  });
32
35
 
33
- afterEach(() => {
36
+ afterEach(async () => {
37
+ // Shut down any in-flight enrichment work before removing the test directory
38
+ try { await getEnrichmentService().shutdown(); } catch { /* ignore */ }
39
+ _resetEnrichmentService();
34
40
  if (existsSync(testDir)) {
35
41
  rmSync(testDir, { recursive: true, force: true });
36
42
  }
37
43
  });
38
44
 
45
+ afterAll(async () => {
46
+ try { await getEnrichmentService().shutdown(); } catch { /* ignore */ }
47
+ _resetEnrichmentService();
48
+ });
49
+
39
50
  describe('heartbeat check with age threshold', () => {
40
51
  test('does not commit when workspace is clean', async () => {
41
52
  const heartbeat = new HeartbeatService({
@@ -344,4 +355,132 @@ describe('HeartbeatService', () => {
344
355
  await heartbeat.stop(); // Idempotent
345
356
  });
346
357
  });
358
+
359
+ describe('custom commit message provider', () => {
360
+ test('heartbeat commit uses custom provider message', async () => {
361
+ writeFileSync(join(testDir, 'file.txt'), 'content');
362
+
363
+ const customProvider: CommitMessageProvider = {
364
+ buildImmediateMessage(ctx: CommitContext): CommitMessageResult {
365
+ return {
366
+ message: `CUSTOM-HEARTBEAT: ${ctx.changedFiles.length} files via ${ctx.trigger}`,
367
+ metadata: { customProvider: true, trigger: ctx.trigger },
368
+ };
369
+ },
370
+ };
371
+
372
+ let currentTime = 1000000;
373
+ const heartbeat = new HeartbeatService({
374
+ ageThresholdMs: 5 * 60 * 1000,
375
+ fileThreshold: 100,
376
+ getServices: () => services,
377
+ now: () => currentTime,
378
+ commitMessageProvider: customProvider,
379
+ });
380
+
381
+ // First check registers dirty state
382
+ await heartbeat.check();
383
+ // Advance time past threshold
384
+ currentTime += 6 * 60 * 1000;
385
+ // Second check commits
386
+ const result = await heartbeat.check();
387
+ expect(result.committed).toBe(1);
388
+
389
+ const commitMsg = execFileSync('git', ['log', '-1', '--pretty=%B'], {
390
+ cwd: testDir,
391
+ encoding: 'utf-8',
392
+ });
393
+ expect(commitMsg).toContain('CUSTOM-HEARTBEAT:');
394
+ expect(commitMsg).toContain('via heartbeat');
395
+ expect(commitMsg).toContain('customProvider: true');
396
+ });
397
+
398
+ test('shutdown commit uses custom provider message', async () => {
399
+ writeFileSync(join(testDir, 'unsaved.txt'), 'uncommitted content');
400
+
401
+ const customProvider: CommitMessageProvider = {
402
+ buildImmediateMessage(ctx: CommitContext): CommitMessageResult {
403
+ return {
404
+ message: `CUSTOM-SHUTDOWN: saving ${ctx.changedFiles.length} files`,
405
+ metadata: { shutdownProvider: true },
406
+ };
407
+ },
408
+ };
409
+
410
+ const heartbeat = new HeartbeatService({
411
+ getServices: () => services,
412
+ commitMessageProvider: customProvider,
413
+ });
414
+
415
+ const result = await heartbeat.commitAllPending();
416
+ expect(result.committed).toBe(1);
417
+
418
+ const commitMsg = execFileSync('git', ['log', '-1', '--pretty=%B'], {
419
+ cwd: testDir,
420
+ encoding: 'utf-8',
421
+ });
422
+ expect(commitMsg).toContain('CUSTOM-SHUTDOWN: saving');
423
+ expect(commitMsg).toContain('shutdownProvider: true');
424
+ });
425
+
426
+ test('custom provider receives correct context fields for heartbeat trigger', async () => {
427
+ writeFileSync(join(testDir, 'a.txt'), 'a');
428
+ writeFileSync(join(testDir, 'b.txt'), 'b');
429
+
430
+ let capturedCtx: CommitContext | null = null;
431
+ const customProvider: CommitMessageProvider = {
432
+ buildImmediateMessage(ctx: CommitContext): CommitMessageResult {
433
+ capturedCtx = ctx;
434
+ return { message: 'capture-context' };
435
+ },
436
+ };
437
+
438
+ let currentTime = 1000000;
439
+ const heartbeat = new HeartbeatService({
440
+ ageThresholdMs: 5 * 60 * 1000,
441
+ fileThreshold: 100,
442
+ getServices: () => services,
443
+ now: () => currentTime,
444
+ commitMessageProvider: customProvider,
445
+ });
446
+
447
+ // Register dirty state
448
+ await heartbeat.check();
449
+ // Advance past threshold
450
+ currentTime += 6 * 60 * 1000;
451
+ await heartbeat.check();
452
+
453
+ expect(capturedCtx).not.toBeNull();
454
+ expect(capturedCtx!.trigger).toBe('heartbeat');
455
+ expect(capturedCtx!.workspaceDir).toBe(testDir);
456
+ expect(capturedCtx!.changedFiles).toContain('a.txt');
457
+ expect(capturedCtx!.changedFiles).toContain('b.txt');
458
+ expect(capturedCtx!.timestampMs).toBe(currentTime);
459
+ expect(capturedCtx!.reason).toBeDefined();
460
+ });
461
+
462
+ test('custom provider receives correct context fields for shutdown trigger', async () => {
463
+ writeFileSync(join(testDir, 'shutdown-file.txt'), 'data');
464
+
465
+ let capturedCtx: CommitContext | null = null;
466
+ const customProvider: CommitMessageProvider = {
467
+ buildImmediateMessage(ctx: CommitContext): CommitMessageResult {
468
+ capturedCtx = ctx;
469
+ return { message: 'capture-shutdown-context' };
470
+ },
471
+ };
472
+
473
+ const heartbeat = new HeartbeatService({
474
+ getServices: () => services,
475
+ commitMessageProvider: customProvider,
476
+ });
477
+
478
+ await heartbeat.commitAllPending();
479
+
480
+ expect(capturedCtx).not.toBeNull();
481
+ expect(capturedCtx!.trigger).toBe('shutdown');
482
+ expect(capturedCtx!.workspaceDir).toBe(testDir);
483
+ expect(capturedCtx!.changedFiles).toContain('shutdown-file.txt');
484
+ });
485
+ });
347
486
  });
@@ -0,0 +1,155 @@
1
+ import { readFileSync, existsSync } from 'node:fs';
2
+ import { getLogger } from '../util/logger.js';
3
+ import { getWorkspacePromptPath } from '../util/platform.js';
4
+ import { getConfig } from '../config/loader.js';
5
+ import { createConversation } from '../memory/conversation-store.js';
6
+ import type { AgentHeartbeatAlert } from '../daemon/ipc-contract.js';
7
+
8
+ const log = getLogger('agent-heartbeat');
9
+
10
+ const DEFAULT_CHECKLIST = `- Check the current weather and note anything notable
11
+ - Review any recent news headlines worth flagging
12
+ - Look for calendar events or reminders coming up soon`;
13
+
14
+ export interface AgentHeartbeatDeps {
15
+ processMessage: (conversationId: string, content: string) => Promise<{ messageId: string }>;
16
+ alerter: (alert: AgentHeartbeatAlert) => void;
17
+ /** Override for current hour (0-23), for testing. */
18
+ getCurrentHour?: () => number;
19
+ }
20
+
21
+ export class AgentHeartbeatService {
22
+ private readonly deps: AgentHeartbeatDeps;
23
+ private timer: ReturnType<typeof setInterval> | null = null;
24
+ private activeRun: Promise<void> | null = null;
25
+
26
+ constructor(deps: AgentHeartbeatDeps) {
27
+ this.deps = deps;
28
+ }
29
+
30
+ start(): void {
31
+ const config = getConfig().agentHeartbeat;
32
+ if (!config.enabled) {
33
+ log.info('Agent heartbeat disabled by config');
34
+ return;
35
+ }
36
+ if (this.timer) return;
37
+
38
+ log.info({ intervalMs: config.intervalMs }, 'Agent heartbeat service started');
39
+ this.timer = setInterval(() => {
40
+ this.runOnce().catch((err) => {
41
+ log.error({ err }, 'Agent heartbeat runOnce failed');
42
+ });
43
+ }, config.intervalMs);
44
+ }
45
+
46
+ async stop(): Promise<void> {
47
+ if (this.timer) {
48
+ clearInterval(this.timer);
49
+ this.timer = null;
50
+ }
51
+ if (this.activeRun) {
52
+ let timerId: ReturnType<typeof setTimeout>;
53
+ const timeout = new Promise<void>((resolve) => { timerId = setTimeout(resolve, 5_000); });
54
+ await Promise.race([this.activeRun, timeout]);
55
+ clearTimeout(timerId!);
56
+ }
57
+ log.info('Agent heartbeat service stopped');
58
+ }
59
+
60
+ async runOnce(): Promise<void> {
61
+ const config = getConfig().agentHeartbeat;
62
+ if (!config.enabled) return;
63
+
64
+ // Active hours guard — only applied when both bounds are set.
65
+ // The schema rejects configs where only one bound is provided.
66
+ if (config.activeHoursStart != null && config.activeHoursEnd != null) {
67
+ const hour = this.deps.getCurrentHour?.() ?? new Date().getHours();
68
+ if (!isWithinActiveHours(hour, config.activeHoursStart, config.activeHoursEnd)) {
69
+ log.debug({ hour, activeHoursStart: config.activeHoursStart, activeHoursEnd: config.activeHoursEnd }, 'Outside active hours, skipping');
70
+ return;
71
+ }
72
+ }
73
+
74
+ // Overlap prevention
75
+ if (this.activeRun) {
76
+ log.debug('Previous heartbeat run still active, skipping');
77
+ return;
78
+ }
79
+
80
+ const run = this.executeRun();
81
+ this.activeRun = run;
82
+ try {
83
+ await run;
84
+ } finally {
85
+ this.activeRun = null;
86
+ }
87
+ }
88
+
89
+ private async executeRun(): Promise<void> {
90
+ log.info('Running agent heartbeat');
91
+
92
+ try {
93
+ const checklist = this.readChecklist();
94
+ const prompt = this.buildPrompt(checklist);
95
+
96
+ const conversation = createConversation({
97
+ title: 'Agent Heartbeat',
98
+ threadType: 'background',
99
+ });
100
+
101
+ await this.deps.processMessage(conversation.id, prompt);
102
+ log.info({ conversationId: conversation.id }, 'Agent heartbeat completed');
103
+ } catch (err) {
104
+ log.error({ err }, 'Agent heartbeat failed');
105
+ try {
106
+ this.deps.alerter({
107
+ type: 'agent_heartbeat_alert',
108
+ title: 'Agent Heartbeat Failed',
109
+ body: err instanceof Error ? err.message : String(err),
110
+ });
111
+ } catch (alertErr) {
112
+ log.warn({ alertErr }, 'Failed to broadcast heartbeat alert');
113
+ }
114
+ }
115
+ }
116
+
117
+ private readChecklist(): string {
118
+ const heartbeatPath = getWorkspacePromptPath('HEARTBEAT.md');
119
+ if (existsSync(heartbeatPath)) {
120
+ try {
121
+ return readFileSync(heartbeatPath, 'utf-8');
122
+ } catch (err) {
123
+ log.warn({ err, heartbeatPath }, 'Failed to read HEARTBEAT.md, using default checklist');
124
+ }
125
+ }
126
+ return DEFAULT_CHECKLIST;
127
+ }
128
+
129
+ /** @internal Exposed for testing. */
130
+ buildPrompt(checklist: string): string {
131
+ return `You are running a periodic heartbeat check. Review the following checklist and take any necessary actions.
132
+
133
+ <heartbeat-checklist>
134
+ ${checklist}
135
+ </heartbeat-checklist>
136
+
137
+ <heartbeat-disposition>
138
+ After completing your review, end your response with one of:
139
+ - HEARTBEAT_OK — if everything looks good, no action needed
140
+ - HEARTBEAT_ALERT — if you found issues that need attention (describe them before this marker)
141
+ </heartbeat-disposition>`;
142
+ }
143
+ }
144
+
145
+ /**
146
+ * Check if the given hour falls within the active window.
147
+ * Handles overnight windows (e.g. start=22, end=6).
148
+ */
149
+ function isWithinActiveHours(hour: number, start: number, end: number): boolean {
150
+ if (start <= end) {
151
+ return hour >= start && hour < end;
152
+ }
153
+ // Overnight window: e.g. 22-6 means 22,23,0,1,2,3,4,5
154
+ return hour >= start || hour < end;
155
+ }
@@ -25,6 +25,8 @@ const bundlerLog = getLogger('app-bundler');
25
25
  import { APP_VERSION } from '../version.js';
26
26
  const PACKAGE_VERSION = APP_VERSION;
27
27
 
28
+ const SHORT_HASH_LENGTH = 8;
29
+ const HASH_DISPLAY_LENGTH = 12;
28
30
  const MAX_BUNDLE_SIZE_BYTES = 25 * 1024 * 1024; // 25 MB
29
31
  const ASSET_FETCH_TIMEOUT_MS = 10_000;
30
32
 
@@ -35,21 +37,33 @@ interface FetchedAsset {
35
37
 
36
38
  /**
37
39
  * Extract all remote (http/https) URLs from HTML content.
38
- * Looks in src=, href=, and CSS url() references.
40
+ * Looks in src=, href= on all elements except <a> tags, and CSS url() references.
39
41
  */
40
42
  export function extractRemoteUrls(html: string): string[] {
41
43
  const urls = new Set<string>();
42
44
 
43
- // Match src="..." and href="..." attributes (double or single quotes, or unquoted)
44
- const attrRe = /\b(?:src|href)\s*=\s*(?:"([^"]*?)"|'([^']*?)'|([^\s>]+))/gi;
45
+ // Match src="..." attributes on any element
46
+ const srcRe = /\bsrc\s*=\s*(?:"([^"]*?)"|'([^']*?)'|([^\s>]+))/gi;
45
47
  let m: RegExpExecArray | null;
46
- while ((m = attrRe.exec(html)) !== null) {
48
+ while ((m = srcRe.exec(html)) !== null) {
47
49
  const url = m[1] ?? m[2] ?? m[3];
48
50
  if (url && /^https?:\/\//i.test(url)) {
49
51
  urls.add(url);
50
52
  }
51
53
  }
52
54
 
55
+ // Match href="..." on any element except navigation/resolution tags (not assets).
56
+ // Captures the tag name and href value so we can skip them.
57
+ const hrefRe = /<(\w+)\b[^>]*?\bhref\s*=\s*(?:"([^"]*?)"|'([^']*?)'|([^\s>]+))[^>]*?\/?>/gi;
58
+ while ((m = hrefRe.exec(html)) !== null) {
59
+ const tagName = m[1];
60
+ if (['a', 'base', 'area'].includes(tagName.toLowerCase())) continue;
61
+ const url = m[2] ?? m[3] ?? m[4];
62
+ if (url && /^https?:\/\//i.test(url)) {
63
+ urls.add(url);
64
+ }
65
+ }
66
+
53
67
  // Match CSS url() references (inline styles and <style> blocks)
54
68
  const urlRe = /url\(\s*(?:"([^"]*?)"|'([^']*?)'|([^)"'\s]+))\s*\)/gi;
55
69
  while ((m = urlRe.exec(html)) !== null) {
@@ -67,7 +81,7 @@ export function extractRemoteUrls(html: string): string[] {
67
81
  * Uses a hash of the URL to avoid collisions, preserving the original extension.
68
82
  */
69
83
  function assetFilename(url: string): string {
70
- const hash = createHash('sha256').update(url).digest('hex').slice(0, 12);
84
+ const hash = createHash('sha256').update(url).digest('hex').slice(0, HASH_DISPLAY_LENGTH);
71
85
  let ext = '';
72
86
  try {
73
87
  const parsed = new URL(url);
@@ -104,13 +118,17 @@ export async function materializeAssets(
104
118
  try {
105
119
  const controller = new AbortController();
106
120
  const timeout = setTimeout(() => controller.abort(), ASSET_FETCH_TIMEOUT_MS);
107
- const resp = await fetch(url, { signal: controller.signal });
108
- clearTimeout(timeout);
109
- if (!resp.ok) {
110
- bundlerLog.warn({ url, status: resp.status }, 'Failed to fetch asset, keeping original URL');
111
- return;
121
+ let buf: Buffer;
122
+ try {
123
+ const resp = await fetch(url, { signal: controller.signal });
124
+ if (!resp.ok) {
125
+ bundlerLog.warn({ url, status: resp.status }, 'Failed to fetch asset, keeping original URL');
126
+ return;
127
+ }
128
+ buf = Buffer.from(await resp.arrayBuffer());
129
+ } finally {
130
+ clearTimeout(timeout);
112
131
  }
113
- const buf = Buffer.from(await resp.arrayBuffer());
114
132
  const filename = assetFilename(url);
115
133
  const archivePath = `assets/${filename}`;
116
134
  assets.push({ archivePath, data: buf });
@@ -121,9 +139,12 @@ export async function materializeAssets(
121
139
  }),
122
140
  );
123
141
 
124
- // Rewrite URLs in HTML — replace each occurrence of the original URL with the local path
142
+ // Rewrite URLs in HTML — replace each occurrence of the original URL with the local path.
143
+ // Sort by length descending so longer URLs are replaced first, preventing prefix collisions
144
+ // (e.g. "https://cdn/x" replacing part of "https://cdn/x/y.png").
145
+ const sortedEntries = [...urlMap.entries()].sort((a, b) => b[0].length - a[0].length);
125
146
  let rewrittenHtml = html;
126
- for (const [originalUrl, localPath] of urlMap) {
147
+ for (const [originalUrl, localPath] of sortedEntries) {
127
148
  // Escape regex special chars in the URL
128
149
  const escaped = originalUrl.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
129
150
  rewrittenHtml = rewrittenHtml.replace(new RegExp(escaped, 'g'), localPath);
@@ -196,7 +217,7 @@ export async function packageApp(
196
217
  const allAssets = [...allAssetsMap.values()];
197
218
 
198
219
  // Create the zip archive
199
- const bundleFilename = `${app.name.replace(/[^a-zA-Z0-9_-]/g, '_')}-${randomUUID().slice(0, 8)}.vellumapp`;
220
+ const bundleFilename = `${app.name.replace(/[^a-zA-Z0-9_-]/g, '_')}-${randomUUID().slice(0, SHORT_HASH_LENGTH)}.vellumapp`;
200
221
  const bundlePath = join(tmpdir(), bundleFilename);
201
222
 
202
223
  await new Promise<void>((resolve, reject) => {
@@ -0,0 +1,95 @@
1
+ /**
2
+ * Call-answer bridge: auto-consumes user replies in-thread as answers
3
+ * to pending call questions, routing them to the live call orchestrator.
4
+ *
5
+ * When a call has a pending question and the user sends a normal message
6
+ * in the conversation thread, this bridge intercepts the message before
7
+ * the agent loop, forwards the answer to the orchestrator, and returns
8
+ * `{ handled: true }` so the caller can skip agent processing.
9
+ */
10
+
11
+ import { getLogger } from '../util/logger.js';
12
+ import {
13
+ getActiveCallSessionForConversation,
14
+ getPendingQuestion,
15
+ answerPendingQuestion,
16
+ recordCallEvent,
17
+ getCallSession,
18
+ } from './call-store.js';
19
+ import { getCallOrchestrator } from './call-state.js';
20
+ import * as conversationStore from '../memory/conversation-store.js';
21
+
22
+ const log = getLogger('call-bridge');
23
+
24
+ export interface CallBridgeResult {
25
+ handled: boolean;
26
+ reason?: string;
27
+ }
28
+
29
+ /**
30
+ * Attempt to route a user message as an answer to a pending call question.
31
+ *
32
+ * @param conversationId - The conversation the message belongs to.
33
+ * @param userText - The user's message text.
34
+ * @param _userMessageId - The persisted message ID (reserved for future use).
35
+ * @returns `{ handled: true }` if the answer was consumed by the call system,
36
+ * `{ handled: false, reason }` otherwise.
37
+ */
38
+ export async function tryHandlePendingCallAnswer(
39
+ conversationId: string,
40
+ userText: string,
41
+ _userMessageId?: string,
42
+ ): Promise<CallBridgeResult> {
43
+ // 1. Find an active call for this conversation
44
+ const callSession = getActiveCallSessionForConversation(conversationId);
45
+ if (!callSession) {
46
+ return { handled: false, reason: 'no_active_call' };
47
+ }
48
+
49
+ // 2. Check for a pending question
50
+ const pendingQuestion = getPendingQuestion(callSession.id);
51
+ if (!pendingQuestion) {
52
+ return { handled: false, reason: 'no_pending_question' };
53
+ }
54
+
55
+ // 3. Check that the orchestrator is alive and waiting
56
+ const orchestrator = getCallOrchestrator(callSession.id);
57
+ if (!orchestrator) {
58
+ // The call may have ended between the question being asked and the
59
+ // user replying. Persist a follow-up message so the user knows.
60
+ const freshSession = getCallSession(callSession.id);
61
+ const ended = freshSession && (freshSession.status === 'completed' || freshSession.status === 'failed');
62
+ if (ended) {
63
+ conversationStore.addMessage(
64
+ conversationId,
65
+ 'assistant',
66
+ JSON.stringify([{
67
+ type: 'text',
68
+ text: 'The call ended before your answer could be relayed to the caller.',
69
+ }]),
70
+ );
71
+ }
72
+ return { handled: false, reason: 'orchestrator_not_found' };
73
+ }
74
+
75
+ if (orchestrator.getState() !== 'waiting_on_user') {
76
+ return { handled: false, reason: 'orchestrator_not_waiting' };
77
+ }
78
+
79
+ // 4. Route the answer through the orchestrator
80
+ const accepted = await orchestrator.handleUserAnswer(userText);
81
+ if (!accepted) {
82
+ return { handled: false, reason: 'orchestrator_rejected' };
83
+ }
84
+
85
+ // 5. Persist the answered state
86
+ answerPendingQuestion(pendingQuestion.id, userText);
87
+ recordCallEvent(callSession.id, 'user_answered', { answer: userText });
88
+
89
+ log.info(
90
+ { conversationId, callSessionId: callSession.id, questionId: pendingQuestion.id },
91
+ 'User reply routed as call answer via bridge',
92
+ );
93
+
94
+ return { handled: true };
95
+ }
@@ -0,0 +1,48 @@
1
+ import { getConfig } from '../config/loader.js';
2
+
3
+ // Emergency/high-risk numbers that should never be called
4
+ export const DENIED_NUMBERS = new Set([
5
+ '911', '112', '999', '000', '110', '119',
6
+ ]);
7
+
8
+ /**
9
+ * Check whether a phone number is a denied emergency number.
10
+ *
11
+ * Normalizes E.164 variants by stripping the leading '+' and then checking
12
+ * whether the resulting digit string exactly matches a denied number or could
13
+ * be a country-code-prefixed emergency number (e.g. +1911, +44999, +61000).
14
+ * Country codes are 1–3 digits, so we try every valid split.
15
+ */
16
+ export function isDeniedNumber(phoneNumber: string): boolean {
17
+ // Strip leading '+' to get a digits-only string
18
+ const digits = phoneNumber.startsWith('+') ? phoneNumber.slice(1) : phoneNumber;
19
+
20
+ // Exact match (covers bare short codes like "911", "112")
21
+ if (DENIED_NUMBERS.has(digits)) return true;
22
+
23
+ // Try splitting off 1-, 2-, or 3-digit country codes and check if the
24
+ // remainder is a denied number. This catches patterns like +1911, +44999.
25
+ for (let ccLen = 1; ccLen <= 3; ccLen++) {
26
+ if (digits.length > ccLen) {
27
+ const remainder = digits.slice(ccLen);
28
+ if (DENIED_NUMBERS.has(remainder)) return true;
29
+ }
30
+ }
31
+
32
+ return false;
33
+ }
34
+
35
+ // Call limits — backed by config with hardcoded fallbacks
36
+ export function getMaxCallDurationMs(): number {
37
+ return getConfig().calls.maxDurationSeconds * 1000;
38
+ }
39
+
40
+ export function getUserConsultationTimeoutMs(): number {
41
+ return getConfig().calls.userConsultTimeoutSeconds * 1000;
42
+ }
43
+
44
+ export const SILENCE_TIMEOUT_MS = 30 * 1000; // 30 seconds
45
+
46
+ // Legacy named exports for backward compatibility (use functions above for config-backed values)
47
+ export const MAX_CALL_DURATION_MS = 3600 * 1000; // fallback default; prefer getMaxCallDurationMs()
48
+ export const USER_CONSULTATION_TIMEOUT_MS = 120 * 1000; // fallback default; prefer getUserConsultationTimeoutMs()