@vellumai/assistant 0.3.4 → 0.3.6

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 (506) hide show
  1. package/Dockerfile +2 -0
  2. package/README.md +88 -2
  3. package/eslint.config.mjs +31 -0
  4. package/package.json +1 -1
  5. package/scripts/ipc/check-swift-decoder-drift.ts +4 -1
  6. package/scripts/ipc/generate-swift.ts +31 -2
  7. package/src/__tests__/__snapshots__/ipc-snapshot.test.ts.snap +438 -1
  8. package/src/__tests__/approval-conversation-turn.test.ts +214 -0
  9. package/src/__tests__/approval-hardcoded-copy-guard.test.ts +41 -0
  10. package/src/__tests__/approval-message-composer.test.ts +253 -0
  11. package/src/__tests__/browser-manager.test.ts +1 -0
  12. package/src/__tests__/call-conversation-messages.test.ts +130 -0
  13. package/src/__tests__/call-domain.test.ts +12 -2
  14. package/src/__tests__/call-orchestrator.test.ts +799 -249
  15. package/src/__tests__/call-pointer-messages.test.ts +148 -0
  16. package/src/__tests__/call-recovery.test.ts +3 -0
  17. package/src/__tests__/call-routes-http.test.ts +32 -2
  18. package/src/__tests__/call-store.test.ts +3 -0
  19. package/src/__tests__/channel-approval-routes.test.ts +1277 -98
  20. package/src/__tests__/channel-approval.test.ts +37 -0
  21. package/src/__tests__/channel-approvals.test.ts +36 -50
  22. package/src/__tests__/channel-guardian.test.ts +630 -22
  23. package/src/__tests__/channel-readiness-service.test.ts +324 -0
  24. package/src/__tests__/checker.test.ts +14 -7
  25. package/src/__tests__/clarification-resolver.test.ts +44 -24
  26. package/src/__tests__/commit-message-enrichment-service.test.ts +9 -4
  27. package/src/__tests__/computer-use-session-working-dir.test.ts +8 -0
  28. package/src/__tests__/config-schema.test.ts +14 -8
  29. package/src/__tests__/context-window-manager.test.ts +30 -2
  30. package/src/__tests__/contradiction-checker.test.ts +20 -5
  31. package/src/__tests__/credential-security-invariants.test.ts +7 -2
  32. package/src/__tests__/daemon-lifecycle.test.ts +13 -12
  33. package/src/__tests__/db-migration-rollback.test.ts +752 -0
  34. package/src/__tests__/dictation-mode-detection.test.ts +63 -0
  35. package/src/__tests__/dynamic-skill-workflow-prompt.test.ts +2 -0
  36. package/src/__tests__/entity-search.test.ts +615 -0
  37. package/src/__tests__/fuzzy-match-property.test.ts +5 -5
  38. package/src/__tests__/guardian-action-store.test.ts +123 -0
  39. package/src/__tests__/guardian-action-sweep.test.ts +277 -0
  40. package/src/__tests__/guardian-dispatch.test.ts +389 -0
  41. package/src/__tests__/guardian-question-copy.test.ts +47 -0
  42. package/src/__tests__/handlers-telegram-config.test.ts +4 -2
  43. package/src/__tests__/handlers-twilio-config.test.ts +533 -0
  44. package/src/__tests__/intent-routing.test.ts +2 -0
  45. package/src/__tests__/ipc-snapshot.test.ts +291 -1
  46. package/src/__tests__/memory-upsert-concurrency.test.ts +828 -0
  47. package/src/__tests__/messaging-send-tool.test.ts +65 -0
  48. package/src/__tests__/model-intents.test.ts +96 -0
  49. package/src/__tests__/no-direct-anthropic-sdk-imports.test.ts +42 -0
  50. package/src/__tests__/oauth2-gateway-transport.test.ts +130 -0
  51. package/src/__tests__/onboarding-starter-tasks.test.ts +2 -0
  52. package/src/__tests__/provider-commit-message-generator.test.ts +89 -13
  53. package/src/__tests__/provider-error-scenarios.test.ts +621 -0
  54. package/src/__tests__/provider-fail-open-selection.test.ts +119 -0
  55. package/src/__tests__/qdrant-manager.test.ts +27 -20
  56. package/src/__tests__/relay-server.test.ts +779 -40
  57. package/src/__tests__/run-orchestrator-assistant-events.test.ts +6 -0
  58. package/src/__tests__/run-orchestrator.test.ts +42 -4
  59. package/src/__tests__/runtime-runs-http.test.ts +17 -1
  60. package/src/__tests__/runtime-runs.test.ts +16 -0
  61. package/src/__tests__/schedule-store.test.ts +18 -4
  62. package/src/__tests__/scheduler-recurrence.test.ts +13 -4
  63. package/src/__tests__/session-abort-tool-results.test.ts +6 -0
  64. package/src/__tests__/session-agent-loop.test.ts +857 -0
  65. package/src/__tests__/session-conflict-gate.test.ts +6 -0
  66. package/src/__tests__/session-pre-run-repair.test.ts +6 -0
  67. package/src/__tests__/session-profile-injection.test.ts +6 -0
  68. package/src/__tests__/session-provider-retry-repair.test.ts +6 -0
  69. package/src/__tests__/session-queue.test.ts +6 -0
  70. package/src/__tests__/session-runtime-assembly.test.ts +321 -13
  71. package/src/__tests__/session-slash-known.test.ts +6 -0
  72. package/src/__tests__/session-slash-queue.test.ts +6 -0
  73. package/src/__tests__/session-slash-unknown.test.ts +6 -0
  74. package/src/__tests__/session-surfaces-task-progress.test.ts +2 -0
  75. package/src/__tests__/session-tool-setup-app-refresh.test.ts +1 -0
  76. package/src/__tests__/session-tool-setup-memory-scope.test.ts +1 -0
  77. package/src/__tests__/session-tool-setup-side-effect-flag.test.ts +1 -0
  78. package/src/__tests__/session-workspace-injection.test.ts +6 -0
  79. package/src/__tests__/session-workspace-tool-tracking.test.ts +6 -0
  80. package/src/__tests__/skills.test.ts +2 -0
  81. package/src/__tests__/sms-messaging-provider.test.ts +126 -0
  82. package/src/__tests__/starter-task-flow.test.ts +2 -0
  83. package/src/__tests__/swarm-dag-pathological.test.ts +535 -0
  84. package/src/__tests__/system-prompt.test.ts +2 -0
  85. package/src/__tests__/task-management-tools.test.ts +2 -2
  86. package/src/__tests__/task-runner.test.ts +14 -4
  87. package/src/__tests__/terminal-tools.test.ts +25 -19
  88. package/src/__tests__/tool-execution-abort-cleanup.test.ts +545 -0
  89. package/src/__tests__/tool-executor-shell-integration.test.ts +11 -11
  90. package/src/__tests__/tool-executor.test.ts +23 -24
  91. package/src/__tests__/trust-store.test.ts +3 -3
  92. package/src/__tests__/twilio-rest.test.ts +29 -0
  93. package/src/__tests__/twilio-routes-elevenlabs.test.ts +3 -0
  94. package/src/__tests__/twilio-routes-twiml.test.ts +11 -0
  95. package/src/__tests__/twilio-routes.test.ts +167 -11
  96. package/src/__tests__/twitter-cli-error-shaping.test.ts +2 -2
  97. package/src/__tests__/user-reference.test.ts +2 -0
  98. package/src/__tests__/voice-quality.test.ts +222 -0
  99. package/src/__tests__/web-search.test.ts +46 -30
  100. package/src/__tests__/work-item-output.test.ts +110 -0
  101. package/src/agent/loop.ts +1 -1
  102. package/src/agent-heartbeat/agent-heartbeat-service.ts +2 -10
  103. package/src/amazon/client.ts +1418 -0
  104. package/src/amazon/request-extractor.ts +135 -0
  105. package/src/amazon/session.ts +109 -0
  106. package/src/autonomy/autonomy-store.ts +5 -5
  107. package/src/browser-extension-relay/client.ts +124 -0
  108. package/src/browser-extension-relay/protocol.ts +63 -0
  109. package/src/browser-extension-relay/server.ts +177 -0
  110. package/src/bundler/app-bundler.ts +3 -3
  111. package/src/bundler/bundle-signer.ts +1 -1
  112. package/src/bundler/signature-verifier.ts +1 -1
  113. package/src/calls/call-conversation-messages.ts +33 -0
  114. package/src/calls/call-domain.ts +114 -10
  115. package/src/calls/call-orchestrator.ts +268 -59
  116. package/src/calls/call-pointer-messages.ts +53 -0
  117. package/src/calls/call-recovery.ts +3 -8
  118. package/src/calls/call-store.ts +69 -87
  119. package/src/calls/elevenlabs-config.ts +3 -2
  120. package/src/calls/guardian-action-sweep.ts +105 -0
  121. package/src/calls/guardian-dispatch.ts +203 -0
  122. package/src/calls/guardian-question-copy.ts +133 -0
  123. package/src/calls/relay-server.ts +466 -8
  124. package/src/calls/speaker-identification.ts +1 -1
  125. package/src/calls/twilio-config.ts +22 -14
  126. package/src/calls/twilio-provider.ts +6 -4
  127. package/src/calls/twilio-rest.ts +308 -7
  128. package/src/calls/twilio-routes.ts +65 -12
  129. package/src/calls/types.ts +3 -1
  130. package/src/channels/types.ts +25 -0
  131. package/src/cli/amazon.ts +815 -0
  132. package/src/cli/config-commands.ts +2 -2
  133. package/src/cli/core-commands.ts +4 -3
  134. package/src/cli/influencer.ts +244 -0
  135. package/src/cli/map.ts +89 -6
  136. package/src/cli.ts +1 -1
  137. package/src/config/agent-schema.ts +171 -0
  138. package/src/config/bundled-skills/amazon/SKILL.md +127 -0
  139. package/src/config/bundled-skills/amazon/icon.svg +13 -0
  140. package/src/config/bundled-skills/api-mapping/SKILL.md +78 -0
  141. package/src/config/bundled-skills/browser/SKILL.md +1 -0
  142. package/src/config/bundled-skills/browser/TOOLS.json +17 -0
  143. package/src/config/bundled-skills/browser/tools/browser-wait-for-download.ts +25 -0
  144. package/src/config/bundled-skills/doordash/SKILL.md +51 -51
  145. package/src/config/bundled-skills/email-setup/SKILL.md +14 -5
  146. package/src/config/bundled-skills/google-oauth-setup/SKILL.md +183 -0
  147. package/src/config/bundled-skills/influencer/SKILL.md +144 -0
  148. package/src/config/bundled-skills/knowledge-graph/SKILL.md +15 -0
  149. package/src/config/bundled-skills/knowledge-graph/TOOLS.json +56 -0
  150. package/src/config/bundled-skills/knowledge-graph/tools/graph-query.ts +185 -0
  151. package/src/config/bundled-skills/macos-automation/icon.svg +12 -0
  152. package/src/config/bundled-skills/media-processing/SKILL.md +176 -0
  153. package/src/config/bundled-skills/media-processing/TOOLS.json +230 -0
  154. package/src/config/bundled-skills/media-processing/__tests__/concurrency-pool.test.ts +77 -0
  155. package/src/config/bundled-skills/media-processing/__tests__/cost-tracker.test.ts +69 -0
  156. package/src/config/bundled-skills/media-processing/__tests__/preprocess.test.ts +303 -0
  157. package/src/config/bundled-skills/media-processing/services/concurrency-pool.ts +55 -0
  158. package/src/config/bundled-skills/media-processing/services/cost-tracker.ts +86 -0
  159. package/src/config/bundled-skills/media-processing/services/gemini-map.ts +339 -0
  160. package/src/config/bundled-skills/media-processing/services/preprocess.ts +551 -0
  161. package/src/config/bundled-skills/media-processing/services/processing-pipeline.ts +259 -0
  162. package/src/config/bundled-skills/media-processing/services/reduce.ts +197 -0
  163. package/src/config/bundled-skills/media-processing/tools/analyze-keyframes.ts +136 -0
  164. package/src/config/bundled-skills/media-processing/tools/extract-keyframes.ts +59 -0
  165. package/src/config/bundled-skills/media-processing/tools/generate-clip.ts +195 -0
  166. package/src/config/bundled-skills/media-processing/tools/ingest-media.ts +197 -0
  167. package/src/config/bundled-skills/media-processing/tools/media-diagnostics.ts +143 -0
  168. package/src/config/bundled-skills/media-processing/tools/media-status.ts +75 -0
  169. package/src/config/bundled-skills/media-processing/tools/query-media-events.ts +65 -0
  170. package/src/config/bundled-skills/messaging/SKILL.md +33 -8
  171. package/src/config/bundled-skills/messaging/tools/messaging-analyze-style.ts +4 -7
  172. package/src/config/bundled-skills/messaging/tools/messaging-reply.ts +2 -1
  173. package/src/config/bundled-skills/messaging/tools/messaging-send.ts +5 -1
  174. package/src/config/bundled-skills/phone-calls/SKILL.md +88 -23
  175. package/src/config/bundled-skills/twitter/SKILL.md +19 -3
  176. package/src/config/bundled-skills/twitter/icon.svg +14 -0
  177. package/src/config/bundled-tool-registry.ts +310 -0
  178. package/src/config/calls-schema.ts +181 -0
  179. package/src/config/core-schema.ts +309 -0
  180. package/src/config/defaults.ts +28 -3
  181. package/src/config/env-registry.ts +162 -0
  182. package/src/config/env.ts +175 -0
  183. package/src/config/loader.ts +6 -6
  184. package/src/config/memory-schema.ts +528 -0
  185. package/src/config/sandbox-schema.ts +55 -0
  186. package/src/config/schema.ts +158 -1133
  187. package/src/config/skill-state.ts +1 -1
  188. package/src/config/skills-schema.ts +32 -0
  189. package/src/config/skills.ts +35 -24
  190. package/src/config/system-prompt.ts +131 -56
  191. package/src/config/templates/IDENTITY.md +2 -2
  192. package/src/config/templates/SOUL.md +1 -1
  193. package/src/config/types.ts +1 -0
  194. package/src/config/user-reference.ts +4 -9
  195. package/src/config/vellum-skills/catalog.json +6 -7
  196. package/src/config/vellum-skills/chatgpt-import/tools/chatgpt-import.ts +5 -1
  197. package/src/config/vellum-skills/slack-oauth-setup/SKILL.md +4 -3
  198. package/src/config/vellum-skills/sms-setup/SKILL.md +216 -0
  199. package/src/config/vellum-skills/twilio-setup/SKILL.md +40 -8
  200. package/src/context/window-manager.ts +27 -7
  201. package/src/daemon/approval-generators.ts +186 -0
  202. package/src/daemon/approved-devices-store.ts +140 -0
  203. package/src/daemon/assistant-attachments.ts +1 -1
  204. package/src/daemon/classifier.ts +35 -32
  205. package/src/daemon/config-watcher.ts +1 -1
  206. package/src/daemon/daemon-control.ts +217 -0
  207. package/src/daemon/handlers/apps.ts +2 -3
  208. package/src/daemon/handlers/config-channels.ts +158 -0
  209. package/src/daemon/handlers/config-inbox.ts +540 -0
  210. package/src/daemon/handlers/config-ingress.ts +231 -0
  211. package/src/daemon/handlers/config-integrations.ts +258 -0
  212. package/src/daemon/handlers/config-model.ts +143 -0
  213. package/src/daemon/handlers/config-parental.ts +163 -0
  214. package/src/daemon/handlers/config-scheduling.ts +172 -0
  215. package/src/daemon/handlers/config-slack.ts +92 -0
  216. package/src/daemon/handlers/config-telegram.ts +301 -0
  217. package/src/daemon/handlers/config-tools.ts +177 -0
  218. package/src/daemon/handlers/config-trust.ts +104 -0
  219. package/src/daemon/handlers/config-twilio.ts +1080 -0
  220. package/src/daemon/handlers/config.ts +53 -1689
  221. package/src/daemon/handlers/diagnostics.ts +1 -1
  222. package/src/daemon/handlers/dictation.ts +180 -0
  223. package/src/daemon/handlers/documents.ts +18 -32
  224. package/src/daemon/handlers/identity.ts +14 -23
  225. package/src/daemon/handlers/index.ts +11 -0
  226. package/src/daemon/handlers/misc.ts +3 -5
  227. package/src/daemon/handlers/pairing.ts +98 -0
  228. package/src/daemon/handlers/sessions.ts +56 -5
  229. package/src/daemon/handlers/shared.ts +6 -1
  230. package/src/daemon/handlers/skills.ts +1 -1
  231. package/src/daemon/handlers/twitter-auth.ts +2 -0
  232. package/src/daemon/handlers/work-items.ts +17 -9
  233. package/src/daemon/handlers/workspace-files.ts +4 -3
  234. package/src/daemon/install-cli-launchers.ts +113 -0
  235. package/src/daemon/ipc-contract/apps.ts +356 -0
  236. package/src/daemon/ipc-contract/browser.ts +74 -0
  237. package/src/daemon/ipc-contract/computer-use.ts +151 -0
  238. package/src/daemon/ipc-contract/diagnostics.ts +56 -0
  239. package/src/daemon/ipc-contract/documents.ts +74 -0
  240. package/src/daemon/ipc-contract/inbox.ts +209 -0
  241. package/src/daemon/ipc-contract/integrations.ts +284 -0
  242. package/src/daemon/ipc-contract/memory.ts +48 -0
  243. package/src/daemon/ipc-contract/messages.ts +211 -0
  244. package/src/daemon/ipc-contract/pairing.ts +45 -0
  245. package/src/daemon/ipc-contract/parental-control.ts +95 -0
  246. package/src/daemon/ipc-contract/schedules.ts +97 -0
  247. package/src/daemon/ipc-contract/sessions.ts +315 -0
  248. package/src/daemon/ipc-contract/shared.ts +42 -0
  249. package/src/daemon/ipc-contract/skills.ts +120 -0
  250. package/src/daemon/ipc-contract/subagents.ts +58 -0
  251. package/src/daemon/ipc-contract/surfaces.ts +250 -0
  252. package/src/daemon/ipc-contract/trust.ts +60 -0
  253. package/src/daemon/ipc-contract/work-items.ts +225 -0
  254. package/src/daemon/ipc-contract/workspace.ts +113 -0
  255. package/src/daemon/ipc-contract-inventory.json +70 -0
  256. package/src/daemon/ipc-contract-inventory.ts +55 -29
  257. package/src/daemon/ipc-contract.ts +229 -2426
  258. package/src/daemon/ipc-protocol.ts +1 -1
  259. package/src/daemon/ipc-validate.ts +7 -0
  260. package/src/daemon/lifecycle.ts +97 -377
  261. package/src/daemon/pairing-store.ts +177 -0
  262. package/src/daemon/providers-setup.ts +43 -0
  263. package/src/daemon/ride-shotgun-handler.ts +68 -3
  264. package/src/daemon/server.ts +66 -46
  265. package/src/daemon/session-agent-loop-handlers.ts +421 -0
  266. package/src/daemon/session-agent-loop.ts +117 -275
  267. package/src/daemon/session-dynamic-profile.ts +1 -1
  268. package/src/daemon/session-history.ts +1 -1
  269. package/src/daemon/session-media-retry.ts +1 -1
  270. package/src/daemon/session-messaging.ts +37 -2
  271. package/src/daemon/session-notifiers.ts +5 -25
  272. package/src/daemon/session-process.ts +99 -59
  273. package/src/daemon/session-queue-manager.ts +96 -4
  274. package/src/daemon/session-runtime-assembly.ts +199 -10
  275. package/src/daemon/session-surfaces.ts +19 -4
  276. package/src/daemon/session-tool-setup.ts +30 -30
  277. package/src/daemon/session-workspace.ts +1 -1
  278. package/src/daemon/session.ts +35 -2
  279. package/src/daemon/shutdown-handlers.ts +122 -0
  280. package/src/daemon/trace-emitter.ts +1 -1
  281. package/src/daemon/watch-handler.ts +36 -33
  282. package/src/doordash/cart-queries.ts +787 -0
  283. package/src/doordash/client.ts +144 -127
  284. package/src/doordash/order-queries.ts +85 -0
  285. package/src/doordash/queries.ts +10 -1308
  286. package/src/doordash/search-queries.ts +203 -0
  287. package/src/doordash/session.ts +3 -2
  288. package/src/doordash/store-queries.ts +246 -0
  289. package/src/doordash/types.ts +367 -0
  290. package/src/email/providers/agentmail.ts +2 -1
  291. package/src/email/providers/index.ts +3 -2
  292. package/src/email/service.ts +3 -2
  293. package/src/errors.ts +43 -0
  294. package/src/home-base/prebuilt/seed.ts +1 -1
  295. package/src/hooks/cli.ts +6 -5
  296. package/src/hooks/config.ts +6 -8
  297. package/src/hooks/discovery.ts +6 -5
  298. package/src/hooks/manager.ts +4 -3
  299. package/src/hooks/runner.ts +2 -2
  300. package/src/hooks/templates.ts +5 -5
  301. package/src/inbound/public-ingress-urls.ts +6 -4
  302. package/src/index.ts +4 -2
  303. package/src/influencer/client.ts +1104 -0
  304. package/src/instrument.ts +4 -3
  305. package/src/logfire.ts +4 -3
  306. package/src/memory/admin.ts +25 -35
  307. package/src/memory/attachments-store.ts +4 -7
  308. package/src/memory/channel-delivery-store.ts +30 -1
  309. package/src/memory/channel-guardian-store.ts +202 -2
  310. package/src/memory/clarification-resolver.ts +37 -33
  311. package/src/memory/conflict-store.ts +67 -61
  312. package/src/memory/contradiction-checker.ts +141 -117
  313. package/src/memory/conversation-store.ts +335 -51
  314. package/src/memory/db-connection.ts +27 -4
  315. package/src/memory/db-init.ts +265 -4
  316. package/src/memory/db.ts +14 -1
  317. package/src/memory/embedding-backend.ts +27 -5
  318. package/src/memory/embedding-ollama.ts +2 -1
  319. package/src/memory/entity-extractor.ts +38 -35
  320. package/src/memory/guardian-action-store.ts +430 -0
  321. package/src/memory/inbox-escalation-projection.ts +59 -0
  322. package/src/memory/inbox-thread-store.ts +218 -0
  323. package/src/memory/ingress-invite-store.ts +338 -0
  324. package/src/memory/ingress-member-store.ts +350 -0
  325. package/src/memory/items-extractor.ts +91 -97
  326. package/src/memory/job-handlers/index-maintenance.ts +3 -3
  327. package/src/memory/job-handlers/media-processing.ts +69 -0
  328. package/src/memory/job-handlers/summarization.ts +32 -26
  329. package/src/memory/job-utils.ts +3 -10
  330. package/src/memory/jobs-store.ts +8 -10
  331. package/src/memory/jobs-worker.ts +55 -36
  332. package/src/memory/media-store.ts +759 -0
  333. package/src/memory/migrations/001-job-deferrals.ts +45 -0
  334. package/src/memory/migrations/002-tool-invocations-fk.ts +43 -0
  335. package/src/memory/migrations/003-memory-fts-backfill.ts +24 -0
  336. package/src/memory/migrations/004-entity-relation-dedup.ts +87 -0
  337. package/src/memory/migrations/005-fingerprint-scope-unique.ts +80 -0
  338. package/src/memory/migrations/006-scope-salted-fingerprints.ts +62 -0
  339. package/src/memory/migrations/007-assistant-id-to-self.ts +254 -0
  340. package/src/memory/migrations/008-remove-assistant-id-columns.ts +208 -0
  341. package/src/memory/migrations/009-llm-usage-events-drop-assistant-id.ts +83 -0
  342. package/src/memory/migrations/010-ext-conv-bindings-channel-chat-unique.ts +56 -0
  343. package/src/memory/migrations/011-call-sessions-provider-sid-dedup.ts +63 -0
  344. package/src/memory/migrations/012-call-sessions-add-initiated-from.ts +19 -0
  345. package/src/memory/migrations/013-guardian-action-tables.ts +68 -0
  346. package/src/memory/migrations/014-backfill-inbox-thread-state.ts +76 -0
  347. package/src/memory/migrations/015-drop-active-search-index.ts +27 -0
  348. package/src/memory/migrations/016-memory-segments-indexes.ts +11 -0
  349. package/src/memory/migrations/017-memory-items-indexes.ts +10 -0
  350. package/src/memory/migrations/018-remaining-table-indexes.ts +13 -0
  351. package/src/memory/migrations/index.ts +24 -0
  352. package/src/memory/migrations/registry.ts +79 -0
  353. package/src/memory/migrations/validate-migration-state.ts +69 -0
  354. package/src/memory/qdrant-manager.ts +49 -8
  355. package/src/memory/query-builder.ts +1 -1
  356. package/src/memory/raw-query.ts +119 -0
  357. package/src/memory/recall-cache.ts +4 -1
  358. package/src/memory/retriever.ts +165 -47
  359. package/src/memory/schema-migration.ts +25 -984
  360. package/src/memory/schema.ts +228 -7
  361. package/src/memory/search/entity.ts +205 -31
  362. package/src/memory/search/lexical.ts +81 -52
  363. package/src/memory/search/ranking.ts +27 -23
  364. package/src/memory/search/semantic.ts +157 -19
  365. package/src/memory/search/types.ts +24 -0
  366. package/src/memory/shared-app-links-store.ts +4 -5
  367. package/src/memory/validation.ts +19 -0
  368. package/src/messaging/draft-store.ts +5 -6
  369. package/src/messaging/provider-types.ts +2 -0
  370. package/src/messaging/providers/sms/adapter.ts +201 -0
  371. package/src/messaging/providers/sms/client.ts +93 -0
  372. package/src/messaging/providers/sms/types.ts +7 -0
  373. package/src/messaging/providers/telegram-bot/adapter.ts +2 -5
  374. package/src/messaging/providers/whatsapp/adapter.ts +136 -0
  375. package/src/messaging/providers/whatsapp/client.ts +67 -0
  376. package/src/messaging/style-analyzer.ts +5 -4
  377. package/src/messaging/thread-summarizer.ts +61 -69
  378. package/src/messaging/triage-engine.ts +62 -71
  379. package/src/migrations/config-merge.ts +53 -0
  380. package/src/migrations/data-layout.ts +68 -0
  381. package/src/migrations/data-merge.ts +33 -0
  382. package/src/migrations/hooks-merge.ts +90 -0
  383. package/src/migrations/index.ts +6 -0
  384. package/src/migrations/log.ts +23 -0
  385. package/src/migrations/skills-merge.ts +33 -0
  386. package/src/migrations/workspace-layout.ts +79 -0
  387. package/src/permissions/checker.ts +133 -11
  388. package/src/permissions/prompter.ts +14 -0
  389. package/src/permissions/shell-identity.ts +31 -1
  390. package/src/permissions/trust-store.ts +21 -1
  391. package/src/providers/anthropic/client.ts +4 -4
  392. package/src/providers/failover.ts +2 -2
  393. package/src/providers/model-intents.ts +70 -0
  394. package/src/providers/ollama/client.ts +2 -1
  395. package/src/providers/provider-send-message.ts +176 -0
  396. package/src/providers/registry.ts +71 -30
  397. package/src/providers/retry.ts +35 -1
  398. package/src/providers/types.ts +12 -1
  399. package/src/runtime/approval-conversation-turn.ts +97 -0
  400. package/src/runtime/approval-message-composer.ts +253 -0
  401. package/src/runtime/channel-approval-parser.ts +36 -2
  402. package/src/runtime/channel-approvals.ts +11 -24
  403. package/src/runtime/channel-guardian-service.ts +88 -21
  404. package/src/runtime/channel-readiness-service.ts +418 -0
  405. package/src/runtime/channel-readiness-types.ts +35 -0
  406. package/src/runtime/channel-retry-sweep.ts +184 -0
  407. package/src/runtime/guardian-context-resolver.ts +108 -0
  408. package/src/runtime/http-server.ts +275 -717
  409. package/src/runtime/http-types.ts +59 -3
  410. package/src/runtime/middleware/auth.ts +116 -0
  411. package/src/runtime/middleware/error-handler.ts +33 -0
  412. package/src/runtime/middleware/twilio-validation.ts +127 -0
  413. package/src/runtime/routes/app-routes.ts +1 -1
  414. package/src/runtime/routes/call-routes.ts +51 -7
  415. package/src/runtime/routes/channel-delivery-routes.ts +170 -0
  416. package/src/runtime/routes/channel-guardian-routes.ts +1191 -0
  417. package/src/runtime/routes/channel-inbound-routes.ts +1152 -0
  418. package/src/runtime/routes/channel-route-shared.ts +144 -0
  419. package/src/runtime/routes/channel-routes.ts +32 -1588
  420. package/src/runtime/routes/conversation-routes.ts +50 -7
  421. package/src/runtime/routes/events-routes.ts +2 -2
  422. package/src/runtime/routes/identity-routes.ts +126 -0
  423. package/src/runtime/routes/pairing-routes.ts +143 -0
  424. package/src/runtime/routes/run-routes.ts +15 -1
  425. package/src/runtime/run-orchestrator.ts +86 -35
  426. package/src/schedule/schedule-store.ts +36 -32
  427. package/src/schedule/scheduler.ts +3 -3
  428. package/src/security/encrypted-store.ts +5 -7
  429. package/src/security/oauth2.ts +45 -15
  430. package/src/security/parental-control-store.ts +183 -0
  431. package/src/security/secret-allowlist.ts +4 -3
  432. package/src/security/secret-scanner.ts +5 -5
  433. package/src/security/secure-keys.ts +1 -1
  434. package/src/security/token-manager.ts +3 -2
  435. package/src/services/vercel-deploy.ts +6 -2
  436. package/src/skills/tool-manifest.ts +3 -3
  437. package/src/skills/vellum-catalog-remote.ts +75 -16
  438. package/src/slack/slack-webhook.ts +2 -1
  439. package/src/swarm/orchestrator.ts +92 -1
  440. package/src/swarm/router-planner.ts +6 -9
  441. package/src/swarm/worker-prompts.ts +9 -12
  442. package/src/tasks/task-compiler.ts +19 -28
  443. package/src/tasks/task-runner.ts +1 -1
  444. package/src/tools/assets/materialize.ts +2 -2
  445. package/src/tools/assets/search.ts +15 -14
  446. package/src/tools/browser/__tests__/auth-detector.test.ts +1 -0
  447. package/src/tools/browser/auto-navigate.ts +1 -0
  448. package/src/tools/browser/browser-execution.ts +10 -1
  449. package/src/tools/browser/browser-manager.ts +119 -4
  450. package/src/tools/browser/network-recorder.ts +5 -0
  451. package/src/tools/calls/call-start.ts +1 -0
  452. package/src/tools/credentials/broker.ts +11 -2
  453. package/src/tools/credentials/metadata-store.ts +18 -14
  454. package/src/tools/credentials/post-connect-hooks.ts +61 -0
  455. package/src/tools/credentials/vault.ts +49 -23
  456. package/src/tools/execution-target.ts +11 -1
  457. package/src/tools/executor.ts +68 -9
  458. package/src/tools/host-terminal/cli-discover.ts +1 -1
  459. package/src/tools/network/script-proxy/http-forwarder.ts +1 -1
  460. package/src/tools/network/script-proxy/mitm-handler.ts +1 -1
  461. package/src/tools/network/script-proxy/server.ts +1 -1
  462. package/src/tools/network/script-proxy/session-manager.ts +6 -5
  463. package/src/tools/network/web-fetch.ts +18 -2
  464. package/src/tools/network/web-search.ts +8 -4
  465. package/src/tools/reminder/reminder-store.ts +14 -15
  466. package/src/tools/schedule/create.ts +1 -0
  467. package/src/tools/schedule/list.ts +2 -1
  468. package/src/tools/shared/filesystem/file-ops-service.ts +5 -7
  469. package/src/tools/skills/skill-script-runner.ts +24 -9
  470. package/src/tools/skills/skill-tool-factory.ts +1 -0
  471. package/src/tools/tasks/work-item-enqueue.ts +2 -2
  472. package/src/tools/terminal/evaluate-typescript.ts +21 -12
  473. package/src/tools/terminal/parser.ts +50 -0
  474. package/src/tools/types.ts +2 -0
  475. package/src/tools/watcher/delete.ts +6 -0
  476. package/src/tools/weather/service.ts +1 -1
  477. package/src/twitter/client.ts +190 -24
  478. package/src/twitter/router.ts +1 -1
  479. package/src/twitter/session.ts +4 -3
  480. package/src/util/clipboard.ts +1 -1
  481. package/src/util/errors.ts +65 -8
  482. package/src/util/fs.ts +40 -0
  483. package/src/util/json.ts +10 -0
  484. package/src/util/log-redact.ts +189 -0
  485. package/src/util/logger.ts +19 -17
  486. package/src/util/object.ts +3 -0
  487. package/src/util/platform.ts +105 -363
  488. package/src/util/pricing.ts +1 -1
  489. package/src/util/promise-guard.ts +1 -1
  490. package/src/util/retry.ts +19 -0
  491. package/src/util/row-mapper.ts +79 -0
  492. package/src/util/silently.ts +21 -0
  493. package/src/watcher/engine.ts +5 -1
  494. package/src/watcher/provider-types.ts +20 -0
  495. package/src/watcher/providers/github.ts +156 -0
  496. package/src/watcher/providers/gmail.ts +1 -0
  497. package/src/watcher/providers/google-calendar.ts +1 -0
  498. package/src/watcher/providers/linear.ts +460 -0
  499. package/src/watcher/providers/slack.ts +1 -0
  500. package/src/work-items/work-item-runner.ts +1 -1
  501. package/src/workspace/git-service.ts +1 -1
  502. package/src/workspace/provider-commit-message-generator.ts +51 -22
  503. package/src/__tests__/call-bridge.test.ts +0 -517
  504. package/src/__tests__/session-process-bridge.test.ts +0 -244
  505. package/src/calls/call-bridge.ts +0 -168
  506. package/src/config/vellum-skills/google-oauth-setup/SKILL.md +0 -199
@@ -0,0 +1,828 @@
1
+ /**
2
+ * Atomicity tests for memory UPSERT paths.
3
+ *
4
+ * SQLite is single-writer, and indexMessageNow / createOrUpdatePendingConflict
5
+ * are synchronous functions. Because every call runs to completion before the
6
+ * next microtask starts, the Promise.all / Promise.resolve().then() pattern
7
+ * used here does NOT create true concurrent execution — calls still run
8
+ * sequentially.
9
+ *
10
+ * What these tests DO verify is the correctness of the ON CONFLICT /
11
+ * IMMEDIATE-transaction logic when the same logical operation is repeated many
12
+ * times (e.g. duplicate indexer runs for the same messageId). That covers the
13
+ * most common real-world correctness problem: a retry or a duplicate dispatch
14
+ * reaching the same code path more than once.
15
+ *
16
+ * True OS-level thread concurrency would require spawning separate worker
17
+ * processes and is not tested here.
18
+ */
19
+
20
+ import { afterAll, beforeEach, describe, expect, mock, test } from 'bun:test';
21
+ import { mkdtempSync, rmSync } from 'node:fs';
22
+ import { tmpdir } from 'node:os';
23
+ import { join } from 'node:path';
24
+ import { eq } from 'drizzle-orm';
25
+
26
+ const testDir = mkdtempSync(join(tmpdir(), 'memory-upsert-concurrency-'));
27
+
28
+ mock.module('../util/platform.js', () => ({
29
+ getDataDir: () => testDir,
30
+ isMacOS: () => process.platform === 'darwin',
31
+ isLinux: () => process.platform === 'linux',
32
+ isWindows: () => process.platform === 'win32',
33
+ getSocketPath: () => join(testDir, 'test.sock'),
34
+ getPidPath: () => join(testDir, 'test.pid'),
35
+ getDbPath: () => join(testDir, 'test.db'),
36
+ getLogPath: () => join(testDir, 'test.log'),
37
+ ensureDataDir: () => {},
38
+ }));
39
+
40
+ mock.module('../util/logger.js', () => ({
41
+ getLogger: () => new Proxy({} as Record<string, unknown>, {
42
+ get: () => () => {},
43
+ }),
44
+ }));
45
+
46
+ mock.module('../memory/qdrant-client.js', () => ({
47
+ getQdrantClient: () => ({
48
+ searchWithFilter: async () => [],
49
+ upsertPoints: async () => {},
50
+ deletePoints: async () => {},
51
+ }),
52
+ initQdrantClient: () => {},
53
+ }));
54
+
55
+ import { DEFAULT_CONFIG } from '../config/defaults.js';
56
+
57
+ const TEST_CONFIG = {
58
+ ...DEFAULT_CONFIG,
59
+ memory: {
60
+ ...DEFAULT_CONFIG.memory,
61
+ enabled: true,
62
+ extraction: {
63
+ ...DEFAULT_CONFIG.memory.extraction,
64
+ useLLM: false,
65
+ },
66
+ },
67
+ };
68
+
69
+ mock.module('../config/loader.js', () => ({
70
+ loadConfig: () => TEST_CONFIG,
71
+ getConfig: () => TEST_CONFIG,
72
+ loadRawConfig: () => ({}),
73
+ saveRawConfig: () => {},
74
+ invalidateConfigCache: () => {},
75
+ }));
76
+
77
+ import { getDb, initializeDb, resetDb } from '../memory/db.js';
78
+ import {
79
+ conversations,
80
+ memoryItems,
81
+ memorySegments,
82
+ messages,
83
+ } from '../memory/schema.js';
84
+ import { indexMessageNow } from '../memory/indexer.js';
85
+ import { createOrUpdatePendingConflict, listPendingConflicts } from '../memory/conflict-store.js';
86
+
87
+ // Initialize DB once for the entire file. Each test cleans its own tables.
88
+ initializeDb();
89
+
90
+ afterAll(() => {
91
+ resetDb();
92
+ try {
93
+ rmSync(testDir, { recursive: true });
94
+ } catch {
95
+ // best effort cleanup
96
+ }
97
+ });
98
+
99
+ function resetTables() {
100
+ const db = getDb();
101
+ db.run('DELETE FROM memory_item_conflicts');
102
+ db.run('DELETE FROM memory_item_entities');
103
+ db.run('DELETE FROM memory_entity_relations');
104
+ db.run('DELETE FROM memory_entities');
105
+ db.run('DELETE FROM memory_item_sources');
106
+ db.run('DELETE FROM memory_embeddings');
107
+ db.run('DELETE FROM memory_summaries');
108
+ db.run('DELETE FROM memory_items');
109
+ db.run('DELETE FROM memory_segment_fts');
110
+ db.run('DELETE FROM memory_segments');
111
+ db.run('DELETE FROM memory_jobs');
112
+ db.run('DELETE FROM messages');
113
+ db.run('DELETE FROM conversations');
114
+ }
115
+
116
+ /** Insert a minimal conversation + message row for FK references. */
117
+ function seedConversationAndMessage(
118
+ conversationId: string,
119
+ messageId: string,
120
+ text: string,
121
+ ): void {
122
+ const db = getDb();
123
+ const now = Date.now();
124
+ db.insert(conversations).values({
125
+ id: conversationId,
126
+ title: null,
127
+ createdAt: now,
128
+ updatedAt: now,
129
+ totalInputTokens: 0,
130
+ totalOutputTokens: 0,
131
+ totalEstimatedCost: 0,
132
+ contextSummary: null,
133
+ contextCompactedMessageCount: 0,
134
+ contextCompactedAt: null,
135
+ }).run();
136
+
137
+ db.insert(messages).values({
138
+ id: messageId,
139
+ conversationId,
140
+ role: 'user',
141
+ content: JSON.stringify([{ type: 'text', text }]),
142
+ createdAt: now,
143
+ }).run();
144
+ }
145
+
146
+ /** Insert a pair of memory items that can serve as conflict participants. */
147
+ function seedItemPair(suffix: string, scopeId = 'default'): { existingItemId: string; candidateItemId: string } {
148
+ const db = getDb();
149
+ const now = Date.now();
150
+ const existingItemId = `existing-${suffix}`;
151
+ const candidateItemId = `candidate-${suffix}`;
152
+ db.insert(memoryItems).values([
153
+ {
154
+ id: existingItemId,
155
+ kind: 'preference',
156
+ subject: 'framework preference',
157
+ statement: `Existing statement ${suffix}`,
158
+ status: 'active',
159
+ confidence: 0.8,
160
+ importance: 0.7,
161
+ fingerprint: `fp-existing-${suffix}`,
162
+ verificationState: 'assistant_inferred',
163
+ scopeId,
164
+ firstSeenAt: now,
165
+ lastSeenAt: now,
166
+ },
167
+ {
168
+ id: candidateItemId,
169
+ kind: 'preference',
170
+ subject: 'framework preference',
171
+ statement: `Candidate statement ${suffix}`,
172
+ status: 'pending_clarification',
173
+ confidence: 0.8,
174
+ importance: 0.7,
175
+ fingerprint: `fp-candidate-${suffix}`,
176
+ verificationState: 'assistant_inferred',
177
+ scopeId,
178
+ firstSeenAt: now,
179
+ lastSeenAt: now,
180
+ },
181
+ ]).run();
182
+ return { existingItemId, candidateItemId };
183
+ }
184
+
185
+ // ─────────────────────────────────────────────────────────────────────────────
186
+ // Test suite: segment UPSERT atomicity under parallel indexer load
187
+ // ─────────────────────────────────────────────────────────────────────────────
188
+
189
+ describe('segment UPSERT atomicity under repeated indexer invocations', () => {
190
+ beforeEach(() => {
191
+ resetTables();
192
+ });
193
+
194
+ test('repeated indexing of the same message does not create duplicate segments', async () => {
195
+ // Index the same messageId multiple times (simulating duplicate indexer
196
+ // dispatches, retries, or a race at the call site). The ON CONFLICT DO
197
+ // UPDATE on memorySegments.id must absorb every duplicate call.
198
+ const conversationId = 'conv-parallel-segment-dedup';
199
+ const messageId = 'msg-parallel-segment-dedup';
200
+ const text = 'I prefer TypeScript over plain JavaScript for large projects.';
201
+
202
+ seedConversationAndMessage(conversationId, messageId, text);
203
+
204
+ const db = getDb();
205
+ const config = TEST_CONFIG.memory;
206
+
207
+ // Call indexMessageNow N times for the same messageId. Even though we use
208
+ // Promise.all, these synchronous calls still run sequentially — the point is
209
+ // to verify that repeated indexer runs for the same messageId do not produce
210
+ // duplicate segment rows (i.e. the ON CONFLICT DO UPDATE absorbs them).
211
+ const WORKERS = 8;
212
+ await Promise.all(
213
+ Array.from({ length: WORKERS }, () =>
214
+ Promise.resolve().then(() =>
215
+ indexMessageNow(
216
+ {
217
+ messageId,
218
+ conversationId,
219
+ role: 'user',
220
+ content: JSON.stringify([{ type: 'text', text }]),
221
+ createdAt: Date.now(),
222
+ },
223
+ config,
224
+ ),
225
+ ),
226
+ ),
227
+ );
228
+
229
+ const segments = db
230
+ .select()
231
+ .from(memorySegments)
232
+ .where(eq(memorySegments.messageId, messageId))
233
+ .all();
234
+
235
+ // Each physical segment (identified by segmentId = messageId + segmentIndex)
236
+ // must appear exactly once regardless of how many indexer calls ran.
237
+ const idCounts = new Map<string, number>();
238
+ for (const seg of segments) {
239
+ idCounts.set(seg.id, (idCounts.get(seg.id) ?? 0) + 1);
240
+ }
241
+ for (const [segId, count] of idCounts) {
242
+ expect(count).toBe(1);
243
+ expect(segId.startsWith(messageId)).toBe(true);
244
+ }
245
+ });
246
+
247
+ test('indexing distinct messages produces independent segment sets', async () => {
248
+ // Different messages indexed in the same batch must each produce their own
249
+ // non-overlapping segments with correct messageId back-references.
250
+ const now = Date.now();
251
+ const conversationId = 'conv-parallel-distinct';
252
+ const db = getDb();
253
+
254
+ db.insert(conversations).values({
255
+ id: conversationId,
256
+ title: null,
257
+ createdAt: now,
258
+ updatedAt: now,
259
+ totalInputTokens: 0,
260
+ totalOutputTokens: 0,
261
+ totalEstimatedCost: 0,
262
+ contextSummary: null,
263
+ contextCompactedMessageCount: 0,
264
+ contextCompactedAt: null,
265
+ }).run();
266
+
267
+ const MSG_COUNT = 6;
268
+ for (let i = 0; i < MSG_COUNT; i++) {
269
+ db.insert(messages).values({
270
+ id: `msg-distinct-${i}`,
271
+ conversationId,
272
+ role: 'user',
273
+ content: JSON.stringify([{ type: 'text', text: `Distinct message content for worker ${i}, covering a unique topic that should be stored separately.` }]),
274
+ createdAt: now + i,
275
+ }).run();
276
+ }
277
+
278
+ const config = TEST_CONFIG.memory;
279
+
280
+ // Call indexMessageNow once per distinct messageId. The calls run
281
+ // sequentially (synchronous functions), but grouping them here mirrors
282
+ // how a batch indexer would dispatch multiple messages and lets us assert
283
+ // that each message produces its own non-overlapping segment set.
284
+ await Promise.all(
285
+ Array.from({ length: MSG_COUNT }, (_, i) => {
286
+ const msgId = `msg-distinct-${i}`;
287
+ return Promise.resolve().then(() =>
288
+ indexMessageNow(
289
+ {
290
+ messageId: msgId,
291
+ conversationId,
292
+ role: 'user',
293
+ content: JSON.stringify([{ type: 'text', text: `Distinct message content for worker ${i}, covering a unique topic that should be stored separately.` }]),
294
+ createdAt: now + i,
295
+ },
296
+ config,
297
+ ),
298
+ );
299
+ }),
300
+ );
301
+
302
+ // Every segment must reference its own message and no segment may appear
303
+ // for the wrong messageId.
304
+ for (let i = 0; i < MSG_COUNT; i++) {
305
+ const msgId = `msg-distinct-${i}`;
306
+ const segs = db
307
+ .select()
308
+ .from(memorySegments)
309
+ .where(eq(memorySegments.messageId, msgId))
310
+ .all();
311
+
312
+ // At least one segment must have been written.
313
+ expect(segs.length).toBeGreaterThanOrEqual(1);
314
+
315
+ // Segment IDs must be of the form `${msgId}:${index}`.
316
+ for (const seg of segs) {
317
+ expect(seg.id.startsWith(msgId + ':')).toBe(true);
318
+ expect(seg.messageId).toBe(msgId);
319
+ expect(seg.conversationId).toBe(conversationId);
320
+ }
321
+ }
322
+ });
323
+
324
+ test('re-indexing with identical content does not change the stored segment', () => {
325
+ // When an indexer re-processes an already-indexed segment (same id + same
326
+ // content hash), the ON CONFLICT DO UPDATE path must run but the row must
327
+ // remain semantically equivalent to the original.
328
+ const conversationId = 'conv-stable-rehash';
329
+ const messageId = 'msg-stable-rehash';
330
+ const text = 'My preferred timezone is America/Los_Angeles and I work remotely.';
331
+
332
+ seedConversationAndMessage(conversationId, messageId, text);
333
+
334
+ const config = TEST_CONFIG.memory;
335
+
336
+ const firstResult = indexMessageNow(
337
+ { messageId, conversationId, role: 'user', content: JSON.stringify([{ type: 'text', text }]), createdAt: Date.now() },
338
+ config,
339
+ );
340
+
341
+ const db = getDb();
342
+ const segmentsAfterFirst = db
343
+ .select()
344
+ .from(memorySegments)
345
+ .where(eq(memorySegments.messageId, messageId))
346
+ .all();
347
+
348
+ // Re-index twice more with the same payload.
349
+ indexMessageNow(
350
+ { messageId, conversationId, role: 'user', content: JSON.stringify([{ type: 'text', text }]), createdAt: Date.now() },
351
+ config,
352
+ );
353
+ indexMessageNow(
354
+ { messageId, conversationId, role: 'user', content: JSON.stringify([{ type: 'text', text }]), createdAt: Date.now() },
355
+ config,
356
+ );
357
+
358
+ const segmentsAfterRehash = db
359
+ .select()
360
+ .from(memorySegments)
361
+ .where(eq(memorySegments.messageId, messageId))
362
+ .all();
363
+
364
+ // Segment count must not have grown.
365
+ expect(segmentsAfterRehash.length).toBe(segmentsAfterFirst.length);
366
+
367
+ // Content hashes must match between first and subsequent indexings.
368
+ const firstById = new Map(segmentsAfterFirst.map((s) => [s.id, s]));
369
+ for (const seg of segmentsAfterRehash) {
370
+ const original = firstById.get(seg.id);
371
+ expect(original).toBeDefined();
372
+ expect(seg.contentHash).toBe(original!.contentHash);
373
+ expect(seg.text).toBe(original!.text);
374
+ }
375
+
376
+ // The indexer must have reported the correct segment count both times.
377
+ expect(firstResult.indexedSegments).toBeGreaterThanOrEqual(1);
378
+ });
379
+
380
+ test('re-indexing same message with different content applies last-write semantics', async () => {
381
+ // When indexMessageNow is called twice for the same messageId with different
382
+ // content (simulating an edit followed by a re-index), the ON CONFLICT DO
383
+ // UPDATE must store one row per segmentId. We cannot assert which text
384
+ // "wins" — only that no duplicate rows exist.
385
+ const conversationId = 'conv-edit-race';
386
+ const messageId = 'msg-edit-race';
387
+ const textV1 = 'I prefer React for frontend development work on large projects.';
388
+ const textV2 = 'I prefer Vue for frontend development work on large projects instead.';
389
+
390
+ seedConversationAndMessage(conversationId, messageId, textV1);
391
+
392
+ const config = TEST_CONFIG.memory;
393
+
394
+ // Call indexMessageNow twice with different content for the same messageId,
395
+ // running sequentially. The ON CONFLICT DO UPDATE must absorb both calls
396
+ // and leave exactly one row per segmentId regardless of which content wins.
397
+ await Promise.all([
398
+ Promise.resolve().then(() =>
399
+ indexMessageNow(
400
+ { messageId, conversationId, role: 'user', content: JSON.stringify([{ type: 'text', text: textV1 }]), createdAt: Date.now() },
401
+ config,
402
+ ),
403
+ ),
404
+ Promise.resolve().then(() =>
405
+ indexMessageNow(
406
+ { messageId, conversationId, role: 'user', content: JSON.stringify([{ type: 'text', text: textV2 }]), createdAt: Date.now() },
407
+ config,
408
+ ),
409
+ ),
410
+ ]);
411
+
412
+ const db = getDb();
413
+ const segments = db
414
+ .select()
415
+ .from(memorySegments)
416
+ .where(eq(memorySegments.messageId, messageId))
417
+ .all();
418
+
419
+ // No duplicate segment IDs — each logical segment must appear at most once.
420
+ const ids = segments.map((s) => s.id);
421
+ const uniqueIds = new Set(ids);
422
+ expect(uniqueIds.size).toBe(ids.length);
423
+ });
424
+ });
425
+
426
+ // ─────────────────────────────────────────────────────────────────────────────
427
+ // Test suite: conflict creation UPSERT atomicity
428
+ // ─────────────────────────────────────────────────────────────────────────────
429
+
430
+ describe('conflict creation UPSERT atomicity', () => {
431
+ beforeEach(() => {
432
+ resetTables();
433
+ });
434
+
435
+ test('repeated createOrUpdatePendingConflict calls for the same pair produce exactly one conflict row', async () => {
436
+ // Critical UPSERT path: the same conflict pair inserted multiple times
437
+ // (e.g. duplicate worker dispatches, retries). The IMMEDIATE transaction
438
+ // guard in createOrUpdatePendingConflict must ensure only one row exists.
439
+ const pair = seedItemPair('parallel-create');
440
+
441
+ // Call createOrUpdatePendingConflict N times for the same pair. Calls run
442
+ // sequentially (synchronous); the test verifies that repeated calls produce
443
+ // exactly one conflict row — the IMMEDIATE transaction deduplication path.
444
+ const WORKERS = 10;
445
+ const results = await Promise.all(
446
+ Array.from({ length: WORKERS }, (_, i) =>
447
+ Promise.resolve().then(() =>
448
+ createOrUpdatePendingConflict({
449
+ scopeId: 'default',
450
+ existingItemId: pair.existingItemId,
451
+ candidateItemId: pair.candidateItemId,
452
+ relationship: 'ambiguous_contradiction',
453
+ clarificationQuestion: `Worker ${i} discovered a contradiction`,
454
+ }),
455
+ ),
456
+ ),
457
+ );
458
+
459
+ // All callers must receive the same conflict ID — the deduplication path
460
+ // returns the existing row on the second and subsequent calls.
461
+ const firstId = results[0].id;
462
+ for (const result of results) {
463
+ expect(result.id).toBe(firstId);
464
+ }
465
+
466
+ // Exactly one pending conflict row in the DB.
467
+ const pending = listPendingConflicts('default');
468
+ expect(pending).toHaveLength(1);
469
+ expect(pending[0].id).toBe(firstId);
470
+ });
471
+
472
+ test('conflict creation for different pairs produces distinct rows without cross-contamination', async () => {
473
+ // Each unique item pair must get its own conflict row — deduplication must
474
+ // be scoped to the pair, not global. Also exercises the idempotent
475
+ // insert-then-update path within each pair.
476
+ const PAIR_COUNT = 6;
477
+ const pairs = Array.from({ length: PAIR_COUNT }, (_, i) => seedItemPair(`multi-pair-${i}`));
478
+
479
+ // For each pair, make two calls: one insert and one update. All calls run
480
+ // sequentially. The test verifies that each pair ends up with exactly one
481
+ // conflict row (no cross-pair contamination, idempotent update path works).
482
+ await Promise.all(
483
+ pairs.flatMap((pair) => [
484
+ // First call: insert with 'contradiction'.
485
+ Promise.resolve().then(() =>
486
+ createOrUpdatePendingConflict({
487
+ scopeId: 'default',
488
+ existingItemId: pair.existingItemId,
489
+ candidateItemId: pair.candidateItemId,
490
+ relationship: 'contradiction',
491
+ }),
492
+ ),
493
+ // Second call: update to 'ambiguous_contradiction' — tests the idempotent update path.
494
+ Promise.resolve().then(() =>
495
+ createOrUpdatePendingConflict({
496
+ scopeId: 'default',
497
+ existingItemId: pair.existingItemId,
498
+ candidateItemId: pair.candidateItemId,
499
+ relationship: 'ambiguous_contradiction',
500
+ }),
501
+ ),
502
+ ]),
503
+ );
504
+
505
+ // Each pair must have produced exactly one pending conflict.
506
+ const pending = listPendingConflicts('default');
507
+ expect(pending).toHaveLength(PAIR_COUNT);
508
+
509
+ // All conflict IDs must be unique.
510
+ const ids = pending.map((c) => c.id);
511
+ expect(new Set(ids).size).toBe(PAIR_COUNT);
512
+
513
+ // Each returned conflict must reference the correct item pair.
514
+ for (let i = 0; i < PAIR_COUNT; i++) {
515
+ const pair = pairs[i];
516
+ const found = pending.find(
517
+ (c) => c.existingItemId === pair.existingItemId && c.candidateItemId === pair.candidateItemId,
518
+ );
519
+ expect(found).toBeDefined();
520
+ // The update call ran after the insert, so relationship is ambiguous_contradiction.
521
+ expect(found!.relationship).toBe('ambiguous_contradiction');
522
+ }
523
+ });
524
+
525
+ test('repeated updates to the same conflict row converge to a consistent state', async () => {
526
+ // Multiple update calls for the same conflict (e.g. repeated worker runs).
527
+ // All updates must succeed (last writer wins is acceptable) and the row
528
+ // must remain internally consistent.
529
+ const pair = seedItemPair('concurrent-update');
530
+ const first = createOrUpdatePendingConflict({
531
+ scopeId: 'default',
532
+ existingItemId: pair.existingItemId,
533
+ candidateItemId: pair.candidateItemId,
534
+ relationship: 'contradiction',
535
+ clarificationQuestion: 'Initial question',
536
+ });
537
+
538
+ // Call createOrUpdatePendingConflict N times against the same existing row.
539
+ // Calls are sequential; the test verifies the row stays consistent (one row,
540
+ // valid status/relationship) after repeated updates — last writer wins.
541
+ const UPDATES = 8;
542
+ const results = await Promise.all(
543
+ Array.from({ length: UPDATES }, (_, i) =>
544
+ Promise.resolve().then(() =>
545
+ createOrUpdatePendingConflict({
546
+ scopeId: 'default',
547
+ existingItemId: pair.existingItemId,
548
+ candidateItemId: pair.candidateItemId,
549
+ relationship: 'ambiguous_contradiction',
550
+ clarificationQuestion: `Updated question from worker ${i}`,
551
+ }),
552
+ ),
553
+ ),
554
+ );
555
+
556
+ // All calls must return the same conflict ID.
557
+ for (const result of results) {
558
+ expect(result.id).toBe(first.id);
559
+ }
560
+
561
+ // Still exactly one row in the DB.
562
+ const pending = listPendingConflicts('default');
563
+ expect(pending).toHaveLength(1);
564
+
565
+ // The row must be consistent: valid status, valid relationship.
566
+ const conflict = pending[0];
567
+ expect(conflict.status).toBe('pending_clarification');
568
+ expect(conflict.relationship).toBe('ambiguous_contradiction');
569
+ });
570
+
571
+ test('scope isolation ensures conflicts in different scopes do not interfere', async () => {
572
+ // Conflicts created in different scopes must not cross-contaminate each
573
+ // other's conflict sets — scopeId must be part of the deduplication key.
574
+ const SCOPES = ['scope-alpha', 'scope-beta', 'scope-gamma'];
575
+ const scopePairs = SCOPES.map((scope) => ({ scope, pair: seedItemPair(`scope-${scope}`, scope) }));
576
+
577
+ // Make 3 calls per scope for all scopes. Calls run sequentially; the test
578
+ // verifies that each scope produces exactly one conflict row and that there
579
+ // is no cross-scope contamination from repeated same-scope calls.
580
+ await Promise.all(
581
+ scopePairs.flatMap(({ scope, pair }) =>
582
+ Array.from({ length: 3 }, () =>
583
+ Promise.resolve().then(() =>
584
+ createOrUpdatePendingConflict({
585
+ scopeId: scope,
586
+ existingItemId: pair.existingItemId,
587
+ candidateItemId: pair.candidateItemId,
588
+ relationship: 'contradiction',
589
+ }),
590
+ ),
591
+ ),
592
+ ),
593
+ );
594
+
595
+ for (const scope of SCOPES) {
596
+ const pending = listPendingConflicts(scope);
597
+ // Exactly one conflict per scope, no cross-scope leakage.
598
+ expect(pending).toHaveLength(1);
599
+ expect(pending[0].scopeId).toBe(scope);
600
+ }
601
+ });
602
+ });
603
+
604
+ // ─────────────────────────────────────────────────────────────────────────────
605
+ // Test suite: memory segment job atomicity
606
+ // ─────────────────────────────────────────────────────────────────────────────
607
+
608
+ describe('memory segment job atomicity under repeated indexer invocations', () => {
609
+ beforeEach(() => {
610
+ resetTables();
611
+ });
612
+
613
+ test('each unique (messageId, segmentIndex) pair generates at most one segment row', async () => {
614
+ // Re-index the same messages multiple times to verify that the job+segment
615
+ // transaction boundary is respected and no duplicate segment rows appear for
616
+ // the same logical (messageId, segmentIndex) identity.
617
+ const conversationId = 'conv-job-atomicity';
618
+ const now = Date.now();
619
+ const db = getDb();
620
+
621
+ db.insert(conversations).values({
622
+ id: conversationId,
623
+ title: null,
624
+ createdAt: now,
625
+ updatedAt: now,
626
+ totalInputTokens: 0,
627
+ totalOutputTokens: 0,
628
+ totalEstimatedCost: 0,
629
+ contextSummary: null,
630
+ contextCompactedMessageCount: 0,
631
+ contextCompactedAt: null,
632
+ }).run();
633
+
634
+ const MSG_COUNT = 5;
635
+ const REPEATS = 4; // how many times each message is re-indexed
636
+ for (let i = 0; i < MSG_COUNT; i++) {
637
+ db.insert(messages).values({
638
+ id: `msg-atomicity-${i}`,
639
+ conversationId,
640
+ role: 'user',
641
+ content: JSON.stringify([{ type: 'text', text: `Message ${i}: I prefer TypeScript and always follow functional programming patterns in my projects.` }]),
642
+ createdAt: now + i,
643
+ }).run();
644
+ }
645
+
646
+ const config = TEST_CONFIG.memory;
647
+
648
+ // Repeat indexMessageNow REPEATS times for each of MSG_COUNT messages. All
649
+ // calls run sequentially; the test verifies that repeated indexing of the
650
+ // same (messageId, segmentIndex) never produces duplicate segment rows.
651
+ await Promise.all(
652
+ Array.from({ length: REPEATS }, () =>
653
+ Array.from({ length: MSG_COUNT }, (_, i) => {
654
+ const msgId = `msg-atomicity-${i}`;
655
+ return Promise.resolve().then(() =>
656
+ indexMessageNow(
657
+ {
658
+ messageId: msgId,
659
+ conversationId,
660
+ role: 'user',
661
+ content: JSON.stringify([{ type: 'text', text: `Message ${i}: I prefer TypeScript and always follow functional programming patterns in my projects.` }]),
662
+ createdAt: now + i,
663
+ },
664
+ config,
665
+ ),
666
+ );
667
+ }),
668
+ ).flat(),
669
+ );
670
+
671
+ // For every message, count distinct segment IDs — there must be no
672
+ // duplicates regardless of how many indexer calls ran.
673
+ for (let i = 0; i < MSG_COUNT; i++) {
674
+ const msgId = `msg-atomicity-${i}`;
675
+ const segs = db
676
+ .select()
677
+ .from(memorySegments)
678
+ .where(eq(memorySegments.messageId, msgId))
679
+ .all();
680
+
681
+ const segIds = segs.map((s) => s.id);
682
+ const uniqueSegIds = new Set(segIds);
683
+ expect(uniqueSegIds.size).toBe(segIds.length);
684
+ }
685
+ });
686
+
687
+ test('indexer result counts are consistent with actual stored segment counts', async () => {
688
+ // The IndexMessageResult.indexedSegments value returned by indexMessageNow
689
+ // must always match the number of rows stored in memory_segments for that
690
+ // message. Under repeated indexing the stored count stays stable while
691
+ // every result reports the same logical segment count.
692
+ const conversationId = 'conv-count-consistency';
693
+ const messageId = 'msg-count-consistency';
694
+ const text = 'I always prefer concise code reviews and I work in a distributed team across multiple timezones.';
695
+
696
+ seedConversationAndMessage(conversationId, messageId, text);
697
+
698
+ const config = TEST_CONFIG.memory;
699
+
700
+ // Index the same message RUNS times sequentially. The test verifies that
701
+ // the returned indexedSegments count is stable across all runs and matches
702
+ // the number of rows actually stored in the DB.
703
+ const RUNS = 5;
704
+ const results = await Promise.all(
705
+ Array.from({ length: RUNS }, () =>
706
+ Promise.resolve().then(() =>
707
+ indexMessageNow(
708
+ { messageId, conversationId, role: 'user', content: JSON.stringify([{ type: 'text', text }]), createdAt: Date.now() },
709
+ config,
710
+ ),
711
+ ),
712
+ ),
713
+ );
714
+
715
+ const db = getDb();
716
+ const storedSegments = db
717
+ .select()
718
+ .from(memorySegments)
719
+ .where(eq(memorySegments.messageId, messageId))
720
+ .all();
721
+
722
+ // All runs must agree on the segment count.
723
+ const firstCount = results[0].indexedSegments;
724
+ for (const result of results) {
725
+ expect(result.indexedSegments).toBe(firstCount);
726
+ }
727
+
728
+ // Stored count must equal the reported logical count.
729
+ expect(storedSegments.length).toBe(firstCount);
730
+ });
731
+ });
732
+
733
+ // ─────────────────────────────────────────────────────────────────────────────
734
+ // Test suite: memory_items fingerprint uniqueness under race conditions
735
+ // ─────────────────────────────────────────────────────────────────────────────
736
+
737
+ describe('memory_items fingerprint uniqueness under race conditions', () => {
738
+ beforeEach(() => {
739
+ resetTables();
740
+ });
741
+
742
+ test('duplicate inserts with identical fingerprints produce exactly one row', () => {
743
+ // The memory_items table has a unique constraint on (fingerprint, scope_id).
744
+ // Two sequential inserts for the same fingerprint simulate duplicate extractor
745
+ // runs. Only one INSERT must land; the second must be absorbed by ON CONFLICT.
746
+ const db = getDb();
747
+ const now = Date.now();
748
+ const fingerprint = 'fp-race-unique-test-concurrency';
749
+ const scopeId = 'default';
750
+
751
+ // Use raw SQL to replicate what the items-extractor would do when a second
752
+ // run tries to INSERT the same fingerprint that already exists.
753
+ const raw = (db as unknown as { $client: import('bun:sqlite').Database }).$client;
754
+
755
+ raw.run(`
756
+ INSERT INTO memory_items (
757
+ id, kind, subject, statement, status, confidence, importance,
758
+ fingerprint, verification_state, scope_id, first_seen_at, last_seen_at
759
+ ) VALUES (
760
+ 'item-race-1', 'preference', 'code style', 'I prefer tabs over spaces.',
761
+ 'active', 0.8, 0.6, '${fingerprint}', 'user_reported', '${scopeId}',
762
+ ${now}, ${now}
763
+ )
764
+ `);
765
+
766
+ // Second "worker" tries to insert the same fingerprint — must not create a
767
+ // duplicate. INSERT OR IGNORE / ON CONFLICT DO NOTHING is the expected
768
+ // behavior for the unique constraint.
769
+ expect(() => {
770
+ raw.run(`
771
+ INSERT OR IGNORE INTO memory_items (
772
+ id, kind, subject, statement, status, confidence, importance,
773
+ fingerprint, verification_state, scope_id, first_seen_at, last_seen_at
774
+ ) VALUES (
775
+ 'item-race-2', 'preference', 'code style', 'I prefer tabs over spaces.',
776
+ 'active', 0.8, 0.6, '${fingerprint}', 'user_reported', '${scopeId}',
777
+ ${now + 1}, ${now + 1}
778
+ )
779
+ `);
780
+ }).not.toThrow();
781
+
782
+ const rows = db
783
+ .select()
784
+ .from(memoryItems)
785
+ .all()
786
+ .filter((r) => r.fingerprint === fingerprint);
787
+
788
+ // Only the first insert must have landed.
789
+ expect(rows).toHaveLength(1);
790
+ expect(rows[0].id).toBe('item-race-1');
791
+ });
792
+
793
+ test('bare INSERT without IGNORE throws on duplicate fingerprint+scopeId', () => {
794
+ // Verify the DB-level unique constraint is actually enforced so that any code
795
+ // path that accidentally omits ON CONFLICT will fail loudly rather than silently
796
+ // producing inconsistent state.
797
+ const db = getDb();
798
+ const now = Date.now();
799
+ const fingerprint = 'fp-constraint-enforcement-test';
800
+
801
+ const raw = (db as unknown as { $client: import('bun:sqlite').Database }).$client;
802
+
803
+ raw.run(`
804
+ INSERT INTO memory_items (
805
+ id, kind, subject, statement, status, confidence, importance,
806
+ fingerprint, verification_state, scope_id, first_seen_at, last_seen_at
807
+ ) VALUES (
808
+ 'item-constraint-a', 'preference', 'editor', 'I use VS Code.',
809
+ 'active', 0.9, 0.7, '${fingerprint}', 'user_reported', 'default',
810
+ ${now}, ${now}
811
+ )
812
+ `);
813
+
814
+ // A bare INSERT (no ON CONFLICT) for the same fingerprint+scope_id must throw.
815
+ expect(() => {
816
+ raw.run(`
817
+ INSERT INTO memory_items (
818
+ id, kind, subject, statement, status, confidence, importance,
819
+ fingerprint, verification_state, scope_id, first_seen_at, last_seen_at
820
+ ) VALUES (
821
+ 'item-constraint-b', 'preference', 'editor', 'I use VS Code.',
822
+ 'active', 0.9, 0.7, '${fingerprint}', 'user_reported', 'default',
823
+ ${now + 1}, ${now + 1}
824
+ )
825
+ `);
826
+ }).toThrow();
827
+ });
828
+ });