@vellumai/assistant 0.5.5 → 0.5.7

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 (382) hide show
  1. package/.env.example +16 -2
  2. package/ARCHITECTURE.md +6 -75
  3. package/Dockerfile +4 -5
  4. package/README.md +0 -2
  5. package/bun.lock +0 -414
  6. package/docs/architecture/keychain-broker.md +45 -240
  7. package/docs/architecture/security.md +0 -17
  8. package/docs/credential-execution-service.md +2 -2
  9. package/node_modules/@vellumai/ces-contracts/package.json +1 -0
  10. package/node_modules/@vellumai/ces-contracts/src/rpc.ts +119 -0
  11. package/node_modules/@vellumai/credential-storage/package.json +1 -0
  12. package/node_modules/@vellumai/egress-proxy/package.json +1 -0
  13. package/package.json +2 -3
  14. package/src/__tests__/actor-token-service.test.ts +1 -2
  15. package/src/__tests__/assistant-feature-flags-integration.test.ts +30 -29
  16. package/src/__tests__/browser-skill-endstate.test.ts +6 -5
  17. package/src/__tests__/btw-routes.test.ts +0 -39
  18. package/src/__tests__/call-domain.test.ts +0 -128
  19. package/src/__tests__/ces-rpc-credential-backend.test.ts +199 -0
  20. package/src/__tests__/channel-approval-routes.test.ts +0 -5
  21. package/src/__tests__/channel-readiness-service.test.ts +1 -60
  22. package/src/__tests__/checker.test.ts +4 -2
  23. package/src/__tests__/cli-command-risk-guard.test.ts +112 -0
  24. package/src/__tests__/config-schema-cmd.test.ts +0 -1
  25. package/src/__tests__/config-schema.test.ts +3 -3
  26. package/src/__tests__/context-window-manager.test.ts +78 -0
  27. package/src/__tests__/conversation-attention-telegram.test.ts +0 -5
  28. package/src/__tests__/conversation-init.benchmark.test.ts +0 -2
  29. package/src/__tests__/conversation-skill-tools.test.ts +0 -54
  30. package/src/__tests__/conversation-title-service.test.ts +117 -1
  31. package/src/__tests__/credential-execution-feature-gates.test.ts +28 -14
  32. package/src/__tests__/credential-execution-managed-contract.test.ts +33 -18
  33. package/src/__tests__/credential-security-e2e.test.ts +0 -66
  34. package/src/__tests__/credential-security-invariants.test.ts +4 -45
  35. package/src/__tests__/credentials-cli.test.ts +78 -0
  36. package/src/__tests__/db-migration-rollback.test.ts +2015 -1
  37. package/src/__tests__/docker-signing-key-bootstrap.test.ts +98 -0
  38. package/src/__tests__/dynamic-skill-workflow-prompt.test.ts +6 -4
  39. package/src/__tests__/guardian-routing-state.test.ts +0 -5
  40. package/src/__tests__/host-shell-tool.test.ts +6 -7
  41. package/src/__tests__/http-user-message-parity.test.ts +3 -103
  42. package/src/__tests__/inbound-invite-redemption.test.ts +0 -4
  43. package/src/__tests__/inline-skill-load-permissions.test.ts +6 -8
  44. package/src/__tests__/intent-routing.test.ts +0 -13
  45. package/src/__tests__/jobs-store-qdrant-breaker.test.ts +178 -0
  46. package/src/__tests__/keychain-broker-client.test.ts +161 -22
  47. package/src/__tests__/memory-jobs-worker-backoff.test.ts +150 -0
  48. package/src/__tests__/memory-regressions.test.ts +8 -30
  49. package/src/__tests__/migration-export-http.test.ts +2 -2
  50. package/src/__tests__/migration-import-commit-http.test.ts +2 -2
  51. package/src/__tests__/migration-import-preflight-http.test.ts +2 -2
  52. package/src/__tests__/migration-validate-http.test.ts +2 -2
  53. package/src/__tests__/non-member-access-request.test.ts +0 -5
  54. package/src/__tests__/notification-decision-fallback.test.ts +4 -0
  55. package/src/__tests__/notification-decision-identity.test.ts +4 -0
  56. package/src/__tests__/permission-types.test.ts +1 -0
  57. package/src/__tests__/provider-managed-proxy-integration.test.ts +5 -6
  58. package/src/__tests__/qdrant-manager.test.ts +28 -2
  59. package/src/__tests__/registry.test.ts +0 -6
  60. package/src/__tests__/require-fresh-approval.test.ts +4 -0
  61. package/src/__tests__/runtime-attachment-metadata.test.ts +0 -4
  62. package/src/__tests__/secret-routes-managed-proxy.test.ts +0 -4
  63. package/src/__tests__/secure-keys.test.ts +83 -263
  64. package/src/__tests__/shell-identity.test.ts +96 -6
  65. package/src/__tests__/skill-feature-flags-integration.test.ts +22 -14
  66. package/src/__tests__/skill-feature-flags.test.ts +46 -45
  67. package/src/__tests__/skill-load-feature-flag.test.ts +7 -10
  68. package/src/__tests__/skill-load-inline-command.test.ts +8 -12
  69. package/src/__tests__/skill-load-inline-includes.test.ts +6 -10
  70. package/src/__tests__/skill-load-tool.test.ts +0 -2
  71. package/src/__tests__/skill-projection-feature-flag.test.ts +33 -29
  72. package/src/__tests__/skills.test.ts +0 -2
  73. package/src/__tests__/slack-inbound-verification.test.ts +0 -4
  74. package/src/__tests__/suggestion-routes.test.ts +1 -32
  75. package/src/__tests__/system-prompt.test.ts +0 -1
  76. package/src/__tests__/tool-executor-lifecycle-events.test.ts +4 -0
  77. package/src/__tests__/tool-executor-shell-integration.test.ts +5 -3
  78. package/src/__tests__/tool-executor.test.ts +4 -0
  79. package/src/__tests__/trusted-contact-lifecycle-notifications.test.ts +0 -5
  80. package/src/__tests__/trusted-contact-multichannel.test.ts +0 -4
  81. package/src/__tests__/update-bulletin.test.ts +0 -2
  82. package/src/__tests__/vellum-self-knowledge-inline-command.test.ts +6 -9
  83. package/src/__tests__/voice-scoped-grant-consumer.test.ts +0 -6
  84. package/src/__tests__/workspace-migration-015-migrate-credentials-to-keychain.test.ts +252 -0
  85. package/src/__tests__/workspace-migration-016-migrate-credentials-from-keychain.test.ts +218 -0
  86. package/src/__tests__/workspace-migration-down-functions.test.ts +1009 -0
  87. package/src/__tests__/workspace-migrations-runner.test.ts +114 -0
  88. package/src/calls/audio-store.test.ts +97 -0
  89. package/src/calls/audio-store.ts +205 -0
  90. package/src/calls/call-controller.ts +85 -7
  91. package/src/calls/call-domain.ts +3 -0
  92. package/src/calls/call-store.ts +10 -3
  93. package/src/calls/fish-audio-client.ts +117 -0
  94. package/src/calls/relay-server.ts +27 -0
  95. package/src/calls/twilio-routes.ts +2 -1
  96. package/src/calls/types.ts +1 -0
  97. package/src/calls/voice-ingress-preflight.ts +0 -42
  98. package/src/calls/voice-quality.ts +26 -5
  99. package/src/calls/voice-session-bridge.ts +6 -12
  100. package/src/cli/commands/config.ts +1 -4
  101. package/src/cli/commands/conversations.ts +0 -18
  102. package/src/cli/commands/credentials.ts +34 -4
  103. package/src/cli/commands/oauth/index.ts +7 -0
  104. package/src/cli/commands/oauth/platform.ts +179 -0
  105. package/src/cli/commands/platform.ts +3 -3
  106. package/src/config/assistant-feature-flags.ts +186 -5
  107. package/src/config/bundled-skills/messaging/SKILL.md +5 -5
  108. package/src/config/bundled-skills/phone-calls/TOOLS.json +4 -0
  109. package/src/config/bundled-skills/settings/TOOLS.json +2 -2
  110. package/src/config/bundled-skills/settings/tools/voice-config-update.ts +42 -0
  111. package/src/config/bundled-tool-registry.ts +1 -11
  112. package/src/config/env-registry.ts +1 -1
  113. package/src/config/env.ts +16 -16
  114. package/src/config/feature-flag-registry.json +48 -16
  115. package/src/config/loader.ts +98 -31
  116. package/src/config/schema.ts +4 -25
  117. package/src/config/schemas/calls.ts +13 -0
  118. package/src/config/schemas/fish-audio.ts +39 -0
  119. package/src/config/schemas/memory.ts +0 -4
  120. package/src/config/schemas/platform.ts +1 -1
  121. package/src/config/schemas/security.ts +4 -4
  122. package/src/config/types.ts +0 -1
  123. package/src/contacts/contact-store.ts +39 -0
  124. package/src/contacts/types.ts +2 -0
  125. package/src/context/window-manager.ts +53 -2
  126. package/src/credential-execution/approval-bridge.ts +1 -0
  127. package/src/credential-execution/executable-discovery.ts +28 -4
  128. package/src/credential-execution/feature-gates.ts +16 -0
  129. package/src/credential-execution/process-manager.ts +38 -0
  130. package/src/daemon/assistant-attachments.ts +9 -0
  131. package/src/daemon/config-watcher.ts +6 -4
  132. package/src/daemon/conversation-agent-loop.ts +0 -60
  133. package/src/daemon/conversation-memory.ts +0 -117
  134. package/src/daemon/conversation-runtime-assembly.ts +0 -2
  135. package/src/daemon/conversation-tool-setup.ts +0 -105
  136. package/src/daemon/conversation.ts +10 -1
  137. package/src/daemon/handlers/config-vercel.ts +92 -0
  138. package/src/daemon/handlers/conversations.ts +0 -11
  139. package/src/daemon/handlers/skills.ts +2 -15
  140. package/src/daemon/install-symlink.ts +195 -0
  141. package/src/daemon/lifecycle.ts +229 -96
  142. package/src/daemon/message-types/conversations.ts +3 -4
  143. package/src/daemon/message-types/diagnostics.ts +3 -22
  144. package/src/daemon/message-types/messages.ts +0 -2
  145. package/src/daemon/message-types/upgrades.ts +8 -0
  146. package/src/daemon/server.ts +30 -92
  147. package/src/events/domain-events.ts +2 -1
  148. package/src/followups/followup-store.ts +5 -2
  149. package/src/inbound/platform-callback-registration.ts +3 -3
  150. package/src/instrument.ts +8 -5
  151. package/src/memory/conversation-crud.ts +0 -236
  152. package/src/memory/conversation-title-service.ts +76 -11
  153. package/src/memory/db-init.ts +15 -11
  154. package/src/memory/indexer.ts +15 -106
  155. package/src/memory/items-extractor.ts +15 -1
  156. package/src/memory/job-handlers/conversation-starters.ts +4 -1
  157. package/src/memory/job-handlers/embedding.ts +0 -79
  158. package/src/memory/job-utils.ts +1 -1
  159. package/src/memory/jobs-store.ts +30 -13
  160. package/src/memory/jobs-worker.ts +31 -27
  161. package/src/memory/migrations/001-job-deferrals.ts +19 -0
  162. package/src/memory/migrations/004-entity-relation-dedup.ts +10 -0
  163. package/src/memory/migrations/005-fingerprint-scope-unique.ts +76 -0
  164. package/src/memory/migrations/006-scope-salted-fingerprints.ts +50 -0
  165. package/src/memory/migrations/007-assistant-id-to-self.ts +10 -0
  166. package/src/memory/migrations/008-remove-assistant-id-columns.ts +34 -0
  167. package/src/memory/migrations/009-llm-usage-events-drop-assistant-id.ts +26 -0
  168. package/src/memory/migrations/014-backfill-inbox-thread-state.ts +10 -0
  169. package/src/memory/migrations/015-drop-active-search-index.ts +17 -0
  170. package/src/memory/migrations/019-notification-tables-schema-migration.ts +12 -0
  171. package/src/memory/migrations/020-rename-macos-ios-channel-to-vellum.ts +121 -0
  172. package/src/memory/migrations/024-embedding-vector-blob.ts +74 -0
  173. package/src/memory/migrations/026a-embeddings-nullable-vector-json.ts +82 -0
  174. package/src/memory/migrations/036-normalize-phone-identities.ts +11 -0
  175. package/src/memory/migrations/116-messages-fts.ts +106 -1
  176. package/src/memory/migrations/126-backfill-guardian-principal-id.ts +52 -0
  177. package/src/memory/migrations/127-guardian-principal-id-not-null.ts +77 -0
  178. package/src/memory/migrations/134-contacts-notes-column.ts +13 -0
  179. package/src/memory/migrations/135-backfill-contact-interaction-stats.ts +20 -0
  180. package/src/memory/migrations/136-drop-assistant-id-columns.ts +52 -0
  181. package/src/memory/migrations/140-backfill-usage-cache-accounting.ts +13 -0
  182. package/src/memory/migrations/141-rename-verification-table.ts +54 -0
  183. package/src/memory/migrations/142-rename-verification-session-id-column.ts +25 -0
  184. package/src/memory/migrations/143-rename-guardian-verification-values.ts +35 -0
  185. package/src/memory/migrations/144-rename-voice-to-phone.ts +136 -0
  186. package/src/memory/migrations/145-drop-accounts-table.ts +32 -0
  187. package/src/memory/migrations/147-migrate-reminders-to-schedules.ts +14 -1
  188. package/src/memory/migrations/148-drop-reminders-table.ts +35 -1
  189. package/src/memory/migrations/150-oauth-apps-client-secret-path.ts +69 -1
  190. package/src/memory/migrations/162-guardian-timestamps-epoch-ms.ts +290 -0
  191. package/src/memory/migrations/169-rename-gmail-provider-key-to-google.ts +51 -1
  192. package/src/memory/migrations/174-rename-thread-starters-table.ts +47 -1
  193. package/src/memory/migrations/176-drop-capability-card-state.ts +13 -0
  194. package/src/memory/migrations/180-backfill-inline-attachments-to-disk.ts +16 -0
  195. package/src/memory/migrations/181-rename-thread-starters-checkpoints.ts +28 -1
  196. package/src/memory/migrations/189-drop-simplified-memory.ts +42 -0
  197. package/src/memory/migrations/190-call-session-skip-disclosure.ts +15 -0
  198. package/src/memory/migrations/191-backfill-audio-attachment-mime-types.ts +64 -0
  199. package/src/memory/migrations/192-contacts-user-file-column.ts +15 -0
  200. package/src/memory/migrations/index.ts +5 -3
  201. package/src/memory/migrations/registry.ts +90 -0
  202. package/src/memory/migrations/validate-migration-state.ts +137 -11
  203. package/src/memory/qdrant-circuit-breaker.ts +9 -0
  204. package/src/memory/qdrant-client.ts +4 -6
  205. package/src/memory/qdrant-manager.ts +64 -7
  206. package/src/memory/schema/calls.ts +1 -0
  207. package/src/memory/schema/contacts.ts +1 -0
  208. package/src/memory/schema/conversations.ts +0 -3
  209. package/src/memory/schema/index.ts +0 -2
  210. package/src/messaging/draft-store.ts +2 -2
  211. package/src/notifications/decision-engine.ts +4 -1
  212. package/src/oauth/connection-resolver.ts +6 -4
  213. package/src/permissions/checker.ts +0 -38
  214. package/src/permissions/defaults.ts +3 -3
  215. package/src/permissions/shell-identity.ts +76 -22
  216. package/src/permissions/trust-client.ts +2 -13
  217. package/src/permissions/trust-store.ts +8 -3
  218. package/src/permissions/types.ts +4 -2
  219. package/src/platform/client.ts +35 -7
  220. package/src/prompts/persona-resolver.ts +138 -0
  221. package/src/prompts/system-prompt.ts +36 -4
  222. package/src/prompts/templates/users/default.md +1 -0
  223. package/src/providers/registry.ts +27 -40
  224. package/src/runtime/auth/__tests__/credential-service.test.ts +0 -1
  225. package/src/runtime/auth/__tests__/external-assistant-id.test.ts +13 -68
  226. package/src/runtime/auth/external-assistant-id.ts +13 -59
  227. package/src/runtime/auth/route-policy.ts +29 -1
  228. package/src/runtime/auth/token-service.ts +53 -15
  229. package/src/runtime/channel-readiness-service.ts +1 -16
  230. package/src/runtime/http-server.ts +29 -2
  231. package/src/runtime/middleware/error-handler.ts +1 -9
  232. package/src/runtime/routes/audio-routes.ts +40 -0
  233. package/src/runtime/routes/btw-routes.ts +0 -17
  234. package/src/runtime/routes/conversation-management-routes.ts +0 -36
  235. package/src/runtime/routes/conversation-query-routes.ts +106 -2
  236. package/src/runtime/routes/conversation-routes.ts +4 -43
  237. package/src/runtime/routes/diagnostics-routes.ts +1 -477
  238. package/src/runtime/routes/identity-routes.ts +18 -29
  239. package/src/runtime/routes/inbound-stages/secret-ingress-check.ts +4 -33
  240. package/src/runtime/routes/inbound-stages/transcribe-audio.test.ts +1 -1
  241. package/src/runtime/routes/integrations/vercel.ts +89 -0
  242. package/src/runtime/routes/log-export-routes.ts +5 -0
  243. package/src/runtime/routes/memory-item-routes.test.ts +221 -3
  244. package/src/runtime/routes/memory-item-routes.ts +144 -4
  245. package/src/runtime/routes/migration-rollback-routes.ts +209 -0
  246. package/src/runtime/routes/migration-routes.ts +17 -1
  247. package/src/runtime/routes/notification-routes.ts +58 -0
  248. package/src/runtime/routes/schedule-routes.ts +65 -0
  249. package/src/runtime/routes/settings-routes.ts +41 -1
  250. package/src/runtime/routes/tts-routes.ts +86 -0
  251. package/src/runtime/routes/upgrade-broadcast-routes.ts +175 -0
  252. package/src/runtime/routes/workspace-commit-routes.ts +62 -0
  253. package/src/runtime/routes/workspace-routes.test.ts +22 -1
  254. package/src/runtime/routes/workspace-routes.ts +1 -1
  255. package/src/runtime/routes/workspace-utils.ts +86 -2
  256. package/src/schedule/schedule-store.ts +0 -21
  257. package/src/security/ces-credential-client.ts +59 -22
  258. package/src/security/ces-rpc-credential-backend.ts +85 -0
  259. package/src/security/credential-backend.ts +12 -88
  260. package/src/security/keychain-broker-client.ts +10 -2
  261. package/src/security/secure-keys.ts +94 -113
  262. package/src/skills/catalog-install.ts +13 -7
  263. package/src/skills/inline-command-render.ts +5 -1
  264. package/src/skills/inline-command-runner.ts +30 -2
  265. package/src/telemetry/usage-telemetry-reporter.ts +4 -2
  266. package/src/tools/calls/call-start.ts +1 -0
  267. package/src/tools/executor.ts +0 -4
  268. package/src/tools/memory/handlers.ts +1 -129
  269. package/src/tools/network/script-proxy/session-manager.ts +19 -4
  270. package/src/tools/network/web-fetch.ts +3 -1
  271. package/src/tools/permission-checker.ts +18 -0
  272. package/src/tools/skills/execute.ts +1 -1
  273. package/src/tools/skills/load.ts +9 -2
  274. package/src/tools/types.ts +0 -8
  275. package/src/util/errors.ts +0 -12
  276. package/src/util/platform.ts +8 -55
  277. package/src/util/xml.ts +8 -0
  278. package/src/workspace/git-service.ts +5 -2
  279. package/src/workspace/heartbeat-service.ts +5 -24
  280. package/src/workspace/migrations/001-avatar-rename.ts +15 -0
  281. package/src/workspace/migrations/003-seed-device-id.ts +17 -1
  282. package/src/workspace/migrations/004-extract-collect-usage-data.ts +33 -0
  283. package/src/workspace/migrations/005-add-send-diagnostics.ts +3 -0
  284. package/src/workspace/migrations/006-services-config.ts +49 -0
  285. package/src/workspace/migrations/007-web-search-provider-rename.ts +27 -0
  286. package/src/workspace/migrations/008-voice-timeout-and-max-steps.ts +3 -0
  287. package/src/workspace/migrations/009-backfill-conversation-disk-view.ts +4 -0
  288. package/src/workspace/migrations/010-app-dir-rename.ts +78 -0
  289. package/src/workspace/migrations/011-backfill-installation-id.ts +11 -0
  290. package/src/workspace/migrations/012-rename-conversation-disk-view-dirs.ts +44 -0
  291. package/src/workspace/migrations/013-repair-conversation-disk-view.ts +5 -0
  292. package/src/workspace/migrations/015-migrate-credentials-to-keychain.ts +153 -0
  293. package/src/workspace/migrations/016-extract-feature-flags-to-protected.ts +156 -0
  294. package/src/workspace/migrations/016-migrate-credentials-from-keychain.ts +150 -0
  295. package/src/workspace/migrations/017-seed-persona-dirs.ts +95 -0
  296. package/src/workspace/migrations/migrate-to-workspace-volume.ts +23 -1
  297. package/src/workspace/migrations/registry.ts +8 -0
  298. package/src/workspace/migrations/runner.ts +106 -2
  299. package/src/workspace/migrations/types.ts +4 -0
  300. package/src/__tests__/archive-recall.test.ts +0 -560
  301. package/src/__tests__/claude-code-skill-regression.test.ts +0 -206
  302. package/src/__tests__/claude-code-tool-profiles.test.ts +0 -99
  303. package/src/__tests__/conversation-memory-dirty-tail.test.ts +0 -150
  304. package/src/__tests__/conversation-switch-memory-reduction.test.ts +0 -474
  305. package/src/__tests__/db-memory-archive-migration.test.ts +0 -372
  306. package/src/__tests__/db-memory-brief-state-migration.test.ts +0 -213
  307. package/src/__tests__/db-memory-reducer-checkpoints.test.ts +0 -273
  308. package/src/__tests__/diagnostics-export.test.ts +0 -288
  309. package/src/__tests__/local-gateway-health.test.ts +0 -209
  310. package/src/__tests__/memory-brief-open-loops.test.ts +0 -530
  311. package/src/__tests__/memory-brief-time.test.ts +0 -285
  312. package/src/__tests__/memory-brief-wrapper.test.ts +0 -311
  313. package/src/__tests__/memory-chunk-archive.test.ts +0 -400
  314. package/src/__tests__/memory-chunk-dual-write.test.ts +0 -453
  315. package/src/__tests__/memory-episode-archive.test.ts +0 -370
  316. package/src/__tests__/memory-episode-dual-write.test.ts +0 -626
  317. package/src/__tests__/memory-observation-archive.test.ts +0 -375
  318. package/src/__tests__/memory-observation-dual-write.test.ts +0 -318
  319. package/src/__tests__/memory-reducer-job.test.ts +0 -538
  320. package/src/__tests__/memory-reducer-scheduling.test.ts +0 -473
  321. package/src/__tests__/memory-reducer-store.test.ts +0 -728
  322. package/src/__tests__/memory-reducer-types.test.ts +0 -707
  323. package/src/__tests__/memory-reducer.test.ts +0 -704
  324. package/src/__tests__/memory-simplified-config.test.ts +0 -281
  325. package/src/__tests__/secret-ingress-handler.test.ts +0 -120
  326. package/src/__tests__/simplified-memory-e2e.test.ts +0 -666
  327. package/src/__tests__/simplified-memory-runtime.test.ts +0 -616
  328. package/src/__tests__/swarm-conversation-integration.test.ts +0 -358
  329. package/src/__tests__/swarm-dag-pathological.test.ts +0 -547
  330. package/src/__tests__/swarm-orchestrator.test.ts +0 -463
  331. package/src/__tests__/swarm-plan-validator.test.ts +0 -384
  332. package/src/__tests__/swarm-recursion.test.ts +0 -197
  333. package/src/__tests__/swarm-router-planner.test.ts +0 -234
  334. package/src/__tests__/swarm-tool.test.ts +0 -185
  335. package/src/__tests__/swarm-worker-backend.test.ts +0 -144
  336. package/src/__tests__/swarm-worker-runner.test.ts +0 -288
  337. package/src/commands/__tests__/cc-command-registry.test.ts +0 -396
  338. package/src/commands/cc-command-registry.ts +0 -248
  339. package/src/config/bundled-skills/claude-code/SKILL.md +0 -53
  340. package/src/config/bundled-skills/claude-code/TOOLS.json +0 -47
  341. package/src/config/bundled-skills/claude-code/tools/claude-code.ts +0 -12
  342. package/src/config/bundled-skills/orchestration/SKILL.md +0 -33
  343. package/src/config/bundled-skills/orchestration/TOOLS.json +0 -35
  344. package/src/config/bundled-skills/orchestration/tools/swarm-delegate.ts +0 -12
  345. package/src/config/schemas/memory-simplified.ts +0 -101
  346. package/src/config/schemas/swarm.ts +0 -82
  347. package/src/logfire.ts +0 -135
  348. package/src/memory/archive-recall.ts +0 -516
  349. package/src/memory/archive-store.ts +0 -400
  350. package/src/memory/brief-formatting.ts +0 -33
  351. package/src/memory/brief-open-loops.ts +0 -266
  352. package/src/memory/brief-time.ts +0 -162
  353. package/src/memory/brief.ts +0 -75
  354. package/src/memory/job-handlers/backfill-simplified-memory.ts +0 -462
  355. package/src/memory/job-handlers/reduce-conversation-memory.ts +0 -229
  356. package/src/memory/migrations/185-memory-brief-state.ts +0 -52
  357. package/src/memory/migrations/186-memory-archive.ts +0 -109
  358. package/src/memory/migrations/187-memory-reducer-checkpoints.ts +0 -19
  359. package/src/memory/reducer-scheduler.ts +0 -242
  360. package/src/memory/reducer-store.ts +0 -271
  361. package/src/memory/reducer-types.ts +0 -106
  362. package/src/memory/reducer.ts +0 -467
  363. package/src/memory/schema/memory-archive.ts +0 -121
  364. package/src/memory/schema/memory-brief.ts +0 -55
  365. package/src/runtime/local-gateway-health.ts +0 -275
  366. package/src/security/secret-ingress.ts +0 -68
  367. package/src/swarm/backend-claude-code.ts +0 -225
  368. package/src/swarm/checkpoint.ts +0 -137
  369. package/src/swarm/graph-utils.ts +0 -53
  370. package/src/swarm/index.ts +0 -55
  371. package/src/swarm/limits.ts +0 -66
  372. package/src/swarm/orchestrator.ts +0 -424
  373. package/src/swarm/plan-validator.ts +0 -117
  374. package/src/swarm/router-planner.ts +0 -162
  375. package/src/swarm/router-prompts.ts +0 -39
  376. package/src/swarm/synthesizer.ts +0 -81
  377. package/src/swarm/types.ts +0 -72
  378. package/src/swarm/worker-backend.ts +0 -131
  379. package/src/swarm/worker-prompts.ts +0 -80
  380. package/src/swarm/worker-runner.ts +0 -170
  381. package/src/tools/claude-code/claude-code.ts +0 -610
  382. package/src/tools/swarm/delegate.ts +0 -205
@@ -137,6 +137,24 @@ export class PermissionChecker {
137
137
  }
138
138
 
139
139
  if (result.decision === "prompt") {
140
+ // dangerouslySkipPermissions: when enabled, auto-approve all prompts
141
+ // without user interaction. Deny rules are still respected (they
142
+ // return before reaching this block).
143
+ //
144
+ // Note: unlike guardian auto-approve and temporary overrides below,
145
+ // this intentionally does NOT check `context.requireFreshApproval`.
146
+ // The setting is designed to skip ALL interactive prompts
147
+ // unconditionally — it is an explicit operator opt-out from the
148
+ // permission system, so requireFreshApproval does not apply.
149
+ const cfg = getConfig();
150
+ if (cfg.permissions.dangerouslySkipPermissions) {
151
+ log.info(
152
+ { toolName: name, riskLevel },
153
+ "dangerouslySkipPermissions active — auto-approving without prompt",
154
+ );
155
+ return { allowed: true, decision: "dangerously_skip_permissions", riskLevel };
156
+ }
157
+
140
158
  // Guardian-trust sessions (e.g. scheduled jobs, reminders) should be
141
159
  // able to use bundled tools without interactive approval. The guardian
142
160
  // is the owner - prompting makes no sense when there is no client.
@@ -30,7 +30,7 @@ export class SkillExecuteTool implements Tool {
30
30
  activity: {
31
31
  type: "string",
32
32
  description:
33
- "Brief non-technical explanation of what you are doing and why, shown to the user as a status update.",
33
+ "Brief non-technical explanation of what you are doing and why, shown to the user as a progress update.",
34
34
  },
35
35
  },
36
36
  required: ["tool", "input", "activity"],
@@ -440,9 +440,16 @@ export class SkillLoadTool implements Tool {
440
440
  "Rendered inline command expansions for included skill",
441
441
  );
442
442
  } catch (err) {
443
- log.warn(
443
+ log.error(
444
444
  { err, skillId: childId, parentSkillId: skill.id },
445
- "Failed to render inline commands for included skill, using raw body",
445
+ "Failed to render inline commands for included skill; falling back to sanitized body",
446
+ );
447
+ // Strip raw !`...` inline command tokens so they don't leak into
448
+ // the prompt. Replace with a safe stub to maintain fail-closed
449
+ // contract for raw tokens while still isolating child failures.
450
+ childBody = childBody.replace(
451
+ /!`[^`]*`/g,
452
+ "[inline command unavailable]",
446
453
  );
447
454
  }
448
455
  }
@@ -118,14 +118,6 @@ export interface ToolContext {
118
118
  proxyToolResolver?: ProxyToolResolver;
119
119
  /** When set, only tools in this set may execute. Tools outside the set are blocked with an error. */
120
120
  allowedToolNames?: Set<string>;
121
- /** Request user confirmation for a sub-tool operation (used by claude_code tool). */
122
- requestConfirmation?: (req: {
123
- toolName: string;
124
- input: Record<string, unknown>;
125
- riskLevel: string;
126
- executionTarget?: ExecutionTarget;
127
- principal?: string;
128
- }) => Promise<{ decision: "allow" | "deny" }>;
129
121
  /** Prompt the user for a secret value via native SecureField UI. */
130
122
  requestSecret?: (params: {
131
123
  service: string;
@@ -20,9 +20,6 @@ export enum ErrorCode {
20
20
  // WASM integrity check failures
21
21
  INTEGRITY_ERROR = "INTEGRITY_ERROR",
22
22
 
23
- // Secret detected in inbound content
24
- INGRESS_BLOCKED = "INGRESS_BLOCKED",
25
-
26
23
  // Internal/unexpected errors
27
24
  INTERNAL_ERROR = "INTERNAL_ERROR",
28
25
  }
@@ -178,12 +175,3 @@ export class IntegrityError extends AssistantError {
178
175
  }
179
176
  }
180
177
 
181
- export class IngressBlockedError extends AssistantError {
182
- constructor(
183
- message: string,
184
- public readonly detectedTypes: string[],
185
- ) {
186
- super(message, ErrorCode.INGRESS_BLOCKED);
187
- this.name = "IngressBlockedError";
188
- }
189
- }
@@ -3,7 +3,6 @@ import {
3
3
  existsSync,
4
4
  mkdirSync,
5
5
  readFileSync,
6
- writeFileSync,
7
6
  } from "node:fs";
8
7
  import { homedir } from "node:os";
9
8
  import { join } from "node:path";
@@ -45,25 +44,6 @@ export function getClipboardCommand(): string | null {
45
44
  return null;
46
45
  }
47
46
 
48
- /**
49
- * Read and parse the lockfile (~/.vellum.lock.json).
50
- * Respects BASE_DATA_DIR for non-standard home directories.
51
- * Returns null if the file doesn't exist or is malformed.
52
- */
53
- export function readLockfile(): Record<string, unknown> | null {
54
- const base = getBaseDataDir() || homedir();
55
- const lockPath = join(base, ".vellum.lock.json");
56
- if (!existsSync(lockPath)) return null;
57
- try {
58
- const raw = JSON.parse(readFileSync(lockPath, "utf-8"));
59
- if (raw && typeof raw === "object" && !Array.isArray(raw)) {
60
- return raw as Record<string, unknown>;
61
- }
62
- } catch {
63
- // malformed JSON
64
- }
65
- return null;
66
- }
67
47
 
68
48
  /**
69
49
  * Resolve the instance data directory from the lockfile.
@@ -155,45 +135,18 @@ export function resolveInstanceDataDir(): string | undefined {
155
135
  * (see migration 007-assistant-id-to-self). However, the desktop UI
156
136
  * sends the real assistant ID (e.g., "vellum-true-eel") while the
157
137
  * inbound call path resolves phone numbers to config keys (typically
158
- * "self"). This function maps any known lockfile assistant ID to "self"
138
+ * "self"). This function maps the current assistant's ID to "self"
159
139
  * so both sides use a consistent DB key.
160
- *
161
- * Multi-instance safety: each daemon process runs with a scoped
162
- * BASE_DATA_DIR, so readLockfile() only sees the lockfile for this
163
- * instance. The mapping to "self" is correct because each daemon is
164
- * single-tenant — it only manages its own instance's data.
165
140
  */
166
141
  export function normalizeAssistantId(assistantId: string): string {
167
142
  if (assistantId === "self") return "self";
168
143
 
169
- try {
170
- const lockData = readLockfile();
171
- const assistants = lockData?.assistants as
172
- | Array<Record<string, unknown>>
173
- | undefined;
174
- if (assistants) {
175
- for (const entry of assistants) {
176
- if (entry.assistantId === assistantId) return "self";
177
- }
178
- }
179
- } catch {
180
- // lockfile unreadable — return as-is
181
- }
144
+ const ownName = process.env.VELLUM_ASSISTANT_NAME;
145
+ if (ownName && assistantId === ownName) return "self";
182
146
 
183
147
  return assistantId;
184
148
  }
185
149
 
186
- /**
187
- * Write data to the primary lockfile (~/.vellum.lock.json).
188
- * Respects BASE_DATA_DIR for non-standard home directories.
189
- */
190
- export function writeLockfile(data: Record<string, unknown>): void {
191
- const base = getBaseDataDir() || homedir();
192
- writeFileSync(
193
- join(base, ".vellum.lock.json"),
194
- JSON.stringify(data, null, 2) + "\n",
195
- );
196
- }
197
150
 
198
151
  /**
199
152
  * Returns the root ~/.vellum directory. User-facing files (config, prompt
@@ -436,11 +389,11 @@ export function ensureDataDir(): void {
436
389
  const dirs = [
437
390
  // Root-level dirs (runtime)
438
391
  root,
439
- // protected, signals, hooks are local-only skip in containerized mode
440
- // (credentials via CES HTTP API, trust via gateway API, no IPC signals)
441
- ...(containerized
442
- ? []
443
- : [join(root, "protected"), join(root, "signals"), join(root, "hooks")]),
392
+ // signals dir is needed everywhere (MCP reload, user-message signals)
393
+ join(root, "signals"),
394
+ // protected, hooks are local-only — skip in containerized mode
395
+ // (credentials via CES HTTP API, trust via gateway API)
396
+ ...(containerized ? [] : [join(root, "protected"), join(root, "hooks")]),
444
397
  // Workspace dirs
445
398
  workspace,
446
399
  join(workspace, "skills"),
package/src/util/xml.ts CHANGED
@@ -6,3 +6,11 @@ export function escapeXmlAttr(s: string): string {
6
6
  .replace(/</g, "&lt;")
7
7
  .replace(/>/g, "&gt;");
8
8
  }
9
+
10
+ /** Escape a string for safe inclusion as XML/HTML text content. */
11
+ export function escapeXmlContent(s: string): string {
12
+ return s
13
+ .replace(/&/g, "&amp;")
14
+ .replace(/</g, "&lt;")
15
+ .replace(/>/g, "&gt;");
16
+ }
@@ -753,8 +753,11 @@ export class WorkspaceGitService {
753
753
  * Must be called with the mutex lock held.
754
754
  */
755
755
  private async ensureCommitIdentityLocked(): Promise<void> {
756
- await this.execGit(["config", "user.name", "Vellum Assistant"]);
757
- await this.execGit(["config", "user.email", "assistant@vellum.ai"]);
756
+ const gitName = process.env.ASSISTANT_GIT_USER_NAME || "Vellum Assistant";
757
+ const gitEmail =
758
+ process.env.ASSISTANT_GIT_USER_EMAIL || "assistant@vellum.ai";
759
+ await this.execGit(["config", "user.name", gitName]);
760
+ await this.execGit(["config", "user.email", gitEmail]);
758
761
  }
759
762
 
760
763
  /**
@@ -210,13 +210,11 @@ export class WorkspaceHeartbeatService {
210
210
 
211
211
  try {
212
212
  const now = this.now();
213
- let shutdownFiles: string[] = [];
214
213
  const { committed } = await service.commitIfDirty(
215
214
  (st) => {
216
215
  const uniqueFiles = [
217
216
  ...new Set([...st.staged, ...st.modified, ...st.untracked]),
218
217
  ];
219
- shutdownFiles = uniqueFiles;
220
218
  log.info(
221
219
  { workspaceDir, totalChanges: uniqueFiles.length },
222
220
  "Committing pending changes on shutdown",
@@ -237,28 +235,11 @@ export class WorkspaceHeartbeatService {
237
235
  if (committed) {
238
236
  firstSeenDirty.delete(workspaceDir);
239
237
  result.committed++;
240
-
241
- // Fire-and-forget enrichment
242
- try {
243
- const commitHash = await service.getHeadHash();
244
- const shutdownCtx: CommitContext = {
245
- workspaceDir,
246
- trigger: "shutdown",
247
- changedFiles: shutdownFiles,
248
- timestampMs: this.now(),
249
- };
250
- getEnrichmentService().enqueue({
251
- workspaceDir,
252
- commitHash,
253
- context: shutdownCtx,
254
- gitService: service,
255
- });
256
- } catch (enrichErr) {
257
- log.debug(
258
- { enrichErr },
259
- "Failed to enqueue shutdown enrichment (non-fatal)",
260
- );
261
- }
238
+ // Skip enrichment for shutdown commits — the enrichment queue is
239
+ // about to be shut down anyway, and the fire-and-forget writeNote()
240
+ // can race with subsequent commitAllPending() calls (the async
241
+ // git-notes operation acquires the mutex and may leave behind an
242
+ // index.lock on some git versions, causing the next commit to fail).
262
243
  } else {
263
244
  result.skipped++;
264
245
  }
@@ -22,4 +22,19 @@ export const avatarRenameMigration: WorkspaceMigration = {
22
22
  renameSync(oldTraits, newTraits);
23
23
  }
24
24
  },
25
+ down(workspaceDir: string): void {
26
+ const avatarDir = join(workspaceDir, "data", "avatar");
27
+
28
+ const newImage = join(avatarDir, "avatar-image.png");
29
+ const oldImage = join(avatarDir, "custom-avatar.png");
30
+ if (existsSync(newImage) && !existsSync(oldImage)) {
31
+ renameSync(newImage, oldImage);
32
+ }
33
+
34
+ const newTraits = join(avatarDir, "character-traits.json");
35
+ const oldTraits = join(avatarDir, "avatar-components.json");
36
+ if (existsSync(newTraits) && !existsSync(oldTraits)) {
37
+ renameSync(newTraits, oldTraits);
38
+ }
39
+ },
25
40
  };
@@ -1,4 +1,10 @@
1
- import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
1
+ import {
2
+ existsSync,
3
+ mkdirSync,
4
+ readFileSync,
5
+ unlinkSync,
6
+ writeFileSync,
7
+ } from "node:fs";
2
8
  import { join } from "node:path";
3
9
 
4
10
  import { getDeviceIdBaseDir } from "../../util/device-id.js";
@@ -97,4 +103,14 @@ export const seedDeviceIdMigration: WorkspaceMigration = {
97
103
  // Best-effort — getDeviceId() will generate a new one if this fails.
98
104
  }
99
105
  },
106
+ down(_workspaceDir: string): void {
107
+ // The forward migration seeds deviceId in ~/.vellum/device.json from the
108
+ // lockfile. Reverse by removing device.json entirely — getDeviceId() will
109
+ // generate a fresh one on next startup if needed.
110
+ const base = getDeviceIdBaseDir();
111
+ const devicePath = join(base, ".vellum", "device.json");
112
+ if (existsSync(devicePath)) {
113
+ unlinkSync(devicePath);
114
+ }
115
+ },
100
116
  };
@@ -45,6 +45,39 @@ export const extractCollectUsageDataMigration: WorkspaceMigration = {
45
45
  delete config.assistantFeatureFlagValues;
46
46
  }
47
47
 
48
+ writeFileSync(configPath, JSON.stringify(config, null, 2) + "\n");
49
+ },
50
+ down(workspaceDir: string): void {
51
+ const configPath = join(workspaceDir, "config.json");
52
+ if (!existsSync(configPath)) return;
53
+
54
+ let config: Record<string, unknown>;
55
+ try {
56
+ const raw = JSON.parse(readFileSync(configPath, "utf-8"));
57
+ if (!raw || typeof raw !== "object" || Array.isArray(raw)) return;
58
+ config = raw as Record<string, unknown>;
59
+ } catch {
60
+ return;
61
+ }
62
+
63
+ // Only reverse if collectUsageData was explicitly set to false
64
+ // (the forward migration only persisted false).
65
+ if (!("collectUsageData" in config)) return;
66
+ const value = config.collectUsageData;
67
+ if (typeof value !== "boolean") return;
68
+
69
+ // Restore the feature flag value
70
+ const FLAG_KEY = "feature_flags.collect-usage-data.enabled";
71
+ const flagValues = (config.assistantFeatureFlagValues ?? {}) as Record<
72
+ string,
73
+ unknown
74
+ >;
75
+ flagValues[FLAG_KEY] = value;
76
+ config.assistantFeatureFlagValues = flagValues;
77
+
78
+ // Remove the extracted top-level key
79
+ delete config.collectUsageData;
80
+
48
81
  writeFileSync(configPath, JSON.stringify(config, null, 2) + "\n");
49
82
  },
50
83
  };
@@ -9,4 +9,7 @@ export const addSendDiagnosticsMigration: WorkspaceMigration = {
9
9
  // will sync the UserDefaults value on first startup. This migration exists
10
10
  // as a checkpoint marker for future reference.
11
11
  },
12
+ down(_workspaceDir: string): void {
13
+ // No-op — the forward migration is a checkpoint marker with no data changes.
14
+ },
12
15
  };
@@ -132,6 +132,55 @@ export const servicesConfigMigration: WorkspaceMigration = {
132
132
  delete config.imageGenModel;
133
133
  delete config.webSearchProvider;
134
134
 
135
+ writeFileSync(configPath, JSON.stringify(config, null, 2) + "\n");
136
+ },
137
+ down(workspaceDir: string): void {
138
+ const configPath = join(workspaceDir, "config.json");
139
+ if (!existsSync(configPath)) return;
140
+
141
+ let config: Record<string, unknown>;
142
+ try {
143
+ const raw = JSON.parse(readFileSync(configPath, "utf-8"));
144
+ if (!raw || typeof raw !== "object" || Array.isArray(raw)) return;
145
+ config = raw as Record<string, unknown>;
146
+ } catch {
147
+ return;
148
+ }
149
+
150
+ const services = config.services;
151
+ if (!services || typeof services !== "object" || Array.isArray(services))
152
+ return;
153
+
154
+ const svc = services as Record<string, Record<string, unknown>>;
155
+
156
+ // Extract inference provider and model back to top-level fields.
157
+ // Note: inferenceMode is lost in this rollback — the original config did
158
+ // not store a mode field. This is an accepted lossy reversal.
159
+ if (svc.inference) {
160
+ if (typeof svc.inference.provider === "string") {
161
+ config.provider = svc.inference.provider;
162
+ }
163
+ if (typeof svc.inference.model === "string") {
164
+ config.model = svc.inference.model;
165
+ }
166
+ }
167
+
168
+ // Extract image generation model back to top-level
169
+ if (svc["image-generation"]) {
170
+ if (typeof svc["image-generation"].model === "string") {
171
+ config.imageGenModel = svc["image-generation"].model;
172
+ }
173
+ }
174
+
175
+ // Extract web search provider back to top-level
176
+ if (svc["web-search"]) {
177
+ if (typeof svc["web-search"].provider === "string") {
178
+ config.webSearchProvider = svc["web-search"].provider;
179
+ }
180
+ }
181
+
182
+ delete config.services;
183
+
135
184
  writeFileSync(configPath, JSON.stringify(config, null, 2) + "\n");
136
185
  },
137
186
  };
@@ -34,4 +34,31 @@ export const webSearchProviderRenameMigration: WorkspaceMigration = {
34
34
  ws.provider = "inference-provider-native";
35
35
  writeFileSync(configPath, JSON.stringify(config, null, 2) + "\n");
36
36
  },
37
+ down(workspaceDir: string): void {
38
+ const configPath = join(workspaceDir, "config.json");
39
+ if (!existsSync(configPath)) return;
40
+
41
+ let config: Record<string, unknown>;
42
+ try {
43
+ const raw = JSON.parse(readFileSync(configPath, "utf-8"));
44
+ if (!raw || typeof raw !== "object" || Array.isArray(raw)) return;
45
+ config = raw as Record<string, unknown>;
46
+ } catch {
47
+ return;
48
+ }
49
+
50
+ const services = config.services;
51
+ if (!services || typeof services !== "object" || Array.isArray(services))
52
+ return;
53
+
54
+ const webSearch = (services as Record<string, unknown>)["web-search"];
55
+ if (!webSearch || typeof webSearch !== "object" || Array.isArray(webSearch))
56
+ return;
57
+
58
+ const ws = webSearch as Record<string, unknown>;
59
+ if (ws.provider !== "inference-provider-native") return;
60
+
61
+ ws.provider = "anthropic-native";
62
+ writeFileSync(configPath, JSON.stringify(config, null, 2) + "\n");
63
+ },
37
64
  };
@@ -9,4 +9,7 @@ export const voiceTimeoutAndMaxStepsMigration: WorkspaceMigration = {
9
9
  // Existing users: macOS client will sync UserDefaults values
10
10
  // to config on next startup via settings sync endpoints.
11
11
  },
12
+ down(_workspaceDir: string): void {
13
+ // No-op — the forward migration is a checkpoint marker with no data changes.
14
+ },
12
15
  };
@@ -7,4 +7,8 @@ export const backfillConversationDiskViewMigration: WorkspaceMigration = {
7
7
  run(_workspaceDir: string): void {
8
8
  rebuildConversationDiskViewFromDb();
9
9
  },
10
+ // No-op: the disk view is a derived cache that can be regenerated from the
11
+ // database at any time. Removing it would only cause unnecessary I/O churn
12
+ // since the next forward migration (or startup rebuild) will recreate it.
13
+ down(_workspaceDir: string): void {},
10
14
  };
@@ -76,6 +76,84 @@ export const appDirRenameMigration: WorkspaceMigration = {
76
76
  description:
77
77
  "Rename UUID-based app directories and files to human-readable slugified names",
78
78
 
79
+ down(workspaceDir: string): void {
80
+ const appsDir = join(workspaceDir, "data", "apps");
81
+ if (!existsSync(appsDir)) return;
82
+
83
+ const jsonFiles = readdirSync(appsDir)
84
+ .filter((f) => f.endsWith(".json"))
85
+ .sort();
86
+
87
+ if (jsonFiles.length === 0) return;
88
+
89
+ for (const jsonFile of jsonFiles) {
90
+ const jsonPath = join(appsDir, jsonFile);
91
+ let raw: string;
92
+ try {
93
+ raw = readFileSync(jsonPath, "utf-8");
94
+ } catch {
95
+ continue;
96
+ }
97
+
98
+ let parsed: {
99
+ id?: string;
100
+ name?: string;
101
+ dirName?: string;
102
+ };
103
+ try {
104
+ parsed = JSON.parse(raw);
105
+ } catch {
106
+ continue;
107
+ }
108
+
109
+ const appId = parsed.id;
110
+ if (!appId || !parsed.dirName || !isValidDirName(parsed.dirName)) {
111
+ continue;
112
+ }
113
+
114
+ const dirName = parsed.dirName;
115
+
116
+ // 1. Rename the app directory: {dirName}/ -> {appId}/
117
+ const slugDir = join(appsDir, dirName);
118
+ const uuidDir = join(appsDir, appId);
119
+ if (existsSync(slugDir) && !existsSync(uuidDir) && slugDir !== uuidDir) {
120
+ renameSync(slugDir, uuidDir);
121
+ }
122
+
123
+ // 2. Rename the preview file: {dirName}.preview -> {appId}.preview
124
+ const slugPreview = join(appsDir, `${dirName}.preview`);
125
+ const uuidPreview = join(appsDir, `${appId}.preview`);
126
+ if (
127
+ existsSync(slugPreview) &&
128
+ !existsSync(uuidPreview) &&
129
+ slugPreview !== uuidPreview
130
+ ) {
131
+ renameSync(slugPreview, uuidPreview);
132
+ }
133
+
134
+ // 3. Remove dirName from JSON and rename file: {dirName}.json -> {appId}.json
135
+ const updatedParsed = { ...parsed };
136
+ delete updatedParsed.dirName;
137
+ const updatedJson = JSON.stringify(updatedParsed, null, 2);
138
+
139
+ const uuidJsonFile = `${appId}.json`;
140
+ const uuidJsonPath = join(appsDir, uuidJsonFile);
141
+
142
+ if (jsonFile !== uuidJsonFile) {
143
+ writeFileSync(uuidJsonPath, updatedJson, "utf-8");
144
+ if (existsSync(jsonPath) && jsonPath !== uuidJsonPath) {
145
+ try {
146
+ unlinkSync(jsonPath);
147
+ } catch {
148
+ // Old file cleanup is best-effort
149
+ }
150
+ }
151
+ } else {
152
+ writeFileSync(uuidJsonPath, updatedJson, "utf-8");
153
+ }
154
+ }
155
+ },
156
+
79
157
  run(workspaceDir: string): void {
80
158
  const appsDir = join(workspaceDir, "data", "apps");
81
159
  if (!existsSync(appsDir)) return;
@@ -14,6 +14,17 @@ export const backfillInstallationIdMigration: WorkspaceMigration = {
14
14
  id: "011-backfill-installation-id",
15
15
  description:
16
16
  "Backfill installationId into lockfile from SQLite checkpoint and clean up stale row",
17
+
18
+ down(_workspaceDir: string): void {
19
+ // The forward migration moved an installationId from a SQLite checkpoint
20
+ // into the lockfile entry. Rolling back by removing installationId from
21
+ // the lockfile would break telemetry continuity and the field is harmless
22
+ // to leave in place. The SQLite checkpoint was already deleted and
23
+ // cannot be restored.
24
+ //
25
+ // No-op: leaving installationId in the lockfile is safe and non-disruptive.
26
+ },
27
+
17
28
  run(_workspaceDir: string): void {
18
29
  // a. Read existing installation ID from SQLite, or generate a new one.
19
30
  // On fresh installs the memory_checkpoints table may not exist yet,
@@ -17,6 +17,10 @@ import type { WorkspaceMigration } from "./types.js";
17
17
  const LEGACY_CONVERSATION_DIR_PATTERN =
18
18
  /^(.*)_(\d{4}-\d{2}-\d{2}T\d{2}-\d{2}-\d{2}\.\d{3}Z)$/;
19
19
 
20
+ /** Matches the new timestamp-first format: {timestamp}_{conversationId} */
21
+ const NEW_CONVERSATION_DIR_PATTERN =
22
+ /^(\d{4}-\d{2}-\d{2}T\d{2}-\d{2}-\d{2}\.\d{3}Z)_(.+)$/;
23
+
20
24
  function parseLegacyConversationDirName(
21
25
  dirName: string,
22
26
  ): { conversationId: string; timestamp: string } | null {
@@ -29,11 +33,51 @@ function parseLegacyConversationDirName(
29
33
  };
30
34
  }
31
35
 
36
+ function parseNewConversationDirName(
37
+ dirName: string,
38
+ ): { timestamp: string; conversationId: string } | null {
39
+ const match = dirName.match(NEW_CONVERSATION_DIR_PATTERN);
40
+ if (!match) return null;
41
+
42
+ return {
43
+ timestamp: match[1],
44
+ conversationId: match[2],
45
+ };
46
+ }
47
+
32
48
  export const renameConversationDiskViewDirsMigration: WorkspaceMigration = {
33
49
  id: "012-rename-conversation-disk-view-dirs",
34
50
  description:
35
51
  "Rename legacy conversation disk-view directories to timestamp-first names",
36
52
 
53
+ down(workspaceDir: string): void {
54
+ const conversationsDir = join(workspaceDir, "conversations");
55
+ if (!existsSync(conversationsDir)) return;
56
+
57
+ const entries = readdirSync(conversationsDir, { withFileTypes: true })
58
+ .filter((entry) => entry.isDirectory())
59
+ .map((entry) => entry.name)
60
+ .sort();
61
+
62
+ for (const dirName of entries) {
63
+ const parsed = parseNewConversationDirName(dirName);
64
+ if (!parsed) continue;
65
+
66
+ const sourcePath = join(conversationsDir, dirName);
67
+ const targetName = `${parsed.conversationId}_${parsed.timestamp}`;
68
+ const targetPath = join(conversationsDir, targetName);
69
+
70
+ if (sourcePath === targetPath) continue;
71
+ if (existsSync(targetPath)) continue;
72
+
73
+ try {
74
+ renameSync(sourcePath, targetPath);
75
+ } catch {
76
+ // Best-effort: leave the directory in place if a single rename fails.
77
+ }
78
+ }
79
+ },
80
+
37
81
  run(workspaceDir: string): void {
38
82
  const conversationsDir = join(workspaceDir, "conversations");
39
83
  if (!existsSync(conversationsDir)) return;
@@ -8,4 +8,9 @@ export const repairConversationDiskViewMigration: WorkspaceMigration = {
8
8
  run(_workspaceDir: string): void {
9
9
  rebuildConversationDiskViewFromDb();
10
10
  },
11
+ // No-op: this is a repair migration that rebuilds derived disk-view data
12
+ // from the database. There is no meaningful reverse operation — the data
13
+ // is a cache that can be regenerated, and removing it would just cause
14
+ // unnecessary churn on the next forward run.
15
+ down(_workspaceDir: string): void {},
11
16
  };