@vellumai/assistant 0.6.0 → 0.6.2

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 (358) hide show
  1. package/AGENTS.md +4 -0
  2. package/ARCHITECTURE.md +68 -15
  3. package/Dockerfile +2 -2
  4. package/bun.lock +6 -2
  5. package/docker-entrypoint.sh +42 -1
  6. package/docs/architecture/integrations.md +1 -1
  7. package/docs/architecture/memory.md +21 -24
  8. package/node_modules/@vellumai/ces-contracts/src/handles.ts +7 -9
  9. package/openapi.yaml +539 -4
  10. package/package.json +5 -1
  11. package/src/__tests__/anthropic-provider.test.ts +160 -95
  12. package/src/__tests__/app-dir-path-guard.test.ts +1 -0
  13. package/src/__tests__/app-executors.test.ts +47 -1
  14. package/src/__tests__/app-source-watcher.test.ts +159 -0
  15. package/src/__tests__/assistant-event-hub.test.ts +30 -0
  16. package/src/__tests__/checker.test.ts +138 -172
  17. package/src/__tests__/cli-command-risk-guard.test.ts +1 -1
  18. package/src/__tests__/config-schema.test.ts +5 -0
  19. package/src/__tests__/context-overflow-approval.test.ts +5 -5
  20. package/src/__tests__/conversation-agent-loop-overflow.test.ts +4 -6
  21. package/src/__tests__/conversation-agent-loop.test.ts +4 -51
  22. package/src/__tests__/conversation-analysis-routes.test.ts +169 -0
  23. package/src/__tests__/conversation-directories-parse.test.ts +105 -0
  24. package/src/__tests__/conversation-history-web-search.test.ts +1 -1
  25. package/src/__tests__/conversation-runtime-assembly.test.ts +653 -832
  26. package/src/__tests__/conversation-runtime-workspace.test.ts +1 -93
  27. package/src/__tests__/conversation-tool-setup-app-refresh.test.ts +17 -4
  28. package/src/__tests__/conversation-wipe.test.ts +2 -6
  29. package/src/__tests__/conversation-workspace-cache-state.test.ts +6 -12
  30. package/src/__tests__/conversation-workspace-injection.test.ts +25 -26
  31. package/src/__tests__/conversation-workspace-tool-tracking.test.ts +1 -1
  32. package/src/__tests__/copy-composer-tc-templates.test.ts +335 -0
  33. package/src/__tests__/credential-execution-approval-bridge.test.ts +0 -2
  34. package/src/__tests__/date-context.test.ts +76 -210
  35. package/src/__tests__/db-schedule-syntax-migration.test.ts +16 -1
  36. package/src/__tests__/file-list-tool.test.ts +219 -0
  37. package/src/__tests__/first-greeting.test.ts +1 -1
  38. package/src/__tests__/heartbeat-service.test.ts +180 -3
  39. package/src/__tests__/identity-routes.test.ts +328 -0
  40. package/src/__tests__/init-feature-flag-overrides.test.ts +167 -0
  41. package/src/__tests__/injection-block.test.ts +24 -0
  42. package/src/__tests__/inline-command-runner.test.ts +7 -5
  43. package/src/__tests__/install-skill-routing.test.ts +7 -6
  44. package/src/__tests__/jobs-store-qdrant-breaker.test.ts +15 -14
  45. package/src/__tests__/list-messages-tool-merge.test.ts +300 -0
  46. package/src/__tests__/llm-context-normalization.test.ts +18 -18
  47. package/src/__tests__/llm-context-route-provider.test.ts +101 -0
  48. package/src/__tests__/llm-request-log-turn-query.test.ts +162 -0
  49. package/src/__tests__/log-export-workspace.test.ts +257 -100
  50. package/src/__tests__/managed-credential-catalog-cli.test.ts +12 -14
  51. package/src/__tests__/mcp-abort-signal.test.ts +5 -0
  52. package/src/__tests__/mcp-client-auth.test.ts +5 -0
  53. package/src/__tests__/memory-recall-log-store.test.ts +132 -0
  54. package/src/__tests__/migration-export-streaming.test.ts +304 -0
  55. package/src/__tests__/migration-import-commit-http.test.ts +11 -10
  56. package/src/__tests__/mock-fetch.ts +87 -0
  57. package/src/__tests__/navigate-settings-tab.test.ts +14 -1
  58. package/src/__tests__/notification-broadcaster.test.ts +65 -0
  59. package/src/__tests__/notification-decision-recipient-context.test.ts +282 -0
  60. package/src/__tests__/onboarding-template-contract.test.ts +63 -14
  61. package/src/__tests__/parser.test.ts +32 -0
  62. package/src/__tests__/permission-checker-host-gate.test.ts +452 -0
  63. package/src/__tests__/permission-controls-v2-flag.test.ts +55 -0
  64. package/src/__tests__/permission-mode-sse.test.ts +418 -0
  65. package/src/__tests__/permission-mode-store.test.ts +277 -0
  66. package/src/__tests__/permission-mode.test.ts +101 -0
  67. package/src/__tests__/pkb-autoinject.test.ts +96 -0
  68. package/src/__tests__/platform-bash-auto-approve.test.ts +359 -0
  69. package/src/__tests__/profiler-routes.test.ts +502 -0
  70. package/src/__tests__/profiler-run-store.test.ts +441 -0
  71. package/src/__tests__/proxy-approval-callback.test.ts +4 -75
  72. package/src/__tests__/registry.test.ts +1 -1
  73. package/src/__tests__/require-fresh-approval.test.ts +0 -2
  74. package/src/__tests__/sandbox-diagnostics.test.ts +1 -32
  75. package/src/__tests__/sandbox-host-parity.test.ts +5 -4
  76. package/src/__tests__/scheduler-reuse-conversation.test.ts +368 -0
  77. package/src/__tests__/scrub-corrupted-image-attachments.test.ts +278 -0
  78. package/src/__tests__/search-skills-unified.test.ts +4 -3
  79. package/src/__tests__/send-endpoint-busy.test.ts +42 -3
  80. package/src/__tests__/set-permission-mode.test.ts +274 -0
  81. package/src/__tests__/skill-load-feature-flag.test.ts +12 -0
  82. package/src/__tests__/skill-memory.test.ts +2 -783
  83. package/src/__tests__/strip-memory-injections.test.ts +187 -0
  84. package/src/__tests__/subagent-detail.test.ts +84 -0
  85. package/src/__tests__/subagent-disposal.test.ts +308 -0
  86. package/src/__tests__/subagent-manager-notify.test.ts +19 -10
  87. package/src/__tests__/subagent-notify-parent.test.ts +390 -0
  88. package/src/__tests__/subagent-role-registry.test.ts +108 -0
  89. package/src/__tests__/subagent-tool-filtering.test.ts +71 -0
  90. package/src/__tests__/subagent-tools.test.ts +464 -4
  91. package/src/__tests__/system-prompt-ask-mode.test.ts +139 -0
  92. package/src/__tests__/task-memory-cleanup.test.ts +12 -12
  93. package/src/__tests__/terminal-sandbox.test.ts +1 -1
  94. package/src/__tests__/terminal-tools.test.ts +16 -29
  95. package/src/__tests__/test-preload.ts +18 -0
  96. package/src/__tests__/tool-domain-event-publisher.test.ts +0 -1
  97. package/src/__tests__/tool-executor-lifecycle-events.test.ts +1 -8
  98. package/src/__tests__/tool-executor.test.ts +4 -27
  99. package/src/__tests__/tool-side-effects-slack-dm.test.ts +1 -0
  100. package/src/__tests__/top-level-renderer.test.ts +10 -13
  101. package/src/__tests__/transport-hints-queue.test.ts +77 -0
  102. package/src/__tests__/trust-store.test.ts +4 -4
  103. package/src/__tests__/trusted-contact-lifecycle-notifications.test.ts +116 -2
  104. package/src/__tests__/workspace-migration-028-recover-conversations-from-disk-view.test.ts +387 -0
  105. package/src/__tests__/workspace-migration-030-seed-pkb-autoinject.test.ts +168 -0
  106. package/src/__tests__/workspace-policy.test.ts +2 -7
  107. package/src/agent/loop.ts +6 -29
  108. package/src/approvals/guardian-request-resolvers.ts +24 -0
  109. package/src/avatar/traits-png-sync.ts +3 -3
  110. package/src/channels/types.ts +5 -0
  111. package/src/cli/__tests__/run-assistant-command.ts +56 -0
  112. package/src/cli/__tests__/unknown-command.test.ts +33 -0
  113. package/src/cli/commands/__tests__/email-download.test.ts +245 -0
  114. package/src/cli/commands/__tests__/email-list.test.ts +192 -0
  115. package/src/cli/commands/__tests__/email-register.test.ts +186 -0
  116. package/src/cli/commands/__tests__/email-send.test.ts +291 -0
  117. package/src/cli/commands/__tests__/email-status.test.ts +181 -0
  118. package/src/cli/commands/__tests__/email-unregister.test.ts +139 -0
  119. package/src/cli/commands/__tests__/routes.test.ts +562 -0
  120. package/src/cli/commands/conversations.ts +1 -8
  121. package/src/cli/commands/default-action.ts +68 -1
  122. package/src/cli/commands/email.ts +584 -835
  123. package/src/cli/commands/memory.ts +1 -34
  124. package/src/cli/commands/notifications.ts +7 -2
  125. package/src/cli/commands/oauth/__tests__/connect.test.ts +27 -0
  126. package/src/cli/commands/oauth/connect.ts +25 -5
  127. package/src/cli/commands/platform/__tests__/connect.test.ts +1 -1
  128. package/src/cli/commands/platform/__tests__/disconnect.test.ts +1 -1
  129. package/src/cli/commands/platform/__tests__/status.test.ts +1 -1
  130. package/src/cli/commands/routes.ts +396 -0
  131. package/src/cli/commands/skills.ts +130 -20
  132. package/src/cli/program.ts +11 -2
  133. package/src/cli.ts +1 -120
  134. package/src/config/assistant-feature-flags.ts +59 -55
  135. package/src/config/bundled-skills/app-builder/SKILL.md +91 -5
  136. package/src/config/bundled-skills/gmail/SKILL.md +13 -8
  137. package/src/config/bundled-skills/gmail/TOOLS.json +1 -1
  138. package/src/config/bundled-skills/gmail/tools/gmail-sender-digest.ts +2 -1
  139. package/src/config/bundled-skills/messaging/SKILL.md +7 -0
  140. package/src/config/bundled-skills/schedule/SKILL.md +22 -2
  141. package/src/config/bundled-skills/schedule/TOOLS.json +8 -0
  142. package/src/config/bundled-skills/settings/TOOLS.json +1 -1
  143. package/src/config/bundled-skills/settings/tools/avatar-get.ts +3 -13
  144. package/src/config/bundled-skills/settings/tools/avatar-remove.ts +2 -4
  145. package/src/config/bundled-skills/settings/tools/avatar-update.ts +5 -2
  146. package/src/config/bundled-skills/settings/tools/navigate-settings-tab.ts +8 -3
  147. package/src/config/bundled-skills/slack/SKILL.md +2 -0
  148. package/src/config/bundled-skills/subagent/SKILL.md +43 -3
  149. package/src/config/bundled-skills/subagent/TOOLS.json +29 -4
  150. package/src/config/env-registry.ts +63 -0
  151. package/src/config/feature-flag-registry.json +17 -1
  152. package/src/config/schema.ts +8 -0
  153. package/src/config/schemas/filing.ts +51 -0
  154. package/src/config/schemas/heartbeat.ts +15 -12
  155. package/src/config/schemas/memory-lifecycle.ts +12 -0
  156. package/src/config/schemas/security.ts +14 -0
  157. package/src/config/schemas/services.ts +8 -0
  158. package/src/credential-execution/approval-bridge.ts +0 -1
  159. package/src/credential-execution/managed-catalog.ts +3 -7
  160. package/src/daemon/app-source-watcher.ts +93 -0
  161. package/src/daemon/config-watcher.ts +85 -3
  162. package/src/daemon/context-overflow-approval.ts +0 -1
  163. package/src/daemon/conversation-agent-loop-handlers.ts +20 -0
  164. package/src/daemon/conversation-agent-loop.ts +179 -65
  165. package/src/daemon/conversation-attachments.ts +0 -1
  166. package/src/daemon/conversation-history.ts +4 -19
  167. package/src/daemon/conversation-lifecycle.ts +8 -14
  168. package/src/daemon/conversation-messaging.ts +3 -0
  169. package/src/daemon/conversation-process.ts +30 -8
  170. package/src/daemon/conversation-queue-manager.ts +8 -0
  171. package/src/daemon/conversation-runtime-assembly.ts +359 -308
  172. package/src/daemon/conversation-surfaces.ts +65 -0
  173. package/src/daemon/conversation-tool-setup.ts +44 -17
  174. package/src/daemon/conversation-workspace.ts +1 -2
  175. package/src/daemon/conversation.ts +19 -3
  176. package/src/daemon/date-context.ts +26 -53
  177. package/src/daemon/first-greeting.ts +1 -1
  178. package/src/daemon/handlers/conversations.ts +5 -7
  179. package/src/daemon/handlers/shared.test.ts +143 -0
  180. package/src/daemon/handlers/shared.ts +70 -5
  181. package/src/daemon/handlers/skills.ts +11 -18
  182. package/src/daemon/lifecycle.ts +220 -158
  183. package/src/daemon/message-types/conversations.ts +29 -6
  184. package/src/daemon/message-types/messages.ts +9 -2
  185. package/src/daemon/message-types/notifications.ts +12 -0
  186. package/src/daemon/message-types/schedules.ts +1 -0
  187. package/src/daemon/message-types/settings.ts +18 -0
  188. package/src/daemon/profiler-run-store.ts +557 -0
  189. package/src/daemon/server.ts +87 -10
  190. package/src/daemon/shutdown-handlers.ts +5 -0
  191. package/src/daemon/tool-side-effects.ts +23 -3
  192. package/src/daemon/transport-hints.ts +33 -0
  193. package/src/export/transcript-formatter.ts +148 -0
  194. package/src/filing/filing-service.ts +228 -0
  195. package/src/heartbeat/heartbeat-service.ts +96 -7
  196. package/src/index.ts +1 -1
  197. package/src/mcp/client.ts +6 -0
  198. package/src/mcp/mcp-oauth-provider.ts +149 -27
  199. package/src/memory/admin.ts +33 -32
  200. package/src/memory/app-store.ts +69 -0
  201. package/src/memory/conversation-bootstrap.ts +1 -1
  202. package/src/memory/conversation-crud.ts +151 -117
  203. package/src/memory/conversation-directories.ts +39 -0
  204. package/src/memory/conversation-group-migration.ts +66 -6
  205. package/src/memory/conversation-queries.ts +58 -12
  206. package/src/memory/conversation-title-service.ts +1 -0
  207. package/src/memory/db-init.ts +182 -376
  208. package/src/memory/embedding-local.ts +1 -1
  209. package/src/memory/graph/bootstrap.ts +75 -66
  210. package/src/memory/graph/capability-seed.ts +167 -17
  211. package/src/memory/graph/consolidation.ts +38 -4
  212. package/src/memory/graph/conversation-graph-memory.ts +133 -104
  213. package/src/memory/graph/extraction-job.ts +9 -4
  214. package/src/memory/graph/extraction.ts +66 -23
  215. package/src/memory/graph/graph-memory-state-store.ts +37 -0
  216. package/src/memory/graph/graph-search.ts +29 -15
  217. package/src/memory/graph/injection.ts +38 -8
  218. package/src/memory/graph/inspect.ts +12 -3
  219. package/src/memory/graph/retriever.ts +365 -262
  220. package/src/memory/graph/store.test.ts +48 -0
  221. package/src/memory/graph/store.ts +150 -11
  222. package/src/memory/graph/tool-handlers.ts +84 -209
  223. package/src/memory/graph/tools.ts +8 -52
  224. package/src/memory/graph/types.ts +24 -0
  225. package/src/memory/group-crud.ts +25 -9
  226. package/src/memory/job-handlers/cleanup.ts +44 -1
  227. package/src/memory/jobs-store.ts +70 -60
  228. package/src/memory/jobs-worker.ts +44 -28
  229. package/src/memory/llm-request-log-store.ts +96 -12
  230. package/src/memory/memory-recall-log-store.ts +49 -5
  231. package/src/memory/migrations/203-drop-memory-items-tables.ts +33 -1
  232. package/src/memory/migrations/206-memory-graph-node-edits.ts +19 -0
  233. package/src/memory/migrations/206-scrub-corrupted-image-attachments.ts +131 -0
  234. package/src/memory/migrations/207-conversation-graph-memory-state.ts +20 -0
  235. package/src/memory/migrations/208-conversations-last-message-at.ts +35 -0
  236. package/src/memory/migrations/209-strip-thinking-from-consolidated.ts +85 -0
  237. package/src/memory/migrations/210-schedule-reuse-conversation.ts +13 -0
  238. package/src/memory/migrations/211-memory-recall-logs-query-context.ts +21 -0
  239. package/src/memory/migrations/212-llm-request-logs-created-at-index.ts +19 -0
  240. package/src/memory/migrations/index.ts +8 -0
  241. package/src/memory/migrations/registry.ts +8 -0
  242. package/src/memory/schema/conversations.ts +14 -0
  243. package/src/memory/schema/infrastructure.ts +8 -1
  244. package/src/memory/schema/memory-core.ts +0 -51
  245. package/src/memory/schema/memory-graph.ts +15 -0
  246. package/src/memory/task-memory-cleanup.ts +30 -11
  247. package/src/messaging/provider.ts +1 -1
  248. package/src/notifications/broadcaster.ts +6 -0
  249. package/src/notifications/conversation-pairing.ts +12 -4
  250. package/src/notifications/copy-composer.ts +86 -0
  251. package/src/notifications/decision-engine.ts +35 -0
  252. package/src/notifications/emit-signal.ts +14 -0
  253. package/src/notifications/signal.ts +11 -0
  254. package/src/oauth/platform-connection.test.ts +2 -2
  255. package/src/oauth/seed-providers.ts +1 -0
  256. package/src/permissions/checker.ts +15 -4
  257. package/src/permissions/defaults.ts +7 -8
  258. package/src/permissions/permission-mode-store.ts +180 -0
  259. package/src/permissions/permission-mode.ts +31 -0
  260. package/src/permissions/prompter.ts +0 -2
  261. package/src/permissions/workspace-policy.ts +9 -0
  262. package/src/platform/client.ts +1 -1
  263. package/src/prompts/system-prompt.ts +59 -7
  264. package/src/prompts/templates/BOOTSTRAP-REFERENCE.md +100 -0
  265. package/src/prompts/templates/BOOTSTRAP.md +76 -162
  266. package/src/prompts/templates/HEARTBEAT.md +3 -1
  267. package/src/prompts/templates/SOUL.md +30 -9
  268. package/src/prompts/templates/UPDATES.md +8 -0
  269. package/src/providers/anthropic/client.ts +107 -219
  270. package/src/runtime/assistant-event-hub.ts +22 -0
  271. package/src/runtime/auth/route-policy.ts +23 -0
  272. package/src/runtime/auth/token-service.ts +8 -0
  273. package/src/runtime/http-server.ts +32 -2
  274. package/src/runtime/http-types.ts +12 -1
  275. package/src/runtime/migrations/vbundle-builder.ts +389 -3
  276. package/src/runtime/migrations/vbundle-importer.ts +8 -6
  277. package/src/runtime/routes/__tests__/user-route-dispatcher.test.ts +378 -0
  278. package/src/runtime/routes/app-management-routes.ts +1 -11
  279. package/src/runtime/routes/approval-strategies/guardian-callback-strategy.ts +26 -0
  280. package/src/runtime/routes/archive-utils.ts +29 -0
  281. package/src/runtime/routes/avatar-routes.ts +2 -9
  282. package/src/runtime/routes/btw-routes.ts +14 -1
  283. package/src/runtime/routes/conversation-analysis-routes.ts +185 -0
  284. package/src/runtime/routes/conversation-management-routes.ts +1 -14
  285. package/src/runtime/routes/conversation-query-routes.ts +49 -3
  286. package/src/runtime/routes/conversation-routes.ts +270 -44
  287. package/src/runtime/routes/group-routes.ts +22 -8
  288. package/src/runtime/routes/heartbeat-routes.ts +4 -10
  289. package/src/runtime/routes/identity-routes.ts +53 -18
  290. package/src/runtime/routes/llm-context-normalization.ts +14 -10
  291. package/src/runtime/routes/log-export/AGENTS.md +104 -0
  292. package/src/runtime/routes/log-export/__tests__/workspace-allowlist-error-contract.test.ts +103 -0
  293. package/src/runtime/routes/log-export/__tests__/workspace-allowlist.test.ts +716 -0
  294. package/src/runtime/routes/log-export/workspace-allowlist.ts +458 -0
  295. package/src/runtime/routes/log-export-routes.ts +41 -278
  296. package/src/runtime/routes/memory-item-routes.test.ts +168 -233
  297. package/src/runtime/routes/migration-routes.ts +18 -7
  298. package/src/runtime/routes/profiler-routes.ts +350 -0
  299. package/src/runtime/routes/schedule-routes.ts +27 -12
  300. package/src/runtime/routes/settings-routes.ts +95 -8
  301. package/src/runtime/routes/subagents-routes.ts +28 -7
  302. package/src/runtime/routes/user-route-dispatcher.ts +223 -0
  303. package/src/runtime/routes/user-routes.ts +41 -0
  304. package/src/runtime/routes/workspace-routes.ts +0 -1
  305. package/src/schedule/schedule-store.ts +30 -0
  306. package/src/schedule/scheduler.ts +45 -18
  307. package/src/skills/catalog-install.ts +10 -2
  308. package/src/skills/inline-command-runner.ts +12 -14
  309. package/src/skills/managed-store.ts +2 -2
  310. package/src/skills/skill-memory.ts +1 -293
  311. package/src/subagent/index.ts +13 -3
  312. package/src/subagent/manager.ts +308 -29
  313. package/src/subagent/types.ts +68 -0
  314. package/src/tasks/task-runner.ts +4 -4
  315. package/src/tools/apps/executors.ts +29 -4
  316. package/src/tools/filesystem/list.ts +93 -0
  317. package/src/tools/permission-checker.ts +78 -18
  318. package/src/tools/registry.ts +4 -0
  319. package/src/tools/schedule/create.ts +3 -0
  320. package/src/tools/schedule/list.ts +1 -0
  321. package/src/tools/schedule/update.ts +6 -0
  322. package/src/tools/secret-detection-handler.ts +0 -1
  323. package/src/tools/shared/filesystem/errors.ts +5 -0
  324. package/src/tools/shared/filesystem/file-ops-service.ts +90 -2
  325. package/src/tools/shared/filesystem/types.ts +17 -0
  326. package/src/tools/shared/shell-output.ts +31 -2
  327. package/src/tools/skills/sandbox-runner.ts +3 -6
  328. package/src/tools/subagent/abort.ts +12 -2
  329. package/src/tools/subagent/message.ts +9 -2
  330. package/src/tools/subagent/notify-parent.ts +79 -0
  331. package/src/tools/subagent/read.ts +29 -8
  332. package/src/tools/subagent/resolve.ts +21 -0
  333. package/src/tools/subagent/spawn.ts +2 -0
  334. package/src/tools/subagent/status.ts +11 -1
  335. package/src/tools/system/avatar-generator.ts +3 -3
  336. package/src/tools/system/register.ts +23 -0
  337. package/src/tools/system/set-permission-mode.ts +103 -0
  338. package/src/tools/terminal/parser.ts +30 -5
  339. package/src/tools/terminal/safe-env.ts +16 -1
  340. package/src/tools/terminal/sandbox-diagnostics.ts +4 -4
  341. package/src/tools/terminal/sandbox.ts +4 -1
  342. package/src/tools/terminal/shell.ts +3 -5
  343. package/src/tools/tool-manifest.ts +6 -0
  344. package/src/tools/types.ts +2 -3
  345. package/src/util/logger.ts +1 -1
  346. package/src/util/platform.ts +50 -17
  347. package/src/watcher/provider-types.ts +1 -1
  348. package/src/workspace/migrations/023-move-config-files-to-workspace.ts +2 -2
  349. package/src/workspace/migrations/024-move-runtime-files-to-workspace.ts +2 -2
  350. package/src/workspace/migrations/028-recover-conversations-from-disk-view.ts +270 -0
  351. package/src/workspace/migrations/029-seed-pkb.ts +85 -0
  352. package/src/workspace/migrations/030-seed-pkb-autoinject.ts +73 -0
  353. package/src/workspace/migrations/registry.ts +6 -0
  354. package/src/workspace/top-level-renderer.ts +5 -9
  355. package/src/__tests__/cli-memory.test.ts +0 -377
  356. package/src/__tests__/clipboard.test.ts +0 -88
  357. package/src/cli/cli-memory.ts +0 -179
  358. package/src/util/clipboard.ts +0 -34
@@ -301,6 +301,168 @@ describe("getRequestLogsByMessageId — turn-aware query", () => {
301
301
  expect(logs[2]?.id).toBe("log-surviving");
302
302
  });
303
303
 
304
+ test("recovers unlinked logs (messageId IS NULL) within the turn time range", () => {
305
+ // Simulate the race: logs recorded with NULL messageId, backfill hasn't run yet.
306
+ const T = 1_700_000_000_000;
307
+ const db = getDb();
308
+ const conv = createConversation("unlinked-test");
309
+
310
+ db.run(
311
+ sql`INSERT INTO messages (id, conversation_id, role, content, created_at) VALUES ('u1-ul', ${conv.id}, 'user', '"Do the task"', ${T})`,
312
+ );
313
+ db.run(
314
+ sql`INSERT INTO messages (id, conversation_id, role, content, created_at) VALUES ('a1-ul', ${conv.id}, 'assistant', '"Done!"', ${T + 30000})`,
315
+ );
316
+
317
+ // Unlinked log: messageId is NULL (backfill hasn't run yet)
318
+ db.insert(llmRequestLogs)
319
+ .values({
320
+ id: "log-unlinked-1",
321
+ conversationId: conv.id,
322
+ messageId: null,
323
+ provider: "anthropic",
324
+ requestPayload: '{"step":1}',
325
+ responsePayload: '{"tool":"bash"}',
326
+ createdAt: T + 5000,
327
+ })
328
+ .run();
329
+
330
+ // Linked log: already backfilled to the assistant message
331
+ db.insert(llmRequestLogs)
332
+ .values({
333
+ id: "log-linked-1",
334
+ conversationId: conv.id,
335
+ messageId: "a1-ul",
336
+ provider: "anthropic",
337
+ requestPayload: '{"step":2}',
338
+ responsePayload: '{"text":"Done!"}',
339
+ createdAt: T + 29_000,
340
+ })
341
+ .run();
342
+
343
+ const logs = getRequestLogsByMessageId("a1-ul");
344
+ expect(logs).toHaveLength(2);
345
+ expect(logs[0]?.id).toBe("log-unlinked-1");
346
+ expect(logs[1]?.id).toBe("log-linked-1");
347
+
348
+ // Verify opportunistic backfill ran: the unlinked log should now have a messageId
349
+ const backfilledLog = db
350
+ .select({ messageId: llmRequestLogs.messageId })
351
+ .from(llmRequestLogs)
352
+ .where(sql`${llmRequestLogs.id} = 'log-unlinked-1'`)
353
+ .get();
354
+ expect(backfilledLog?.messageId).toBe("a1-ul");
355
+ });
356
+
357
+ test("unlinked logs from different conversations don't bleed", () => {
358
+ const T = 1_700_000_000_000;
359
+ const db = getDb();
360
+ const convA = createConversation("conv-a");
361
+ const convB = createConversation("conv-b");
362
+
363
+ // Conversation A: user + assistant + unlinked log
364
+ db.run(
365
+ sql`INSERT INTO messages (id, conversation_id, role, content, created_at) VALUES ('u1-a', ${convA.id}, 'user', '"Hello A"', ${T})`,
366
+ );
367
+ db.run(
368
+ sql`INSERT INTO messages (id, conversation_id, role, content, created_at) VALUES ('a1-a', ${convA.id}, 'assistant', '"Hi A"', ${T + 10000})`,
369
+ );
370
+ db.insert(llmRequestLogs)
371
+ .values({
372
+ id: "log-conv-a",
373
+ conversationId: convA.id,
374
+ messageId: null,
375
+ provider: "anthropic",
376
+ requestPayload: '{"conv":"A"}',
377
+ responsePayload: '{"r":"A"}',
378
+ createdAt: T + 5000,
379
+ })
380
+ .run();
381
+
382
+ // Conversation B: user + assistant + unlinked log (overlapping timestamps)
383
+ db.run(
384
+ sql`INSERT INTO messages (id, conversation_id, role, content, created_at) VALUES ('u1-b', ${convB.id}, 'user', '"Hello B"', ${T})`,
385
+ );
386
+ db.run(
387
+ sql`INSERT INTO messages (id, conversation_id, role, content, created_at) VALUES ('a1-b', ${convB.id}, 'assistant', '"Hi B"', ${T + 10000})`,
388
+ );
389
+ db.insert(llmRequestLogs)
390
+ .values({
391
+ id: "log-conv-b",
392
+ conversationId: convB.id,
393
+ messageId: null,
394
+ provider: "anthropic",
395
+ requestPayload: '{"conv":"B"}',
396
+ responsePayload: '{"r":"B"}',
397
+ createdAt: T + 5000,
398
+ })
399
+ .run();
400
+
401
+ // Query from conv A → should only find conv A's log
402
+ const logsA = getRequestLogsByMessageId("a1-a");
403
+ expect(logsA).toHaveLength(1);
404
+ expect(logsA[0]?.id).toBe("log-conv-a");
405
+
406
+ // Query from conv B → should only find conv B's log
407
+ const logsB = getRequestLogsByMessageId("a1-b");
408
+ expect(logsB).toHaveLength(1);
409
+ expect(logsB[0]?.id).toBe("log-conv-b");
410
+ });
411
+
412
+ test("unlinked logs from different turns don't bleed", () => {
413
+ const T = 1_700_000_000_000;
414
+ const db = getDb();
415
+ const conv = createConversation("two-turn-unlinked");
416
+
417
+ // Turn 1: user → assistant (with linked log)
418
+ db.run(
419
+ sql`INSERT INTO messages (id, conversation_id, role, content, created_at) VALUES ('u1-t', ${conv.id}, 'user', '"Turn 1"', ${T})`,
420
+ );
421
+ db.run(
422
+ sql`INSERT INTO messages (id, conversation_id, role, content, created_at) VALUES ('a1-t', ${conv.id}, 'assistant', '"Answer 1"', ${T + 10000})`,
423
+ );
424
+ db.insert(llmRequestLogs)
425
+ .values({
426
+ id: "log-turn1-unlinked",
427
+ conversationId: conv.id,
428
+ messageId: null,
429
+ provider: "anthropic",
430
+ requestPayload: '{"turn":1}',
431
+ responsePayload: '{"r":1}',
432
+ createdAt: T + 5000,
433
+ })
434
+ .run();
435
+
436
+ // Turn 2: user → assistant (with unlinked log)
437
+ db.run(
438
+ sql`INSERT INTO messages (id, conversation_id, role, content, created_at) VALUES ('u2-t', ${conv.id}, 'user', '"Turn 2"', ${T + 60000})`,
439
+ );
440
+ db.run(
441
+ sql`INSERT INTO messages (id, conversation_id, role, content, created_at) VALUES ('a2-t', ${conv.id}, 'assistant', '"Answer 2"', ${T + 70000})`,
442
+ );
443
+ db.insert(llmRequestLogs)
444
+ .values({
445
+ id: "log-turn2-unlinked",
446
+ conversationId: conv.id,
447
+ messageId: null,
448
+ provider: "anthropic",
449
+ requestPayload: '{"turn":2}',
450
+ responsePayload: '{"r":2}',
451
+ createdAt: T + 65000,
452
+ })
453
+ .run();
454
+
455
+ // Query turn 2 → should only find turn 2's unlinked log
456
+ const turn2Logs = getRequestLogsByMessageId("a2-t");
457
+ expect(turn2Logs).toHaveLength(1);
458
+ expect(turn2Logs[0]?.id).toBe("log-turn2-unlinked");
459
+
460
+ // Query turn 1 → should only find turn 1's unlinked log
461
+ const turn1Logs = getRequestLogsByMessageId("a1-t");
462
+ expect(turn1Logs).toHaveLength(1);
463
+ expect(turn1Logs[0]?.id).toBe("log-turn1-unlinked");
464
+ });
465
+
304
466
  test("relinkLlmRequestLogs moves logs from deleted messages to consolidated message", async () => {
305
467
  const conv = createConversation("relink-test");
306
468
 
@@ -5,8 +5,6 @@
5
5
  * - audit-data.json with tool invocation records
6
6
  * - daemon-logs/ with log file contents
7
7
  * - config-snapshot.json with sanitized config
8
- * - workspace/ with text files, SQL dumps for .db files, and proper
9
- * filtering (excluded directories, binary files, symlinks).
10
8
  */
11
9
 
12
10
  import { spawnSync } from "node:child_process";
@@ -16,7 +14,6 @@ import {
16
14
  readdirSync,
17
15
  readFileSync,
18
16
  rmSync,
19
- symlinkSync,
20
17
  writeFileSync,
21
18
  } from "node:fs";
22
19
  import { tmpdir } from "node:os";
@@ -24,8 +21,7 @@ import { join } from "node:path";
24
21
  import { describe, expect, mock, test } from "bun:test";
25
22
 
26
23
  // Set up temp directories before mocking
27
- const testDir = process.env.VELLUM_WORKSPACE_DIR!;
28
- const testWorkspaceDir = testDir;
24
+ const testWorkspaceDir = process.env.VELLUM_WORKSPACE_DIR!;
29
25
  mkdirSync(testWorkspaceDir, { recursive: true });
30
26
 
31
27
  mock.module("../util/logger.js", () => ({
@@ -52,11 +48,13 @@ initializeDb();
52
48
  const routes = logExportRouteDefinitions();
53
49
  const exportRoute = routes.find((r) => r.endpoint === "export")!;
54
50
 
55
- async function callExport(): Promise<Response> {
51
+ async function callExport(
52
+ body: Record<string, unknown> = {},
53
+ ): Promise<Response> {
56
54
  const req = new Request("http://localhost/v1/export", {
57
55
  method: "POST",
58
56
  headers: { "Content-Type": "application/json" },
59
- body: JSON.stringify({}),
57
+ body: JSON.stringify(body),
60
58
  });
61
59
  const url = new URL(req.url);
62
60
  return exportRoute.handler({
@@ -85,73 +83,66 @@ async function extractArchive(res: Response): Promise<string> {
85
83
  return extractDir;
86
84
  }
87
85
 
88
- /** Recursively lists all files under a directory as relative paths. */
89
- function listFiles(dir: string, base = dir): string[] {
90
- const result: string[] = [];
91
- for (const entry of readdirSync(dir, { withFileTypes: true })) {
92
- const full = join(dir, entry.name);
93
- if (entry.isDirectory()) {
94
- result.push(...listFiles(full, base));
95
- } else {
96
- result.push(full.slice(base.length + 1));
97
- }
98
- }
99
- return result;
100
- }
101
-
102
86
  // ---------------------------------------------------------------------------
103
- // Seed workspace files
87
+ // Seed test data
104
88
  // ---------------------------------------------------------------------------
105
89
 
106
- // Text filesshould be included
107
- writeFileSync(join(testWorkspaceDir, "IDENTITY.md"), "# My Identity\nHello");
108
- mkdirSync(join(testWorkspaceDir, "notes"), { recursive: true });
109
- writeFileSync(join(testWorkspaceDir, "notes", "daily.txt"), "Some daily notes");
110
-
111
- // SQLite DB file — should be dumped as .sql
112
- mkdirSync(join(testWorkspaceDir, "data", "db"), { recursive: true });
113
- // Create a real sqlite db with a table
114
- import { Database } from "bun:sqlite";
115
- const wsDbPath = join(testWorkspaceDir, "data", "db", "assistant.db");
116
- const wsDb = new Database(wsDbPath);
117
- wsDb.run("CREATE TABLE test_table (id INTEGER PRIMARY KEY, name TEXT)");
118
- wsDb.run("INSERT INTO test_table (name) VALUES ('hello')");
119
- wsDb.close();
120
-
121
- // Excluded directory: embedding-models/
122
- mkdirSync(join(testWorkspaceDir, "embedding-models"), { recursive: true });
90
+ // config.json at workspace root needed for config-snapshot test
123
91
  writeFileSync(
124
- join(testWorkspaceDir, "embedding-models", "model.bin"),
125
- "large binary model data",
92
+ join(testWorkspaceDir, "config.json"),
93
+ JSON.stringify({ provider: "anthropic" }),
126
94
  );
127
95
 
128
- // Excluded directory: data/qdrant/
129
- mkdirSync(join(testWorkspaceDir, "data", "qdrant"), { recursive: true });
130
- writeFileSync(
131
- join(testWorkspaceDir, "data", "qdrant", "index.bin"),
132
- "vector index data",
96
+ // Conversation directories — used for workspace allowlist tests
97
+ const conversationsDir = join(testWorkspaceDir, "conversations");
98
+ mkdirSync(conversationsDir, { recursive: true });
99
+
100
+ function seedConversation(name: string, body: string) {
101
+ const dir = join(conversationsDir, name);
102
+ mkdirSync(dir, { recursive: true });
103
+ writeFileSync(join(dir, "meta.json"), "{}\n");
104
+ writeFileSync(join(dir, "messages.jsonl"), body);
105
+ }
106
+
107
+ seedConversation(
108
+ "2025-01-10T00-00-00.000Z_conv-jan10",
109
+ '{"role":"user","content":"jan 10"}\n',
110
+ );
111
+ seedConversation(
112
+ "2025-01-15T00-00-00.000Z_conv-jan15",
113
+ '{"role":"user","content":"jan 15"}\n',
133
114
  );
115
+ seedConversation(
116
+ "2025-01-20T00-00-00.000Z_conv-jan20",
117
+ '{"role":"user","content":"jan 20"}\n',
118
+ );
119
+ seedConversation(
120
+ "2025-01-25T00-00-00.000Z_conv-jan25",
121
+ '{"role":"user","content":"jan 25"}\n',
122
+ );
123
+ seedConversation("malformed-name", '{"role":"user","content":"x"}\n');
134
124
 
135
- // Binary fileshould be skipped
125
+ // Daemon log files used for date filtering tests
126
+ const logsDir = join(testWorkspaceDir, "data", "logs");
127
+ mkdirSync(logsDir, { recursive: true });
136
128
  writeFileSync(
137
- join(testWorkspaceDir, "binary-file.dat"),
138
- Buffer.from([0x48, 0x65, 0x6c, 0x00, 0x6f]), // contains null byte
129
+ join(logsDir, "assistant-2025-01-10.log"),
130
+ "log entry from Jan 10\n",
139
131
  );
140
-
141
- // config.json at workspace root — should be skipped (already in configSnapshot)
142
132
  writeFileSync(
143
- join(testWorkspaceDir, "config.json"),
144
- JSON.stringify({ provider: "anthropic" }),
133
+ join(logsDir, "assistant-2025-01-15.log"),
134
+ "log entry from Jan 15\n",
145
135
  );
146
-
147
- // Symlink pointing outside workspace — should be skipped
148
- const outsideFile = join(testDir, "outside-secret.txt");
149
- writeFileSync(outsideFile, "sensitive data outside workspace");
150
- try {
151
- symlinkSync(outsideFile, join(testWorkspaceDir, "sneaky-link.txt"));
152
- } catch {
153
- // Symlink creation may fail on some platforms; tests will still pass
154
- }
136
+ writeFileSync(
137
+ join(logsDir, "assistant-2025-01-20.log"),
138
+ "log entry from Jan 20\n",
139
+ );
140
+ writeFileSync(
141
+ join(logsDir, "assistant-2025-01-25.log"),
142
+ "log entry from Jan 25\n",
143
+ );
144
+ // Non-dated log file — should always be included regardless of time filter
145
+ writeFileSync(join(logsDir, "vellum.log"), "non-dated log content\n");
155
146
 
156
147
  // ---------------------------------------------------------------------------
157
148
  // Tests
@@ -185,90 +176,256 @@ describe("POST /v1/export — tar.gz archive", () => {
185
176
  }
186
177
  });
187
178
 
188
- test("archive contains workspace text files", async () => {
179
+ test("archive contains config-snapshot.json when config exists", async () => {
189
180
  const res = await callExport();
190
181
  const dir = await extractArchive(res);
191
182
  try {
192
- const identity = readFileSync(
193
- join(dir, "workspace", "IDENTITY.md"),
183
+ const configContent = readFileSync(
184
+ join(dir, "config-snapshot.json"),
194
185
  "utf-8",
195
186
  );
196
- expect(identity).toBe("# My Identity\nHello");
187
+ const parsed = JSON.parse(configContent);
188
+ expect(parsed.provider).toBe("anthropic");
189
+ } finally {
190
+ rmSync(dir, { recursive: true, force: true });
191
+ }
192
+ });
193
+ });
197
194
 
198
- const daily = readFileSync(
199
- join(dir, "workspace", "notes", "daily.txt"),
200
- "utf-8",
201
- );
202
- expect(daily).toBe("Some daily notes");
195
+ describe("POST /v1/export daemon log date filtering", () => {
196
+ test("excludes log files before startTime", async () => {
197
+ // startTime = Jan 14 — should exclude assistant-2025-01-10.log
198
+ const startTime = new Date("2025-01-14T00:00:00.000Z").getTime();
199
+ const res = await callExport({ startTime });
200
+ const dir = await extractArchive(res);
201
+ try {
202
+ const logFiles = readdirSync(join(dir, "daemon-logs"));
203
+ expect(logFiles).not.toContain("assistant-2025-01-10.log");
204
+ expect(logFiles).toContain("assistant-2025-01-15.log");
205
+ expect(logFiles).toContain("assistant-2025-01-20.log");
206
+ expect(logFiles).toContain("assistant-2025-01-25.log");
203
207
  } finally {
204
208
  rmSync(dir, { recursive: true, force: true });
205
209
  }
206
210
  });
207
211
 
208
- test("archive contains SQLite DB dumps as .sql files", async () => {
209
- const res = await callExport();
212
+ test("excludes log files after endTime", async () => {
213
+ // endTime = Jan 22 — should exclude assistant-2025-01-25.log
214
+ const endTime = new Date("2025-01-22T00:00:00.000Z").getTime();
215
+ const res = await callExport({ endTime });
210
216
  const dir = await extractArchive(res);
211
217
  try {
212
- const sqlContent = readFileSync(
213
- join(dir, "workspace", "data", "db", "assistant.db.sql"),
214
- "utf-8",
215
- );
216
- expect(sqlContent).toContain("CREATE TABLE");
217
- expect(sqlContent).toContain("test_table");
218
+ const logFiles = readdirSync(join(dir, "daemon-logs"));
219
+ expect(logFiles).toContain("assistant-2025-01-10.log");
220
+ expect(logFiles).toContain("assistant-2025-01-15.log");
221
+ expect(logFiles).toContain("assistant-2025-01-20.log");
222
+ expect(logFiles).not.toContain("assistant-2025-01-25.log");
218
223
  } finally {
219
224
  rmSync(dir, { recursive: true, force: true });
220
225
  }
221
226
  });
222
227
 
223
- test("archive excludes embedding-models/ and data/qdrant/", async () => {
228
+ test("filters log files by both startTime and endTime", async () => {
229
+ // startTime = Jan 14, endTime = Jan 22 — should only include Jan 15 and Jan 20
230
+ const startTime = new Date("2025-01-14T00:00:00.000Z").getTime();
231
+ const endTime = new Date("2025-01-22T00:00:00.000Z").getTime();
232
+ const res = await callExport({ startTime, endTime });
233
+ const dir = await extractArchive(res);
234
+ try {
235
+ const logFiles = readdirSync(join(dir, "daemon-logs"));
236
+ expect(logFiles).not.toContain("assistant-2025-01-10.log");
237
+ expect(logFiles).toContain("assistant-2025-01-15.log");
238
+ expect(logFiles).toContain("assistant-2025-01-20.log");
239
+ expect(logFiles).not.toContain("assistant-2025-01-25.log");
240
+ } finally {
241
+ rmSync(dir, { recursive: true, force: true });
242
+ }
243
+ });
244
+
245
+ test("always includes non-dated log files regardless of time filter", async () => {
246
+ const startTime = new Date("2025-01-14T00:00:00.000Z").getTime();
247
+ const endTime = new Date("2025-01-22T00:00:00.000Z").getTime();
248
+ const res = await callExport({ startTime, endTime });
249
+ const dir = await extractArchive(res);
250
+ try {
251
+ const logFiles = readdirSync(join(dir, "daemon-logs"));
252
+ expect(logFiles).toContain("vellum.log");
253
+ } finally {
254
+ rmSync(dir, { recursive: true, force: true });
255
+ }
256
+ });
257
+
258
+ test("includes all log files when no time filter is specified", async () => {
224
259
  const res = await callExport();
225
260
  const dir = await extractArchive(res);
226
261
  try {
227
- const files = listFiles(join(dir, "workspace"));
228
- const embeddingFiles = files.filter((f) =>
229
- f.startsWith("embedding-models/"),
230
- );
231
- const qdrantFiles = files.filter((f) => f.startsWith("data/qdrant/"));
232
- expect(embeddingFiles).toHaveLength(0);
233
- expect(qdrantFiles).toHaveLength(0);
262
+ const logFiles = readdirSync(join(dir, "daemon-logs"));
263
+ expect(logFiles).toContain("assistant-2025-01-10.log");
264
+ expect(logFiles).toContain("assistant-2025-01-15.log");
265
+ expect(logFiles).toContain("assistant-2025-01-20.log");
266
+ expect(logFiles).toContain("assistant-2025-01-25.log");
267
+ expect(logFiles).toContain("vellum.log");
234
268
  } finally {
235
269
  rmSync(dir, { recursive: true, force: true });
236
270
  }
237
271
  });
272
+ });
238
273
 
239
- test("archive excludes binary files and config.json at workspace root", async () => {
274
+ describe("POST /v1/export workspace allowlist", () => {
275
+ test("includes all valid conversation dirs by default", async () => {
240
276
  const res = await callExport();
241
277
  const dir = await extractArchive(res);
242
278
  try {
243
- const files = listFiles(join(dir, "workspace"));
244
- expect(files).not.toContain("binary-file.dat");
245
- expect(files).not.toContain("config.json");
279
+ const convs = readdirSync(join(dir, "workspace", "conversations"));
280
+ expect(convs).toContain("2025-01-10T00-00-00.000Z_conv-jan10");
281
+ expect(convs).toContain("2025-01-15T00-00-00.000Z_conv-jan15");
282
+ expect(convs).toContain("2025-01-20T00-00-00.000Z_conv-jan20");
283
+ expect(convs).toContain("2025-01-25T00-00-00.000Z_conv-jan25");
284
+ expect(convs).not.toContain("malformed-name");
246
285
  } finally {
247
286
  rmSync(dir, { recursive: true, force: true });
248
287
  }
249
288
  });
250
289
 
251
- test("archive excludes symlinks", async () => {
290
+ test("skips malformed conversation dir names", async () => {
252
291
  const res = await callExport();
253
292
  const dir = await extractArchive(res);
254
293
  try {
255
- const files = listFiles(join(dir, "workspace"));
256
- expect(files).not.toContain("sneaky-link.txt");
294
+ const convs = readdirSync(join(dir, "workspace", "conversations"));
295
+ expect(convs).not.toContain("malformed-name");
257
296
  } finally {
258
297
  rmSync(dir, { recursive: true, force: true });
259
298
  }
260
299
  });
261
300
 
262
- test("archive contains config-snapshot.json when config exists", async () => {
301
+ test("filters conversation dirs by startTime", async () => {
302
+ const startTime = Date.parse("2025-01-14T00:00:00Z");
303
+ const res = await callExport({ startTime });
304
+ const dir = await extractArchive(res);
305
+ try {
306
+ const convs = readdirSync(join(dir, "workspace", "conversations"));
307
+ expect(convs).not.toContain("2025-01-10T00-00-00.000Z_conv-jan10");
308
+ expect(convs).toContain("2025-01-15T00-00-00.000Z_conv-jan15");
309
+ expect(convs).toContain("2025-01-20T00-00-00.000Z_conv-jan20");
310
+ expect(convs).toContain("2025-01-25T00-00-00.000Z_conv-jan25");
311
+ } finally {
312
+ rmSync(dir, { recursive: true, force: true });
313
+ }
314
+ });
315
+
316
+ test("filters conversation dirs by endTime", async () => {
317
+ const endTime = Date.parse("2025-01-22T00:00:00Z");
318
+ const res = await callExport({ endTime });
319
+ const dir = await extractArchive(res);
320
+ try {
321
+ const convs = readdirSync(join(dir, "workspace", "conversations"));
322
+ expect(convs).toContain("2025-01-10T00-00-00.000Z_conv-jan10");
323
+ expect(convs).toContain("2025-01-15T00-00-00.000Z_conv-jan15");
324
+ expect(convs).toContain("2025-01-20T00-00-00.000Z_conv-jan20");
325
+ expect(convs).not.toContain("2025-01-25T00-00-00.000Z_conv-jan25");
326
+ } finally {
327
+ rmSync(dir, { recursive: true, force: true });
328
+ }
329
+ });
330
+
331
+ test("filters conversation dirs by both startTime and endTime", async () => {
332
+ const startTime = Date.parse("2025-01-14T00:00:00Z");
333
+ const endTime = Date.parse("2025-01-22T00:00:00Z");
334
+ const res = await callExport({ startTime, endTime });
335
+ const dir = await extractArchive(res);
336
+ try {
337
+ const convs = readdirSync(join(dir, "workspace", "conversations"));
338
+ expect(convs).not.toContain("2025-01-10T00-00-00.000Z_conv-jan10");
339
+ expect(convs).toContain("2025-01-15T00-00-00.000Z_conv-jan15");
340
+ expect(convs).toContain("2025-01-20T00-00-00.000Z_conv-jan20");
341
+ expect(convs).not.toContain("2025-01-25T00-00-00.000Z_conv-jan25");
342
+ } finally {
343
+ rmSync(dir, { recursive: true, force: true });
344
+ }
345
+ });
346
+
347
+ test("filters conversation dirs by conversationId", async () => {
348
+ const res = await callExport({ conversationId: "conv-jan15" });
349
+ const dir = await extractArchive(res);
350
+ try {
351
+ const convs = readdirSync(join(dir, "workspace", "conversations"));
352
+ expect(convs).toContain("2025-01-15T00-00-00.000Z_conv-jan15");
353
+ expect(convs).not.toContain("2025-01-10T00-00-00.000Z_conv-jan10");
354
+ expect(convs).not.toContain("2025-01-20T00-00-00.000Z_conv-jan20");
355
+ expect(convs).not.toContain("2025-01-25T00-00-00.000Z_conv-jan25");
356
+ expect(convs).not.toContain("malformed-name");
357
+ } finally {
358
+ rmSync(dir, { recursive: true, force: true });
359
+ }
360
+ });
361
+
362
+ test("conversationId + time filter intersect", async () => {
363
+ const res = await callExport({
364
+ conversationId: "conv-jan15",
365
+ startTime: Date.parse("2025-02-01T00:00:00Z"),
366
+ });
367
+ const dir = await extractArchive(res);
368
+ try {
369
+ const conversationsPath = join(dir, "workspace", "conversations");
370
+ let convs: string[] = [];
371
+ try {
372
+ convs = readdirSync(conversationsPath);
373
+ } catch {
374
+ // Directory does not exist — acceptable per the test contract.
375
+ }
376
+ expect(convs).toEqual([]);
377
+ } finally {
378
+ rmSync(dir, { recursive: true, force: true });
379
+ }
380
+ });
381
+
382
+ test("conversation dir contents survive the round trip", async () => {
263
383
  const res = await callExport();
264
384
  const dir = await extractArchive(res);
265
385
  try {
266
- const configContent = readFileSync(
267
- join(dir, "config-snapshot.json"),
268
- "utf-8",
386
+ const messagesPath = join(
387
+ dir,
388
+ "workspace",
389
+ "conversations",
390
+ "2025-01-15T00-00-00.000Z_conv-jan15",
391
+ "messages.jsonl",
269
392
  );
270
- const parsed = JSON.parse(configContent);
271
- expect(parsed.provider).toBe("anthropic");
393
+ const content = readFileSync(messagesPath, "utf-8");
394
+ expect(content).toBe('{"role":"user","content":"jan 15"}\n');
395
+ } finally {
396
+ rmSync(dir, { recursive: true, force: true });
397
+ }
398
+ });
399
+
400
+ test("treats empty-string conversationId as no filter", async () => {
401
+ const res = await callExport({ conversationId: "" });
402
+ const dir = await extractArchive(res);
403
+ try {
404
+ // With conversationId === "" (which the rest of handleExport treats as
405
+ // unfiltered), workspace conversations should also be unfiltered. All
406
+ // four canonical conversation dirs should be present.
407
+ const conversationsDir = join(dir, "workspace", "conversations");
408
+ const entries = readdirSync(conversationsDir);
409
+ expect(entries).toContain("2025-01-10T00-00-00.000Z_conv-jan10");
410
+ expect(entries).toContain("2025-01-15T00-00-00.000Z_conv-jan15");
411
+ expect(entries).toContain("2025-01-20T00-00-00.000Z_conv-jan20");
412
+ expect(entries).toContain("2025-01-25T00-00-00.000Z_conv-jan25");
413
+ } finally {
414
+ rmSync(dir, { recursive: true, force: true });
415
+ }
416
+ });
417
+
418
+ test("treats startTime=0 and endTime=0 as no filter", async () => {
419
+ const res = await callExport({ startTime: 0, endTime: 0 });
420
+ const dir = await extractArchive(res);
421
+ try {
422
+ const conversationsDir = join(dir, "workspace", "conversations");
423
+ const entries = readdirSync(conversationsDir);
424
+ // All four canonical conversation dirs should be present (no filtering).
425
+ expect(entries).toContain("2025-01-10T00-00-00.000Z_conv-jan10");
426
+ expect(entries).toContain("2025-01-15T00-00-00.000Z_conv-jan15");
427
+ expect(entries).toContain("2025-01-20T00-00-00.000Z_conv-jan20");
428
+ expect(entries).toContain("2025-01-25T00-00-00.000Z_conv-jan25");
272
429
  } finally {
273
430
  rmSync(dir, { recursive: true, force: true });
274
431
  }