@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
@@ -0,0 +1,335 @@
1
+ /**
2
+ * Tests for the trusted contact lifecycle fallback copy templates.
3
+ *
4
+ * Verifies that `ingress.trusted_contact.guardian_decision` and
5
+ * `ingress.trusted_contact.denied` templates in copy-composer.ts render
6
+ * display names when available and fall back to Slack <@ID> mention format
7
+ * for raw user IDs on the Slack source channel.
8
+ */
9
+ import { describe, expect, test } from "bun:test";
10
+
11
+ import { composeFallbackCopy } from "../notifications/copy-composer.js";
12
+ import type { NotificationSignal } from "../notifications/signal.js";
13
+
14
+ // ── Helpers ──────────────────────────────────────────────────────────────────
15
+
16
+ function buildGuardianDecisionSignal(
17
+ payloadOverrides: Record<string, unknown> = {},
18
+ sourceChannel: "slack" | "telegram" | "vellum" = "slack",
19
+ ): NotificationSignal {
20
+ return {
21
+ signalId: "test-signal-gd",
22
+ createdAt: Date.now(),
23
+ sourceChannel,
24
+ sourceContextId: "test-ctx-1",
25
+ sourceEventName: "ingress.trusted_contact.guardian_decision",
26
+ contextPayload: {
27
+ sourceChannel,
28
+ requesterExternalUserId: "U07CLDQ4TB3",
29
+ requesterChatId: "D0AQ9C5PPPF",
30
+ decidedByExternalUserId: "U099H19C0KA",
31
+ requesterDisplayName: null,
32
+ decidedByDisplayName: null,
33
+ decision: "denied",
34
+ ...payloadOverrides,
35
+ },
36
+ attentionHints: {
37
+ requiresAction: false,
38
+ urgency: "medium",
39
+ isAsyncBackground: false,
40
+ visibleInSourceNow: false,
41
+ },
42
+ };
43
+ }
44
+
45
+ function buildDeniedSignal(
46
+ payloadOverrides: Record<string, unknown> = {},
47
+ sourceChannel: "slack" | "telegram" | "vellum" = "slack",
48
+ ): NotificationSignal {
49
+ return {
50
+ signalId: "test-signal-denied",
51
+ createdAt: Date.now(),
52
+ sourceChannel,
53
+ sourceContextId: "test-ctx-2",
54
+ sourceEventName: "ingress.trusted_contact.denied",
55
+ contextPayload: {
56
+ sourceChannel,
57
+ requesterExternalUserId: "U07CLDQ4TB3",
58
+ requesterChatId: "D0AQ9C5PPPF",
59
+ decidedByExternalUserId: "U099H19C0KA",
60
+ requesterDisplayName: null,
61
+ decidedByDisplayName: null,
62
+ decision: "denied",
63
+ ...payloadOverrides,
64
+ },
65
+ attentionHints: {
66
+ requiresAction: false,
67
+ urgency: "low",
68
+ isAsyncBackground: false,
69
+ visibleInSourceNow: false,
70
+ },
71
+ };
72
+ }
73
+
74
+ // ── guardian_decision template ────────────────────────────────────────────────
75
+
76
+ describe("guardian_decision fallback copy", () => {
77
+ test("uses display names when both are present", () => {
78
+ const signal = buildGuardianDecisionSignal({
79
+ requesterDisplayName: "Alice",
80
+ decidedByDisplayName: "Bob",
81
+ decision: "denied",
82
+ });
83
+ const result = composeFallbackCopy(signal, ["vellum"]);
84
+ const copy = result.vellum!;
85
+
86
+ expect(copy.title).toBe("Trusted Contact Decision");
87
+ expect(copy.body).toBe("Alice's access request has been denied by Bob.");
88
+ });
89
+
90
+ test("falls back to Slack <@ID> mention format when display names are absent on Slack", () => {
91
+ const signal = buildGuardianDecisionSignal({
92
+ requesterDisplayName: null,
93
+ decidedByDisplayName: null,
94
+ requesterExternalUserId: "U07CLDQ4TB3",
95
+ decidedByExternalUserId: "U099H19C0KA",
96
+ sourceChannel: "slack",
97
+ decision: "denied",
98
+ });
99
+ const result = composeFallbackCopy(signal, ["vellum"]);
100
+ const copy = result.vellum!;
101
+
102
+ expect(copy.body).toBe(
103
+ "<@U07CLDQ4TB3>'s access request has been denied by <@U099H19C0KA>.",
104
+ );
105
+ });
106
+
107
+ test("uses raw user IDs without Slack formatting on non-Slack channels", () => {
108
+ const signal = buildGuardianDecisionSignal(
109
+ {
110
+ requesterDisplayName: null,
111
+ decidedByDisplayName: null,
112
+ requesterExternalUserId: "U07CLDQ4TB3",
113
+ decidedByExternalUserId: "U099H19C0KA",
114
+ sourceChannel: "telegram",
115
+ decision: "denied",
116
+ },
117
+ "telegram",
118
+ );
119
+ const result = composeFallbackCopy(signal, ["vellum"]);
120
+ const copy = result.vellum!;
121
+
122
+ expect(copy.body).toBe(
123
+ "U07CLDQ4TB3's access request has been denied by U099H19C0KA.",
124
+ );
125
+ expect(copy.body).not.toContain("<@");
126
+ });
127
+
128
+ test("produces distinct copy for approved vs denied decisions", () => {
129
+ const deniedSignal = buildGuardianDecisionSignal({
130
+ requesterDisplayName: "Alice",
131
+ decidedByDisplayName: "Bob",
132
+ decision: "denied",
133
+ });
134
+ const approvedSignal = buildGuardianDecisionSignal({
135
+ requesterDisplayName: "Alice",
136
+ decidedByDisplayName: "Bob",
137
+ decision: "approved",
138
+ });
139
+
140
+ const deniedCopy = composeFallbackCopy(deniedSignal, ["vellum"]).vellum!;
141
+ const approvedCopy = composeFallbackCopy(approvedSignal, [
142
+ "vellum",
143
+ ]).vellum!;
144
+
145
+ expect(deniedCopy.body).toContain("denied");
146
+ expect(deniedCopy.body).not.toContain("approved");
147
+ expect(approvedCopy.body).toContain("approved");
148
+ expect(approvedCopy.body).not.toContain("denied");
149
+ });
150
+
151
+ test("does not expose raw conversation IDs (requesterChatId) in output", () => {
152
+ const signal = buildGuardianDecisionSignal({
153
+ requesterDisplayName: null,
154
+ decidedByDisplayName: null,
155
+ requesterChatId: "D0AQ9C5PPPF",
156
+ requesterExternalUserId: "U07CLDQ4TB3",
157
+ decidedByExternalUserId: "U099H19C0KA",
158
+ });
159
+ const result = composeFallbackCopy(signal, ["vellum"]);
160
+ const copy = result.vellum!;
161
+
162
+ expect(copy.body).not.toContain("D0AQ9C5PPPF");
163
+ });
164
+
165
+ test("falls back to 'Someone' when requester identity is entirely absent", () => {
166
+ const signal = buildGuardianDecisionSignal({
167
+ requesterDisplayName: null,
168
+ requesterExternalUserId: null,
169
+ decidedByDisplayName: "Bob",
170
+ decision: "denied",
171
+ });
172
+ const result = composeFallbackCopy(signal, ["vellum"]);
173
+ const copy = result.vellum!;
174
+
175
+ expect(copy.body).toBe("Someone's access request has been denied by Bob.");
176
+ });
177
+
178
+ test("falls back to 'a guardian' when decider identity is entirely absent", () => {
179
+ const signal = buildGuardianDecisionSignal({
180
+ requesterDisplayName: "Alice",
181
+ decidedByDisplayName: null,
182
+ decidedByExternalUserId: null,
183
+ decision: "approved",
184
+ });
185
+ const result = composeFallbackCopy(signal, ["vellum"]);
186
+ const copy = result.vellum!;
187
+
188
+ expect(copy.body).toBe(
189
+ "Alice's access request has been approved by a guardian.",
190
+ );
191
+ });
192
+
193
+ test("prefers display name over Slack user ID when both are present", () => {
194
+ const signal = buildGuardianDecisionSignal({
195
+ requesterDisplayName: "Alice",
196
+ requesterExternalUserId: "U07CLDQ4TB3",
197
+ decidedByDisplayName: "Bob",
198
+ decidedByExternalUserId: "U099H19C0KA",
199
+ sourceChannel: "slack",
200
+ decision: "denied",
201
+ });
202
+ const result = composeFallbackCopy(signal, ["vellum"]);
203
+ const copy = result.vellum!;
204
+
205
+ expect(copy.body).toBe("Alice's access request has been denied by Bob.");
206
+ expect(copy.body).not.toContain("<@");
207
+ });
208
+
209
+ test("sanitizes control characters from display names", () => {
210
+ const signal = buildGuardianDecisionSignal({
211
+ requesterDisplayName: "Alice\x00\x07\nEvil",
212
+ decidedByDisplayName: "Bob\r\nMalicious",
213
+ decision: "approved",
214
+ });
215
+ const result = composeFallbackCopy(signal, ["vellum"]);
216
+ const copy = result.vellum!;
217
+
218
+ expect(copy.body).not.toMatch(/[\x00-\x1f\x7f-\x9f]/);
219
+ expect(copy.body).toContain("Alice");
220
+ expect(copy.body).toContain("Bob");
221
+ });
222
+
223
+ test("clamps excessively long display names", () => {
224
+ const longName = "A".repeat(200);
225
+ const signal = buildGuardianDecisionSignal({
226
+ requesterDisplayName: longName,
227
+ decidedByDisplayName: "Bob",
228
+ decision: "denied",
229
+ });
230
+ const result = composeFallbackCopy(signal, ["vellum"]);
231
+ const copy = result.vellum!;
232
+
233
+ // sanitizeIdentityField clamps to 120 chars + ellipsis
234
+ expect(copy.body.length).toBeLessThan(longName.length);
235
+ expect(copy.body).toContain("…");
236
+ });
237
+ });
238
+
239
+ // ── denied template ──────────────────────────────────────────────────────────
240
+
241
+ describe("trusted_contact.denied fallback copy", () => {
242
+ test("uses display name when present", () => {
243
+ const signal = buildDeniedSignal({
244
+ requesterDisplayName: "Alice",
245
+ });
246
+ const result = composeFallbackCopy(signal, ["vellum"]);
247
+ const copy = result.vellum!;
248
+
249
+ expect(copy.title).toBe("Trusted Contact Denied");
250
+ expect(copy.body).toBe(
251
+ "A trusted contact request from Alice has been denied.",
252
+ );
253
+ });
254
+
255
+ test("falls back to Slack <@ID> mention format on Slack when display name absent", () => {
256
+ const signal = buildDeniedSignal({
257
+ requesterDisplayName: null,
258
+ requesterExternalUserId: "U07CLDQ4TB3",
259
+ sourceChannel: "slack",
260
+ });
261
+ const result = composeFallbackCopy(signal, ["vellum"]);
262
+ const copy = result.vellum!;
263
+
264
+ expect(copy.body).toBe(
265
+ "A trusted contact request from <@U07CLDQ4TB3> has been denied.",
266
+ );
267
+ });
268
+
269
+ test("uses raw user ID without Slack formatting on non-Slack channels", () => {
270
+ const signal = buildDeniedSignal(
271
+ {
272
+ requesterDisplayName: null,
273
+ requesterExternalUserId: "U07CLDQ4TB3",
274
+ sourceChannel: "telegram",
275
+ },
276
+ "telegram",
277
+ );
278
+ const result = composeFallbackCopy(signal, ["vellum"]);
279
+ const copy = result.vellum!;
280
+
281
+ expect(copy.body).toBe(
282
+ "A trusted contact request from U07CLDQ4TB3 has been denied.",
283
+ );
284
+ expect(copy.body).not.toContain("<@");
285
+ });
286
+
287
+ test("falls back to 'Someone' when requester identity is absent", () => {
288
+ const signal = buildDeniedSignal({
289
+ requesterDisplayName: null,
290
+ requesterExternalUserId: null,
291
+ });
292
+ const result = composeFallbackCopy(signal, ["vellum"]);
293
+ const copy = result.vellum!;
294
+
295
+ expect(copy.body).toBe(
296
+ "A trusted contact request from Someone has been denied.",
297
+ );
298
+ });
299
+
300
+ test("does not expose raw conversation IDs (requesterChatId) in output", () => {
301
+ const signal = buildDeniedSignal({
302
+ requesterDisplayName: null,
303
+ requesterExternalUserId: "U07CLDQ4TB3",
304
+ requesterChatId: "D0AQ9C5PPPF",
305
+ });
306
+ const result = composeFallbackCopy(signal, ["vellum"]);
307
+ const copy = result.vellum!;
308
+
309
+ expect(copy.body).not.toContain("D0AQ9C5PPPF");
310
+ });
311
+
312
+ test("sanitizes control characters from display names", () => {
313
+ const signal = buildDeniedSignal({
314
+ requesterDisplayName: "Alice\x00\x07\nEvil",
315
+ });
316
+ const result = composeFallbackCopy(signal, ["vellum"]);
317
+ const copy = result.vellum!;
318
+
319
+ expect(copy.body).not.toMatch(/[\x00-\x1f\x7f-\x9f]/);
320
+ expect(copy.body).toContain("Alice");
321
+ });
322
+
323
+ test("clamps excessively long display names", () => {
324
+ const longName = "A".repeat(200);
325
+ const signal = buildDeniedSignal({
326
+ requesterDisplayName: longName,
327
+ });
328
+ const result = composeFallbackCopy(signal, ["vellum"]);
329
+ const copy = result.vellum!;
330
+
331
+ // sanitizeIdentityField clamps to 120 chars + ellipsis
332
+ expect(copy.body.length).toBeLessThan(longName.length);
333
+ expect(copy.body).toContain("…");
334
+ });
335
+ });
@@ -95,7 +95,6 @@ function makePrompter(
95
95
  allowlistOptions: unknown[],
96
96
  scopeOptions: unknown[],
97
97
  diff: unknown,
98
- sandboxed: unknown,
99
98
  sessionId: string | undefined,
100
99
  executionTarget: unknown,
101
100
  persistentDecisionsAllowed: unknown,
@@ -109,7 +108,6 @@ function makePrompter(
109
108
  allowlistOptions,
110
109
  scopeOptions,
111
110
  diff,
112
- sandboxed,
113
111
  sessionId,
114
112
  executionTarget,
115
113
  persistentDecisionsAllowed,
@@ -1,219 +1,10 @@
1
1
  import { describe, expect, test } from "bun:test";
2
2
 
3
3
  import {
4
- buildTemporalContext,
5
4
  extractUserTimeZoneFromRecall,
5
+ formatTurnTimestamp,
6
6
  } from "../daemon/date-context.js";
7
7
 
8
- // Fixed timestamps for deterministic assertions (all UTC midday to avoid DST edge cases).
9
-
10
- /** Wednesday 2026-02-18 12:00 UTC */
11
- const WED_FEB_18 = Date.UTC(2026, 1, 18, 12, 0, 0);
12
-
13
- /** Tuesday 2026-12-29 12:00 UTC - year boundary */
14
- const TUE_DEC_29 = Date.UTC(2026, 11, 29, 12, 0, 0);
15
-
16
- // ---------------------------------------------------------------------------
17
- // Basic structure
18
- // ---------------------------------------------------------------------------
19
-
20
- describe("buildTemporalContext", () => {
21
- test("returns output wrapped in <temporal_context> tags", () => {
22
- const result = buildTemporalContext({ nowMs: WED_FEB_18, timeZone: "UTC" });
23
- expect(result).toStartWith("<temporal_context>");
24
- expect(result).toEndWith("</temporal_context>");
25
- });
26
-
27
- test("includes today date, weekday, time and offset on one line", () => {
28
- const result = buildTemporalContext({ nowMs: WED_FEB_18, timeZone: "UTC" });
29
- expect(result).toContain("Today: 2026-02-18 (Wed) 12:00 +00:00");
30
- });
31
-
32
- test("includes timezone", () => {
33
- const result = buildTemporalContext({
34
- nowMs: WED_FEB_18,
35
- timeZone: "America/New_York",
36
- });
37
- expect(result).toContain("TZ: America/New_York");
38
- });
39
-
40
- test("does not include UTC time, timezone source, or seconds", () => {
41
- const result = buildTemporalContext({ nowMs: WED_FEB_18, timeZone: "UTC" });
42
- expect(result).not.toContain("Current UTC time");
43
- expect(result).not.toContain("Current local time");
44
- expect(result).not.toContain("Timezone source:");
45
- // No seconds in the time
46
- expect(result).not.toContain("12:00:00");
47
- });
48
-
49
- test("does not include week definitions, next weekend, next work week, or horizon dates", () => {
50
- const result = buildTemporalContext({ nowMs: WED_FEB_18, timeZone: "UTC" });
51
- expect(result).not.toContain("Week definitions");
52
- expect(result).not.toContain("Next weekend");
53
- expect(result).not.toContain("Next work week");
54
- expect(result).not.toContain("Upcoming dates");
55
- });
56
-
57
- test("uses user timezone when provided", () => {
58
- const result = buildTemporalContext({
59
- nowMs: WED_FEB_18,
60
- hostTimeZone: "UTC",
61
- userTimeZone: "America/New_York",
62
- });
63
- expect(result).toContain("TZ: America/New_York");
64
- expect(result).toContain("Today: 2026-02-18 (Wed) 07:00 -05:00");
65
- expect(result).not.toContain("(host fallback)");
66
- });
67
-
68
- test("shows user TZ only when different from primary timezone", () => {
69
- // When user timezone equals the primary timezone, omit it
70
- const sameResult = buildTemporalContext({
71
- nowMs: WED_FEB_18,
72
- hostTimeZone: "UTC",
73
- configuredUserTimeZone: "UTC",
74
- });
75
- expect(sameResult).not.toContain("User TZ:");
76
-
77
- // When user timezone differs from host, it becomes the primary timezone
78
- // and the host timezone is shown as a secondary annotation
79
- const diffResult = buildTemporalContext({
80
- nowMs: WED_FEB_18,
81
- hostTimeZone: "UTC",
82
- userTimeZone: "America/New_York",
83
- });
84
- expect(diffResult).toContain("TZ: America/New_York");
85
- expect(diffResult).toContain("Host TZ: UTC");
86
- expect(diffResult).not.toContain("User TZ:");
87
- });
88
-
89
- test("shows host TZ only when different from primary timezone", () => {
90
- // When host timezone equals the primary timezone, omit it
91
- const sameResult = buildTemporalContext({
92
- nowMs: WED_FEB_18,
93
- hostTimeZone: "UTC",
94
- timeZone: "UTC",
95
- });
96
- expect(sameResult).not.toContain("Host TZ:");
97
-
98
- // When different, include it
99
- const diffResult = buildTemporalContext({
100
- nowMs: WED_FEB_18,
101
- hostTimeZone: "UTC",
102
- userTimeZone: "America/New_York",
103
- });
104
- expect(diffResult).toContain("Host TZ: UTC");
105
- });
106
-
107
- test("uses configured user timezone when profile timezone is unavailable", () => {
108
- const result = buildTemporalContext({
109
- nowMs: WED_FEB_18,
110
- hostTimeZone: "UTC",
111
- configuredUserTimeZone: "America/Chicago",
112
- userTimeZone: null,
113
- });
114
- expect(result).toContain("TZ: America/Chicago");
115
- expect(result).toContain("Today: 2026-02-18 (Wed) 06:00 -06:00");
116
- expect(result).not.toContain("(host fallback)");
117
- });
118
-
119
- test("configured user timezone takes precedence over profile timezone", () => {
120
- const result = buildTemporalContext({
121
- nowMs: WED_FEB_18,
122
- hostTimeZone: "UTC",
123
- configuredUserTimeZone: "America/Los_Angeles",
124
- userTimeZone: "America/New_York",
125
- });
126
- expect(result).toContain("TZ: America/Los_Angeles");
127
- expect(result).toContain("Today: 2026-02-18 (Wed) 04:00 -08:00");
128
- });
129
-
130
- test("falls back to host timezone with (host fallback) suffix", () => {
131
- const result = buildTemporalContext({
132
- nowMs: WED_FEB_18,
133
- hostTimeZone: "UTC",
134
- userTimeZone: null,
135
- });
136
- expect(result).toContain("TZ: UTC (host fallback)");
137
- });
138
-
139
- test("accepts UTC/GMT offset-style user timezone values", () => {
140
- const result = buildTemporalContext({
141
- nowMs: WED_FEB_18,
142
- hostTimeZone: "UTC",
143
- userTimeZone: "UTC+2",
144
- });
145
- expect(result).toContain("TZ: Etc/GMT-2");
146
- expect(result).toContain("Today: 2026-02-18 (Wed) 14:00 +02:00");
147
- expect(result).not.toContain("(host fallback)");
148
- });
149
-
150
- test("accepts fractional UTC/GMT offset-style user timezone values", () => {
151
- const result = buildTemporalContext({
152
- nowMs: WED_FEB_18,
153
- hostTimeZone: "UTC",
154
- userTimeZone: "UTC+5:30",
155
- });
156
- expect(result).toContain("TZ: +05:30");
157
- expect(result).toContain("Today: 2026-02-18 (Wed) 17:30 +05:30");
158
- expect(result).not.toContain("(host fallback)");
159
- });
160
-
161
- test("formats midnight hours as 00 (never 24)", () => {
162
- const justAfterMidnight = Date.UTC(2026, 1, 19, 0, 5, 0);
163
- const result = buildTemporalContext({
164
- nowMs: justAfterMidnight,
165
- timeZone: "UTC",
166
- });
167
- expect(result).toContain("00:05 +00:00");
168
- expect(result).not.toContain("24:05");
169
- });
170
-
171
- test("Today line includes full YYYY-MM-DD format with year", () => {
172
- const result = buildTemporalContext({ nowMs: WED_FEB_18, timeZone: "UTC" });
173
- expect(result).toMatch(/Today: \d{4}-\d{2}-\d{2} \(\w{3}\) \d{2}:\d{2}/);
174
- expect(result).toContain("2026-02-18");
175
- });
176
-
177
- test("handles year boundary correctly", () => {
178
- const result = buildTemporalContext({ nowMs: TUE_DEC_29, timeZone: "UTC" });
179
- expect(result).toContain("Today: 2026-12-29 (Tue)");
180
- });
181
- });
182
-
183
- // ---------------------------------------------------------------------------
184
- // DST-safe timezone behavior
185
- // ---------------------------------------------------------------------------
186
-
187
- describe("DST-safe timezone behavior", () => {
188
- test("date labels are correct in US Eastern timezone", () => {
189
- const result = buildTemporalContext({
190
- nowMs: WED_FEB_18,
191
- timeZone: "America/New_York",
192
- });
193
- expect(result).toContain("Today: 2026-02-18 (Wed) 07:00 -05:00");
194
- });
195
-
196
- test("date labels are correct in timezone ahead of UTC", () => {
197
- // Feb 18 23:00 UTC = Feb 19 08:00 JST
198
- const nearMidnight = Date.UTC(2026, 1, 18, 23, 0, 0);
199
- const result = buildTemporalContext({
200
- nowMs: nearMidnight,
201
- timeZone: "Asia/Tokyo",
202
- });
203
- expect(result).toContain("Today: 2026-02-19 (Thu) 08:00 +09:00");
204
- });
205
-
206
- test("local offset tracks daylight saving changes", () => {
207
- // Jul 1 12:00 UTC = Jul 1 08:00 EDT
208
- const summer = Date.UTC(2026, 6, 1, 12, 0, 0);
209
- const result = buildTemporalContext({
210
- nowMs: summer,
211
- timeZone: "America/New_York",
212
- });
213
- expect(result).toContain("Today: 2026-07-01 (Wed) 08:00 -04:00");
214
- });
215
- });
216
-
217
8
  // ---------------------------------------------------------------------------
218
9
  // extractUserTimeZoneFromRecall
219
10
  // ---------------------------------------------------------------------------
@@ -293,3 +84,78 @@ describe("extractUserTimeZoneFromRecall", () => {
293
84
  expect(extractUserTimeZoneFromRecall(text)).toBe("America/Denver");
294
85
  });
295
86
  });
87
+
88
+ // ---------------------------------------------------------------------------
89
+ // formatTurnTimestamp
90
+ // ---------------------------------------------------------------------------
91
+
92
+ describe("formatTurnTimestamp", () => {
93
+ /** 2026-04-02 06:52:33 UTC (Thursday) */
94
+ const THU_APR_02_0652 = Date.UTC(2026, 3, 2, 6, 52, 33);
95
+
96
+ test("includes seconds in the timestamp", () => {
97
+ const result = formatTurnTimestamp({
98
+ nowMs: THU_APR_02_0652,
99
+ timeZone: "America/Chicago",
100
+ });
101
+ expect(result).toContain("01:52:33");
102
+ });
103
+
104
+ test("timezone name appears in parentheses", () => {
105
+ const result = formatTurnTimestamp({
106
+ nowMs: THU_APR_02_0652,
107
+ timeZone: "America/Chicago",
108
+ });
109
+ expect(result).toContain("(America/Chicago)");
110
+ });
111
+
112
+ test("produces expected full format", () => {
113
+ const result = formatTurnTimestamp({
114
+ nowMs: THU_APR_02_0652,
115
+ timeZone: "America/Chicago",
116
+ });
117
+ expect(result).toBe(
118
+ "2026-04-02 (Thu) 01:52:33 -05:00 (America/Chicago)",
119
+ );
120
+ });
121
+
122
+ test("handles UTC fallback when no timezone provided", () => {
123
+ const result = formatTurnTimestamp({
124
+ nowMs: THU_APR_02_0652,
125
+ hostTimeZone: "UTC",
126
+ });
127
+ expect(result).toBe("2026-04-02 (Thu) 06:52:33 +00:00 (UTC)");
128
+ });
129
+
130
+ test("handles user timezone override", () => {
131
+ const result = formatTurnTimestamp({
132
+ nowMs: THU_APR_02_0652,
133
+ hostTimeZone: "UTC",
134
+ userTimeZone: "Asia/Tokyo",
135
+ });
136
+ expect(result).toBe("2026-04-02 (Thu) 15:52:33 +09:00 (Asia/Tokyo)");
137
+ });
138
+
139
+ test("handles DST correctly", () => {
140
+ // Jul 1 12:00:30 UTC = Jul 1 08:00:30 EDT (Eastern Daylight Time, -04:00)
141
+ const summerWithSeconds = Date.UTC(2026, 6, 1, 12, 0, 30);
142
+ const result = formatTurnTimestamp({
143
+ nowMs: summerWithSeconds,
144
+ timeZone: "America/New_York",
145
+ });
146
+ expect(result).toBe(
147
+ "2026-07-01 (Wed) 08:00:30 -04:00 (America/New_York)",
148
+ );
149
+ });
150
+
151
+ test("formats midnight as 00", () => {
152
+ // 2026-02-19 00:00:15 UTC
153
+ const justAfterMidnight = Date.UTC(2026, 1, 19, 0, 0, 15);
154
+ const result = formatTurnTimestamp({
155
+ nowMs: justAfterMidnight,
156
+ timeZone: "UTC",
157
+ });
158
+ expect(result).toContain("00:00:15");
159
+ expect(result).not.toContain("24:");
160
+ });
161
+ });
@@ -42,6 +42,7 @@ describe("schedule_syntax column migration", () => {
42
42
  routing_hints_json TEXT NOT NULL DEFAULT '{}',
43
43
  status TEXT NOT NULL DEFAULT 'active',
44
44
  quiet INTEGER NOT NULL DEFAULT 0,
45
+ reuse_conversation INTEGER NOT NULL DEFAULT 0,
45
46
  created_at INTEGER NOT NULL,
46
47
  updated_at INTEGER NOT NULL
47
48
  )
@@ -108,7 +109,7 @@ describe("schedule_syntax column migration", () => {
108
109
  `INSERT INTO cron_jobs (id, name, enabled, cron_expression, timezone, message, next_run_at, last_run_at, last_status, retry_count, created_by, created_at, updated_at) VALUES ('old-1', 'Old Job', 1, '0 9 * * *', NULL, 'hello', ${now + 60000}, NULL, NULL, 0, 'agent', ${now}, ${now})`,
109
110
  );
110
111
 
111
- // Run the migration
112
+ // Run the migrations
112
113
  try {
113
114
  raw.exec(
114
115
  `ALTER TABLE cron_jobs ADD COLUMN schedule_syntax TEXT NOT NULL DEFAULT 'cron'`,
@@ -116,6 +117,13 @@ describe("schedule_syntax column migration", () => {
116
117
  } catch {
117
118
  /* already exists */
118
119
  }
120
+ try {
121
+ raw.exec(
122
+ `ALTER TABLE cron_jobs ADD COLUMN reuse_conversation INTEGER NOT NULL DEFAULT 0`,
123
+ );
124
+ } catch {
125
+ /* already exists */
126
+ }
119
127
 
120
128
  const row = db
121
129
  .select()
@@ -167,6 +175,13 @@ describe("schedule_syntax column migration", () => {
167
175
  } catch {
168
176
  /* ok */
169
177
  }
178
+ try {
179
+ raw.exec(
180
+ `ALTER TABLE cron_jobs ADD COLUMN reuse_conversation INTEGER NOT NULL DEFAULT 0`,
181
+ );
182
+ } catch {
183
+ /* ok */
184
+ }
170
185
 
171
186
  const now = Date.now();
172
187
  raw.exec(