@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
@@ -1,8 +1,9 @@
1
1
  import { and, asc, eq } from 'drizzle-orm';
2
2
  import { v4 as uuid } from 'uuid';
3
- import { getDb } from './db.js';
3
+ import { getDb, getSqlite, rawAll } from './db.js';
4
4
  import { enqueueMemoryJob } from './jobs-store.js';
5
5
  import { memoryItemConflicts, memoryItems } from './schema.js';
6
+ import { clampUnitInterval } from './validation.js';
6
7
 
7
8
  export type MemoryConflictRelationship =
8
9
  | 'contradiction'
@@ -64,48 +65,54 @@ export interface ApplyConflictResolutionInput {
64
65
  }
65
66
 
66
67
  export function createOrUpdatePendingConflict(input: CreatePendingConflictInput): MemoryItemConflict {
67
- const db = getDb();
68
- const now = Date.now();
69
- const scopeId = input.scopeId;
70
- const existing = getPendingConflictByPair(scopeId, input.existingItemId, input.candidateItemId);
68
+ // Wrap in BEGIN IMMEDIATE so the SELECT-then-INSERT is atomic against concurrent
69
+ // writers. Without this, two parallel memory workers could both observe no
70
+ // existing conflict and both attempt to INSERT the same pair, resulting in a
71
+ // duplicate or an unexpected constraint violation.
72
+ return getSqlite().transaction((): MemoryItemConflict => {
73
+ const db = getDb();
74
+ const now = Date.now();
75
+ const scopeId = input.scopeId;
76
+ const existing = getPendingConflictByPair(scopeId, input.existingItemId, input.candidateItemId);
71
77
 
72
- if (existing) {
73
- db.update(memoryItemConflicts)
74
- .set({
75
- relationship: input.relationship,
76
- clarificationQuestion: input.clarificationQuestion !== undefined ? input.clarificationQuestion : existing.clarificationQuestion,
77
- updatedAt: now,
78
- })
79
- .where(eq(memoryItemConflicts.id, existing.id))
80
- .run();
81
- const updated = getConflictById(existing.id);
82
- if (!updated) {
83
- throw new Error(`Failed to reload updated conflict: ${existing.id}`);
78
+ if (existing) {
79
+ db.update(memoryItemConflicts)
80
+ .set({
81
+ relationship: input.relationship,
82
+ clarificationQuestion: input.clarificationQuestion !== undefined ? input.clarificationQuestion : existing.clarificationQuestion,
83
+ updatedAt: now,
84
+ })
85
+ .where(eq(memoryItemConflicts.id, existing.id))
86
+ .run();
87
+ const updated = getConflictById(existing.id);
88
+ if (!updated) {
89
+ throw new Error(`Failed to reload updated conflict: ${existing.id}`);
90
+ }
91
+ return updated;
84
92
  }
85
- return updated;
86
- }
87
93
 
88
- const id = uuid();
89
- db.insert(memoryItemConflicts).values({
90
- id,
91
- scopeId,
92
- existingItemId: input.existingItemId,
93
- candidateItemId: input.candidateItemId,
94
- relationship: input.relationship,
95
- status: 'pending_clarification',
96
- clarificationQuestion: input.clarificationQuestion ?? null,
97
- resolutionNote: null,
98
- lastAskedAt: null,
99
- resolvedAt: null,
100
- createdAt: now,
101
- updatedAt: now,
102
- }).run();
94
+ const id = uuid();
95
+ db.insert(memoryItemConflicts).values({
96
+ id,
97
+ scopeId,
98
+ existingItemId: input.existingItemId,
99
+ candidateItemId: input.candidateItemId,
100
+ relationship: input.relationship,
101
+ status: 'pending_clarification',
102
+ clarificationQuestion: input.clarificationQuestion ?? null,
103
+ resolutionNote: null,
104
+ lastAskedAt: null,
105
+ resolvedAt: null,
106
+ createdAt: now,
107
+ updatedAt: now,
108
+ }).run();
103
109
 
104
- const created = getConflictById(id);
105
- if (!created) {
106
- throw new Error(`Failed to load created conflict: ${id}`);
107
- }
108
- return created;
110
+ const created = getConflictById(id);
111
+ if (!created) {
112
+ throw new Error(`Failed to load created conflict: ${id}`);
113
+ }
114
+ return created;
115
+ }).immediate();
109
116
  }
110
117
 
111
118
  export function getConflictById(conflictId: string): MemoryItemConflict | null {
@@ -155,9 +162,25 @@ export function listPendingConflicts(scopeId: string, limit = 100): MemoryItemCo
155
162
 
156
163
  export function listPendingConflictDetails(scopeId: string, limit = 100): PendingConflictDetail[] {
157
164
  if (limit <= 0) return [];
158
- const db = getDb();
159
- const raw = (db as unknown as { $client: { query: (q: string) => { all: (...params: unknown[]) => unknown[] } } }).$client;
160
- const rows = raw.query(`
165
+ interface ConflictDetailRow {
166
+ id: string;
167
+ scope_id: string;
168
+ existing_item_id: string;
169
+ candidate_item_id: string;
170
+ relationship: string;
171
+ status: MemoryConflictStatus;
172
+ clarification_question: string | null;
173
+ resolution_note: string | null;
174
+ last_asked_at: number | null;
175
+ resolved_at: number | null;
176
+ created_at: number;
177
+ updated_at: number;
178
+ existing_statement: string;
179
+ candidate_statement: string;
180
+ existing_kind: string;
181
+ candidate_kind: string;
182
+ }
183
+ const rows = rawAll<ConflictDetailRow>(`
161
184
  SELECT
162
185
  c.id,
163
186
  c.scope_id,
@@ -182,24 +205,7 @@ export function listPendingConflictDetails(scopeId: string, limit = 100): Pendin
182
205
  AND c.status = 'pending_clarification'
183
206
  ORDER BY c.created_at ASC
184
207
  LIMIT ?
185
- `).all(scopeId, limit) as Array<{
186
- id: string;
187
- scope_id: string;
188
- existing_item_id: string;
189
- candidate_item_id: string;
190
- relationship: string;
191
- status: MemoryConflictStatus;
192
- clarification_question: string | null;
193
- resolution_note: string | null;
194
- last_asked_at: number | null;
195
- resolved_at: number | null;
196
- created_at: number;
197
- updated_at: number;
198
- existing_statement: string;
199
- candidate_statement: string;
200
- existing_kind: string;
201
- candidate_kind: string;
202
- }>;
208
+ `, scopeId, limit);
203
209
 
204
210
  return rows.map((row) => ({
205
211
  id: row.id,
@@ -314,7 +320,7 @@ export function applyConflictResolution(input: ApplyConflictResolutionInput): bo
314
320
  status: 'active',
315
321
  invalidAt: null,
316
322
  lastSeenAt: Math.max(existingItem.lastSeenAt, candidateItem.lastSeenAt, now),
317
- confidence: Math.max(existingItem.confidence, candidateItem.confidence),
323
+ confidence: clampUnitInterval(Math.max(existingItem.confidence, candidateItem.confidence)),
318
324
  })
319
325
  .where(eq(memoryItems.id, existingItem.id))
320
326
  .run();
@@ -1,14 +1,15 @@
1
- import Anthropic from '@anthropic-ai/sdk';
2
1
  import { eq } from 'drizzle-orm';
3
2
  import { getConfig } from '../config/loader.js';
4
3
  import { getLogger } from '../util/logger.js';
5
4
  import { truncate } from '../util/truncate.js';
5
+ import { getConfiguredProvider, createTimeout, extractToolUse, userMessage } from '../providers/provider-send-message.js';
6
6
  import { areStatementsCoherent } from './conflict-intent.js';
7
7
  import { isConflictKindEligible, isStatementConflictEligible } from './conflict-policy.js';
8
8
  import { createOrUpdatePendingConflict } from './conflict-store.js';
9
- import { getDb } from './db.js';
9
+ import { getDb, getSqlite, rawAll } from './db.js';
10
10
  import { enqueueMemoryJob } from './jobs-store.js';
11
11
  import { memoryItems } from './schema.js';
12
+ import { clampUnitInterval } from './validation.js';
12
13
 
13
14
  const log = getLogger('memory-contradiction-checker');
14
15
 
@@ -56,13 +57,14 @@ export async function checkContradictions(newItemId: string): Promise<void> {
56
57
  return;
57
58
  }
58
59
 
59
- const config = getConfig();
60
- const apiKey = config.apiKeys.anthropic ?? process.env.ANTHROPIC_API_KEY;
61
- if (!apiKey) {
62
- log.debug('No Anthropic API key available for contradiction checking');
60
+ const provider = getConfiguredProvider();
61
+ if (!provider) {
62
+ log.debug('Configured provider unavailable for contradiction checking');
63
63
  return;
64
64
  }
65
65
 
66
+ const config = getConfig();
67
+
66
68
  if (!isConflictKindEligible(newItem.kind, config.memory.conflicts)) {
67
69
  log.debug({ newItemId, kind: newItem.kind }, 'Skipping contradiction check — kind not eligible for conflicts');
68
70
  return;
@@ -91,14 +93,14 @@ export async function checkContradictions(newItemId: string): Promise<void> {
91
93
  }
92
94
 
93
95
  try {
94
- const result = await classifyRelationship(apiKey, existing, newItem);
95
- await handleRelationship(result, existing, newItem);
96
- // Only stop when the new item itself is invalidated (update case).
97
- // For contradiction, the old item is invalidated but the new item remains
98
- // active and should continue to be checked against remaining candidates.
99
- // For ambiguous contradiction, we pause retrieval eligibility for the new
100
- // item and ask for clarification on a later turn.
101
- if (result.relationship === 'update' || result.relationship === 'ambiguous_contradiction') break;
96
+ const result = await classifyRelationship(existing, newItem);
97
+ const mutated = handleRelationship(result, existing, newItem);
98
+ // Only stop when the new item itself was actually invalidated (update case)
99
+ // or gated (ambiguous_contradiction). For contradiction, the old item is
100
+ // invalidated but the new item remains active and should continue to be
101
+ // checked against remaining candidates. Skip the break when the transaction
102
+ // detected stale data and performed no mutation.
103
+ if (mutated && (result.relationship === 'update' || result.relationship === 'ambiguous_contradiction')) break;
102
104
  } catch (err) {
103
105
  const message = err instanceof Error ? err.message : String(err);
104
106
  log.warn({ err: message, newItemId, existingId: existing.id }, 'Contradiction classification failed for pair');
@@ -123,8 +125,6 @@ interface MemoryItemRow {
123
125
  * Uses LIKE queries on subject and keyword overlap on statement.
124
126
  */
125
127
  function findSimilarItems(item: MemoryItemRow): MemoryItemRow[] {
126
- const db = getDb();
127
- const raw = (db as unknown as { $client: { query: (q: string) => { all: (...params: unknown[]) => unknown[] } } }).$client;
128
128
 
129
129
  // Extract significant words from subject for LIKE matching
130
130
  const subjectWords = item.subject
@@ -172,7 +172,7 @@ function findSimilarItems(item: MemoryItemRow): MemoryItemRow[] {
172
172
  `;
173
173
 
174
174
  try {
175
- const rows = raw.query(sqlQuery).all(item.kind, item.id, item.scopeId) as Array<{
175
+ interface SimilarItemRow {
176
176
  id: string;
177
177
  kind: string;
178
178
  subject: string;
@@ -182,7 +182,8 @@ function findSimilarItems(item: MemoryItemRow): MemoryItemRow[] {
182
182
  importance: number | null;
183
183
  scope_id: string;
184
184
  last_seen_at: number;
185
- }>;
185
+ }
186
+ const rows = rawAll<SimilarItemRow>(sqlQuery, item.kind, item.id, item.scopeId);
186
187
 
187
188
  return rows.map((row) => ({
188
189
  id: row.id,
@@ -205,25 +206,23 @@ function findSimilarItems(item: MemoryItemRow): MemoryItemRow[] {
205
206
  * Use LLM to classify the relationship between two memory items.
206
207
  */
207
208
  async function classifyRelationship(
208
- apiKey: string,
209
209
  existingItem: MemoryItemRow,
210
210
  newItem: MemoryItemRow,
211
211
  ): Promise<ClassifyResult> {
212
- const client = new Anthropic({ apiKey });
212
+ const provider = getConfiguredProvider()!;
213
213
 
214
- const userMessage = [
214
+ const userContent = [
215
215
  `Subject: ${newItem.subject}`,
216
216
  '',
217
217
  `Old statement: ${existingItem.statement}`,
218
218
  `New statement: ${newItem.statement}`,
219
219
  ].join('\n');
220
220
 
221
- const response = await Promise.race([
222
- client.messages.create({
223
- model: 'claude-haiku-4-5-20251001',
224
- max_tokens: 256,
225
- system: CONTRADICTION_SYSTEM_PROMPT,
226
- tools: [{
221
+ const { signal, cleanup } = createTimeout(CONTRADICTION_LLM_TIMEOUT_MS);
222
+ try {
223
+ const response = await provider.sendMessage(
224
+ [userMessage(userContent)],
225
+ [{
227
226
  name: 'classify_relationship',
228
227
  description: 'Classify the relationship between two memory statements',
229
228
  input_schema: {
@@ -242,109 +241,134 @@ async function classifyRelationship(
242
241
  required: ['relationship', 'explanation'],
243
242
  },
244
243
  }],
245
- tool_choice: { type: 'tool' as const, name: 'classify_relationship' },
246
- messages: [{ role: 'user' as const, content: userMessage }],
247
- }),
248
- new Promise<never>((_, reject) =>
249
- setTimeout(() => reject(new Error('Contradiction check LLM timeout')), CONTRADICTION_LLM_TIMEOUT_MS),
250
- ),
251
- ]);
252
-
253
- const toolBlock = response.content.find((b) => b.type === 'tool_use');
254
- if (!toolBlock || toolBlock.type !== 'tool_use') {
255
- throw new Error('No tool_use block in contradiction check response');
256
- }
244
+ CONTRADICTION_SYSTEM_PROMPT,
245
+ {
246
+ config: {
247
+ modelIntent: 'latency-optimized',
248
+ max_tokens: 256,
249
+ tool_choice: { type: 'tool' as const, name: 'classify_relationship' },
250
+ },
251
+ signal,
252
+ },
253
+ );
254
+ cleanup();
255
+
256
+ const toolBlock = extractToolUse(response);
257
+ if (!toolBlock) {
258
+ throw new Error('No tool_use block in contradiction check response');
259
+ }
257
260
 
258
- const input = toolBlock.input as { relationship?: string; explanation?: string };
259
- const relationship = input.relationship as Relationship;
260
- if (!['contradiction', 'update', 'complement', 'ambiguous_contradiction'].includes(relationship)) {
261
- throw new Error(`Invalid relationship type: ${relationship}`);
262
- }
261
+ const input = toolBlock.input as { relationship?: string; explanation?: string };
262
+ const relationship = input.relationship as Relationship;
263
+ if (!['contradiction', 'update', 'complement', 'ambiguous_contradiction'].includes(relationship)) {
264
+ throw new Error(`Invalid relationship type: ${relationship}`);
265
+ }
263
266
 
264
- return {
265
- relationship,
266
- explanation: truncate(String(input.explanation ?? ''), 500, ''),
267
- };
267
+ return {
268
+ relationship,
269
+ explanation: truncate(String(input.explanation ?? ''), 500, ''),
270
+ };
271
+ } finally {
272
+ cleanup();
273
+ }
268
274
  }
269
275
 
270
276
  /**
271
277
  * Handle the classified relationship between an existing and new memory item.
278
+ *
279
+ * Wrapped in a SQLite transaction so that multi-row mutations (e.g. invalidating
280
+ * the old item AND setting validFrom on the new one) are atomic. The transaction
281
+ * also re-verifies both items are still active before mutating, preventing a
282
+ * TOCTOU race when multiple workers process contradictions concurrently.
272
283
  */
273
- async function handleRelationship(
284
+ function handleRelationship(
274
285
  result: ClassifyResult,
275
286
  existingItem: MemoryItemRow,
276
287
  newItem: MemoryItemRow,
277
- ): Promise<void> {
278
- const db = getDb();
279
- const now = Date.now();
280
-
281
- switch (result.relationship) {
282
- case 'contradiction': {
283
- // Invalidate the old item (don't delete for audit trail), set validFrom on new item
284
- log.info(
285
- { existingId: existingItem.id, newId: newItem.id, explanation: result.explanation },
286
- 'Contradiction detected invalidating old item',
287
- );
288
- db.update(memoryItems)
289
- .set({ invalidAt: now })
290
- .where(eq(memoryItems.id, existingItem.id))
291
- .run();
292
- db.update(memoryItems)
293
- .set({ validFrom: now })
294
- .where(eq(memoryItems.id, newItem.id))
295
- .run();
296
- break;
297
- }
298
- case 'update': {
299
- // Merge info — update old item's statement, bump lastSeenAt
300
- log.debug(
301
- { existingId: existingItem.id, newId: newItem.id, explanation: result.explanation },
302
- 'Update detected — merging into existing item',
303
- );
304
- db.update(memoryItems)
305
- .set({
306
- statement: newItem.statement,
307
- lastSeenAt: Math.max(existingItem.lastSeenAt, newItem.lastSeenAt),
308
- confidence: Math.max(existingItem.confidence, newItem.confidence),
309
- })
310
- .where(eq(memoryItems.id, existingItem.id))
311
- .run();
312
- // Re-embed the existing item so its vector matches the updated statement
313
- enqueueMemoryJob('embed_item', { itemId: existingItem.id });
314
- // Invalidate the new item since its content has been merged into the existing one
315
- db.update(memoryItems)
316
- .set({ invalidAt: now })
317
- .where(eq(memoryItems.id, newItem.id))
318
- .run();
319
- break;
288
+ ): boolean {
289
+ if (result.relationship === 'complement') {
290
+ log.debug(
291
+ { existingId: existingItem.id, newId: newItem.id, explanation: result.explanation },
292
+ 'Complement detected — keeping both items',
293
+ );
294
+ return false;
295
+ }
296
+
297
+ return getSqlite().transaction(() => {
298
+ const db = getDb();
299
+ const now = Date.now();
300
+
301
+ // Re-check both items inside the transaction to guard against concurrent mutations
302
+ const freshExisting = db.select().from(memoryItems).where(eq(memoryItems.id, existingItem.id)).get();
303
+ const freshNew = db.select().from(memoryItems).where(eq(memoryItems.id, newItem.id)).get();
304
+
305
+ if (!freshExisting || freshExisting.status !== 'active' || freshExisting.invalidAt != null) {
306
+ log.debug({ existingId: existingItem.id }, 'Existing item no longer active — skipping');
307
+ return false;
320
308
  }
321
- case 'complement': {
322
- // Both items can coexist no changes needed
323
- log.debug(
324
- { existingId: existingItem.id, newId: newItem.id, explanation: result.explanation },
325
- 'Complement detected — keeping both items',
326
- );
327
- break;
309
+ if (!freshNew || (freshNew.status !== 'active' && result.relationship !== 'ambiguous_contradiction') || freshNew.invalidAt != null) {
310
+ log.debug({ newId: newItem.id }, 'New item no longer active — skipping');
311
+ return false;
328
312
  }
329
- case 'ambiguous_contradiction': {
330
- log.info(
331
- { existingId: existingItem.id, newId: newItem.id, explanation: result.explanation },
332
- 'Ambiguous contradiction detected — gating candidate pending clarification',
333
- );
334
- db.update(memoryItems)
335
- .set({ status: 'pending_clarification' })
336
- .where(eq(memoryItems.id, newItem.id))
337
- .run();
338
- createOrUpdatePendingConflict({
339
- scopeId: newItem.scopeId,
340
- existingItemId: existingItem.id,
341
- candidateItemId: newItem.id,
342
- relationship: 'ambiguous_contradiction',
343
- clarificationQuestion: buildClarificationQuestion(existingItem.statement, newItem.statement),
344
- });
345
- break;
313
+
314
+ switch (result.relationship) {
315
+ case 'contradiction': {
316
+ log.info(
317
+ { existingId: existingItem.id, newId: newItem.id, explanation: result.explanation },
318
+ 'Contradiction detected — invalidating old item',
319
+ );
320
+ db.update(memoryItems)
321
+ .set({ invalidAt: now })
322
+ .where(eq(memoryItems.id, existingItem.id))
323
+ .run();
324
+ db.update(memoryItems)
325
+ .set({ validFrom: now })
326
+ .where(eq(memoryItems.id, newItem.id))
327
+ .run();
328
+ return true;
329
+ }
330
+ case 'update': {
331
+ log.debug(
332
+ { existingId: existingItem.id, newId: newItem.id, explanation: result.explanation },
333
+ 'Update detected — merging into existing item',
334
+ );
335
+ db.update(memoryItems)
336
+ .set({
337
+ statement: newItem.statement,
338
+ lastSeenAt: Math.max(freshExisting.lastSeenAt, freshNew!.lastSeenAt),
339
+ confidence: clampUnitInterval(Math.max(freshExisting.confidence, freshNew!.confidence)),
340
+ })
341
+ .where(eq(memoryItems.id, existingItem.id))
342
+ .run();
343
+ enqueueMemoryJob('embed_item', { itemId: existingItem.id });
344
+ db.update(memoryItems)
345
+ .set({ invalidAt: now })
346
+ .where(eq(memoryItems.id, newItem.id))
347
+ .run();
348
+ return true;
349
+ }
350
+ case 'ambiguous_contradiction': {
351
+ log.info(
352
+ { existingId: existingItem.id, newId: newItem.id, explanation: result.explanation },
353
+ 'Ambiguous contradiction detected — gating candidate pending clarification',
354
+ );
355
+ db.update(memoryItems)
356
+ .set({ status: 'pending_clarification' })
357
+ .where(eq(memoryItems.id, newItem.id))
358
+ .run();
359
+ createOrUpdatePendingConflict({
360
+ scopeId: newItem.scopeId,
361
+ existingItemId: existingItem.id,
362
+ candidateItemId: newItem.id,
363
+ relationship: 'ambiguous_contradiction',
364
+ clarificationQuestion: buildClarificationQuestion(existingItem.statement, newItem.statement),
365
+ });
366
+ return true;
367
+ }
368
+ default:
369
+ return false;
346
370
  }
347
- }
371
+ }).immediate();
348
372
  }
349
373
 
350
374
  function escapeSqlLike(s: string): string {