@vellumai/assistant 0.5.6 → 0.5.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (442) hide show
  1. package/.env.example +16 -2
  2. package/ARCHITECTURE.md +6 -75
  3. package/Dockerfile +3 -2
  4. package/README.md +0 -2
  5. package/bun.lock +0 -414
  6. package/docker-entrypoint.sh +9 -0
  7. package/docs/architecture/keychain-broker.md +45 -240
  8. package/docs/architecture/memory.md +13 -11
  9. package/docs/architecture/security.md +0 -17
  10. package/docs/credential-execution-service.md +2 -2
  11. package/node_modules/@vellumai/ces-contracts/package.json +1 -0
  12. package/node_modules/@vellumai/ces-contracts/src/error.ts +1 -1
  13. package/node_modules/@vellumai/ces-contracts/src/grants.ts +1 -1
  14. package/node_modules/@vellumai/ces-contracts/src/handles.ts +1 -1
  15. package/node_modules/@vellumai/ces-contracts/src/index.ts +1 -1
  16. package/node_modules/@vellumai/ces-contracts/src/rpc.ts +120 -1
  17. package/node_modules/@vellumai/credential-storage/package.json +1 -0
  18. package/node_modules/@vellumai/egress-proxy/package.json +1 -0
  19. package/package.json +2 -3
  20. package/src/__tests__/actor-token-service.test.ts +0 -114
  21. package/src/__tests__/approval-cascade.test.ts +0 -1
  22. package/src/__tests__/assistant-feature-flags-integration.test.ts +30 -29
  23. package/src/__tests__/browser-fill-credential.test.ts +1 -1
  24. package/src/__tests__/browser-skill-endstate.test.ts +6 -5
  25. package/src/__tests__/btw-routes.test.ts +0 -39
  26. package/src/__tests__/call-controller.test.ts +0 -1
  27. package/src/__tests__/call-domain.test.ts +0 -128
  28. package/src/__tests__/ces-rpc-credential-backend.test.ts +199 -0
  29. package/src/__tests__/ces-startup-timeout.test.ts +40 -0
  30. package/src/__tests__/channel-approval-routes.test.ts +0 -5
  31. package/src/__tests__/channel-readiness-service.test.ts +1 -60
  32. package/src/__tests__/checker.test.ts +4 -2
  33. package/src/__tests__/cli-command-risk-guard.test.ts +112 -0
  34. package/src/__tests__/config-schema-cmd.test.ts +0 -2
  35. package/src/__tests__/config-schema.test.ts +3 -1
  36. package/src/__tests__/conversation-abort-tool-results.test.ts +0 -1
  37. package/src/__tests__/conversation-agent-loop-overflow.test.ts +0 -2
  38. package/src/__tests__/conversation-agent-loop.test.ts +2 -4
  39. package/src/__tests__/conversation-attention-telegram.test.ts +0 -5
  40. package/src/__tests__/conversation-confirmation-signals.test.ts +0 -1
  41. package/src/__tests__/conversation-error.test.ts +15 -1
  42. package/src/__tests__/conversation-init.benchmark.test.ts +0 -2
  43. package/src/__tests__/conversation-messaging-secret-redirect.test.ts +1 -1
  44. package/src/__tests__/conversation-pre-run-repair.test.ts +0 -1
  45. package/src/__tests__/conversation-provider-retry-repair.test.ts +0 -1
  46. package/src/__tests__/conversation-queue.test.ts +0 -1
  47. package/src/__tests__/conversation-skill-tools.test.ts +0 -54
  48. package/src/__tests__/conversation-slash-queue.test.ts +0 -1
  49. package/src/__tests__/conversation-slash-unknown.test.ts +0 -1
  50. package/src/__tests__/conversation-title-service.test.ts +87 -0
  51. package/src/__tests__/conversation-workspace-injection.test.ts +0 -1
  52. package/src/__tests__/conversation-workspace-tool-tracking.test.ts +0 -1
  53. package/src/__tests__/credential-execution-client.test.ts +5 -2
  54. package/src/__tests__/credential-execution-feature-gates.test.ts +59 -30
  55. package/src/__tests__/credential-execution-managed-contract.test.ts +35 -20
  56. package/src/__tests__/credential-security-e2e.test.ts +1 -67
  57. package/src/__tests__/credential-security-invariants.test.ts +6 -50
  58. package/src/__tests__/credentials-cli.test.ts +82 -3
  59. package/src/__tests__/daemon-credential-client.test.ts +123 -0
  60. package/src/__tests__/db-migration-rollback.test.ts +2015 -1
  61. package/src/__tests__/deterministic-verification-control-plane.test.ts +1 -0
  62. package/src/__tests__/docker-signing-key-bootstrap.test.ts +34 -143
  63. package/src/__tests__/dynamic-skill-workflow-prompt.test.ts +6 -4
  64. package/src/__tests__/gateway-client-managed-outbound.test.ts +79 -1
  65. package/src/__tests__/guardian-routing-state.test.ts +0 -5
  66. package/src/__tests__/host-shell-tool.test.ts +6 -7
  67. package/src/__tests__/http-user-message-parity.test.ts +3 -103
  68. package/src/__tests__/inbound-invite-redemption.test.ts +0 -4
  69. package/src/__tests__/inline-skill-load-permissions.test.ts +6 -8
  70. package/src/__tests__/intent-routing.test.ts +0 -13
  71. package/src/__tests__/jobs-store-qdrant-breaker.test.ts +178 -0
  72. package/src/__tests__/journal-context.test.ts +335 -0
  73. package/src/__tests__/keychain-broker-client.test.ts +161 -22
  74. package/src/__tests__/memory-context-benchmark.benchmark.test.ts +0 -3
  75. package/src/__tests__/memory-jobs-worker-backoff.test.ts +150 -0
  76. package/src/__tests__/memory-lifecycle-e2e.test.ts +70 -25
  77. package/src/__tests__/memory-recall-quality.test.ts +48 -17
  78. package/src/__tests__/memory-regressions.test.ts +408 -363
  79. package/src/__tests__/memory-retrieval.benchmark.test.ts +0 -3
  80. package/src/__tests__/migration-export-http.test.ts +2 -2
  81. package/src/__tests__/migration-import-commit-http.test.ts +2 -2
  82. package/src/__tests__/migration-import-preflight-http.test.ts +2 -2
  83. package/src/__tests__/migration-validate-http.test.ts +2 -2
  84. package/src/__tests__/non-member-access-request.test.ts +2 -7
  85. package/src/__tests__/notification-decision-fallback.test.ts +4 -0
  86. package/src/__tests__/notification-decision-identity.test.ts +4 -0
  87. package/src/__tests__/notification-decision-strategy.test.ts +71 -0
  88. package/src/__tests__/oauth-cli.test.ts +5 -1
  89. package/src/__tests__/permission-types.test.ts +1 -0
  90. package/src/__tests__/provider-commit-message-generator.test.ts +0 -37
  91. package/src/__tests__/provider-error-scenarios.test.ts +0 -267
  92. package/src/__tests__/provider-managed-proxy-integration.test.ts +5 -6
  93. package/src/__tests__/provider-streaming.benchmark.test.ts +2 -81
  94. package/src/__tests__/qdrant-manager.test.ts +28 -2
  95. package/src/__tests__/registry.test.ts +0 -6
  96. package/src/__tests__/relay-server.test.ts +1 -2
  97. package/src/__tests__/runtime-attachment-metadata.test.ts +0 -4
  98. package/src/__tests__/script-proxy-injection-runtime.test.ts +1 -1
  99. package/src/__tests__/secret-onetime-send.test.ts +1 -1
  100. package/src/__tests__/secret-routes-managed-proxy.test.ts +0 -4
  101. package/src/__tests__/secure-keys.test.ts +95 -272
  102. package/src/__tests__/shell-identity.test.ts +96 -6
  103. package/src/__tests__/skill-feature-flags-integration.test.ts +22 -14
  104. package/src/__tests__/skill-feature-flags.test.ts +46 -45
  105. package/src/__tests__/skill-load-feature-flag.test.ts +7 -10
  106. package/src/__tests__/skill-load-inline-command.test.ts +8 -12
  107. package/src/__tests__/skill-load-inline-includes.test.ts +6 -10
  108. package/src/__tests__/skill-load-tool.test.ts +0 -2
  109. package/src/__tests__/skill-memory.test.ts +17 -3
  110. package/src/__tests__/skill-projection-feature-flag.test.ts +33 -29
  111. package/src/__tests__/skills.test.ts +0 -2
  112. package/src/__tests__/slack-inbound-verification.test.ts +0 -4
  113. package/src/__tests__/stale-approval-dedup.test.ts +171 -0
  114. package/src/__tests__/stt-hints.test.ts +437 -0
  115. package/src/__tests__/suggestion-routes.test.ts +1 -32
  116. package/src/__tests__/system-prompt.test.ts +0 -1
  117. package/src/__tests__/task-memory-cleanup.test.ts +14 -0
  118. package/src/__tests__/tool-executor-shell-integration.test.ts +5 -3
  119. package/src/__tests__/trusted-contact-lifecycle-notifications.test.ts +0 -5
  120. package/src/__tests__/trusted-contact-multichannel.test.ts +0 -4
  121. package/src/__tests__/twilio-routes-twiml.test.ts +139 -1
  122. package/src/__tests__/update-bulletin.test.ts +0 -2
  123. package/src/__tests__/vellum-self-knowledge-inline-command.test.ts +6 -9
  124. package/src/__tests__/voice-quality.test.ts +58 -0
  125. package/src/__tests__/voice-scoped-grant-consumer.test.ts +0 -7
  126. package/src/__tests__/workspace-migration-015-migrate-credentials-to-keychain.test.ts +252 -0
  127. package/src/__tests__/workspace-migration-016-migrate-credentials-from-keychain.test.ts +220 -0
  128. package/src/__tests__/workspace-migration-down-functions.test.ts +1009 -0
  129. package/src/__tests__/workspace-migrations-runner.test.ts +114 -0
  130. package/src/acp/agent-process.ts +9 -1
  131. package/src/agent/loop.ts +1 -1
  132. package/src/approvals/guardian-request-resolvers.ts +164 -38
  133. package/src/calls/__tests__/tts-text-sanitizer.test.ts +254 -0
  134. package/src/calls/audio-store.test.ts +97 -0
  135. package/src/calls/audio-store.ts +205 -0
  136. package/src/calls/call-controller.ts +90 -8
  137. package/src/calls/call-domain.ts +3 -0
  138. package/src/calls/call-store.ts +10 -3
  139. package/src/calls/fish-audio-client.ts +129 -0
  140. package/src/calls/relay-server.ts +27 -0
  141. package/src/calls/stt-hints.ts +189 -0
  142. package/src/calls/tts-text-sanitizer.ts +61 -0
  143. package/src/calls/twilio-routes.ts +34 -5
  144. package/src/calls/types.ts +1 -0
  145. package/src/calls/voice-ingress-preflight.ts +0 -42
  146. package/src/calls/voice-quality.ts +38 -5
  147. package/src/calls/voice-session-bridge.ts +7 -12
  148. package/src/cli/commands/avatar.ts +2 -2
  149. package/src/cli/commands/config.ts +1 -4
  150. package/src/cli/commands/credentials.ts +128 -82
  151. package/src/cli/commands/doctor.ts +2 -2
  152. package/src/cli/commands/keys.ts +7 -7
  153. package/src/cli/commands/memory.ts +1 -1
  154. package/src/cli/commands/oauth/connections.ts +11 -29
  155. package/src/cli/commands/oauth/index.ts +7 -0
  156. package/src/cli/commands/oauth/platform.ts +525 -0
  157. package/src/cli/commands/platform.ts +3 -3
  158. package/src/cli/lib/daemon-credential-client.ts +284 -0
  159. package/src/cli.ts +1 -1
  160. package/src/config/assistant-feature-flags.ts +186 -5
  161. package/src/config/bundled-skills/AGENTS.md +34 -0
  162. package/src/config/bundled-skills/acp/SKILL.md +10 -0
  163. package/src/config/bundled-skills/app-builder/SKILL.md +0 -4
  164. package/src/config/bundled-skills/messaging/SKILL.md +5 -5
  165. package/src/config/bundled-skills/messaging/tools/messaging-analyze-style.ts +2 -2
  166. package/src/config/bundled-skills/phone-calls/TOOLS.json +4 -0
  167. package/src/config/bundled-skills/playbooks/tools/playbook-create.ts +1 -0
  168. package/src/config/bundled-skills/playbooks/tools/playbook-update.ts +1 -0
  169. package/src/config/bundled-skills/settings/SKILL.md +15 -2
  170. package/src/config/bundled-skills/settings/TOOLS.json +47 -2
  171. package/src/config/bundled-skills/settings/tools/avatar-remove.ts +59 -0
  172. package/src/config/bundled-skills/settings/tools/avatar-update.ts +80 -0
  173. package/src/config/bundled-skills/settings/tools/voice-config-update.ts +42 -0
  174. package/src/config/bundled-skills/slack/SKILL.md +1 -1
  175. package/src/config/bundled-tool-registry.ts +5 -11
  176. package/src/config/defaults.ts +0 -2
  177. package/src/config/env-registry.ts +5 -5
  178. package/src/config/env.ts +21 -14
  179. package/src/config/feature-flag-registry.json +49 -9
  180. package/src/config/loader.ts +106 -42
  181. package/src/config/schema.ts +9 -29
  182. package/src/config/schemas/calls.ts +30 -0
  183. package/src/config/schemas/fish-audio.ts +39 -0
  184. package/src/config/schemas/inference.ts +2 -2
  185. package/src/config/schemas/journal.ts +16 -0
  186. package/src/config/schemas/memory-processing.ts +2 -2
  187. package/src/config/schemas/security.ts +0 -4
  188. package/src/config/types.ts +1 -1
  189. package/src/contacts/contact-store.ts +39 -0
  190. package/src/contacts/types.ts +2 -0
  191. package/src/credential-execution/approval-bridge.ts +1 -0
  192. package/src/credential-execution/executable-discovery.ts +28 -4
  193. package/src/credential-execution/feature-gates.ts +16 -0
  194. package/src/credential-execution/process-manager.ts +38 -0
  195. package/src/credential-execution/startup-timeout.ts +36 -0
  196. package/src/daemon/approval-generators.ts +3 -9
  197. package/src/daemon/assistant-attachments.ts +9 -0
  198. package/src/daemon/config-watcher.ts +5 -0
  199. package/src/daemon/conversation-error.ts +13 -1
  200. package/src/daemon/conversation-memory.ts +1 -2
  201. package/src/daemon/conversation-process.ts +18 -1
  202. package/src/daemon/conversation-surfaces.ts +30 -1
  203. package/src/daemon/conversation-tool-setup.ts +0 -105
  204. package/src/daemon/conversation.ts +21 -1
  205. package/src/daemon/guardian-action-generators.ts +3 -9
  206. package/src/daemon/handlers/config-vercel.ts +92 -0
  207. package/src/daemon/handlers/skills.ts +2 -15
  208. package/src/daemon/install-symlink.ts +195 -0
  209. package/src/daemon/lifecycle.ts +234 -51
  210. package/src/daemon/message-types/conversations.ts +4 -4
  211. package/src/daemon/message-types/diagnostics.ts +3 -22
  212. package/src/daemon/message-types/messages.ts +0 -2
  213. package/src/daemon/message-types/upgrades.ts +8 -0
  214. package/src/daemon/server.ts +32 -95
  215. package/src/events/domain-events.ts +2 -1
  216. package/src/inbound/platform-callback-registration.ts +3 -3
  217. package/src/instrument.ts +8 -5
  218. package/src/memory/app-store.ts +31 -0
  219. package/src/memory/conversation-title-service.ts +50 -1
  220. package/src/memory/db-init.ts +16 -0
  221. package/src/memory/indexer.ts +19 -10
  222. package/src/memory/items-extractor.ts +328 -321
  223. package/src/memory/job-handlers/conversation-starters.ts +4 -1
  224. package/src/memory/job-handlers/summarization.ts +26 -16
  225. package/src/memory/jobs-store.ts +63 -6
  226. package/src/memory/jobs-worker.ts +31 -7
  227. package/src/memory/journal-memory.ts +214 -0
  228. package/src/memory/migrations/001-job-deferrals.ts +19 -0
  229. package/src/memory/migrations/004-entity-relation-dedup.ts +10 -0
  230. package/src/memory/migrations/005-fingerprint-scope-unique.ts +76 -0
  231. package/src/memory/migrations/006-scope-salted-fingerprints.ts +50 -0
  232. package/src/memory/migrations/007-assistant-id-to-self.ts +10 -0
  233. package/src/memory/migrations/008-remove-assistant-id-columns.ts +34 -0
  234. package/src/memory/migrations/009-llm-usage-events-drop-assistant-id.ts +26 -0
  235. package/src/memory/migrations/014-backfill-inbox-thread-state.ts +10 -0
  236. package/src/memory/migrations/015-drop-active-search-index.ts +17 -0
  237. package/src/memory/migrations/019-notification-tables-schema-migration.ts +12 -0
  238. package/src/memory/migrations/020-rename-macos-ios-channel-to-vellum.ts +121 -0
  239. package/src/memory/migrations/024-embedding-vector-blob.ts +74 -0
  240. package/src/memory/migrations/026a-embeddings-nullable-vector-json.ts +82 -0
  241. package/src/memory/migrations/036-normalize-phone-identities.ts +11 -0
  242. package/src/memory/migrations/116-messages-fts.ts +106 -1
  243. package/src/memory/migrations/126-backfill-guardian-principal-id.ts +52 -0
  244. package/src/memory/migrations/127-guardian-principal-id-not-null.ts +77 -0
  245. package/src/memory/migrations/134-contacts-notes-column.ts +13 -0
  246. package/src/memory/migrations/135-backfill-contact-interaction-stats.ts +20 -0
  247. package/src/memory/migrations/136-drop-assistant-id-columns.ts +52 -0
  248. package/src/memory/migrations/140-backfill-usage-cache-accounting.ts +13 -0
  249. package/src/memory/migrations/141-rename-verification-table.ts +54 -0
  250. package/src/memory/migrations/142-rename-verification-session-id-column.ts +25 -0
  251. package/src/memory/migrations/143-rename-guardian-verification-values.ts +35 -0
  252. package/src/memory/migrations/144-rename-voice-to-phone.ts +136 -0
  253. package/src/memory/migrations/145-drop-accounts-table.ts +32 -0
  254. package/src/memory/migrations/147-migrate-reminders-to-schedules.ts +14 -1
  255. package/src/memory/migrations/148-drop-reminders-table.ts +35 -1
  256. package/src/memory/migrations/150-oauth-apps-client-secret-path.ts +69 -1
  257. package/src/memory/migrations/162-guardian-timestamps-epoch-ms.ts +290 -0
  258. package/src/memory/migrations/169-rename-gmail-provider-key-to-google.ts +51 -1
  259. package/src/memory/migrations/174-rename-thread-starters-table.ts +47 -1
  260. package/src/memory/migrations/176-drop-capability-card-state.ts +13 -0
  261. package/src/memory/migrations/180-backfill-inline-attachments-to-disk.ts +16 -0
  262. package/src/memory/migrations/181-rename-thread-starters-checkpoints.ts +28 -1
  263. package/src/memory/migrations/190-call-session-skip-disclosure.ts +15 -0
  264. package/src/memory/migrations/191-backfill-audio-attachment-mime-types.ts +64 -0
  265. package/src/memory/migrations/192-contacts-user-file-column.ts +15 -0
  266. package/src/memory/migrations/193-add-source-type-columns.ts +81 -0
  267. package/src/memory/migrations/index.ts +5 -0
  268. package/src/memory/migrations/registry.ts +98 -0
  269. package/src/memory/migrations/validate-migration-state.ts +137 -11
  270. package/src/memory/qdrant-circuit-breaker.ts +9 -0
  271. package/src/memory/qdrant-manager.ts +64 -7
  272. package/src/memory/retriever.test.ts +37 -25
  273. package/src/memory/retriever.ts +24 -49
  274. package/src/memory/schema/calls.ts +1 -0
  275. package/src/memory/schema/contacts.ts +1 -0
  276. package/src/memory/schema/memory-core.ts +2 -0
  277. package/src/memory/search/formatting.ts +7 -44
  278. package/src/memory/search/staleness.ts +4 -0
  279. package/src/memory/search/tier-classifier.ts +10 -2
  280. package/src/memory/search/types.ts +2 -5
  281. package/src/memory/task-memory-cleanup.ts +4 -3
  282. package/src/notifications/adapters/slack.ts +168 -6
  283. package/src/notifications/broadcaster.ts +1 -0
  284. package/src/notifications/copy-composer.ts +59 -2
  285. package/src/notifications/decision-engine.ts +4 -1
  286. package/src/notifications/signal.ts +2 -0
  287. package/src/notifications/types.ts +2 -0
  288. package/src/oauth/connection-resolver.ts +6 -4
  289. package/src/permissions/checker.ts +0 -38
  290. package/src/permissions/shell-identity.ts +76 -22
  291. package/src/permissions/types.ts +4 -2
  292. package/src/platform/client.ts +35 -7
  293. package/src/prompts/journal-context.ts +133 -0
  294. package/src/prompts/persona-resolver.ts +194 -0
  295. package/src/prompts/system-prompt.ts +44 -4
  296. package/src/prompts/templates/SOUL.md +10 -0
  297. package/src/prompts/templates/users/default.md +1 -0
  298. package/src/providers/provider-send-message.ts +3 -32
  299. package/src/providers/registry.ts +29 -179
  300. package/src/providers/types.ts +1 -1
  301. package/src/runtime/access-request-helper.ts +4 -0
  302. package/src/runtime/auth/__tests__/credential-service.test.ts +0 -1
  303. package/src/runtime/auth/__tests__/external-assistant-id.test.ts +13 -68
  304. package/src/runtime/auth/__tests__/guard-tests.test.ts +9 -50
  305. package/src/runtime/auth/external-assistant-id.ts +13 -59
  306. package/src/runtime/auth/route-policy.ts +17 -1
  307. package/src/runtime/auth/token-service.ts +43 -138
  308. package/src/runtime/channel-readiness-service.ts +1 -16
  309. package/src/runtime/gateway-client.ts +47 -4
  310. package/src/runtime/guardian-decision-types.ts +45 -4
  311. package/src/runtime/http-server.ts +31 -3
  312. package/src/runtime/middleware/error-handler.ts +1 -9
  313. package/src/runtime/routes/access-request-decision.ts +2 -2
  314. package/src/runtime/routes/app-management-routes.ts +2 -1
  315. package/src/runtime/routes/approval-strategies/guardian-callback-strategy.ts +219 -30
  316. package/src/runtime/routes/approval-strategies/guardian-text-engine-strategy.ts +37 -14
  317. package/src/runtime/routes/audio-routes.ts +40 -0
  318. package/src/runtime/routes/btw-routes.ts +0 -17
  319. package/src/runtime/routes/channel-readiness-routes.ts +9 -4
  320. package/src/runtime/routes/conversation-query-routes.ts +63 -1
  321. package/src/runtime/routes/conversation-routes.ts +4 -44
  322. package/src/runtime/routes/debug-routes.ts +12 -9
  323. package/src/runtime/routes/diagnostics-routes.ts +1 -477
  324. package/src/runtime/routes/guardian-approval-interception.ts +168 -11
  325. package/src/runtime/routes/guardian-approval-prompt.ts +6 -1
  326. package/src/runtime/routes/guardian-approval-reply-helpers.ts +103 -21
  327. package/src/runtime/routes/identity-routes.ts +19 -30
  328. package/src/runtime/routes/inbound-message-handler.ts +31 -1
  329. package/src/runtime/routes/inbound-stages/acl-enforcement.ts +64 -5
  330. package/src/runtime/routes/inbound-stages/background-dispatch.ts +52 -40
  331. package/src/runtime/routes/inbound-stages/secret-ingress-check.ts +4 -33
  332. package/src/runtime/routes/inbound-stages/transcribe-audio.test.ts +1 -1
  333. package/src/runtime/routes/integrations/twilio.ts +52 -10
  334. package/src/runtime/routes/integrations/vercel.ts +89 -0
  335. package/src/runtime/routes/log-export-routes.ts +5 -0
  336. package/src/runtime/routes/memory-item-routes.test.ts +3 -3
  337. package/src/runtime/routes/memory-item-routes.ts +46 -14
  338. package/src/runtime/routes/migration-rollback-routes.ts +209 -0
  339. package/src/runtime/routes/migration-routes.ts +17 -1
  340. package/src/runtime/routes/notification-routes.ts +58 -0
  341. package/src/runtime/routes/schedule-routes.ts +65 -0
  342. package/src/runtime/routes/secret-routes.ts +141 -10
  343. package/src/runtime/routes/settings-routes.ts +41 -1
  344. package/src/runtime/routes/tts-routes.ts +96 -0
  345. package/src/runtime/routes/upgrade-broadcast-routes.ts +26 -2
  346. package/src/runtime/routes/workspace-commit-routes.ts +62 -0
  347. package/src/runtime/routes/workspace-routes.test.ts +22 -1
  348. package/src/runtime/routes/workspace-routes.ts +1 -1
  349. package/src/runtime/routes/workspace-utils.ts +86 -2
  350. package/src/security/ces-credential-client.ts +75 -29
  351. package/src/security/ces-rpc-credential-backend.ts +86 -0
  352. package/src/security/credential-backend.ts +22 -92
  353. package/src/security/keychain-broker-client.ts +10 -2
  354. package/src/security/secure-keys.ts +113 -115
  355. package/src/skills/catalog-install.ts +6 -32
  356. package/src/skills/skill-memory.ts +1 -0
  357. package/src/subagent/manager.ts +2 -5
  358. package/src/telemetry/usage-telemetry-reporter.ts +4 -2
  359. package/src/tools/acp/spawn.ts +78 -1
  360. package/src/tools/calls/call-start.ts +1 -0
  361. package/src/tools/credentials/vault.ts +5 -3
  362. package/src/tools/executor.ts +0 -4
  363. package/src/tools/memory/definitions.ts +3 -2
  364. package/src/tools/memory/handlers.ts +10 -7
  365. package/src/tools/network/script-proxy/session-manager.ts +19 -4
  366. package/src/tools/network/web-fetch.ts +3 -1
  367. package/src/tools/skills/execute.ts +1 -1
  368. package/src/tools/terminal/safe-env.ts +1 -0
  369. package/src/tools/types.ts +0 -8
  370. package/src/util/browser.ts +15 -0
  371. package/src/util/errors.ts +0 -12
  372. package/src/util/platform.ts +4 -51
  373. package/src/workspace/git-service.ts +5 -2
  374. package/src/workspace/migrations/001-avatar-rename.ts +15 -0
  375. package/src/workspace/migrations/003-seed-device-id.ts +17 -1
  376. package/src/workspace/migrations/004-extract-collect-usage-data.ts +33 -0
  377. package/src/workspace/migrations/005-add-send-diagnostics.ts +3 -0
  378. package/src/workspace/migrations/006-services-config.ts +49 -0
  379. package/src/workspace/migrations/007-web-search-provider-rename.ts +27 -0
  380. package/src/workspace/migrations/008-voice-timeout-and-max-steps.ts +3 -0
  381. package/src/workspace/migrations/009-backfill-conversation-disk-view.ts +4 -0
  382. package/src/workspace/migrations/010-app-dir-rename.ts +78 -0
  383. package/src/workspace/migrations/011-backfill-installation-id.ts +11 -0
  384. package/src/workspace/migrations/012-rename-conversation-disk-view-dirs.ts +44 -0
  385. package/src/workspace/migrations/013-repair-conversation-disk-view.ts +5 -0
  386. package/src/workspace/migrations/015-migrate-credentials-to-keychain.ts +153 -0
  387. package/src/workspace/migrations/016-extract-feature-flags-to-protected.ts +156 -0
  388. package/src/workspace/migrations/016-migrate-credentials-from-keychain.ts +150 -0
  389. package/src/workspace/migrations/017-seed-persona-dirs.ts +96 -0
  390. package/src/workspace/migrations/018-rekey-compound-credential-keys.ts +184 -0
  391. package/src/workspace/migrations/019-scope-journal-to-guardian.ts +103 -0
  392. package/src/workspace/migrations/migrate-to-workspace-volume.ts +27 -5
  393. package/src/workspace/migrations/registry.ts +12 -0
  394. package/src/workspace/migrations/runner.ts +106 -2
  395. package/src/workspace/migrations/types.ts +4 -0
  396. package/src/workspace/provider-commit-message-generator.ts +12 -21
  397. package/src/__tests__/claude-code-skill-regression.test.ts +0 -206
  398. package/src/__tests__/claude-code-tool-profiles.test.ts +0 -99
  399. package/src/__tests__/diagnostics-export.test.ts +0 -288
  400. package/src/__tests__/local-gateway-health.test.ts +0 -209
  401. package/src/__tests__/provider-fail-open-selection.test.ts +0 -271
  402. package/src/__tests__/provider-failover-actual-provider.test.ts +0 -66
  403. package/src/__tests__/secret-ingress-handler.test.ts +0 -120
  404. package/src/__tests__/swarm-conversation-integration.test.ts +0 -358
  405. package/src/__tests__/swarm-dag-pathological.test.ts +0 -547
  406. package/src/__tests__/swarm-orchestrator.test.ts +0 -463
  407. package/src/__tests__/swarm-plan-validator.test.ts +0 -384
  408. package/src/__tests__/swarm-recursion.test.ts +0 -197
  409. package/src/__tests__/swarm-router-planner.test.ts +0 -234
  410. package/src/__tests__/swarm-tool.test.ts +0 -185
  411. package/src/__tests__/swarm-worker-backend.test.ts +0 -144
  412. package/src/__tests__/swarm-worker-runner.test.ts +0 -288
  413. package/src/commands/__tests__/cc-command-registry.test.ts +0 -396
  414. package/src/commands/cc-command-registry.ts +0 -248
  415. package/src/config/bundled-skills/claude-code/SKILL.md +0 -53
  416. package/src/config/bundled-skills/claude-code/TOOLS.json +0 -47
  417. package/src/config/bundled-skills/claude-code/tools/claude-code.ts +0 -12
  418. package/src/config/bundled-skills/orchestration/SKILL.md +0 -33
  419. package/src/config/bundled-skills/orchestration/TOOLS.json +0 -35
  420. package/src/config/bundled-skills/orchestration/tools/swarm-delegate.ts +0 -12
  421. package/src/config/schemas/swarm.ts +0 -82
  422. package/src/logfire.ts +0 -135
  423. package/src/memory/search/lexical.ts +0 -48
  424. package/src/providers/failover.ts +0 -186
  425. package/src/runtime/local-gateway-health.ts +0 -275
  426. package/src/security/secret-ingress.ts +0 -68
  427. package/src/swarm/backend-claude-code.ts +0 -225
  428. package/src/swarm/checkpoint.ts +0 -137
  429. package/src/swarm/graph-utils.ts +0 -53
  430. package/src/swarm/index.ts +0 -55
  431. package/src/swarm/limits.ts +0 -66
  432. package/src/swarm/orchestrator.ts +0 -424
  433. package/src/swarm/plan-validator.ts +0 -117
  434. package/src/swarm/router-planner.ts +0 -162
  435. package/src/swarm/router-prompts.ts +0 -39
  436. package/src/swarm/synthesizer.ts +0 -81
  437. package/src/swarm/types.ts +0 -72
  438. package/src/swarm/worker-backend.ts +0 -131
  439. package/src/swarm/worker-prompts.ts +0 -80
  440. package/src/swarm/worker-runner.ts +0 -170
  441. package/src/tools/claude-code/claude-code.ts +0 -610
  442. package/src/tools/swarm/delegate.ts +0 -205
@@ -9,6 +9,7 @@ import { and, desc, eq } from "drizzle-orm";
9
9
  import { v4 as uuid } from "uuid";
10
10
 
11
11
  import { loadSkillCatalog } from "../../config/skills.js";
12
+ import { resolveGuardianPersona } from "../../prompts/persona-resolver.js";
12
13
  import { buildCoreIdentityContext } from "../../prompts/system-prompt.js";
13
14
  import {
14
15
  createTimeout,
@@ -171,7 +172,9 @@ async function generateStarters(scopeId: string): Promise<GeneratedStarter[]> {
171
172
 
172
173
  // Truncate identity context to prevent oversized prompts when SOUL.md /
173
174
  // IDENTITY.md / USER.md are large.
174
- const rawIdentityContext = buildCoreIdentityContext();
175
+ const rawIdentityContext = buildCoreIdentityContext({
176
+ userPersona: resolveGuardianPersona(),
177
+ });
175
178
  const identityContext = rawIdentityContext
176
179
  ? truncate(rawIdentityContext, 2000, "\n…[truncated]")
177
180
  : null;
@@ -19,18 +19,25 @@ import { memorySegments, memorySummaries } from "../schema.js";
19
19
  const log = getLogger("memory-jobs-worker");
20
20
 
21
21
  const SUMMARY_LLM_TIMEOUT_MS = 20_000;
22
- const SUMMARY_MAX_TOKENS = 800;
22
+ const SUMMARY_MAX_TOKENS = 500;
23
23
 
24
24
  const CONVERSATION_SUMMARY_SYSTEM_PROMPT = [
25
- "You are a memory summarization system. Your job is to produce a compact, information-dense summary of a conversation.",
25
+ "You compress conversation transcripts into compact summaries for semantic search and memory retrieval.",
26
+ "Focus on durable facts, not transient discussion.",
27
+ "Preserve: goals, decisions, constraints, preferences, names, technical details, actions taken.",
28
+ "Remove: filler, pleasantries, tool invocation details, transient status updates.",
26
29
  "",
27
- "Guidelines:",
28
- "- Focus on key facts, decisions, user preferences, and actionable information.",
29
- "- Preserve concrete details: names, file paths, tool choices, technical decisions, constraints.",
30
- "- Remove filler, pleasantries, and transient discussion that has no lasting value.",
31
- "- Use concise bullet points grouped by topic.",
32
- "- Target 400-600 tokens. Be dense but readable.",
33
- "- If updating an existing summary with new data, merge new information and remove anything that was superseded.",
30
+ "Return concise markdown:",
31
+ "## Topic",
32
+ "One-line description of what the conversation is about.",
33
+ "## Key Facts",
34
+ "Bullet points of concrete facts, names, decisions, preferences.",
35
+ "## Outcomes",
36
+ "What was decided, resolved, or accomplished.",
37
+ "## Open Items",
38
+ "Unresolved questions, pending tasks, or follow-ups (omit section if none).",
39
+ "",
40
+ "Target 200-400 tokens. Be dense.",
34
41
  ].join("\n");
35
42
 
36
43
  export async function buildConversationSummaryJob(
@@ -62,9 +69,8 @@ export async function buildConversationSummaryJob(
62
69
 
63
70
  // Build segment text for LLM input (chronological order)
64
71
  const segmentTexts = rows
65
- .slice(0, 30)
66
72
  .reverse()
67
- .map((row) => `[${row.role}] ${truncate(row.text, 400)}`)
73
+ .map((row) => `[${row.role}] ${truncate(row.text, 600)}`)
68
74
  .join("\n\n");
69
75
 
70
76
  const summaryText = await summarizeWithLLM(
@@ -208,14 +214,18 @@ async function summarizeWithLLM(
208
214
  }
209
215
 
210
216
  function buildFallbackSummary(
211
- _existingSummary: string | null,
217
+ existingSummary: string | null,
212
218
  newContent: string,
213
219
  label: string,
214
220
  ): string {
215
221
  const lines = newContent.split("\n").filter((l) => l.trim().length > 0);
216
- const snippets = lines
217
- .slice(0, 20)
218
- .map((l) => `- ${truncate(l.trim(), 180)}`);
219
- const parts: string[] = [`${label} summary`, "", ...snippets];
222
+ if (lines.length === 0) return existingSummary ?? `${label} (no content)`;
223
+ const head = lines.slice(0, 3).map((l) => `- ${truncate(l.trim(), 200)}`);
224
+ const tail =
225
+ lines.length > 6
226
+ ? lines.slice(-3).map((l) => `- ${truncate(l.trim(), 200)}`)
227
+ : [];
228
+ const parts = [`${label} summary`, "", ...head];
229
+ if (tail.length > 0) parts.push("", "...", "", ...tail);
220
230
  return parts.join("\n");
221
231
  }
@@ -1,9 +1,13 @@
1
- import { and, asc, eq, inArray, lte, notInArray } from "drizzle-orm";
1
+ import { and, asc, eq, inArray, lte, notInArray, sql } from "drizzle-orm";
2
2
  import { v4 as uuid } from "uuid";
3
3
 
4
4
  import { getLogger } from "../util/logger.js";
5
5
  import { truncate } from "../util/truncate.js";
6
6
  import { getDb, rawAll, rawChanges } from "./db.js";
7
+ import {
8
+ isQdrantBreakerOpen,
9
+ shouldAllowQdrantProbe,
10
+ } from "./qdrant-circuit-breaker.js";
7
11
  import { memoryJobs } from "./schema.js";
8
12
 
9
13
  const log = getLogger("memory-jobs-store");
@@ -82,6 +86,38 @@ export function enqueueMemoryJob(
82
86
  return id;
83
87
  }
84
88
 
89
+ /**
90
+ * Upsert a debounced job: if a pending job of the same type and conversation
91
+ * already exists, push its `runAfter` forward instead of creating a duplicate.
92
+ * This prevents rapid message indexing from spawning redundant jobs.
93
+ */
94
+ export function upsertDebouncedJob(
95
+ type: MemoryJobType,
96
+ payload: { conversationId: string },
97
+ runAfter: number,
98
+ ): void {
99
+ const db = getDb();
100
+ const existing = db
101
+ .select()
102
+ .from(memoryJobs)
103
+ .where(
104
+ and(
105
+ eq(memoryJobs.type, type),
106
+ eq(memoryJobs.status, "pending"),
107
+ sql`json_extract(${memoryJobs.payload}, '$.conversationId') = ${payload.conversationId}`,
108
+ ),
109
+ )
110
+ .get();
111
+ if (existing) {
112
+ db.update(memoryJobs)
113
+ .set({ runAfter, updatedAt: Date.now() })
114
+ .where(eq(memoryJobs.id, existing.id))
115
+ .run();
116
+ } else {
117
+ enqueueMemoryJob(type, payload, runAfter);
118
+ }
119
+ }
120
+
85
121
  export function enqueueCleanupStaleSupersededItemsJob(
86
122
  retentionMs?: number,
87
123
  ): string {
@@ -201,14 +237,33 @@ export function claimMemoryJobs(limit: number): MemoryJob[] {
201
237
  .all();
202
238
 
203
239
  const remainingSlots = limit - nonEmbedCandidates.length;
240
+
241
+ // When the Qdrant circuit breaker is open, skip embed jobs entirely —
242
+ // they would just be claimed → fail → deferred, wasting CPU cycles.
243
+ // Exception: if the cooldown has elapsed (breaker ready for half-open probe),
244
+ // allow exactly 1 embed job through so the breaker can self-heal.
245
+ const breakerOpen = isQdrantBreakerOpen();
246
+ const probeAllowed = breakerOpen && shouldAllowQdrantProbe();
247
+ const skipEmbedJobs = breakerOpen && !probeAllowed;
248
+ const embedLimit = probeAllowed ? 1 : remainingSlots;
249
+
250
+ if (skipEmbedJobs && remainingSlots > 0) {
251
+ log.debug("Skipping embed job claims — Qdrant circuit breaker is open");
252
+ }
253
+ if (probeAllowed && remainingSlots > 0) {
254
+ log.debug(
255
+ "Allowing 1 embed probe job — Qdrant circuit breaker cooldown elapsed",
256
+ );
257
+ }
258
+
204
259
  const embedCandidates =
205
- remainingSlots > 0
260
+ remainingSlots > 0 && !skipEmbedJobs
206
261
  ? db
207
262
  .select()
208
263
  .from(memoryJobs)
209
264
  .where(and(pendingFilter, inArray(memoryJobs.type, EMBED_JOB_TYPES)))
210
265
  .orderBy(asc(memoryJobs.runAfter), asc(memoryJobs.createdAt))
211
- .limit(remainingSlots)
266
+ .limit(embedLimit)
212
267
  .all()
213
268
  : [];
214
269
 
@@ -243,8 +298,8 @@ export function completeMemoryJob(id: string): void {
243
298
 
244
299
  /** Max times a job can be deferred before it is marked as failed. */
245
300
  const MAX_DEFERRALS = 50;
246
- /** Warn when deferrals reach 80% of the limit. */
247
- const DEFER_WARNING_THRESHOLD = Math.floor(MAX_DEFERRALS * 0.8);
301
+ /** Log warnings at these milestone counts to avoid flooding logs. */
302
+ const DEFERRAL_WARN_MILESTONES = [40, 45];
248
303
  /** Base delay in ms for deferred jobs (grows with exponential backoff). */
249
304
  const DEFER_BASE_DELAY_MS = 30_000;
250
305
  /** Maximum delay cap for deferred jobs (5 minutes). */
@@ -286,7 +341,9 @@ export function deferMemoryJob(id: string): "deferred" | "failed" {
286
341
  return "failed";
287
342
  }
288
343
 
289
- if (deferrals >= DEFER_WARNING_THRESHOLD) {
344
+ // Log at milestones only (40, 45) to avoid flooding logs.
345
+ // At 50, the job fails via the check above, so 40 and 45 are the warnings.
346
+ if (DEFERRAL_WARN_MILESTONES.includes(deferrals)) {
290
347
  log.warn(
291
348
  { jobId: id, type: row.type, deferrals, max: MAX_DEFERRALS },
292
349
  "Job approaching max deferral limit",
@@ -44,6 +44,9 @@ import { QdrantCircuitOpenError } from "./qdrant-circuit-breaker.js";
44
44
 
45
45
  const log = getLogger("memory-jobs-worker");
46
46
 
47
+ export const POLL_INTERVAL_MIN_MS = 1_500;
48
+ export const POLL_INTERVAL_MAX_MS = 30_000;
49
+
47
50
  export interface MemoryJobsWorker {
48
51
  runOnce(): Promise<number>;
49
52
  stop(): void;
@@ -57,24 +60,45 @@ export function startMemoryJobsWorker(): MemoryJobsWorker {
57
60
 
58
61
  let stopped = false;
59
62
  let tickRunning = false;
63
+ let timer: ReturnType<typeof setTimeout>;
64
+ let currentIntervalMs = POLL_INTERVAL_MIN_MS;
60
65
 
61
66
  const tick = async () => {
62
67
  if (stopped || tickRunning) return;
63
68
  tickRunning = true;
64
69
  try {
65
- await runMemoryJobsOnce({ enableScheduledCleanup: true });
70
+ const processed = await runMemoryJobsOnce({
71
+ enableScheduledCleanup: true,
72
+ });
73
+ if (processed > 0) {
74
+ currentIntervalMs = POLL_INTERVAL_MIN_MS;
75
+ } else {
76
+ currentIntervalMs = Math.min(
77
+ currentIntervalMs * 2,
78
+ POLL_INTERVAL_MAX_MS,
79
+ );
80
+ }
66
81
  } catch (err) {
67
82
  log.error({ err }, "Memory worker tick failed");
83
+ currentIntervalMs = Math.min(currentIntervalMs * 2, POLL_INTERVAL_MAX_MS);
68
84
  } finally {
69
85
  tickRunning = false;
70
86
  }
71
87
  };
72
88
 
73
- const timer = setInterval(() => {
74
- void tick();
75
- }, 1500);
76
- timer.unref();
77
- void tick();
89
+ const scheduleTick = () => {
90
+ if (stopped) return;
91
+ timer = setTimeout(() => {
92
+ void tick().then(() => {
93
+ if (!stopped) scheduleTick();
94
+ });
95
+ }, currentIntervalMs);
96
+ (timer as NodeJS.Timeout).unref?.();
97
+ };
98
+
99
+ void tick().then(() => {
100
+ if (!stopped) scheduleTick();
101
+ });
78
102
 
79
103
  return {
80
104
  async runOnce(): Promise<number> {
@@ -82,7 +106,7 @@ export function startMemoryJobsWorker(): MemoryJobsWorker {
82
106
  },
83
107
  stop(): void {
84
108
  stopped = true;
85
- clearInterval(timer);
109
+ clearTimeout(timer);
86
110
  },
87
111
  };
88
112
  }
@@ -0,0 +1,214 @@
1
+ import { readdirSync, readFileSync, statSync } from "node:fs";
2
+ import { join } from "node:path";
3
+
4
+ import { and, eq } from "drizzle-orm";
5
+ import { v4 as uuid } from "uuid";
6
+
7
+ import { getLogger } from "../util/logger.js";
8
+ import { getWorkspaceDir } from "../util/platform.js";
9
+ import { type DrizzleDb, getDb } from "./db.js";
10
+ import { computeMemoryFingerprint } from "./fingerprint.js";
11
+ import { enqueueMemoryJob } from "./jobs-store.js";
12
+ import { memoryItems, memoryItemSources } from "./schema.js";
13
+
14
+ const log = getLogger("memory-journal");
15
+
16
+ /**
17
+ * Process a single journal `.md` file: read content, derive subject, compute
18
+ * fingerprint, upsert to DB, and enqueue an embed job.
19
+ *
20
+ * Returns `true` if a new memory item was inserted, `false` if it already
21
+ * existed (or was skipped).
22
+ */
23
+ function upsertSingleJournalFile(
24
+ filepath: string,
25
+ filename: string,
26
+ messageCreatedAt: number,
27
+ scopeId: string,
28
+ messageId: string,
29
+ db: DrizzleDb,
30
+ ): boolean {
31
+ const content = readFileSync(filepath, "utf-8");
32
+
33
+ // Derive subject from filename:
34
+ // strip .md extension, strip leading date prefix, replace hyphens with spaces, capitalize first letter
35
+ const basename = filename.replace(/\.md$/, "");
36
+ const withoutDate = basename.replace(/^\d{4}-\d{2}-\d{2}-?/, "");
37
+ const withSpaces = withoutDate.replace(/-/g, " ");
38
+ const subject =
39
+ withSpaces.length > 0
40
+ ? withSpaces.charAt(0).toUpperCase() + withSpaces.slice(1)
41
+ : basename;
42
+
43
+ const fingerprint = computeMemoryFingerprint(
44
+ scopeId,
45
+ "journal",
46
+ subject,
47
+ content,
48
+ );
49
+
50
+ const existing = db
51
+ .select()
52
+ .from(memoryItems)
53
+ .where(
54
+ and(
55
+ eq(memoryItems.fingerprint, fingerprint),
56
+ eq(memoryItems.scopeId, scopeId),
57
+ ),
58
+ )
59
+ .get();
60
+
61
+ let memoryItemId: string;
62
+ let inserted = false;
63
+
64
+ if (existing) {
65
+ memoryItemId = existing.id;
66
+ db.update(memoryItems)
67
+ .set({
68
+ lastSeenAt: messageCreatedAt,
69
+ status: "active",
70
+ })
71
+ .where(eq(memoryItems.id, existing.id))
72
+ .run();
73
+ } else {
74
+ memoryItemId = uuid();
75
+ db.insert(memoryItems)
76
+ .values({
77
+ id: memoryItemId,
78
+ kind: "journal",
79
+ subject,
80
+ statement: content,
81
+ status: "active",
82
+ confidence: 0.95,
83
+ importance: 0.8,
84
+ fingerprint,
85
+ sourceType: "extraction",
86
+ sourceMessageRole: "assistant",
87
+ verificationState: "assistant_inferred",
88
+ scopeId,
89
+ firstSeenAt: messageCreatedAt,
90
+ lastSeenAt: messageCreatedAt,
91
+ lastUsedAt: null,
92
+ supersedes: null,
93
+ overrideConfidence: null,
94
+ })
95
+ .run();
96
+ inserted = true;
97
+ }
98
+
99
+ db.insert(memoryItemSources)
100
+ .values({
101
+ memoryItemId,
102
+ messageId,
103
+ evidence: content,
104
+ createdAt: Date.now(),
105
+ })
106
+ .onConflictDoNothing()
107
+ .run();
108
+
109
+ enqueueMemoryJob("embed_item", { itemId: memoryItemId });
110
+
111
+ return inserted;
112
+ }
113
+
114
+ /**
115
+ * Scan the journal directory for `.md` files created during (or after) the
116
+ * given message timestamp and upsert them as journal memory items with the
117
+ * raw, unedited file content as the `statement`.
118
+ *
119
+ * Also scans immediate subdirectories (e.g. per-user folders like
120
+ * `journal/sidd/`) so that user-scoped journal entries are indexed alongside
121
+ * root-level files.
122
+ *
123
+ * This bypasses the LLM extraction layer entirely — journal memories are
124
+ * stored verbatim so they are never summarised or rewritten.
125
+ *
126
+ * Returns the number of newly inserted items.
127
+ */
128
+ export function upsertJournalMemoriesFromDisk(
129
+ messageCreatedAt: number,
130
+ scopeId: string,
131
+ messageId: string,
132
+ ): number {
133
+ try {
134
+ const journalDir = join(getWorkspaceDir(), "journal");
135
+
136
+ let files: string[];
137
+ try {
138
+ files = readdirSync(journalDir);
139
+ } catch {
140
+ // Directory doesn't exist — no journal entries
141
+ return 0;
142
+ }
143
+
144
+ // Filter for .md files, excluding readme.md (case-insensitive)
145
+ const mdFiles = files.filter(
146
+ (f) => f.endsWith(".md") && f.toLowerCase() !== "readme.md",
147
+ );
148
+
149
+ let upserted = 0;
150
+ const db = getDb();
151
+
152
+ for (const filename of mdFiles) {
153
+ try {
154
+ const filepath = join(journalDir, filename);
155
+ const stat = statSync(filepath);
156
+ if (!stat.isFile()) continue;
157
+
158
+ // Only process files created during or after this message
159
+ if (stat.birthtimeMs < messageCreatedAt) continue;
160
+
161
+ if (upsertSingleJournalFile(filepath, filename, messageCreatedAt, scopeId, messageId, db)) {
162
+ upserted += 1;
163
+ }
164
+ } catch (err) {
165
+ log.warn(
166
+ { filename, err: err instanceof Error ? err.message : String(err) },
167
+ "Failed to process journal file for memory — skipping",
168
+ );
169
+ }
170
+ }
171
+
172
+ // Scan per-user journal subdirectories
173
+ for (const entry of files) {
174
+ try {
175
+ const subdirPath = join(journalDir, entry);
176
+ if (!statSync(subdirPath).isDirectory()) continue;
177
+
178
+ const subFiles = readdirSync(subdirPath).filter(
179
+ (f) => f.endsWith(".md") && f.toLowerCase() !== "readme.md",
180
+ );
181
+
182
+ for (const filename of subFiles) {
183
+ try {
184
+ const filepath = join(subdirPath, filename);
185
+ const stat = statSync(filepath);
186
+ if (!stat.isFile()) continue;
187
+
188
+ // Only process files created during or after this message
189
+ if (stat.birthtimeMs < messageCreatedAt) continue;
190
+
191
+ if (upsertSingleJournalFile(filepath, filename, messageCreatedAt, scopeId, messageId, db)) {
192
+ upserted += 1;
193
+ }
194
+ } catch (err) {
195
+ log.warn(
196
+ { filename, err: err instanceof Error ? err.message : String(err) },
197
+ "Failed to process journal file for memory — skipping",
198
+ );
199
+ }
200
+ }
201
+ } catch {
202
+ // Skip unreadable subdirectories
203
+ }
204
+ }
205
+
206
+ return upserted;
207
+ } catch (err) {
208
+ log.warn(
209
+ { err: err instanceof Error ? err.message : String(err) },
210
+ "Failed to scan journal directory for memories",
211
+ );
212
+ return 0;
213
+ }
214
+ }
@@ -49,3 +49,22 @@ export function migrateJobDeferrals(database: DrizzleDb): void {
49
49
  throw e;
50
50
  }
51
51
  }
52
+
53
+ /**
54
+ * Reverse the deferral reconciliation by moving `deferrals` back into `attempts`
55
+ * for pending embed jobs. Best-effort: jobs that accumulated real deferral counts
56
+ * after the forward migration ran cannot be distinguished from migrated ones.
57
+ */
58
+ export function downJobDeferrals(database: DrizzleDb): void {
59
+ const raw = getSqliteFrom(database);
60
+ raw.exec(/*sql*/ `
61
+ UPDATE memory_jobs
62
+ SET attempts = deferrals,
63
+ deferrals = 0,
64
+ updated_at = ${Date.now()}
65
+ WHERE status = 'pending'
66
+ AND deferrals > 0
67
+ AND attempts = 0
68
+ AND type IN ('embed_segment', 'embed_item', 'embed_summary')
69
+ `);
70
+ }
@@ -91,3 +91,13 @@ export function migrateMemoryEntityRelationDedup(database: DrizzleDb): void {
91
91
  throw e;
92
92
  }
93
93
  }
94
+
95
+ /**
96
+ * No-op down: deduplication is a lossy operation — deleted duplicate rows
97
+ * cannot be restored. The forward migration merged rows by keeping the most
98
+ * recent evidence per (source, target, relation) triple; the discarded rows
99
+ * are permanently lost.
100
+ */
101
+ export function downMemoryEntityRelationDedup(_database: DrizzleDb): void {
102
+ // Intentionally empty — irreversible lossy migration.
103
+ }
@@ -93,3 +93,79 @@ export function migrateMemoryItemsFingerprintScopeUnique(
93
93
  raw.exec("PRAGMA foreign_keys = ON");
94
94
  }
95
95
  }
96
+
97
+ /**
98
+ * Reverse the compound (fingerprint, scope_id) unique index change by rebuilding
99
+ * memory_items with a column-level UNIQUE on fingerprint.
100
+ *
101
+ * WARNING: This is dangerous if data now relies on the compound constraint
102
+ * (i.e., the same fingerprint exists in multiple scopes). In that case, the
103
+ * rebuild will fail with a UNIQUE constraint violation. This is intentional —
104
+ * it prevents silent data loss on rollback.
105
+ */
106
+ export function downMemoryItemsFingerprintScopeUnique(
107
+ database: DrizzleDb,
108
+ ): void {
109
+ const raw = getSqliteFrom(database);
110
+
111
+ // Check if the column-level UNIQUE already exists — if so, nothing to do.
112
+ const tableDdl = raw
113
+ .query(
114
+ `SELECT sql FROM sqlite_master WHERE type = 'table' AND name = 'memory_items'`,
115
+ )
116
+ .get() as { sql: string } | null;
117
+ if (
118
+ !tableDdl ||
119
+ tableDdl.sql.match(/fingerprint\s+TEXT\s+NOT\s+NULL\s+UNIQUE/i)
120
+ ) {
121
+ return;
122
+ }
123
+
124
+ raw.exec("PRAGMA foreign_keys = OFF");
125
+ try {
126
+ raw.exec("BEGIN");
127
+
128
+ raw.exec(/*sql*/ `
129
+ CREATE TABLE memory_items_new (
130
+ id TEXT PRIMARY KEY,
131
+ kind TEXT NOT NULL,
132
+ subject TEXT NOT NULL,
133
+ statement TEXT NOT NULL,
134
+ status TEXT NOT NULL,
135
+ confidence REAL NOT NULL,
136
+ fingerprint TEXT NOT NULL UNIQUE,
137
+ first_seen_at INTEGER NOT NULL,
138
+ last_seen_at INTEGER NOT NULL,
139
+ last_used_at INTEGER,
140
+ importance REAL,
141
+ access_count INTEGER NOT NULL DEFAULT 0,
142
+ valid_from INTEGER,
143
+ invalid_at INTEGER,
144
+ verification_state TEXT NOT NULL DEFAULT 'assistant_inferred',
145
+ scope_id TEXT NOT NULL DEFAULT 'default'
146
+ )
147
+ `);
148
+
149
+ raw.exec(/*sql*/ `
150
+ INSERT INTO memory_items_new
151
+ SELECT id, kind, subject, statement, status, confidence, fingerprint,
152
+ first_seen_at, last_seen_at, last_used_at, importance, access_count,
153
+ valid_from, invalid_at, verification_state, scope_id
154
+ FROM memory_items
155
+ `);
156
+
157
+ raw.exec(/*sql*/ `DROP TABLE memory_items`);
158
+ raw.exec(/*sql*/ `ALTER TABLE memory_items_new RENAME TO memory_items`);
159
+
160
+ raw.exec("COMMIT");
161
+ } catch (e) {
162
+ try {
163
+ raw.exec("ROLLBACK");
164
+ } catch {
165
+ /* no active transaction */
166
+ }
167
+ throw e;
168
+ } finally {
169
+ raw.exec("PRAGMA foreign_keys = ON");
170
+ }
171
+ }
@@ -1,3 +1,5 @@
1
+ import { createHash } from "node:crypto";
2
+
1
3
  import { type DrizzleDb, getSqliteFrom } from "../db-connection.js";
2
4
  import { computeMemoryFingerprint } from "../fingerprint.js";
3
5
 
@@ -75,3 +77,51 @@ export function migrateMemoryItemsScopeSaltedFingerprints(
75
77
  throw e;
76
78
  }
77
79
  }
80
+
81
+ /**
82
+ * Reverse the scope-salted fingerprint migration by recomputing fingerprints
83
+ * WITHOUT the scope_id prefix.
84
+ *
85
+ * Old format: sha256(`${kind}|${subject.toLowerCase()}|${statement.toLowerCase()}`)
86
+ */
87
+ export function downMemoryItemsScopeSaltedFingerprints(
88
+ database: DrizzleDb,
89
+ ): void {
90
+ const raw = getSqliteFrom(database);
91
+
92
+ interface ItemRow {
93
+ id: string;
94
+ kind: string;
95
+ subject: string;
96
+ statement: string;
97
+ }
98
+
99
+ const items = raw
100
+ .query(`SELECT id, kind, subject, statement FROM memory_items`)
101
+ .all() as ItemRow[];
102
+
103
+ if (items.length === 0) return;
104
+
105
+ try {
106
+ raw.exec("BEGIN");
107
+
108
+ const updateStmt = raw.prepare(
109
+ `UPDATE memory_items SET fingerprint = ? WHERE id = ?`,
110
+ );
111
+
112
+ for (const item of items) {
113
+ const normalized = `${item.kind}|${item.subject.toLowerCase()}|${item.statement.toLowerCase()}`;
114
+ const fingerprint = createHash("sha256").update(normalized).digest("hex");
115
+ updateStmt.run(fingerprint, item.id);
116
+ }
117
+
118
+ raw.exec("COMMIT");
119
+ } catch (e) {
120
+ try {
121
+ raw.exec("ROLLBACK");
122
+ } catch {
123
+ /* no active transaction */
124
+ }
125
+ throw e;
126
+ }
127
+ }
@@ -265,3 +265,13 @@ export function migrateAssistantIdToSelf(database: DrizzleDb): void {
265
265
  throw e;
266
266
  }
267
267
  }
268
+
269
+ /**
270
+ * No-op down: the original assistant_id values are not recoverable. The forward
271
+ * migration normalized all assistant_id values to "self" and merged/deduplicated
272
+ * rows where the same logical entity existed under both the real assistantId and
273
+ * "self". The original per-assistant IDs are permanently lost.
274
+ */
275
+ export function downAssistantIdToSelf(_database: DrizzleDb): void {
276
+ // Intentionally empty — original assistant_id values cannot be restored.
277
+ }