@vellumai/assistant 0.5.6 → 0.5.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (442) hide show
  1. package/.env.example +16 -2
  2. package/ARCHITECTURE.md +6 -75
  3. package/Dockerfile +3 -2
  4. package/README.md +0 -2
  5. package/bun.lock +0 -414
  6. package/docker-entrypoint.sh +9 -0
  7. package/docs/architecture/keychain-broker.md +45 -240
  8. package/docs/architecture/memory.md +13 -11
  9. package/docs/architecture/security.md +0 -17
  10. package/docs/credential-execution-service.md +2 -2
  11. package/node_modules/@vellumai/ces-contracts/package.json +1 -0
  12. package/node_modules/@vellumai/ces-contracts/src/error.ts +1 -1
  13. package/node_modules/@vellumai/ces-contracts/src/grants.ts +1 -1
  14. package/node_modules/@vellumai/ces-contracts/src/handles.ts +1 -1
  15. package/node_modules/@vellumai/ces-contracts/src/index.ts +1 -1
  16. package/node_modules/@vellumai/ces-contracts/src/rpc.ts +120 -1
  17. package/node_modules/@vellumai/credential-storage/package.json +1 -0
  18. package/node_modules/@vellumai/egress-proxy/package.json +1 -0
  19. package/package.json +2 -3
  20. package/src/__tests__/actor-token-service.test.ts +0 -114
  21. package/src/__tests__/approval-cascade.test.ts +0 -1
  22. package/src/__tests__/assistant-feature-flags-integration.test.ts +30 -29
  23. package/src/__tests__/browser-fill-credential.test.ts +1 -1
  24. package/src/__tests__/browser-skill-endstate.test.ts +6 -5
  25. package/src/__tests__/btw-routes.test.ts +0 -39
  26. package/src/__tests__/call-controller.test.ts +0 -1
  27. package/src/__tests__/call-domain.test.ts +0 -128
  28. package/src/__tests__/ces-rpc-credential-backend.test.ts +199 -0
  29. package/src/__tests__/ces-startup-timeout.test.ts +40 -0
  30. package/src/__tests__/channel-approval-routes.test.ts +0 -5
  31. package/src/__tests__/channel-readiness-service.test.ts +1 -60
  32. package/src/__tests__/checker.test.ts +4 -2
  33. package/src/__tests__/cli-command-risk-guard.test.ts +112 -0
  34. package/src/__tests__/config-schema-cmd.test.ts +0 -2
  35. package/src/__tests__/config-schema.test.ts +3 -1
  36. package/src/__tests__/conversation-abort-tool-results.test.ts +0 -1
  37. package/src/__tests__/conversation-agent-loop-overflow.test.ts +0 -2
  38. package/src/__tests__/conversation-agent-loop.test.ts +2 -4
  39. package/src/__tests__/conversation-attention-telegram.test.ts +0 -5
  40. package/src/__tests__/conversation-confirmation-signals.test.ts +0 -1
  41. package/src/__tests__/conversation-error.test.ts +15 -1
  42. package/src/__tests__/conversation-init.benchmark.test.ts +0 -2
  43. package/src/__tests__/conversation-messaging-secret-redirect.test.ts +1 -1
  44. package/src/__tests__/conversation-pre-run-repair.test.ts +0 -1
  45. package/src/__tests__/conversation-provider-retry-repair.test.ts +0 -1
  46. package/src/__tests__/conversation-queue.test.ts +0 -1
  47. package/src/__tests__/conversation-skill-tools.test.ts +0 -54
  48. package/src/__tests__/conversation-slash-queue.test.ts +0 -1
  49. package/src/__tests__/conversation-slash-unknown.test.ts +0 -1
  50. package/src/__tests__/conversation-title-service.test.ts +87 -0
  51. package/src/__tests__/conversation-workspace-injection.test.ts +0 -1
  52. package/src/__tests__/conversation-workspace-tool-tracking.test.ts +0 -1
  53. package/src/__tests__/credential-execution-client.test.ts +5 -2
  54. package/src/__tests__/credential-execution-feature-gates.test.ts +59 -30
  55. package/src/__tests__/credential-execution-managed-contract.test.ts +35 -20
  56. package/src/__tests__/credential-security-e2e.test.ts +1 -67
  57. package/src/__tests__/credential-security-invariants.test.ts +6 -50
  58. package/src/__tests__/credentials-cli.test.ts +82 -3
  59. package/src/__tests__/daemon-credential-client.test.ts +123 -0
  60. package/src/__tests__/db-migration-rollback.test.ts +2015 -1
  61. package/src/__tests__/deterministic-verification-control-plane.test.ts +1 -0
  62. package/src/__tests__/docker-signing-key-bootstrap.test.ts +34 -143
  63. package/src/__tests__/dynamic-skill-workflow-prompt.test.ts +6 -4
  64. package/src/__tests__/gateway-client-managed-outbound.test.ts +79 -1
  65. package/src/__tests__/guardian-routing-state.test.ts +0 -5
  66. package/src/__tests__/host-shell-tool.test.ts +6 -7
  67. package/src/__tests__/http-user-message-parity.test.ts +3 -103
  68. package/src/__tests__/inbound-invite-redemption.test.ts +0 -4
  69. package/src/__tests__/inline-skill-load-permissions.test.ts +6 -8
  70. package/src/__tests__/intent-routing.test.ts +0 -13
  71. package/src/__tests__/jobs-store-qdrant-breaker.test.ts +178 -0
  72. package/src/__tests__/journal-context.test.ts +335 -0
  73. package/src/__tests__/keychain-broker-client.test.ts +161 -22
  74. package/src/__tests__/memory-context-benchmark.benchmark.test.ts +0 -3
  75. package/src/__tests__/memory-jobs-worker-backoff.test.ts +150 -0
  76. package/src/__tests__/memory-lifecycle-e2e.test.ts +70 -25
  77. package/src/__tests__/memory-recall-quality.test.ts +48 -17
  78. package/src/__tests__/memory-regressions.test.ts +408 -363
  79. package/src/__tests__/memory-retrieval.benchmark.test.ts +0 -3
  80. package/src/__tests__/migration-export-http.test.ts +2 -2
  81. package/src/__tests__/migration-import-commit-http.test.ts +2 -2
  82. package/src/__tests__/migration-import-preflight-http.test.ts +2 -2
  83. package/src/__tests__/migration-validate-http.test.ts +2 -2
  84. package/src/__tests__/non-member-access-request.test.ts +2 -7
  85. package/src/__tests__/notification-decision-fallback.test.ts +4 -0
  86. package/src/__tests__/notification-decision-identity.test.ts +4 -0
  87. package/src/__tests__/notification-decision-strategy.test.ts +71 -0
  88. package/src/__tests__/oauth-cli.test.ts +5 -1
  89. package/src/__tests__/permission-types.test.ts +1 -0
  90. package/src/__tests__/provider-commit-message-generator.test.ts +0 -37
  91. package/src/__tests__/provider-error-scenarios.test.ts +0 -267
  92. package/src/__tests__/provider-managed-proxy-integration.test.ts +5 -6
  93. package/src/__tests__/provider-streaming.benchmark.test.ts +2 -81
  94. package/src/__tests__/qdrant-manager.test.ts +28 -2
  95. package/src/__tests__/registry.test.ts +0 -6
  96. package/src/__tests__/relay-server.test.ts +1 -2
  97. package/src/__tests__/runtime-attachment-metadata.test.ts +0 -4
  98. package/src/__tests__/script-proxy-injection-runtime.test.ts +1 -1
  99. package/src/__tests__/secret-onetime-send.test.ts +1 -1
  100. package/src/__tests__/secret-routes-managed-proxy.test.ts +0 -4
  101. package/src/__tests__/secure-keys.test.ts +95 -272
  102. package/src/__tests__/shell-identity.test.ts +96 -6
  103. package/src/__tests__/skill-feature-flags-integration.test.ts +22 -14
  104. package/src/__tests__/skill-feature-flags.test.ts +46 -45
  105. package/src/__tests__/skill-load-feature-flag.test.ts +7 -10
  106. package/src/__tests__/skill-load-inline-command.test.ts +8 -12
  107. package/src/__tests__/skill-load-inline-includes.test.ts +6 -10
  108. package/src/__tests__/skill-load-tool.test.ts +0 -2
  109. package/src/__tests__/skill-memory.test.ts +17 -3
  110. package/src/__tests__/skill-projection-feature-flag.test.ts +33 -29
  111. package/src/__tests__/skills.test.ts +0 -2
  112. package/src/__tests__/slack-inbound-verification.test.ts +0 -4
  113. package/src/__tests__/stale-approval-dedup.test.ts +171 -0
  114. package/src/__tests__/stt-hints.test.ts +437 -0
  115. package/src/__tests__/suggestion-routes.test.ts +1 -32
  116. package/src/__tests__/system-prompt.test.ts +0 -1
  117. package/src/__tests__/task-memory-cleanup.test.ts +14 -0
  118. package/src/__tests__/tool-executor-shell-integration.test.ts +5 -3
  119. package/src/__tests__/trusted-contact-lifecycle-notifications.test.ts +0 -5
  120. package/src/__tests__/trusted-contact-multichannel.test.ts +0 -4
  121. package/src/__tests__/twilio-routes-twiml.test.ts +139 -1
  122. package/src/__tests__/update-bulletin.test.ts +0 -2
  123. package/src/__tests__/vellum-self-knowledge-inline-command.test.ts +6 -9
  124. package/src/__tests__/voice-quality.test.ts +58 -0
  125. package/src/__tests__/voice-scoped-grant-consumer.test.ts +0 -7
  126. package/src/__tests__/workspace-migration-015-migrate-credentials-to-keychain.test.ts +252 -0
  127. package/src/__tests__/workspace-migration-016-migrate-credentials-from-keychain.test.ts +220 -0
  128. package/src/__tests__/workspace-migration-down-functions.test.ts +1009 -0
  129. package/src/__tests__/workspace-migrations-runner.test.ts +114 -0
  130. package/src/acp/agent-process.ts +9 -1
  131. package/src/agent/loop.ts +1 -1
  132. package/src/approvals/guardian-request-resolvers.ts +164 -38
  133. package/src/calls/__tests__/tts-text-sanitizer.test.ts +254 -0
  134. package/src/calls/audio-store.test.ts +97 -0
  135. package/src/calls/audio-store.ts +205 -0
  136. package/src/calls/call-controller.ts +90 -8
  137. package/src/calls/call-domain.ts +3 -0
  138. package/src/calls/call-store.ts +10 -3
  139. package/src/calls/fish-audio-client.ts +129 -0
  140. package/src/calls/relay-server.ts +27 -0
  141. package/src/calls/stt-hints.ts +189 -0
  142. package/src/calls/tts-text-sanitizer.ts +61 -0
  143. package/src/calls/twilio-routes.ts +34 -5
  144. package/src/calls/types.ts +1 -0
  145. package/src/calls/voice-ingress-preflight.ts +0 -42
  146. package/src/calls/voice-quality.ts +38 -5
  147. package/src/calls/voice-session-bridge.ts +7 -12
  148. package/src/cli/commands/avatar.ts +2 -2
  149. package/src/cli/commands/config.ts +1 -4
  150. package/src/cli/commands/credentials.ts +128 -82
  151. package/src/cli/commands/doctor.ts +2 -2
  152. package/src/cli/commands/keys.ts +7 -7
  153. package/src/cli/commands/memory.ts +1 -1
  154. package/src/cli/commands/oauth/connections.ts +11 -29
  155. package/src/cli/commands/oauth/index.ts +7 -0
  156. package/src/cli/commands/oauth/platform.ts +525 -0
  157. package/src/cli/commands/platform.ts +3 -3
  158. package/src/cli/lib/daemon-credential-client.ts +284 -0
  159. package/src/cli.ts +1 -1
  160. package/src/config/assistant-feature-flags.ts +186 -5
  161. package/src/config/bundled-skills/AGENTS.md +34 -0
  162. package/src/config/bundled-skills/acp/SKILL.md +10 -0
  163. package/src/config/bundled-skills/app-builder/SKILL.md +0 -4
  164. package/src/config/bundled-skills/messaging/SKILL.md +5 -5
  165. package/src/config/bundled-skills/messaging/tools/messaging-analyze-style.ts +2 -2
  166. package/src/config/bundled-skills/phone-calls/TOOLS.json +4 -0
  167. package/src/config/bundled-skills/playbooks/tools/playbook-create.ts +1 -0
  168. package/src/config/bundled-skills/playbooks/tools/playbook-update.ts +1 -0
  169. package/src/config/bundled-skills/settings/SKILL.md +15 -2
  170. package/src/config/bundled-skills/settings/TOOLS.json +47 -2
  171. package/src/config/bundled-skills/settings/tools/avatar-remove.ts +59 -0
  172. package/src/config/bundled-skills/settings/tools/avatar-update.ts +80 -0
  173. package/src/config/bundled-skills/settings/tools/voice-config-update.ts +42 -0
  174. package/src/config/bundled-skills/slack/SKILL.md +1 -1
  175. package/src/config/bundled-tool-registry.ts +5 -11
  176. package/src/config/defaults.ts +0 -2
  177. package/src/config/env-registry.ts +5 -5
  178. package/src/config/env.ts +21 -14
  179. package/src/config/feature-flag-registry.json +49 -9
  180. package/src/config/loader.ts +106 -42
  181. package/src/config/schema.ts +9 -29
  182. package/src/config/schemas/calls.ts +30 -0
  183. package/src/config/schemas/fish-audio.ts +39 -0
  184. package/src/config/schemas/inference.ts +2 -2
  185. package/src/config/schemas/journal.ts +16 -0
  186. package/src/config/schemas/memory-processing.ts +2 -2
  187. package/src/config/schemas/security.ts +0 -4
  188. package/src/config/types.ts +1 -1
  189. package/src/contacts/contact-store.ts +39 -0
  190. package/src/contacts/types.ts +2 -0
  191. package/src/credential-execution/approval-bridge.ts +1 -0
  192. package/src/credential-execution/executable-discovery.ts +28 -4
  193. package/src/credential-execution/feature-gates.ts +16 -0
  194. package/src/credential-execution/process-manager.ts +38 -0
  195. package/src/credential-execution/startup-timeout.ts +36 -0
  196. package/src/daemon/approval-generators.ts +3 -9
  197. package/src/daemon/assistant-attachments.ts +9 -0
  198. package/src/daemon/config-watcher.ts +5 -0
  199. package/src/daemon/conversation-error.ts +13 -1
  200. package/src/daemon/conversation-memory.ts +1 -2
  201. package/src/daemon/conversation-process.ts +18 -1
  202. package/src/daemon/conversation-surfaces.ts +30 -1
  203. package/src/daemon/conversation-tool-setup.ts +0 -105
  204. package/src/daemon/conversation.ts +21 -1
  205. package/src/daemon/guardian-action-generators.ts +3 -9
  206. package/src/daemon/handlers/config-vercel.ts +92 -0
  207. package/src/daemon/handlers/skills.ts +2 -15
  208. package/src/daemon/install-symlink.ts +195 -0
  209. package/src/daemon/lifecycle.ts +234 -51
  210. package/src/daemon/message-types/conversations.ts +4 -4
  211. package/src/daemon/message-types/diagnostics.ts +3 -22
  212. package/src/daemon/message-types/messages.ts +0 -2
  213. package/src/daemon/message-types/upgrades.ts +8 -0
  214. package/src/daemon/server.ts +32 -95
  215. package/src/events/domain-events.ts +2 -1
  216. package/src/inbound/platform-callback-registration.ts +3 -3
  217. package/src/instrument.ts +8 -5
  218. package/src/memory/app-store.ts +31 -0
  219. package/src/memory/conversation-title-service.ts +50 -1
  220. package/src/memory/db-init.ts +16 -0
  221. package/src/memory/indexer.ts +19 -10
  222. package/src/memory/items-extractor.ts +328 -321
  223. package/src/memory/job-handlers/conversation-starters.ts +4 -1
  224. package/src/memory/job-handlers/summarization.ts +26 -16
  225. package/src/memory/jobs-store.ts +63 -6
  226. package/src/memory/jobs-worker.ts +31 -7
  227. package/src/memory/journal-memory.ts +214 -0
  228. package/src/memory/migrations/001-job-deferrals.ts +19 -0
  229. package/src/memory/migrations/004-entity-relation-dedup.ts +10 -0
  230. package/src/memory/migrations/005-fingerprint-scope-unique.ts +76 -0
  231. package/src/memory/migrations/006-scope-salted-fingerprints.ts +50 -0
  232. package/src/memory/migrations/007-assistant-id-to-self.ts +10 -0
  233. package/src/memory/migrations/008-remove-assistant-id-columns.ts +34 -0
  234. package/src/memory/migrations/009-llm-usage-events-drop-assistant-id.ts +26 -0
  235. package/src/memory/migrations/014-backfill-inbox-thread-state.ts +10 -0
  236. package/src/memory/migrations/015-drop-active-search-index.ts +17 -0
  237. package/src/memory/migrations/019-notification-tables-schema-migration.ts +12 -0
  238. package/src/memory/migrations/020-rename-macos-ios-channel-to-vellum.ts +121 -0
  239. package/src/memory/migrations/024-embedding-vector-blob.ts +74 -0
  240. package/src/memory/migrations/026a-embeddings-nullable-vector-json.ts +82 -0
  241. package/src/memory/migrations/036-normalize-phone-identities.ts +11 -0
  242. package/src/memory/migrations/116-messages-fts.ts +106 -1
  243. package/src/memory/migrations/126-backfill-guardian-principal-id.ts +52 -0
  244. package/src/memory/migrations/127-guardian-principal-id-not-null.ts +77 -0
  245. package/src/memory/migrations/134-contacts-notes-column.ts +13 -0
  246. package/src/memory/migrations/135-backfill-contact-interaction-stats.ts +20 -0
  247. package/src/memory/migrations/136-drop-assistant-id-columns.ts +52 -0
  248. package/src/memory/migrations/140-backfill-usage-cache-accounting.ts +13 -0
  249. package/src/memory/migrations/141-rename-verification-table.ts +54 -0
  250. package/src/memory/migrations/142-rename-verification-session-id-column.ts +25 -0
  251. package/src/memory/migrations/143-rename-guardian-verification-values.ts +35 -0
  252. package/src/memory/migrations/144-rename-voice-to-phone.ts +136 -0
  253. package/src/memory/migrations/145-drop-accounts-table.ts +32 -0
  254. package/src/memory/migrations/147-migrate-reminders-to-schedules.ts +14 -1
  255. package/src/memory/migrations/148-drop-reminders-table.ts +35 -1
  256. package/src/memory/migrations/150-oauth-apps-client-secret-path.ts +69 -1
  257. package/src/memory/migrations/162-guardian-timestamps-epoch-ms.ts +290 -0
  258. package/src/memory/migrations/169-rename-gmail-provider-key-to-google.ts +51 -1
  259. package/src/memory/migrations/174-rename-thread-starters-table.ts +47 -1
  260. package/src/memory/migrations/176-drop-capability-card-state.ts +13 -0
  261. package/src/memory/migrations/180-backfill-inline-attachments-to-disk.ts +16 -0
  262. package/src/memory/migrations/181-rename-thread-starters-checkpoints.ts +28 -1
  263. package/src/memory/migrations/190-call-session-skip-disclosure.ts +15 -0
  264. package/src/memory/migrations/191-backfill-audio-attachment-mime-types.ts +64 -0
  265. package/src/memory/migrations/192-contacts-user-file-column.ts +15 -0
  266. package/src/memory/migrations/193-add-source-type-columns.ts +81 -0
  267. package/src/memory/migrations/index.ts +5 -0
  268. package/src/memory/migrations/registry.ts +98 -0
  269. package/src/memory/migrations/validate-migration-state.ts +137 -11
  270. package/src/memory/qdrant-circuit-breaker.ts +9 -0
  271. package/src/memory/qdrant-manager.ts +64 -7
  272. package/src/memory/retriever.test.ts +37 -25
  273. package/src/memory/retriever.ts +24 -49
  274. package/src/memory/schema/calls.ts +1 -0
  275. package/src/memory/schema/contacts.ts +1 -0
  276. package/src/memory/schema/memory-core.ts +2 -0
  277. package/src/memory/search/formatting.ts +7 -44
  278. package/src/memory/search/staleness.ts +4 -0
  279. package/src/memory/search/tier-classifier.ts +10 -2
  280. package/src/memory/search/types.ts +2 -5
  281. package/src/memory/task-memory-cleanup.ts +4 -3
  282. package/src/notifications/adapters/slack.ts +168 -6
  283. package/src/notifications/broadcaster.ts +1 -0
  284. package/src/notifications/copy-composer.ts +59 -2
  285. package/src/notifications/decision-engine.ts +4 -1
  286. package/src/notifications/signal.ts +2 -0
  287. package/src/notifications/types.ts +2 -0
  288. package/src/oauth/connection-resolver.ts +6 -4
  289. package/src/permissions/checker.ts +0 -38
  290. package/src/permissions/shell-identity.ts +76 -22
  291. package/src/permissions/types.ts +4 -2
  292. package/src/platform/client.ts +35 -7
  293. package/src/prompts/journal-context.ts +133 -0
  294. package/src/prompts/persona-resolver.ts +194 -0
  295. package/src/prompts/system-prompt.ts +44 -4
  296. package/src/prompts/templates/SOUL.md +10 -0
  297. package/src/prompts/templates/users/default.md +1 -0
  298. package/src/providers/provider-send-message.ts +3 -32
  299. package/src/providers/registry.ts +29 -179
  300. package/src/providers/types.ts +1 -1
  301. package/src/runtime/access-request-helper.ts +4 -0
  302. package/src/runtime/auth/__tests__/credential-service.test.ts +0 -1
  303. package/src/runtime/auth/__tests__/external-assistant-id.test.ts +13 -68
  304. package/src/runtime/auth/__tests__/guard-tests.test.ts +9 -50
  305. package/src/runtime/auth/external-assistant-id.ts +13 -59
  306. package/src/runtime/auth/route-policy.ts +17 -1
  307. package/src/runtime/auth/token-service.ts +43 -138
  308. package/src/runtime/channel-readiness-service.ts +1 -16
  309. package/src/runtime/gateway-client.ts +47 -4
  310. package/src/runtime/guardian-decision-types.ts +45 -4
  311. package/src/runtime/http-server.ts +31 -3
  312. package/src/runtime/middleware/error-handler.ts +1 -9
  313. package/src/runtime/routes/access-request-decision.ts +2 -2
  314. package/src/runtime/routes/app-management-routes.ts +2 -1
  315. package/src/runtime/routes/approval-strategies/guardian-callback-strategy.ts +219 -30
  316. package/src/runtime/routes/approval-strategies/guardian-text-engine-strategy.ts +37 -14
  317. package/src/runtime/routes/audio-routes.ts +40 -0
  318. package/src/runtime/routes/btw-routes.ts +0 -17
  319. package/src/runtime/routes/channel-readiness-routes.ts +9 -4
  320. package/src/runtime/routes/conversation-query-routes.ts +63 -1
  321. package/src/runtime/routes/conversation-routes.ts +4 -44
  322. package/src/runtime/routes/debug-routes.ts +12 -9
  323. package/src/runtime/routes/diagnostics-routes.ts +1 -477
  324. package/src/runtime/routes/guardian-approval-interception.ts +168 -11
  325. package/src/runtime/routes/guardian-approval-prompt.ts +6 -1
  326. package/src/runtime/routes/guardian-approval-reply-helpers.ts +103 -21
  327. package/src/runtime/routes/identity-routes.ts +19 -30
  328. package/src/runtime/routes/inbound-message-handler.ts +31 -1
  329. package/src/runtime/routes/inbound-stages/acl-enforcement.ts +64 -5
  330. package/src/runtime/routes/inbound-stages/background-dispatch.ts +52 -40
  331. package/src/runtime/routes/inbound-stages/secret-ingress-check.ts +4 -33
  332. package/src/runtime/routes/inbound-stages/transcribe-audio.test.ts +1 -1
  333. package/src/runtime/routes/integrations/twilio.ts +52 -10
  334. package/src/runtime/routes/integrations/vercel.ts +89 -0
  335. package/src/runtime/routes/log-export-routes.ts +5 -0
  336. package/src/runtime/routes/memory-item-routes.test.ts +3 -3
  337. package/src/runtime/routes/memory-item-routes.ts +46 -14
  338. package/src/runtime/routes/migration-rollback-routes.ts +209 -0
  339. package/src/runtime/routes/migration-routes.ts +17 -1
  340. package/src/runtime/routes/notification-routes.ts +58 -0
  341. package/src/runtime/routes/schedule-routes.ts +65 -0
  342. package/src/runtime/routes/secret-routes.ts +141 -10
  343. package/src/runtime/routes/settings-routes.ts +41 -1
  344. package/src/runtime/routes/tts-routes.ts +96 -0
  345. package/src/runtime/routes/upgrade-broadcast-routes.ts +26 -2
  346. package/src/runtime/routes/workspace-commit-routes.ts +62 -0
  347. package/src/runtime/routes/workspace-routes.test.ts +22 -1
  348. package/src/runtime/routes/workspace-routes.ts +1 -1
  349. package/src/runtime/routes/workspace-utils.ts +86 -2
  350. package/src/security/ces-credential-client.ts +75 -29
  351. package/src/security/ces-rpc-credential-backend.ts +86 -0
  352. package/src/security/credential-backend.ts +22 -92
  353. package/src/security/keychain-broker-client.ts +10 -2
  354. package/src/security/secure-keys.ts +113 -115
  355. package/src/skills/catalog-install.ts +6 -32
  356. package/src/skills/skill-memory.ts +1 -0
  357. package/src/subagent/manager.ts +2 -5
  358. package/src/telemetry/usage-telemetry-reporter.ts +4 -2
  359. package/src/tools/acp/spawn.ts +78 -1
  360. package/src/tools/calls/call-start.ts +1 -0
  361. package/src/tools/credentials/vault.ts +5 -3
  362. package/src/tools/executor.ts +0 -4
  363. package/src/tools/memory/definitions.ts +3 -2
  364. package/src/tools/memory/handlers.ts +10 -7
  365. package/src/tools/network/script-proxy/session-manager.ts +19 -4
  366. package/src/tools/network/web-fetch.ts +3 -1
  367. package/src/tools/skills/execute.ts +1 -1
  368. package/src/tools/terminal/safe-env.ts +1 -0
  369. package/src/tools/types.ts +0 -8
  370. package/src/util/browser.ts +15 -0
  371. package/src/util/errors.ts +0 -12
  372. package/src/util/platform.ts +4 -51
  373. package/src/workspace/git-service.ts +5 -2
  374. package/src/workspace/migrations/001-avatar-rename.ts +15 -0
  375. package/src/workspace/migrations/003-seed-device-id.ts +17 -1
  376. package/src/workspace/migrations/004-extract-collect-usage-data.ts +33 -0
  377. package/src/workspace/migrations/005-add-send-diagnostics.ts +3 -0
  378. package/src/workspace/migrations/006-services-config.ts +49 -0
  379. package/src/workspace/migrations/007-web-search-provider-rename.ts +27 -0
  380. package/src/workspace/migrations/008-voice-timeout-and-max-steps.ts +3 -0
  381. package/src/workspace/migrations/009-backfill-conversation-disk-view.ts +4 -0
  382. package/src/workspace/migrations/010-app-dir-rename.ts +78 -0
  383. package/src/workspace/migrations/011-backfill-installation-id.ts +11 -0
  384. package/src/workspace/migrations/012-rename-conversation-disk-view-dirs.ts +44 -0
  385. package/src/workspace/migrations/013-repair-conversation-disk-view.ts +5 -0
  386. package/src/workspace/migrations/015-migrate-credentials-to-keychain.ts +153 -0
  387. package/src/workspace/migrations/016-extract-feature-flags-to-protected.ts +156 -0
  388. package/src/workspace/migrations/016-migrate-credentials-from-keychain.ts +150 -0
  389. package/src/workspace/migrations/017-seed-persona-dirs.ts +96 -0
  390. package/src/workspace/migrations/018-rekey-compound-credential-keys.ts +184 -0
  391. package/src/workspace/migrations/019-scope-journal-to-guardian.ts +103 -0
  392. package/src/workspace/migrations/migrate-to-workspace-volume.ts +27 -5
  393. package/src/workspace/migrations/registry.ts +12 -0
  394. package/src/workspace/migrations/runner.ts +106 -2
  395. package/src/workspace/migrations/types.ts +4 -0
  396. package/src/workspace/provider-commit-message-generator.ts +12 -21
  397. package/src/__tests__/claude-code-skill-regression.test.ts +0 -206
  398. package/src/__tests__/claude-code-tool-profiles.test.ts +0 -99
  399. package/src/__tests__/diagnostics-export.test.ts +0 -288
  400. package/src/__tests__/local-gateway-health.test.ts +0 -209
  401. package/src/__tests__/provider-fail-open-selection.test.ts +0 -271
  402. package/src/__tests__/provider-failover-actual-provider.test.ts +0 -66
  403. package/src/__tests__/secret-ingress-handler.test.ts +0 -120
  404. package/src/__tests__/swarm-conversation-integration.test.ts +0 -358
  405. package/src/__tests__/swarm-dag-pathological.test.ts +0 -547
  406. package/src/__tests__/swarm-orchestrator.test.ts +0 -463
  407. package/src/__tests__/swarm-plan-validator.test.ts +0 -384
  408. package/src/__tests__/swarm-recursion.test.ts +0 -197
  409. package/src/__tests__/swarm-router-planner.test.ts +0 -234
  410. package/src/__tests__/swarm-tool.test.ts +0 -185
  411. package/src/__tests__/swarm-worker-backend.test.ts +0 -144
  412. package/src/__tests__/swarm-worker-runner.test.ts +0 -288
  413. package/src/commands/__tests__/cc-command-registry.test.ts +0 -396
  414. package/src/commands/cc-command-registry.ts +0 -248
  415. package/src/config/bundled-skills/claude-code/SKILL.md +0 -53
  416. package/src/config/bundled-skills/claude-code/TOOLS.json +0 -47
  417. package/src/config/bundled-skills/claude-code/tools/claude-code.ts +0 -12
  418. package/src/config/bundled-skills/orchestration/SKILL.md +0 -33
  419. package/src/config/bundled-skills/orchestration/TOOLS.json +0 -35
  420. package/src/config/bundled-skills/orchestration/tools/swarm-delegate.ts +0 -12
  421. package/src/config/schemas/swarm.ts +0 -82
  422. package/src/logfire.ts +0 -135
  423. package/src/memory/search/lexical.ts +0 -48
  424. package/src/providers/failover.ts +0 -186
  425. package/src/runtime/local-gateway-health.ts +0 -275
  426. package/src/security/secret-ingress.ts +0 -68
  427. package/src/swarm/backend-claude-code.ts +0 -225
  428. package/src/swarm/checkpoint.ts +0 -137
  429. package/src/swarm/graph-utils.ts +0 -53
  430. package/src/swarm/index.ts +0 -55
  431. package/src/swarm/limits.ts +0 -66
  432. package/src/swarm/orchestrator.ts +0 -424
  433. package/src/swarm/plan-validator.ts +0 -117
  434. package/src/swarm/router-planner.ts +0 -162
  435. package/src/swarm/router-prompts.ts +0 -39
  436. package/src/swarm/synthesizer.ts +0 -81
  437. package/src/swarm/types.ts +0 -72
  438. package/src/swarm/worker-backend.ts +0 -131
  439. package/src/swarm/worker-prompts.ts +0 -80
  440. package/src/swarm/worker-runner.ts +0 -170
  441. package/src/tools/claude-code/claude-code.ts +0 -610
  442. package/src/tools/swarm/delegate.ts +0 -205
@@ -0,0 +1,178 @@
1
+ import { mkdtempSync, rmSync } from "node:fs";
2
+ import { tmpdir } from "node:os";
3
+ import { join } from "node:path";
4
+ import {
5
+ afterAll,
6
+ beforeAll,
7
+ beforeEach,
8
+ describe,
9
+ expect,
10
+ mock,
11
+ test,
12
+ } from "bun:test";
13
+
14
+ const testDir = mkdtempSync(join(tmpdir(), "jobs-store-qdrant-breaker-"));
15
+
16
+ mock.module("../util/platform.js", () => ({
17
+ getDataDir: () => testDir,
18
+ isMacOS: () => process.platform === "darwin",
19
+ isLinux: () => process.platform === "linux",
20
+ isWindows: () => process.platform === "win32",
21
+ getPidPath: () => join(testDir, "test.pid"),
22
+ getDbPath: () => join(testDir, "test.db"),
23
+ getLogPath: () => join(testDir, "test.log"),
24
+ ensureDataDir: () => {},
25
+ }));
26
+
27
+ mock.module("../util/logger.js", () => ({
28
+ getLogger: () =>
29
+ new Proxy({} as Record<string, unknown>, {
30
+ get: () => () => {},
31
+ }),
32
+ }));
33
+
34
+ mock.module("../config/loader.js", () => ({
35
+ loadConfig: () => ({}),
36
+ getConfig: () => ({}),
37
+ invalidateConfigCache: () => {},
38
+ }));
39
+
40
+ import { getDb, initializeDb, resetDb } from "../memory/db.js";
41
+ import {
42
+ claimMemoryJobs,
43
+ enqueueMemoryJob,
44
+ type MemoryJobType,
45
+ } from "../memory/jobs-store.js";
46
+ import {
47
+ _resetQdrantBreaker,
48
+ withQdrantBreaker,
49
+ } from "../memory/qdrant-circuit-breaker.js";
50
+
51
+ describe("claimMemoryJobs with Qdrant circuit breaker", () => {
52
+ beforeAll(() => {
53
+ initializeDb();
54
+ });
55
+
56
+ beforeEach(() => {
57
+ const db = getDb();
58
+ db.run("DELETE FROM memory_jobs");
59
+ _resetQdrantBreaker();
60
+ });
61
+
62
+ afterAll(() => {
63
+ resetDb();
64
+ rmSync(testDir, { recursive: true, force: true });
65
+ });
66
+
67
+ test("claims embed jobs when circuit breaker is closed (healthy)", () => {
68
+ enqueueMemoryJob("embed_segment", { segmentId: "seg-1" });
69
+ enqueueMemoryJob("embed_item", { itemId: "item-1" });
70
+ enqueueMemoryJob("extract_items", { conversationId: "conv-1" });
71
+
72
+ const claimed = claimMemoryJobs(10);
73
+ const types = claimed.map((j) => j.type);
74
+
75
+ expect(types).toContain("embed_segment");
76
+ expect(types).toContain("embed_item");
77
+ expect(types).toContain("extract_items");
78
+ expect(claimed).toHaveLength(3);
79
+ });
80
+
81
+ test("skips embed jobs when circuit breaker is open", async () => {
82
+ // Trip the circuit breaker by recording 5 consecutive failures
83
+ for (let i = 0; i < 5; i++) {
84
+ try {
85
+ await withQdrantBreaker(async () => {
86
+ throw new Error("simulated qdrant failure");
87
+ });
88
+ } catch {
89
+ // expected
90
+ }
91
+ }
92
+
93
+ enqueueMemoryJob("embed_segment", { segmentId: "seg-1" });
94
+ enqueueMemoryJob("embed_item", { itemId: "item-1" });
95
+ enqueueMemoryJob("embed_summary", { summaryId: "sum-1" });
96
+ enqueueMemoryJob("extract_items", { conversationId: "conv-1" });
97
+ enqueueMemoryJob("build_conversation_summary", {
98
+ conversationId: "conv-1",
99
+ });
100
+
101
+ const claimed = claimMemoryJobs(10);
102
+ const types = claimed.map((j) => j.type);
103
+
104
+ // Only non-embed jobs should be claimed
105
+ expect(types).toContain("extract_items");
106
+ expect(types).toContain("build_conversation_summary");
107
+ expect(types).not.toContain("embed_segment");
108
+ expect(types).not.toContain("embed_item");
109
+ expect(types).not.toContain("embed_summary");
110
+ expect(claimed).toHaveLength(2);
111
+ });
112
+
113
+ test("resumes claiming embed jobs after circuit breaker closes", async () => {
114
+ // Trip the circuit breaker
115
+ for (let i = 0; i < 5; i++) {
116
+ try {
117
+ await withQdrantBreaker(async () => {
118
+ throw new Error("simulated qdrant failure");
119
+ });
120
+ } catch {
121
+ // expected
122
+ }
123
+ }
124
+
125
+ // Verify embed jobs are skipped while open
126
+ enqueueMemoryJob("embed_segment", { segmentId: "seg-1" });
127
+ enqueueMemoryJob("extract_items", { conversationId: "conv-1" });
128
+
129
+ const claimedWhileOpen = claimMemoryJobs(10);
130
+ expect(claimedWhileOpen.map((j) => j.type)).not.toContain("embed_segment");
131
+
132
+ // Reset breaker (simulates successful probe closing the circuit)
133
+ _resetQdrantBreaker();
134
+
135
+ // Re-enqueue an embed job (the previous one is now "running")
136
+ enqueueMemoryJob("embed_item", { itemId: "item-2" });
137
+
138
+ const claimedAfterClose = claimMemoryJobs(10);
139
+ const types = claimedAfterClose.map((j) => j.type);
140
+
141
+ expect(types).toContain("embed_item");
142
+ });
143
+
144
+ test("all embed job types are skipped when breaker is open", async () => {
145
+ const embedTypes: MemoryJobType[] = [
146
+ "embed_segment",
147
+ "embed_item",
148
+ "embed_summary",
149
+ "embed_media",
150
+ "embed_attachment",
151
+ ];
152
+
153
+ // Trip the circuit breaker
154
+ for (let i = 0; i < 5; i++) {
155
+ try {
156
+ await withQdrantBreaker(async () => {
157
+ throw new Error("simulated qdrant failure");
158
+ });
159
+ } catch {
160
+ // expected
161
+ }
162
+ }
163
+
164
+ // Enqueue one of each embed type
165
+ for (const type of embedTypes) {
166
+ enqueueMemoryJob(type, { id: `test-${type}` });
167
+ }
168
+ // Also enqueue a non-embed job
169
+ enqueueMemoryJob("extract_entities", { conversationId: "conv-1" });
170
+
171
+ const claimed = claimMemoryJobs(20);
172
+ const types = claimed.map((j) => j.type);
173
+
174
+ // Only the non-embed job should be claimed
175
+ expect(claimed).toHaveLength(1);
176
+ expect(types).toEqual(["extract_entities"]);
177
+ });
178
+ });
@@ -0,0 +1,335 @@
1
+ import { mkdirSync, rmSync, statSync, writeFileSync } from "node:fs";
2
+ import { tmpdir } from "node:os";
3
+ import { join } from "node:path";
4
+ import { afterEach, beforeEach, describe, expect, mock, test } from "bun:test";
5
+
6
+ const TEST_DIR = join(tmpdir(), `vellum-journal-test-${crypto.randomUUID()}`);
7
+
8
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
9
+ const realPlatform = require("../util/platform.js");
10
+ mock.module("../util/platform.js", () => ({
11
+ ...realPlatform,
12
+ getWorkspaceDir: () => TEST_DIR,
13
+ }));
14
+
15
+ const {
16
+ buildJournalContext,
17
+ formatJournalRelativeTime,
18
+ formatJournalAbsoluteTime,
19
+ } = await import("../prompts/journal-context.js");
20
+
21
+ /** Small delay to ensure distinct file birthtimes on APFS. */
22
+ const tick = () => Bun.sleep(5);
23
+
24
+ describe("formatJournalRelativeTime", () => {
25
+ test("returns 'just now' for times less than 60 seconds ago", () => {
26
+ const now = Date.now();
27
+ expect(formatJournalRelativeTime(now - 30_000)).toBe("just now");
28
+ expect(formatJournalRelativeTime(now - 1_000)).toBe("just now");
29
+ expect(formatJournalRelativeTime(now)).toBe("just now");
30
+ });
31
+
32
+ test("returns minutes for times between 1-59 minutes ago", () => {
33
+ const now = Date.now();
34
+ expect(formatJournalRelativeTime(now - 60_000)).toBe("1 minute ago");
35
+ expect(formatJournalRelativeTime(now - 5 * 60_000)).toBe("5 minutes ago");
36
+ expect(formatJournalRelativeTime(now - 59 * 60_000)).toBe("59 minutes ago");
37
+ });
38
+
39
+ test("returns hours for times between 1-23 hours ago", () => {
40
+ const now = Date.now();
41
+ expect(formatJournalRelativeTime(now - 60 * 60_000)).toBe("1 hour ago");
42
+ expect(formatJournalRelativeTime(now - 3 * 60 * 60_000)).toBe(
43
+ "3 hours ago",
44
+ );
45
+ expect(formatJournalRelativeTime(now - 23 * 60 * 60_000)).toBe(
46
+ "23 hours ago",
47
+ );
48
+ });
49
+
50
+ test("returns days for times between 1-6 days ago", () => {
51
+ const now = Date.now();
52
+ expect(formatJournalRelativeTime(now - 24 * 60 * 60_000)).toBe(
53
+ "1 day ago",
54
+ );
55
+ expect(formatJournalRelativeTime(now - 3 * 24 * 60 * 60_000)).toBe(
56
+ "3 days ago",
57
+ );
58
+ expect(formatJournalRelativeTime(now - 6 * 24 * 60 * 60_000)).toBe(
59
+ "6 days ago",
60
+ );
61
+ });
62
+
63
+ test("returns weeks for times 7 or more days ago", () => {
64
+ const now = Date.now();
65
+ expect(formatJournalRelativeTime(now - 7 * 24 * 60 * 60_000)).toBe(
66
+ "1 week ago",
67
+ );
68
+ expect(formatJournalRelativeTime(now - 14 * 24 * 60 * 60_000)).toBe(
69
+ "2 weeks ago",
70
+ );
71
+ expect(formatJournalRelativeTime(now - 30 * 24 * 60 * 60_000)).toBe(
72
+ "4 weeks ago",
73
+ );
74
+ });
75
+ });
76
+
77
+ describe("formatJournalAbsoluteTime", () => {
78
+ test("formats a timestamp as MM/DD/YY HH:MM", () => {
79
+ // 2025-03-15 14:30:00
80
+ const ts = new Date(2025, 2, 15, 14, 30, 0).getTime();
81
+ expect(formatJournalAbsoluteTime(ts)).toBe("03/15/25 14:30");
82
+ });
83
+
84
+ test("zero-pads single-digit months, days, hours, and minutes", () => {
85
+ // 2025-01-05 09:05:00
86
+ const ts = new Date(2025, 0, 5, 9, 5, 0).getTime();
87
+ expect(formatJournalAbsoluteTime(ts)).toBe("01/05/25 09:05");
88
+ });
89
+
90
+ test("handles midnight", () => {
91
+ const ts = new Date(2025, 5, 20, 0, 0, 0).getTime();
92
+ expect(formatJournalAbsoluteTime(ts)).toBe("06/20/25 00:00");
93
+ });
94
+ });
95
+
96
+ describe("buildJournalContext", () => {
97
+ const journalDir = join(TEST_DIR, "journal");
98
+
99
+ beforeEach(() => {
100
+ mkdirSync(journalDir, { recursive: true });
101
+ });
102
+
103
+ afterEach(() => {
104
+ rmSync(TEST_DIR, { recursive: true, force: true });
105
+ });
106
+
107
+ test("returns null when maxEntries is 0", () => {
108
+ const userDir = join(journalDir, "testuser");
109
+ mkdirSync(userDir, { recursive: true });
110
+ writeFileSync(join(userDir, "entry.md"), "content");
111
+ expect(buildJournalContext(0, "testuser")).toBeNull();
112
+ });
113
+
114
+ test("returns null when maxEntries is negative", () => {
115
+ const userDir = join(journalDir, "testuser");
116
+ mkdirSync(userDir, { recursive: true });
117
+ writeFileSync(join(userDir, "entry.md"), "content");
118
+ expect(buildJournalContext(-1, "testuser")).toBeNull();
119
+ });
120
+
121
+ test("returns null when journal directory does not exist", () => {
122
+ rmSync(journalDir, { recursive: true, force: true });
123
+ expect(buildJournalContext(10, "testuser")).toBeNull();
124
+ });
125
+
126
+ test("returns null when journal directory has no .md files", () => {
127
+ const userDir = join(journalDir, "testuser");
128
+ mkdirSync(userDir, { recursive: true });
129
+ writeFileSync(join(userDir, "notes.txt"), "not markdown");
130
+ expect(buildJournalContext(10, "testuser")).toBeNull();
131
+ });
132
+
133
+ test("excludes README.md (case-insensitive)", () => {
134
+ const userDir = join(journalDir, "testuser");
135
+ mkdirSync(userDir, { recursive: true });
136
+ writeFileSync(join(userDir, "README.md"), "readme content");
137
+ writeFileSync(join(userDir, "readme.md"), "readme content lower");
138
+ expect(buildJournalContext(10, "testuser")).toBeNull();
139
+ });
140
+
141
+ test("returns formatted journal context with single entry", () => {
142
+ const userDir = join(journalDir, "testuser");
143
+ mkdirSync(userDir, { recursive: true });
144
+ writeFileSync(join(userDir, "goals.md"), "My goals for this week.");
145
+ const result = buildJournalContext(10, "testuser");
146
+ expect(result).not.toBeNull();
147
+ expect(result).toContain("# Journal");
148
+ expect(result).toContain(
149
+ "Your journal entries, most recent first. These are YOUR words from past conversations.",
150
+ );
151
+ expect(result).toContain("## goals.md — MOST RECENT");
152
+ expect(result).toContain("My goals for this week.");
153
+ // Single entry, window not full — should NOT have LEAVING CONTEXT
154
+ expect(result).not.toContain("LEAVING CONTEXT");
155
+ });
156
+
157
+ test("sorts entries by creation time, newest first", async () => {
158
+ const userDir = join(journalDir, "testuser");
159
+ mkdirSync(userDir, { recursive: true });
160
+ // Small delays between writes ensure distinct birthtimes.
161
+ writeFileSync(join(userDir, "old.md"), "old entry");
162
+ await tick();
163
+ writeFileSync(join(userDir, "mid.md"), "mid entry");
164
+ await tick();
165
+ writeFileSync(join(userDir, "new.md"), "new entry");
166
+
167
+ const result = buildJournalContext(10, "testuser")!;
168
+ const newIdx = result.indexOf("new.md");
169
+ const midIdx = result.indexOf("mid.md");
170
+ const oldIdx = result.indexOf("old.md");
171
+ expect(newIdx).toBeLessThan(midIdx);
172
+ expect(midIdx).toBeLessThan(oldIdx);
173
+ });
174
+
175
+ test("marks most recent entry with MOST RECENT", async () => {
176
+ const userDir = join(journalDir, "testuser");
177
+ mkdirSync(userDir, { recursive: true });
178
+ writeFileSync(join(userDir, "older.md"), "older");
179
+ await tick();
180
+ writeFileSync(join(userDir, "newest.md"), "newest");
181
+
182
+ const result = buildJournalContext(10, "testuser")!;
183
+ expect(result).toContain("## newest.md — MOST RECENT");
184
+ expect(result).not.toContain("## older.md — MOST RECENT");
185
+ });
186
+
187
+ test("marks oldest entry with LEAVING CONTEXT when window is full", async () => {
188
+ const userDir = join(journalDir, "testuser");
189
+ mkdirSync(userDir, { recursive: true });
190
+ // Create in chronological order: c (oldest), b, a (newest)
191
+ writeFileSync(join(userDir, "c.md"), "entry c");
192
+ await tick();
193
+ writeFileSync(join(userDir, "b.md"), "entry b");
194
+ await tick();
195
+ writeFileSync(join(userDir, "a.md"), "entry a");
196
+
197
+ // maxEntries = 3 matches the number of files, so window is full
198
+ const result = buildJournalContext(3, "testuser")!;
199
+ expect(result).toContain("## a.md — MOST RECENT");
200
+ expect(result).toContain("## c.md — LEAVING CONTEXT");
201
+ expect(result).toContain(
202
+ "NOTE: This is the oldest entry in your active context.",
203
+ );
204
+ expect(result).toContain(
205
+ "carry forward anything from here that still matters to you",
206
+ );
207
+ });
208
+
209
+ test("does NOT mark oldest entry with LEAVING CONTEXT when window is not full", async () => {
210
+ const userDir = join(journalDir, "testuser");
211
+ mkdirSync(userDir, { recursive: true });
212
+ writeFileSync(join(userDir, "b.md"), "entry b");
213
+ await tick();
214
+ writeFileSync(join(userDir, "a.md"), "entry a");
215
+
216
+ // maxEntries = 5, only 2 files — window is NOT full
217
+ const result = buildJournalContext(5, "testuser")!;
218
+ expect(result).toContain("## a.md — MOST RECENT");
219
+ expect(result).not.toContain("LEAVING CONTEXT");
220
+ });
221
+
222
+ test("limits entries to maxEntries", async () => {
223
+ const userDir = join(journalDir, "testuser");
224
+ mkdirSync(userDir, { recursive: true });
225
+ // Create files 0-4 sequentially; file 4 is newest
226
+ for (let i = 0; i < 5; i++) {
227
+ writeFileSync(join(userDir, `entry-${i}.md`), `content ${i}`);
228
+ if (i < 4) await tick();
229
+ }
230
+
231
+ const result = buildJournalContext(3, "testuser")!;
232
+ // Should contain only 3 newest entries (entry-4, entry-3, entry-2)
233
+ expect(result).toContain("entry-4.md");
234
+ expect(result).toContain("entry-3.md");
235
+ expect(result).toContain("entry-2.md");
236
+ expect(result).not.toContain("entry-1.md");
237
+ expect(result).not.toContain("entry-0.md");
238
+ });
239
+
240
+ test("maxEntries=1 with exactly one entry marks it MOST RECENT, not LEAVING CONTEXT", () => {
241
+ const userDir = join(journalDir, "testuser");
242
+ mkdirSync(userDir, { recursive: true });
243
+ writeFileSync(join(userDir, "solo.md"), "only entry");
244
+ const result = buildJournalContext(1, "testuser")!;
245
+ expect(result).toContain("## solo.md — MOST RECENT");
246
+ expect(result).not.toContain("LEAVING CONTEXT");
247
+ });
248
+
249
+ test("includes both absolute and relative timestamps in headers", () => {
250
+ const userDir = join(journalDir, "testuser");
251
+ mkdirSync(userDir, { recursive: true });
252
+ writeFileSync(join(userDir, "recent.md"), "recent content");
253
+
254
+ const result = buildJournalContext(10, "testuser")!;
255
+ // File was just created, so relative time should be "just now"
256
+ expect(result).toContain("just now");
257
+ // Absolute time should match the file's birthtime
258
+ const birthtime = statSync(join(userDir, "recent.md")).birthtimeMs;
259
+ const expected = formatJournalAbsoluteTime(birthtime);
260
+ expect(result).toContain(expected);
261
+ });
262
+
263
+ test("middle entries have plain headers with timestamps", async () => {
264
+ const userDir = join(journalDir, "testuser");
265
+ mkdirSync(userDir, { recursive: true });
266
+ // Create in chronological order: last (oldest), middle, first (newest)
267
+ writeFileSync(join(userDir, "last.md"), "last");
268
+ await tick();
269
+ writeFileSync(join(userDir, "middle.md"), "middle");
270
+ await tick();
271
+ writeFileSync(join(userDir, "first.md"), "first");
272
+
273
+ const result = buildJournalContext(3, "testuser")!;
274
+ // Middle entry should have plain header format (no MOST RECENT, no LEAVING CONTEXT)
275
+ // Format: ## middle.md (MM/DD/YY HH:MM, <relative time>)
276
+ expect(result).toMatch(
277
+ /## middle\.md \(\d{2}\/\d{2}\/\d{2} \d{2}:\d{2}, .+\)/,
278
+ );
279
+ });
280
+
281
+ // --- Per-user scoping tests ---
282
+
283
+ test("reads from per-user directory when userSlug is provided", () => {
284
+ const aliceDir = join(journalDir, "alice");
285
+ mkdirSync(aliceDir, { recursive: true });
286
+ writeFileSync(join(aliceDir, "thoughts.md"), "Alice's thoughts");
287
+ writeFileSync(join(aliceDir, "plans.md"), "Alice's plans");
288
+
289
+ const result = buildJournalContext(10, "alice");
290
+ expect(result).not.toBeNull();
291
+ expect(result).toContain("Alice's thoughts");
292
+ expect(result).toContain("Alice's plans");
293
+ });
294
+
295
+ test("returns null when userSlug is provided but directory does not exist", () => {
296
+ // journal/bob/ does not exist
297
+ const result = buildJournalContext(10, "bob");
298
+ expect(result).toBeNull();
299
+ });
300
+
301
+ test("returns null when no userSlug is provided", () => {
302
+ // Even if root journal/ has entries, no slug means null
303
+ writeFileSync(join(journalDir, "orphan.md"), "orphan entry");
304
+ const result = buildJournalContext(10);
305
+ expect(result).toBeNull();
306
+ });
307
+
308
+ test("returns null when userSlug is null", () => {
309
+ writeFileSync(join(journalDir, "orphan.md"), "orphan entry");
310
+ const result = buildJournalContext(10, null);
311
+ expect(result).toBeNull();
312
+ });
313
+
314
+ test("includes write-path directive in header when userSlug is provided", () => {
315
+ const aliceDir = join(journalDir, "alice");
316
+ mkdirSync(aliceDir, { recursive: true });
317
+ writeFileSync(join(aliceDir, "entry.md"), "some content");
318
+
319
+ const result = buildJournalContext(10, "alice")!;
320
+ expect(result).toContain("**Write new entries to:** `journal/alice/`");
321
+ });
322
+
323
+ test("sanitizes path-traversal in userSlug", () => {
324
+ // basename("../etc") => "etc", so it should read from journal/etc/
325
+ const etcDir = join(journalDir, "etc");
326
+ mkdirSync(etcDir, { recursive: true });
327
+ writeFileSync(join(etcDir, "safe.md"), "safe content");
328
+
329
+ const result = buildJournalContext(10, "../etc");
330
+ expect(result).not.toBeNull();
331
+ expect(result).toContain("safe content");
332
+ // Should reference the sanitized path, not the traversal attempt
333
+ expect(result).toContain("`journal/etc/`");
334
+ });
335
+ });