@vellumai/assistant 0.5.6 → 0.5.8

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 (442) hide show
  1. package/.env.example +16 -2
  2. package/ARCHITECTURE.md +6 -75
  3. package/Dockerfile +3 -2
  4. package/README.md +0 -2
  5. package/bun.lock +0 -414
  6. package/docker-entrypoint.sh +9 -0
  7. package/docs/architecture/keychain-broker.md +45 -240
  8. package/docs/architecture/memory.md +13 -11
  9. package/docs/architecture/security.md +0 -17
  10. package/docs/credential-execution-service.md +2 -2
  11. package/node_modules/@vellumai/ces-contracts/package.json +1 -0
  12. package/node_modules/@vellumai/ces-contracts/src/error.ts +1 -1
  13. package/node_modules/@vellumai/ces-contracts/src/grants.ts +1 -1
  14. package/node_modules/@vellumai/ces-contracts/src/handles.ts +1 -1
  15. package/node_modules/@vellumai/ces-contracts/src/index.ts +1 -1
  16. package/node_modules/@vellumai/ces-contracts/src/rpc.ts +120 -1
  17. package/node_modules/@vellumai/credential-storage/package.json +1 -0
  18. package/node_modules/@vellumai/egress-proxy/package.json +1 -0
  19. package/package.json +2 -3
  20. package/src/__tests__/actor-token-service.test.ts +0 -114
  21. package/src/__tests__/approval-cascade.test.ts +0 -1
  22. package/src/__tests__/assistant-feature-flags-integration.test.ts +30 -29
  23. package/src/__tests__/browser-fill-credential.test.ts +1 -1
  24. package/src/__tests__/browser-skill-endstate.test.ts +6 -5
  25. package/src/__tests__/btw-routes.test.ts +0 -39
  26. package/src/__tests__/call-controller.test.ts +0 -1
  27. package/src/__tests__/call-domain.test.ts +0 -128
  28. package/src/__tests__/ces-rpc-credential-backend.test.ts +199 -0
  29. package/src/__tests__/ces-startup-timeout.test.ts +40 -0
  30. package/src/__tests__/channel-approval-routes.test.ts +0 -5
  31. package/src/__tests__/channel-readiness-service.test.ts +1 -60
  32. package/src/__tests__/checker.test.ts +4 -2
  33. package/src/__tests__/cli-command-risk-guard.test.ts +112 -0
  34. package/src/__tests__/config-schema-cmd.test.ts +0 -2
  35. package/src/__tests__/config-schema.test.ts +3 -1
  36. package/src/__tests__/conversation-abort-tool-results.test.ts +0 -1
  37. package/src/__tests__/conversation-agent-loop-overflow.test.ts +0 -2
  38. package/src/__tests__/conversation-agent-loop.test.ts +2 -4
  39. package/src/__tests__/conversation-attention-telegram.test.ts +0 -5
  40. package/src/__tests__/conversation-confirmation-signals.test.ts +0 -1
  41. package/src/__tests__/conversation-error.test.ts +15 -1
  42. package/src/__tests__/conversation-init.benchmark.test.ts +0 -2
  43. package/src/__tests__/conversation-messaging-secret-redirect.test.ts +1 -1
  44. package/src/__tests__/conversation-pre-run-repair.test.ts +0 -1
  45. package/src/__tests__/conversation-provider-retry-repair.test.ts +0 -1
  46. package/src/__tests__/conversation-queue.test.ts +0 -1
  47. package/src/__tests__/conversation-skill-tools.test.ts +0 -54
  48. package/src/__tests__/conversation-slash-queue.test.ts +0 -1
  49. package/src/__tests__/conversation-slash-unknown.test.ts +0 -1
  50. package/src/__tests__/conversation-title-service.test.ts +87 -0
  51. package/src/__tests__/conversation-workspace-injection.test.ts +0 -1
  52. package/src/__tests__/conversation-workspace-tool-tracking.test.ts +0 -1
  53. package/src/__tests__/credential-execution-client.test.ts +5 -2
  54. package/src/__tests__/credential-execution-feature-gates.test.ts +59 -30
  55. package/src/__tests__/credential-execution-managed-contract.test.ts +35 -20
  56. package/src/__tests__/credential-security-e2e.test.ts +1 -67
  57. package/src/__tests__/credential-security-invariants.test.ts +6 -50
  58. package/src/__tests__/credentials-cli.test.ts +82 -3
  59. package/src/__tests__/daemon-credential-client.test.ts +123 -0
  60. package/src/__tests__/db-migration-rollback.test.ts +2015 -1
  61. package/src/__tests__/deterministic-verification-control-plane.test.ts +1 -0
  62. package/src/__tests__/docker-signing-key-bootstrap.test.ts +34 -143
  63. package/src/__tests__/dynamic-skill-workflow-prompt.test.ts +6 -4
  64. package/src/__tests__/gateway-client-managed-outbound.test.ts +79 -1
  65. package/src/__tests__/guardian-routing-state.test.ts +0 -5
  66. package/src/__tests__/host-shell-tool.test.ts +6 -7
  67. package/src/__tests__/http-user-message-parity.test.ts +3 -103
  68. package/src/__tests__/inbound-invite-redemption.test.ts +0 -4
  69. package/src/__tests__/inline-skill-load-permissions.test.ts +6 -8
  70. package/src/__tests__/intent-routing.test.ts +0 -13
  71. package/src/__tests__/jobs-store-qdrant-breaker.test.ts +178 -0
  72. package/src/__tests__/journal-context.test.ts +335 -0
  73. package/src/__tests__/keychain-broker-client.test.ts +161 -22
  74. package/src/__tests__/memory-context-benchmark.benchmark.test.ts +0 -3
  75. package/src/__tests__/memory-jobs-worker-backoff.test.ts +150 -0
  76. package/src/__tests__/memory-lifecycle-e2e.test.ts +70 -25
  77. package/src/__tests__/memory-recall-quality.test.ts +48 -17
  78. package/src/__tests__/memory-regressions.test.ts +408 -363
  79. package/src/__tests__/memory-retrieval.benchmark.test.ts +0 -3
  80. package/src/__tests__/migration-export-http.test.ts +2 -2
  81. package/src/__tests__/migration-import-commit-http.test.ts +2 -2
  82. package/src/__tests__/migration-import-preflight-http.test.ts +2 -2
  83. package/src/__tests__/migration-validate-http.test.ts +2 -2
  84. package/src/__tests__/non-member-access-request.test.ts +2 -7
  85. package/src/__tests__/notification-decision-fallback.test.ts +4 -0
  86. package/src/__tests__/notification-decision-identity.test.ts +4 -0
  87. package/src/__tests__/notification-decision-strategy.test.ts +71 -0
  88. package/src/__tests__/oauth-cli.test.ts +5 -1
  89. package/src/__tests__/permission-types.test.ts +1 -0
  90. package/src/__tests__/provider-commit-message-generator.test.ts +0 -37
  91. package/src/__tests__/provider-error-scenarios.test.ts +0 -267
  92. package/src/__tests__/provider-managed-proxy-integration.test.ts +5 -6
  93. package/src/__tests__/provider-streaming.benchmark.test.ts +2 -81
  94. package/src/__tests__/qdrant-manager.test.ts +28 -2
  95. package/src/__tests__/registry.test.ts +0 -6
  96. package/src/__tests__/relay-server.test.ts +1 -2
  97. package/src/__tests__/runtime-attachment-metadata.test.ts +0 -4
  98. package/src/__tests__/script-proxy-injection-runtime.test.ts +1 -1
  99. package/src/__tests__/secret-onetime-send.test.ts +1 -1
  100. package/src/__tests__/secret-routes-managed-proxy.test.ts +0 -4
  101. package/src/__tests__/secure-keys.test.ts +95 -272
  102. package/src/__tests__/shell-identity.test.ts +96 -6
  103. package/src/__tests__/skill-feature-flags-integration.test.ts +22 -14
  104. package/src/__tests__/skill-feature-flags.test.ts +46 -45
  105. package/src/__tests__/skill-load-feature-flag.test.ts +7 -10
  106. package/src/__tests__/skill-load-inline-command.test.ts +8 -12
  107. package/src/__tests__/skill-load-inline-includes.test.ts +6 -10
  108. package/src/__tests__/skill-load-tool.test.ts +0 -2
  109. package/src/__tests__/skill-memory.test.ts +17 -3
  110. package/src/__tests__/skill-projection-feature-flag.test.ts +33 -29
  111. package/src/__tests__/skills.test.ts +0 -2
  112. package/src/__tests__/slack-inbound-verification.test.ts +0 -4
  113. package/src/__tests__/stale-approval-dedup.test.ts +171 -0
  114. package/src/__tests__/stt-hints.test.ts +437 -0
  115. package/src/__tests__/suggestion-routes.test.ts +1 -32
  116. package/src/__tests__/system-prompt.test.ts +0 -1
  117. package/src/__tests__/task-memory-cleanup.test.ts +14 -0
  118. package/src/__tests__/tool-executor-shell-integration.test.ts +5 -3
  119. package/src/__tests__/trusted-contact-lifecycle-notifications.test.ts +0 -5
  120. package/src/__tests__/trusted-contact-multichannel.test.ts +0 -4
  121. package/src/__tests__/twilio-routes-twiml.test.ts +139 -1
  122. package/src/__tests__/update-bulletin.test.ts +0 -2
  123. package/src/__tests__/vellum-self-knowledge-inline-command.test.ts +6 -9
  124. package/src/__tests__/voice-quality.test.ts +58 -0
  125. package/src/__tests__/voice-scoped-grant-consumer.test.ts +0 -7
  126. package/src/__tests__/workspace-migration-015-migrate-credentials-to-keychain.test.ts +252 -0
  127. package/src/__tests__/workspace-migration-016-migrate-credentials-from-keychain.test.ts +220 -0
  128. package/src/__tests__/workspace-migration-down-functions.test.ts +1009 -0
  129. package/src/__tests__/workspace-migrations-runner.test.ts +114 -0
  130. package/src/acp/agent-process.ts +9 -1
  131. package/src/agent/loop.ts +1 -1
  132. package/src/approvals/guardian-request-resolvers.ts +164 -38
  133. package/src/calls/__tests__/tts-text-sanitizer.test.ts +254 -0
  134. package/src/calls/audio-store.test.ts +97 -0
  135. package/src/calls/audio-store.ts +205 -0
  136. package/src/calls/call-controller.ts +90 -8
  137. package/src/calls/call-domain.ts +3 -0
  138. package/src/calls/call-store.ts +10 -3
  139. package/src/calls/fish-audio-client.ts +129 -0
  140. package/src/calls/relay-server.ts +27 -0
  141. package/src/calls/stt-hints.ts +189 -0
  142. package/src/calls/tts-text-sanitizer.ts +61 -0
  143. package/src/calls/twilio-routes.ts +34 -5
  144. package/src/calls/types.ts +1 -0
  145. package/src/calls/voice-ingress-preflight.ts +0 -42
  146. package/src/calls/voice-quality.ts +38 -5
  147. package/src/calls/voice-session-bridge.ts +7 -12
  148. package/src/cli/commands/avatar.ts +2 -2
  149. package/src/cli/commands/config.ts +1 -4
  150. package/src/cli/commands/credentials.ts +128 -82
  151. package/src/cli/commands/doctor.ts +2 -2
  152. package/src/cli/commands/keys.ts +7 -7
  153. package/src/cli/commands/memory.ts +1 -1
  154. package/src/cli/commands/oauth/connections.ts +11 -29
  155. package/src/cli/commands/oauth/index.ts +7 -0
  156. package/src/cli/commands/oauth/platform.ts +525 -0
  157. package/src/cli/commands/platform.ts +3 -3
  158. package/src/cli/lib/daemon-credential-client.ts +284 -0
  159. package/src/cli.ts +1 -1
  160. package/src/config/assistant-feature-flags.ts +186 -5
  161. package/src/config/bundled-skills/AGENTS.md +34 -0
  162. package/src/config/bundled-skills/acp/SKILL.md +10 -0
  163. package/src/config/bundled-skills/app-builder/SKILL.md +0 -4
  164. package/src/config/bundled-skills/messaging/SKILL.md +5 -5
  165. package/src/config/bundled-skills/messaging/tools/messaging-analyze-style.ts +2 -2
  166. package/src/config/bundled-skills/phone-calls/TOOLS.json +4 -0
  167. package/src/config/bundled-skills/playbooks/tools/playbook-create.ts +1 -0
  168. package/src/config/bundled-skills/playbooks/tools/playbook-update.ts +1 -0
  169. package/src/config/bundled-skills/settings/SKILL.md +15 -2
  170. package/src/config/bundled-skills/settings/TOOLS.json +47 -2
  171. package/src/config/bundled-skills/settings/tools/avatar-remove.ts +59 -0
  172. package/src/config/bundled-skills/settings/tools/avatar-update.ts +80 -0
  173. package/src/config/bundled-skills/settings/tools/voice-config-update.ts +42 -0
  174. package/src/config/bundled-skills/slack/SKILL.md +1 -1
  175. package/src/config/bundled-tool-registry.ts +5 -11
  176. package/src/config/defaults.ts +0 -2
  177. package/src/config/env-registry.ts +5 -5
  178. package/src/config/env.ts +21 -14
  179. package/src/config/feature-flag-registry.json +49 -9
  180. package/src/config/loader.ts +106 -42
  181. package/src/config/schema.ts +9 -29
  182. package/src/config/schemas/calls.ts +30 -0
  183. package/src/config/schemas/fish-audio.ts +39 -0
  184. package/src/config/schemas/inference.ts +2 -2
  185. package/src/config/schemas/journal.ts +16 -0
  186. package/src/config/schemas/memory-processing.ts +2 -2
  187. package/src/config/schemas/security.ts +0 -4
  188. package/src/config/types.ts +1 -1
  189. package/src/contacts/contact-store.ts +39 -0
  190. package/src/contacts/types.ts +2 -0
  191. package/src/credential-execution/approval-bridge.ts +1 -0
  192. package/src/credential-execution/executable-discovery.ts +28 -4
  193. package/src/credential-execution/feature-gates.ts +16 -0
  194. package/src/credential-execution/process-manager.ts +38 -0
  195. package/src/credential-execution/startup-timeout.ts +36 -0
  196. package/src/daemon/approval-generators.ts +3 -9
  197. package/src/daemon/assistant-attachments.ts +9 -0
  198. package/src/daemon/config-watcher.ts +5 -0
  199. package/src/daemon/conversation-error.ts +13 -1
  200. package/src/daemon/conversation-memory.ts +1 -2
  201. package/src/daemon/conversation-process.ts +18 -1
  202. package/src/daemon/conversation-surfaces.ts +30 -1
  203. package/src/daemon/conversation-tool-setup.ts +0 -105
  204. package/src/daemon/conversation.ts +21 -1
  205. package/src/daemon/guardian-action-generators.ts +3 -9
  206. package/src/daemon/handlers/config-vercel.ts +92 -0
  207. package/src/daemon/handlers/skills.ts +2 -15
  208. package/src/daemon/install-symlink.ts +195 -0
  209. package/src/daemon/lifecycle.ts +234 -51
  210. package/src/daemon/message-types/conversations.ts +4 -4
  211. package/src/daemon/message-types/diagnostics.ts +3 -22
  212. package/src/daemon/message-types/messages.ts +0 -2
  213. package/src/daemon/message-types/upgrades.ts +8 -0
  214. package/src/daemon/server.ts +32 -95
  215. package/src/events/domain-events.ts +2 -1
  216. package/src/inbound/platform-callback-registration.ts +3 -3
  217. package/src/instrument.ts +8 -5
  218. package/src/memory/app-store.ts +31 -0
  219. package/src/memory/conversation-title-service.ts +50 -1
  220. package/src/memory/db-init.ts +16 -0
  221. package/src/memory/indexer.ts +19 -10
  222. package/src/memory/items-extractor.ts +328 -321
  223. package/src/memory/job-handlers/conversation-starters.ts +4 -1
  224. package/src/memory/job-handlers/summarization.ts +26 -16
  225. package/src/memory/jobs-store.ts +63 -6
  226. package/src/memory/jobs-worker.ts +31 -7
  227. package/src/memory/journal-memory.ts +214 -0
  228. package/src/memory/migrations/001-job-deferrals.ts +19 -0
  229. package/src/memory/migrations/004-entity-relation-dedup.ts +10 -0
  230. package/src/memory/migrations/005-fingerprint-scope-unique.ts +76 -0
  231. package/src/memory/migrations/006-scope-salted-fingerprints.ts +50 -0
  232. package/src/memory/migrations/007-assistant-id-to-self.ts +10 -0
  233. package/src/memory/migrations/008-remove-assistant-id-columns.ts +34 -0
  234. package/src/memory/migrations/009-llm-usage-events-drop-assistant-id.ts +26 -0
  235. package/src/memory/migrations/014-backfill-inbox-thread-state.ts +10 -0
  236. package/src/memory/migrations/015-drop-active-search-index.ts +17 -0
  237. package/src/memory/migrations/019-notification-tables-schema-migration.ts +12 -0
  238. package/src/memory/migrations/020-rename-macos-ios-channel-to-vellum.ts +121 -0
  239. package/src/memory/migrations/024-embedding-vector-blob.ts +74 -0
  240. package/src/memory/migrations/026a-embeddings-nullable-vector-json.ts +82 -0
  241. package/src/memory/migrations/036-normalize-phone-identities.ts +11 -0
  242. package/src/memory/migrations/116-messages-fts.ts +106 -1
  243. package/src/memory/migrations/126-backfill-guardian-principal-id.ts +52 -0
  244. package/src/memory/migrations/127-guardian-principal-id-not-null.ts +77 -0
  245. package/src/memory/migrations/134-contacts-notes-column.ts +13 -0
  246. package/src/memory/migrations/135-backfill-contact-interaction-stats.ts +20 -0
  247. package/src/memory/migrations/136-drop-assistant-id-columns.ts +52 -0
  248. package/src/memory/migrations/140-backfill-usage-cache-accounting.ts +13 -0
  249. package/src/memory/migrations/141-rename-verification-table.ts +54 -0
  250. package/src/memory/migrations/142-rename-verification-session-id-column.ts +25 -0
  251. package/src/memory/migrations/143-rename-guardian-verification-values.ts +35 -0
  252. package/src/memory/migrations/144-rename-voice-to-phone.ts +136 -0
  253. package/src/memory/migrations/145-drop-accounts-table.ts +32 -0
  254. package/src/memory/migrations/147-migrate-reminders-to-schedules.ts +14 -1
  255. package/src/memory/migrations/148-drop-reminders-table.ts +35 -1
  256. package/src/memory/migrations/150-oauth-apps-client-secret-path.ts +69 -1
  257. package/src/memory/migrations/162-guardian-timestamps-epoch-ms.ts +290 -0
  258. package/src/memory/migrations/169-rename-gmail-provider-key-to-google.ts +51 -1
  259. package/src/memory/migrations/174-rename-thread-starters-table.ts +47 -1
  260. package/src/memory/migrations/176-drop-capability-card-state.ts +13 -0
  261. package/src/memory/migrations/180-backfill-inline-attachments-to-disk.ts +16 -0
  262. package/src/memory/migrations/181-rename-thread-starters-checkpoints.ts +28 -1
  263. package/src/memory/migrations/190-call-session-skip-disclosure.ts +15 -0
  264. package/src/memory/migrations/191-backfill-audio-attachment-mime-types.ts +64 -0
  265. package/src/memory/migrations/192-contacts-user-file-column.ts +15 -0
  266. package/src/memory/migrations/193-add-source-type-columns.ts +81 -0
  267. package/src/memory/migrations/index.ts +5 -0
  268. package/src/memory/migrations/registry.ts +98 -0
  269. package/src/memory/migrations/validate-migration-state.ts +137 -11
  270. package/src/memory/qdrant-circuit-breaker.ts +9 -0
  271. package/src/memory/qdrant-manager.ts +64 -7
  272. package/src/memory/retriever.test.ts +37 -25
  273. package/src/memory/retriever.ts +24 -49
  274. package/src/memory/schema/calls.ts +1 -0
  275. package/src/memory/schema/contacts.ts +1 -0
  276. package/src/memory/schema/memory-core.ts +2 -0
  277. package/src/memory/search/formatting.ts +7 -44
  278. package/src/memory/search/staleness.ts +4 -0
  279. package/src/memory/search/tier-classifier.ts +10 -2
  280. package/src/memory/search/types.ts +2 -5
  281. package/src/memory/task-memory-cleanup.ts +4 -3
  282. package/src/notifications/adapters/slack.ts +168 -6
  283. package/src/notifications/broadcaster.ts +1 -0
  284. package/src/notifications/copy-composer.ts +59 -2
  285. package/src/notifications/decision-engine.ts +4 -1
  286. package/src/notifications/signal.ts +2 -0
  287. package/src/notifications/types.ts +2 -0
  288. package/src/oauth/connection-resolver.ts +6 -4
  289. package/src/permissions/checker.ts +0 -38
  290. package/src/permissions/shell-identity.ts +76 -22
  291. package/src/permissions/types.ts +4 -2
  292. package/src/platform/client.ts +35 -7
  293. package/src/prompts/journal-context.ts +133 -0
  294. package/src/prompts/persona-resolver.ts +194 -0
  295. package/src/prompts/system-prompt.ts +44 -4
  296. package/src/prompts/templates/SOUL.md +10 -0
  297. package/src/prompts/templates/users/default.md +1 -0
  298. package/src/providers/provider-send-message.ts +3 -32
  299. package/src/providers/registry.ts +29 -179
  300. package/src/providers/types.ts +1 -1
  301. package/src/runtime/access-request-helper.ts +4 -0
  302. package/src/runtime/auth/__tests__/credential-service.test.ts +0 -1
  303. package/src/runtime/auth/__tests__/external-assistant-id.test.ts +13 -68
  304. package/src/runtime/auth/__tests__/guard-tests.test.ts +9 -50
  305. package/src/runtime/auth/external-assistant-id.ts +13 -59
  306. package/src/runtime/auth/route-policy.ts +17 -1
  307. package/src/runtime/auth/token-service.ts +43 -138
  308. package/src/runtime/channel-readiness-service.ts +1 -16
  309. package/src/runtime/gateway-client.ts +47 -4
  310. package/src/runtime/guardian-decision-types.ts +45 -4
  311. package/src/runtime/http-server.ts +31 -3
  312. package/src/runtime/middleware/error-handler.ts +1 -9
  313. package/src/runtime/routes/access-request-decision.ts +2 -2
  314. package/src/runtime/routes/app-management-routes.ts +2 -1
  315. package/src/runtime/routes/approval-strategies/guardian-callback-strategy.ts +219 -30
  316. package/src/runtime/routes/approval-strategies/guardian-text-engine-strategy.ts +37 -14
  317. package/src/runtime/routes/audio-routes.ts +40 -0
  318. package/src/runtime/routes/btw-routes.ts +0 -17
  319. package/src/runtime/routes/channel-readiness-routes.ts +9 -4
  320. package/src/runtime/routes/conversation-query-routes.ts +63 -1
  321. package/src/runtime/routes/conversation-routes.ts +4 -44
  322. package/src/runtime/routes/debug-routes.ts +12 -9
  323. package/src/runtime/routes/diagnostics-routes.ts +1 -477
  324. package/src/runtime/routes/guardian-approval-interception.ts +168 -11
  325. package/src/runtime/routes/guardian-approval-prompt.ts +6 -1
  326. package/src/runtime/routes/guardian-approval-reply-helpers.ts +103 -21
  327. package/src/runtime/routes/identity-routes.ts +19 -30
  328. package/src/runtime/routes/inbound-message-handler.ts +31 -1
  329. package/src/runtime/routes/inbound-stages/acl-enforcement.ts +64 -5
  330. package/src/runtime/routes/inbound-stages/background-dispatch.ts +52 -40
  331. package/src/runtime/routes/inbound-stages/secret-ingress-check.ts +4 -33
  332. package/src/runtime/routes/inbound-stages/transcribe-audio.test.ts +1 -1
  333. package/src/runtime/routes/integrations/twilio.ts +52 -10
  334. package/src/runtime/routes/integrations/vercel.ts +89 -0
  335. package/src/runtime/routes/log-export-routes.ts +5 -0
  336. package/src/runtime/routes/memory-item-routes.test.ts +3 -3
  337. package/src/runtime/routes/memory-item-routes.ts +46 -14
  338. package/src/runtime/routes/migration-rollback-routes.ts +209 -0
  339. package/src/runtime/routes/migration-routes.ts +17 -1
  340. package/src/runtime/routes/notification-routes.ts +58 -0
  341. package/src/runtime/routes/schedule-routes.ts +65 -0
  342. package/src/runtime/routes/secret-routes.ts +141 -10
  343. package/src/runtime/routes/settings-routes.ts +41 -1
  344. package/src/runtime/routes/tts-routes.ts +96 -0
  345. package/src/runtime/routes/upgrade-broadcast-routes.ts +26 -2
  346. package/src/runtime/routes/workspace-commit-routes.ts +62 -0
  347. package/src/runtime/routes/workspace-routes.test.ts +22 -1
  348. package/src/runtime/routes/workspace-routes.ts +1 -1
  349. package/src/runtime/routes/workspace-utils.ts +86 -2
  350. package/src/security/ces-credential-client.ts +75 -29
  351. package/src/security/ces-rpc-credential-backend.ts +86 -0
  352. package/src/security/credential-backend.ts +22 -92
  353. package/src/security/keychain-broker-client.ts +10 -2
  354. package/src/security/secure-keys.ts +113 -115
  355. package/src/skills/catalog-install.ts +6 -32
  356. package/src/skills/skill-memory.ts +1 -0
  357. package/src/subagent/manager.ts +2 -5
  358. package/src/telemetry/usage-telemetry-reporter.ts +4 -2
  359. package/src/tools/acp/spawn.ts +78 -1
  360. package/src/tools/calls/call-start.ts +1 -0
  361. package/src/tools/credentials/vault.ts +5 -3
  362. package/src/tools/executor.ts +0 -4
  363. package/src/tools/memory/definitions.ts +3 -2
  364. package/src/tools/memory/handlers.ts +10 -7
  365. package/src/tools/network/script-proxy/session-manager.ts +19 -4
  366. package/src/tools/network/web-fetch.ts +3 -1
  367. package/src/tools/skills/execute.ts +1 -1
  368. package/src/tools/terminal/safe-env.ts +1 -0
  369. package/src/tools/types.ts +0 -8
  370. package/src/util/browser.ts +15 -0
  371. package/src/util/errors.ts +0 -12
  372. package/src/util/platform.ts +4 -51
  373. package/src/workspace/git-service.ts +5 -2
  374. package/src/workspace/migrations/001-avatar-rename.ts +15 -0
  375. package/src/workspace/migrations/003-seed-device-id.ts +17 -1
  376. package/src/workspace/migrations/004-extract-collect-usage-data.ts +33 -0
  377. package/src/workspace/migrations/005-add-send-diagnostics.ts +3 -0
  378. package/src/workspace/migrations/006-services-config.ts +49 -0
  379. package/src/workspace/migrations/007-web-search-provider-rename.ts +27 -0
  380. package/src/workspace/migrations/008-voice-timeout-and-max-steps.ts +3 -0
  381. package/src/workspace/migrations/009-backfill-conversation-disk-view.ts +4 -0
  382. package/src/workspace/migrations/010-app-dir-rename.ts +78 -0
  383. package/src/workspace/migrations/011-backfill-installation-id.ts +11 -0
  384. package/src/workspace/migrations/012-rename-conversation-disk-view-dirs.ts +44 -0
  385. package/src/workspace/migrations/013-repair-conversation-disk-view.ts +5 -0
  386. package/src/workspace/migrations/015-migrate-credentials-to-keychain.ts +153 -0
  387. package/src/workspace/migrations/016-extract-feature-flags-to-protected.ts +156 -0
  388. package/src/workspace/migrations/016-migrate-credentials-from-keychain.ts +150 -0
  389. package/src/workspace/migrations/017-seed-persona-dirs.ts +96 -0
  390. package/src/workspace/migrations/018-rekey-compound-credential-keys.ts +184 -0
  391. package/src/workspace/migrations/019-scope-journal-to-guardian.ts +103 -0
  392. package/src/workspace/migrations/migrate-to-workspace-volume.ts +27 -5
  393. package/src/workspace/migrations/registry.ts +12 -0
  394. package/src/workspace/migrations/runner.ts +106 -2
  395. package/src/workspace/migrations/types.ts +4 -0
  396. package/src/workspace/provider-commit-message-generator.ts +12 -21
  397. package/src/__tests__/claude-code-skill-regression.test.ts +0 -206
  398. package/src/__tests__/claude-code-tool-profiles.test.ts +0 -99
  399. package/src/__tests__/diagnostics-export.test.ts +0 -288
  400. package/src/__tests__/local-gateway-health.test.ts +0 -209
  401. package/src/__tests__/provider-fail-open-selection.test.ts +0 -271
  402. package/src/__tests__/provider-failover-actual-provider.test.ts +0 -66
  403. package/src/__tests__/secret-ingress-handler.test.ts +0 -120
  404. package/src/__tests__/swarm-conversation-integration.test.ts +0 -358
  405. package/src/__tests__/swarm-dag-pathological.test.ts +0 -547
  406. package/src/__tests__/swarm-orchestrator.test.ts +0 -463
  407. package/src/__tests__/swarm-plan-validator.test.ts +0 -384
  408. package/src/__tests__/swarm-recursion.test.ts +0 -197
  409. package/src/__tests__/swarm-router-planner.test.ts +0 -234
  410. package/src/__tests__/swarm-tool.test.ts +0 -185
  411. package/src/__tests__/swarm-worker-backend.test.ts +0 -144
  412. package/src/__tests__/swarm-worker-runner.test.ts +0 -288
  413. package/src/commands/__tests__/cc-command-registry.test.ts +0 -396
  414. package/src/commands/cc-command-registry.ts +0 -248
  415. package/src/config/bundled-skills/claude-code/SKILL.md +0 -53
  416. package/src/config/bundled-skills/claude-code/TOOLS.json +0 -47
  417. package/src/config/bundled-skills/claude-code/tools/claude-code.ts +0 -12
  418. package/src/config/bundled-skills/orchestration/SKILL.md +0 -33
  419. package/src/config/bundled-skills/orchestration/TOOLS.json +0 -35
  420. package/src/config/bundled-skills/orchestration/tools/swarm-delegate.ts +0 -12
  421. package/src/config/schemas/swarm.ts +0 -82
  422. package/src/logfire.ts +0 -135
  423. package/src/memory/search/lexical.ts +0 -48
  424. package/src/providers/failover.ts +0 -186
  425. package/src/runtime/local-gateway-health.ts +0 -275
  426. package/src/security/secret-ingress.ts +0 -68
  427. package/src/swarm/backend-claude-code.ts +0 -225
  428. package/src/swarm/checkpoint.ts +0 -137
  429. package/src/swarm/graph-utils.ts +0 -53
  430. package/src/swarm/index.ts +0 -55
  431. package/src/swarm/limits.ts +0 -66
  432. package/src/swarm/orchestrator.ts +0 -424
  433. package/src/swarm/plan-validator.ts +0 -117
  434. package/src/swarm/router-planner.ts +0 -162
  435. package/src/swarm/router-prompts.ts +0 -39
  436. package/src/swarm/synthesizer.ts +0 -81
  437. package/src/swarm/types.ts +0 -72
  438. package/src/swarm/worker-backend.ts +0 -131
  439. package/src/swarm/worker-prompts.ts +0 -80
  440. package/src/swarm/worker-runner.ts +0 -170
  441. package/src/tools/claude-code/claude-code.ts +0 -610
  442. package/src/tools/swarm/delegate.ts +0 -205
@@ -0,0 +1,171 @@
1
+ import { afterEach, beforeEach, describe, expect, mock, test } from "bun:test";
2
+
3
+ // ---------------------------------------------------------------------------
4
+ // Mocks — must be declared before importing the module under test
5
+ // ---------------------------------------------------------------------------
6
+
7
+ mock.module("../util/logger.js", () => ({
8
+ getLogger: () =>
9
+ new Proxy({} as Record<string, unknown>, {
10
+ get: () => () => {},
11
+ }),
12
+ }));
13
+
14
+ const deliveredMessages: Array<{
15
+ url: string;
16
+ body: Record<string, unknown>;
17
+ }> = [];
18
+
19
+ let deliveryShouldFail = false;
20
+
21
+ mock.module("../runtime/gateway-client.js", () => ({
22
+ deliverChannelReply: async (url: string, body: Record<string, unknown>) => {
23
+ if (deliveryShouldFail) {
24
+ throw new Error("simulated delivery failure");
25
+ }
26
+ deliveredMessages.push({ url, body });
27
+ },
28
+ }));
29
+
30
+ mock.module("../runtime/approval-message-composer.js", () => ({
31
+ composeApprovalMessageGenerative: async () => "Already resolved.",
32
+ }));
33
+
34
+ // ---------------------------------------------------------------------------
35
+ // Import the module under test after mocks are set up
36
+ // ---------------------------------------------------------------------------
37
+
38
+ import type pino from "pino";
39
+
40
+ import {
41
+ clearStaleNotificationCache,
42
+ deliverStaleApprovalReply,
43
+ } from "../runtime/routes/guardian-approval-reply-helpers.js";
44
+
45
+ const noopLogger = new Proxy({} as pino.Logger, {
46
+ get: () => () => {},
47
+ });
48
+
49
+ // ---------------------------------------------------------------------------
50
+ // Tests
51
+ // ---------------------------------------------------------------------------
52
+
53
+ describe("deliverStaleApprovalReply deduplication", () => {
54
+ beforeEach(() => {
55
+ deliveredMessages.length = 0;
56
+ deliveryShouldFail = false;
57
+ clearStaleNotificationCache();
58
+ });
59
+
60
+ afterEach(() => {
61
+ clearStaleNotificationCache();
62
+ });
63
+
64
+ test("sends the first 'approval_already_resolved' notification", async () => {
65
+ await deliverStaleApprovalReply({
66
+ scenario: "approval_already_resolved",
67
+ sourceChannel: "slack",
68
+ replyCallbackUrl: "https://example.com/reply",
69
+ chatId: "chat-1",
70
+ assistantId: "asst-1",
71
+ logger: noopLogger,
72
+ errorLogMessage: "test",
73
+ });
74
+
75
+ expect(deliveredMessages).toHaveLength(1);
76
+ });
77
+
78
+ test("suppresses duplicate 'approval_already_resolved' for the same chat", async () => {
79
+ const params = {
80
+ scenario: "approval_already_resolved" as const,
81
+ sourceChannel: "slack" as const,
82
+ replyCallbackUrl: "https://example.com/reply",
83
+ chatId: "chat-1",
84
+ assistantId: "asst-1",
85
+ logger: noopLogger,
86
+ errorLogMessage: "test",
87
+ };
88
+
89
+ await deliverStaleApprovalReply(params);
90
+ await deliverStaleApprovalReply(params);
91
+ await deliverStaleApprovalReply(params);
92
+
93
+ expect(deliveredMessages).toHaveLength(1);
94
+ });
95
+
96
+ test("allows 'approval_already_resolved' for different chats", async () => {
97
+ const base = {
98
+ scenario: "approval_already_resolved" as const,
99
+ sourceChannel: "slack" as const,
100
+ replyCallbackUrl: "https://example.com/reply",
101
+ assistantId: "asst-1",
102
+ logger: noopLogger,
103
+ errorLogMessage: "test",
104
+ };
105
+
106
+ await deliverStaleApprovalReply({ ...base, chatId: "chat-1" });
107
+ await deliverStaleApprovalReply({ ...base, chatId: "chat-2" });
108
+
109
+ expect(deliveredMessages).toHaveLength(2);
110
+ });
111
+
112
+ test("does not deduplicate non-'approval_already_resolved' scenarios", async () => {
113
+ const params = {
114
+ scenario: "reminder_prompt" as const,
115
+ sourceChannel: "slack" as const,
116
+ replyCallbackUrl: "https://example.com/reply",
117
+ chatId: "chat-1",
118
+ assistantId: "asst-1",
119
+ logger: noopLogger,
120
+ errorLogMessage: "test",
121
+ };
122
+
123
+ await deliverStaleApprovalReply(params);
124
+ await deliverStaleApprovalReply(params);
125
+
126
+ expect(deliveredMessages).toHaveLength(2);
127
+ });
128
+
129
+ test("allows re-send after cache is cleared (simulates TTL expiry)", async () => {
130
+ const params = {
131
+ scenario: "approval_already_resolved" as const,
132
+ sourceChannel: "slack" as const,
133
+ replyCallbackUrl: "https://example.com/reply",
134
+ chatId: "chat-1",
135
+ assistantId: "asst-1",
136
+ logger: noopLogger,
137
+ errorLogMessage: "test",
138
+ };
139
+
140
+ await deliverStaleApprovalReply(params);
141
+ expect(deliveredMessages).toHaveLength(1);
142
+
143
+ // Simulate TTL expiry
144
+ clearStaleNotificationCache();
145
+
146
+ await deliverStaleApprovalReply(params);
147
+ expect(deliveredMessages).toHaveLength(2);
148
+ });
149
+
150
+ test("does not cache dedup key when delivery fails, allowing retries", async () => {
151
+ const params = {
152
+ scenario: "approval_already_resolved" as const,
153
+ sourceChannel: "slack" as const,
154
+ replyCallbackUrl: "https://example.com/reply",
155
+ chatId: "chat-1",
156
+ assistantId: "asst-1",
157
+ logger: noopLogger,
158
+ errorLogMessage: "test",
159
+ };
160
+
161
+ // First attempt fails — should not cache the dedup key
162
+ deliveryShouldFail = true;
163
+ await deliverStaleApprovalReply(params);
164
+ expect(deliveredMessages).toHaveLength(0);
165
+
166
+ // Second attempt succeeds — should not be suppressed by dedup
167
+ deliveryShouldFail = false;
168
+ await deliverStaleApprovalReply(params);
169
+ expect(deliveredMessages).toHaveLength(1);
170
+ });
171
+ });
@@ -0,0 +1,437 @@
1
+ import { beforeEach, describe, expect, mock, test } from "bun:test";
2
+
3
+ import type { ContactWithChannels } from "../contacts/types.js";
4
+
5
+ // ---------------------------------------------------------------------------
6
+ // Mock state for resolveCallHints tests
7
+ // ---------------------------------------------------------------------------
8
+
9
+ let mockAssistantName: string | null = "Velissa";
10
+ let mockGuardianName: string = "Sidd";
11
+ let mockTargetContact: ContactWithChannels | null = null;
12
+ let mockRecentContacts: ContactWithChannels[] = [];
13
+ let mockFindContactByAddressThrows = false;
14
+ let mockListContactsThrows = false;
15
+
16
+ const logWarnFn = mock(() => {});
17
+
18
+ // ---------------------------------------------------------------------------
19
+ // Mock modules — must be before importing module under test
20
+ // ---------------------------------------------------------------------------
21
+
22
+ mock.module("../daemon/identity-helpers.js", () => ({
23
+ getAssistantName: () => mockAssistantName,
24
+ }));
25
+
26
+ mock.module("../prompts/user-reference.js", () => ({
27
+ DEFAULT_USER_REFERENCE: "my human",
28
+ resolveGuardianName: () => mockGuardianName,
29
+ }));
30
+
31
+ mock.module("../contacts/contact-store.js", () => ({
32
+ findContactByAddress: (_type: string, _address: string) => {
33
+ if (mockFindContactByAddressThrows) {
34
+ throw new Error("DB error: findContactByAddress");
35
+ }
36
+ return mockTargetContact;
37
+ },
38
+ findGuardianForChannel: () => null,
39
+ listGuardianChannels: () => null,
40
+ listContacts: (_limit?: number) => {
41
+ if (mockListContactsThrows) {
42
+ throw new Error("DB error: listContacts");
43
+ }
44
+ return mockRecentContacts;
45
+ },
46
+ }));
47
+
48
+ // Bun's mock.module for "../util/logger.js" doesn't intercept the transitive
49
+ // import in stt-hints.ts due to a Bun limitation. Mocking pino at the package
50
+ // level works because getLogger uses a Proxy that lazily creates a pino child
51
+ // logger — intercepting pino itself captures all log calls.
52
+ const mockChildLogger = {
53
+ debug: () => {},
54
+ info: () => {},
55
+ warn: logWarnFn,
56
+ error: () => {},
57
+ child: () => mockChildLogger,
58
+ };
59
+ const mockPinoLogger = Object.assign(() => mockChildLogger, {
60
+ destination: () => ({}),
61
+ multistream: () => ({}),
62
+ });
63
+ mock.module("pino", () => ({ default: mockPinoLogger }));
64
+ mock.module("pino-pretty", () => ({ default: () => ({}) }));
65
+
66
+ // Import after mocking
67
+ import { buildSttHints, resolveCallHints, type SttHintsInput } from "../calls/stt-hints.js";
68
+
69
+ function emptyInput(): SttHintsInput {
70
+ return {
71
+ staticHints: [],
72
+ assistantName: null,
73
+ guardianName: null,
74
+ taskDescription: null,
75
+ targetContactName: null,
76
+ callerContactName: null,
77
+ inviteFriendName: null,
78
+ inviteGuardianName: null,
79
+ recentContactNames: [],
80
+ };
81
+ }
82
+
83
+ describe("buildSttHints", () => {
84
+ test("empty inputs produce empty string", () => {
85
+ expect(buildSttHints(emptyInput())).toBe("");
86
+ });
87
+
88
+ test("static hints included verbatim", () => {
89
+ const input = emptyInput();
90
+ input.staticHints = ["Vellum", "Acme"];
91
+ expect(buildSttHints(input)).toBe("Vellum,Acme");
92
+ });
93
+
94
+ test("assistant name included", () => {
95
+ const input = emptyInput();
96
+ input.assistantName = "Velissa";
97
+ expect(buildSttHints(input)).toBe("Velissa");
98
+ });
99
+
100
+ test("guardian name included", () => {
101
+ const input = emptyInput();
102
+ input.guardianName = "Sidd";
103
+ expect(buildSttHints(input)).toBe("Sidd");
104
+ });
105
+
106
+ test('default guardian name "my human" excluded', () => {
107
+ const input = emptyInput();
108
+ input.guardianName = "my human";
109
+ expect(buildSttHints(input)).toBe("");
110
+ });
111
+
112
+ test("guardian name with whitespace around default sentinel excluded", () => {
113
+ const input = emptyInput();
114
+ input.guardianName = " my human ";
115
+ expect(buildSttHints(input)).toBe("");
116
+ });
117
+
118
+ test("invite friend name included", () => {
119
+ const input = emptyInput();
120
+ input.inviteFriendName = "Alice";
121
+ expect(buildSttHints(input)).toBe("Alice");
122
+ });
123
+
124
+ test("invite guardian name included", () => {
125
+ const input = emptyInput();
126
+ input.inviteGuardianName = "Bob";
127
+ expect(buildSttHints(input)).toBe("Bob");
128
+ });
129
+
130
+ test("target contact name included", () => {
131
+ const input = emptyInput();
132
+ input.targetContactName = "Charlie";
133
+ expect(buildSttHints(input)).toBe("Charlie");
134
+ });
135
+
136
+ test("caller contact name included", () => {
137
+ const input = emptyInput();
138
+ input.callerContactName = "Diana";
139
+ expect(buildSttHints(input)).toBe("Diana");
140
+ });
141
+
142
+ test("recent contact names included", () => {
143
+ const input = emptyInput();
144
+ input.recentContactNames = ["Dave", "Eve"];
145
+ expect(buildSttHints(input)).toBe("Dave,Eve");
146
+ });
147
+
148
+ test("proper nouns extracted from task description", () => {
149
+ const input = emptyInput();
150
+ input.taskDescription = "Call John Smith at Acme Corp";
151
+ const result = buildSttHints(input);
152
+ expect(result).toContain("John");
153
+ expect(result).toContain("Smith");
154
+ expect(result).toContain("Acme");
155
+ expect(result).toContain("Corp");
156
+ // "Call" is the first word of the sentence — should not be extracted
157
+ expect(result).not.toContain("Call");
158
+ });
159
+
160
+ test("proper nouns extracted across sentence boundaries", () => {
161
+ const input = emptyInput();
162
+ input.taskDescription = "Meet with Alice. Then call Bob! Ask Charlie? Done.";
163
+ const result = buildSttHints(input);
164
+ expect(result).toContain("Alice");
165
+ expect(result).toContain("Bob");
166
+ expect(result).toContain("Charlie");
167
+ // First words of sentences should be excluded
168
+ expect(result).not.toContain("Meet");
169
+ expect(result).not.toContain("Then");
170
+ expect(result).not.toContain("Ask");
171
+ expect(result).not.toContain("Done");
172
+ });
173
+
174
+ test("duplicates removed (case-insensitive)", () => {
175
+ const input = emptyInput();
176
+ input.staticHints = ["Vellum", "vellum", "VELLUM"];
177
+ input.recentContactNames = ["Vellum"];
178
+ const result = buildSttHints(input);
179
+ // Should appear only once — the first occurrence is kept
180
+ expect(result).toBe("Vellum");
181
+ });
182
+
183
+ test("empty and whitespace-only entries filtered", () => {
184
+ const input = emptyInput();
185
+ input.staticHints = ["", " ", "Valid", " ", "Also Valid"];
186
+ expect(buildSttHints(input)).toBe("Valid,Also Valid");
187
+ });
188
+
189
+ test("entries are trimmed", () => {
190
+ const input = emptyInput();
191
+ input.staticHints = [" Padded ", " Spaces "];
192
+ expect(buildSttHints(input)).toBe("Padded,Spaces");
193
+ });
194
+
195
+ test("output truncated at MAX_HINTS_LENGTH without partial words", () => {
196
+ const input = emptyInput();
197
+ // Create hints that will exceed 500 chars when joined
198
+ const longHints: string[] = [];
199
+ for (let i = 0; i < 100; i++) {
200
+ longHints.push(`Hint${i}LongWord`);
201
+ }
202
+ input.staticHints = longHints;
203
+ const result = buildSttHints(input);
204
+ expect(result.length).toBeLessThanOrEqual(500);
205
+ // Should not end with a comma (that would indicate a truncation right after a separator)
206
+ expect(result).not.toMatch(/,$/);
207
+ // Every comma-separated part should be a complete hint from our input
208
+ const parts = result.split(",");
209
+ for (const part of parts) {
210
+ expect(input.staticHints).toContain(part);
211
+ }
212
+ });
213
+
214
+ test("all sources combined in correct order", () => {
215
+ const input: SttHintsInput = {
216
+ staticHints: ["StaticOne"],
217
+ assistantName: "Velissa",
218
+ guardianName: "Sidd",
219
+ taskDescription: "Call John at Acme",
220
+ targetContactName: "Target",
221
+ callerContactName: "Caller",
222
+ inviteFriendName: "Friend",
223
+ inviteGuardianName: "Guardian",
224
+ recentContactNames: ["Recent"],
225
+ };
226
+ const result = buildSttHints(input);
227
+ const parts = result.split(",");
228
+ // Verify all expected hints are present
229
+ expect(parts).toContain("StaticOne");
230
+ expect(parts).toContain("Velissa");
231
+ expect(parts).toContain("Sidd");
232
+ expect(parts).toContain("John");
233
+ expect(parts).toContain("Acme");
234
+ expect(parts).toContain("Target");
235
+ expect(parts).toContain("Caller");
236
+ expect(parts).toContain("Friend");
237
+ expect(parts).toContain("Guardian");
238
+ expect(parts).toContain("Recent");
239
+ });
240
+
241
+ test("surnames after abbreviation periods are preserved", () => {
242
+ const input = emptyInput();
243
+ input.taskDescription = "Call Dr. Smith at Acme";
244
+ const result = buildSttHints(input);
245
+ expect(result).toContain("Smith");
246
+ expect(result).toContain("Acme");
247
+ // "Dr" should also appear as a capitalized word
248
+ expect(result).toContain("Dr");
249
+ });
250
+
251
+ test("multiple abbreviation titles preserve following names", () => {
252
+ const input = emptyInput();
253
+ input.taskDescription = "Meet Mr. Johnson and Mrs. Williams at the office";
254
+ const result = buildSttHints(input);
255
+ expect(result).toContain("Johnson");
256
+ expect(result).toContain("Williams");
257
+ });
258
+
259
+ test("non-ASCII letters preserved in names", () => {
260
+ const input = emptyInput();
261
+ input.taskDescription = "Call José García and Łukasz Nowak";
262
+ const result = buildSttHints(input);
263
+ expect(result).toContain("José");
264
+ expect(result).toContain("García");
265
+ expect(result).toContain("Łukasz");
266
+ expect(result).toContain("Nowak");
267
+ });
268
+
269
+ test("accented uppercase letters detected as proper nouns", () => {
270
+ const input = emptyInput();
271
+ input.taskDescription = "Talk to Zoë about the project";
272
+ const result = buildSttHints(input);
273
+ expect(result).toContain("Zoë");
274
+ });
275
+
276
+ test("null and empty string names are excluded", () => {
277
+ const input = emptyInput();
278
+ input.assistantName = "";
279
+ input.guardianName = "";
280
+ input.targetContactName = null;
281
+ input.inviteFriendName = null;
282
+ input.inviteGuardianName = null;
283
+ expect(buildSttHints(input)).toBe("");
284
+ });
285
+ });
286
+
287
+ // ---------------------------------------------------------------------------
288
+ // resolveCallHints — wiring and error resilience
289
+ // ---------------------------------------------------------------------------
290
+
291
+ function makeContact(displayName: string): ContactWithChannels {
292
+ const now = Date.now();
293
+ return {
294
+ id: `contact-${displayName.toLowerCase()}`,
295
+ displayName,
296
+ notes: null,
297
+ lastInteraction: null,
298
+ interactionCount: 0,
299
+ createdAt: now,
300
+ updatedAt: now,
301
+ role: "contact",
302
+ contactType: "human",
303
+ principalId: null,
304
+ userFile: null,
305
+ channels: [],
306
+ };
307
+ }
308
+
309
+ describe("resolveCallHints", () => {
310
+ beforeEach(() => {
311
+ mockAssistantName = "Velissa";
312
+ mockGuardianName = "Sidd";
313
+ mockTargetContact = null;
314
+ mockRecentContacts = [];
315
+ mockFindContactByAddressThrows = false;
316
+ mockListContactsThrows = false;
317
+ logWarnFn.mockClear();
318
+ });
319
+
320
+ test("happy path wires all sources correctly", () => {
321
+ mockTargetContact = makeContact("Alice");
322
+ mockRecentContacts = [makeContact("Bob"), makeContact("Charlie")];
323
+
324
+ const session = {
325
+ task: "Call Alice at Acme",
326
+ toNumber: "+15551234567",
327
+ fromNumber: "+15559876543",
328
+ direction: "outbound" as const,
329
+ inviteFriendName: "Dave",
330
+ inviteGuardianName: "Eve",
331
+ };
332
+
333
+ const result = resolveCallHints(session, ["StaticHint"]);
334
+ const parts = result.split(",");
335
+
336
+ expect(parts).toContain("StaticHint");
337
+ expect(parts).toContain("Velissa");
338
+ expect(parts).toContain("Sidd");
339
+ expect(parts).toContain("Alice");
340
+ expect(parts).toContain("Dave");
341
+ expect(parts).toContain("Eve");
342
+ expect(parts).toContain("Bob");
343
+ expect(parts).toContain("Charlie");
344
+ // Proper nouns from task description
345
+ expect(parts).toContain("Acme");
346
+ expect(logWarnFn).not.toHaveBeenCalled();
347
+ });
348
+
349
+ test("findContactByAddress failure is caught and logged without throwing", () => {
350
+ mockFindContactByAddressThrows = true;
351
+ mockRecentContacts = [makeContact("Bob")];
352
+
353
+ const session = {
354
+ task: null,
355
+ toNumber: "+15551234567",
356
+ fromNumber: "+15559876543",
357
+ direction: "outbound" as const,
358
+ inviteFriendName: null,
359
+ inviteGuardianName: null,
360
+ };
361
+
362
+ // Should not throw
363
+ const result = resolveCallHints(session, []);
364
+ const parts = result.split(",");
365
+
366
+ // Target contact should be absent (lookup failed)
367
+ // But other sources should still work
368
+ expect(parts).toContain("Velissa");
369
+ expect(parts).toContain("Sidd");
370
+ expect(parts).toContain("Bob");
371
+ expect(logWarnFn).toHaveBeenCalled();
372
+ });
373
+
374
+ test("listContacts failure is caught and logged without throwing", () => {
375
+ mockListContactsThrows = true;
376
+ mockTargetContact = makeContact("Alice");
377
+
378
+ const session = {
379
+ task: null,
380
+ toNumber: "+15551234567",
381
+ fromNumber: "+15559876543",
382
+ direction: "outbound" as const,
383
+ inviteFriendName: null,
384
+ inviteGuardianName: null,
385
+ };
386
+
387
+ // Should not throw
388
+ const result = resolveCallHints(session, []);
389
+ const parts = result.split(",");
390
+
391
+ // Recent contacts should be absent (listing failed)
392
+ // But other sources should still work
393
+ expect(parts).toContain("Velissa");
394
+ expect(parts).toContain("Sidd");
395
+ expect(parts).toContain("Alice");
396
+ expect(logWarnFn).toHaveBeenCalled();
397
+ });
398
+
399
+ test("inbound call resolves caller contact from fromNumber", () => {
400
+ mockTargetContact = makeContact("Alice");
401
+ mockRecentContacts = [makeContact("Bob")];
402
+
403
+ const session = {
404
+ task: null,
405
+ toNumber: "+15559876543",
406
+ fromNumber: "+15551234567",
407
+ direction: "inbound" as const,
408
+ inviteFriendName: null,
409
+ inviteGuardianName: null,
410
+ };
411
+
412
+ const result = resolveCallHints(session, []);
413
+ const parts = result.split(",");
414
+
415
+ // For inbound, the contact found via fromNumber should appear as caller, not target
416
+ expect(parts).toContain("Alice");
417
+ expect(parts).toContain("Velissa");
418
+ expect(parts).toContain("Sidd");
419
+ expect(parts).toContain("Bob");
420
+ expect(logWarnFn).not.toHaveBeenCalled();
421
+ });
422
+
423
+ test("null session produces hints from assistant name, guardian name, and recent contacts", () => {
424
+ mockRecentContacts = [makeContact("RecentOne"), makeContact("RecentTwo")];
425
+
426
+ const result = resolveCallHints(null, ["Static"]);
427
+ const parts = result.split(",");
428
+
429
+ expect(parts).toContain("Static");
430
+ expect(parts).toContain("Velissa");
431
+ expect(parts).toContain("Sidd");
432
+ expect(parts).toContain("RecentOne");
433
+ expect(parts).toContain("RecentTwo");
434
+ // No target contact lookup should have been attempted (no session)
435
+ expect(logWarnFn).not.toHaveBeenCalled();
436
+ });
437
+ });
@@ -2,8 +2,7 @@
2
2
  * Unit tests for the GET /v1/suggestion endpoint (handleGetSuggestion).
3
3
  *
4
4
  * Validates happy path, all null-return paths, caching, staleness check,
5
- * quote stripping, word-boundary truncation, empty response rejection,
6
- * and modelIntent verification.
5
+ * quote stripping, empty response rejection, and modelIntent verification.
7
6
  */
8
7
 
9
8
  import { describe, expect, mock, test } from "bun:test";
@@ -293,36 +292,6 @@ describe("GET /v1/suggestion", () => {
293
292
  expect(body.suggestion).toBe("Sure, let's go!");
294
293
  });
295
294
 
296
- test("truncates long suggestions at word boundary", async () => {
297
- // A 60-char string that will exceed the 50-char limit
298
- const longText =
299
- "This is a really long suggestion that goes well beyond fifty chars";
300
- const provider = makeMockProvider(longText);
301
- mockGetConfiguredProvider.mockImplementation(async () => provider);
302
- mockGetConversationByKey.mockImplementation(() => ({
303
- conversationId: "conv-test",
304
- }));
305
- mockGetMessages.mockImplementation(() => [
306
- {
307
- id: "msg-asst-1",
308
- conversationId: "conv-test",
309
- role: "assistant",
310
- content: JSON.stringify([{ type: "text", text: "Hello there" }]),
311
- createdAt: Date.now(),
312
- metadata: null,
313
- },
314
- ]);
315
-
316
- const url = makeUrl({ conversationKey: "test-key" });
317
- const deps = makeDeps();
318
- const res = await handleGetSuggestion(url, deps);
319
- const body = (await res.json()) as { suggestion: string };
320
-
321
- expect(body.suggestion.length).toBeLessThanOrEqual(50);
322
- // Should end at a word boundary (no partial words)
323
- expect(body.suggestion).not.toMatch(/\s$/);
324
- });
325
-
326
295
  test("rejects empty LLM response", async () => {
327
296
  const provider = makeMockProvider("");
328
297
  mockGetConfiguredProvider.mockImplementation(async () => provider);
@@ -86,7 +86,6 @@ mock.module("../config/loader.js", () => ({
86
86
  invalidateConfigCache: () => {},
87
87
  getNestedValue: () => undefined,
88
88
  setNestedValue: () => {},
89
- syncConfigToLockfile: () => {},
90
89
  }));
91
90
 
92
91
  // eslint-disable-next-line @typescript-eslint/no-require-imports