@vellumai/assistant 0.6.0 → 0.6.1

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 (285) 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 +32 -1
  6. package/docs/architecture/integrations.md +1 -1
  7. package/docs/architecture/memory.md +21 -24
  8. package/openapi.yaml +538 -3
  9. package/package.json +5 -1
  10. package/src/__tests__/anthropic-provider.test.ts +160 -95
  11. package/src/__tests__/app-dir-path-guard.test.ts +1 -0
  12. package/src/__tests__/app-executors.test.ts +47 -1
  13. package/src/__tests__/app-source-watcher.test.ts +159 -0
  14. package/src/__tests__/checker.test.ts +38 -6
  15. package/src/__tests__/config-schema.test.ts +5 -0
  16. package/src/__tests__/conversation-agent-loop-overflow.test.ts +4 -6
  17. package/src/__tests__/conversation-agent-loop.test.ts +4 -51
  18. package/src/__tests__/conversation-history-web-search.test.ts +1 -1
  19. package/src/__tests__/conversation-runtime-assembly.test.ts +653 -832
  20. package/src/__tests__/conversation-runtime-workspace.test.ts +1 -93
  21. package/src/__tests__/conversation-tool-setup-app-refresh.test.ts +17 -4
  22. package/src/__tests__/conversation-wipe.test.ts +2 -6
  23. package/src/__tests__/conversation-workspace-cache-state.test.ts +6 -12
  24. package/src/__tests__/conversation-workspace-injection.test.ts +25 -26
  25. package/src/__tests__/conversation-workspace-tool-tracking.test.ts +1 -1
  26. package/src/__tests__/copy-composer-tc-templates.test.ts +335 -0
  27. package/src/__tests__/date-context.test.ts +76 -210
  28. package/src/__tests__/db-schedule-syntax-migration.test.ts +16 -1
  29. package/src/__tests__/file-list-tool.test.ts +219 -0
  30. package/src/__tests__/first-greeting.test.ts +1 -1
  31. package/src/__tests__/heartbeat-service.test.ts +180 -3
  32. package/src/__tests__/identity-routes.test.ts +328 -0
  33. package/src/__tests__/injection-block.test.ts +24 -0
  34. package/src/__tests__/install-skill-routing.test.ts +7 -6
  35. package/src/__tests__/jobs-store-qdrant-breaker.test.ts +15 -14
  36. package/src/__tests__/list-messages-tool-merge.test.ts +300 -0
  37. package/src/__tests__/llm-context-normalization.test.ts +18 -18
  38. package/src/__tests__/llm-context-route-provider.test.ts +101 -0
  39. package/src/__tests__/llm-request-log-turn-query.test.ts +162 -0
  40. package/src/__tests__/log-export-workspace.test.ts +72 -105
  41. package/src/__tests__/mcp-abort-signal.test.ts +5 -0
  42. package/src/__tests__/mcp-client-auth.test.ts +5 -0
  43. package/src/__tests__/memory-recall-log-store.test.ts +132 -0
  44. package/src/__tests__/migration-export-streaming.test.ts +304 -0
  45. package/src/__tests__/migration-import-commit-http.test.ts +11 -10
  46. package/src/__tests__/mock-fetch.ts +87 -0
  47. package/src/__tests__/notification-decision-recipient-context.test.ts +282 -0
  48. package/src/__tests__/onboarding-template-contract.test.ts +62 -14
  49. package/src/__tests__/parser.test.ts +32 -0
  50. package/src/__tests__/permission-checker-host-gate.test.ts +452 -0
  51. package/src/__tests__/permission-controls-v2-flag.test.ts +55 -0
  52. package/src/__tests__/permission-mode-sse.test.ts +418 -0
  53. package/src/__tests__/permission-mode-store.test.ts +277 -0
  54. package/src/__tests__/permission-mode.test.ts +101 -0
  55. package/src/__tests__/platform-bash-auto-approve.test.ts +359 -0
  56. package/src/__tests__/profiler-routes.test.ts +502 -0
  57. package/src/__tests__/profiler-run-store.test.ts +441 -0
  58. package/src/__tests__/proxy-approval-callback.test.ts +4 -75
  59. package/src/__tests__/registry.test.ts +1 -1
  60. package/src/__tests__/sandbox-host-parity.test.ts +5 -4
  61. package/src/__tests__/scheduler-reuse-conversation.test.ts +368 -0
  62. package/src/__tests__/scrub-corrupted-image-attachments.test.ts +278 -0
  63. package/src/__tests__/search-skills-unified.test.ts +4 -3
  64. package/src/__tests__/send-endpoint-busy.test.ts +42 -3
  65. package/src/__tests__/set-permission-mode.test.ts +274 -0
  66. package/src/__tests__/skill-load-feature-flag.test.ts +12 -0
  67. package/src/__tests__/skill-memory.test.ts +2 -783
  68. package/src/__tests__/strip-memory-injections.test.ts +187 -0
  69. package/src/__tests__/subagent-detail.test.ts +84 -0
  70. package/src/__tests__/subagent-disposal.test.ts +308 -0
  71. package/src/__tests__/subagent-manager-notify.test.ts +19 -10
  72. package/src/__tests__/subagent-notify-parent.test.ts +390 -0
  73. package/src/__tests__/subagent-role-registry.test.ts +108 -0
  74. package/src/__tests__/subagent-tool-filtering.test.ts +71 -0
  75. package/src/__tests__/subagent-tools.test.ts +464 -4
  76. package/src/__tests__/system-prompt-ask-mode.test.ts +139 -0
  77. package/src/__tests__/task-memory-cleanup.test.ts +12 -12
  78. package/src/__tests__/terminal-tools.test.ts +17 -27
  79. package/src/__tests__/test-preload.ts +4 -0
  80. package/src/__tests__/tool-executor.test.ts +4 -26
  81. package/src/__tests__/tool-side-effects-slack-dm.test.ts +1 -0
  82. package/src/__tests__/top-level-renderer.test.ts +10 -13
  83. package/src/__tests__/trusted-contact-lifecycle-notifications.test.ts +116 -2
  84. package/src/__tests__/workspace-migration-028-recover-conversations-from-disk-view.test.ts +387 -0
  85. package/src/agent/loop.ts +6 -0
  86. package/src/approvals/guardian-request-resolvers.ts +24 -0
  87. package/src/avatar/traits-png-sync.ts +3 -3
  88. package/src/cli/__tests__/run-assistant-command.ts +29 -0
  89. package/src/cli/commands/__tests__/email-download.test.ts +245 -0
  90. package/src/cli/commands/__tests__/email-list.test.ts +192 -0
  91. package/src/cli/commands/__tests__/email-register.test.ts +186 -0
  92. package/src/cli/commands/__tests__/email-send.test.ts +291 -0
  93. package/src/cli/commands/__tests__/email-status.test.ts +181 -0
  94. package/src/cli/commands/__tests__/email-unregister.test.ts +139 -0
  95. package/src/cli/commands/__tests__/routes.test.ts +562 -0
  96. package/src/cli/commands/conversations.ts +1 -8
  97. package/src/cli/commands/email.ts +584 -835
  98. package/src/cli/commands/memory.ts +1 -34
  99. package/src/cli/commands/notifications.ts +7 -2
  100. package/src/cli/commands/oauth/connect.ts +14 -5
  101. package/src/cli/commands/routes.ts +396 -0
  102. package/src/cli/commands/skills.ts +130 -20
  103. package/src/cli/program.ts +2 -0
  104. package/src/cli.ts +1 -120
  105. package/src/config/bundled-skills/app-builder/SKILL.md +4 -1
  106. package/src/config/bundled-skills/gmail/SKILL.md +2 -2
  107. package/src/config/bundled-skills/messaging/SKILL.md +7 -0
  108. package/src/config/bundled-skills/schedule/SKILL.md +22 -2
  109. package/src/config/bundled-skills/schedule/TOOLS.json +8 -0
  110. package/src/config/bundled-skills/settings/tools/avatar-get.ts +3 -13
  111. package/src/config/bundled-skills/settings/tools/avatar-remove.ts +2 -4
  112. package/src/config/bundled-skills/settings/tools/avatar-update.ts +5 -2
  113. package/src/config/bundled-skills/slack/SKILL.md +2 -0
  114. package/src/config/bundled-skills/subagent/SKILL.md +43 -3
  115. package/src/config/bundled-skills/subagent/TOOLS.json +29 -4
  116. package/src/config/env-registry.ts +63 -0
  117. package/src/config/feature-flag-registry.json +17 -1
  118. package/src/config/schema.ts +8 -0
  119. package/src/config/schemas/filing.ts +51 -0
  120. package/src/config/schemas/heartbeat.ts +15 -12
  121. package/src/config/schemas/memory-lifecycle.ts +12 -0
  122. package/src/config/schemas/security.ts +14 -0
  123. package/src/daemon/app-source-watcher.ts +93 -0
  124. package/src/daemon/config-watcher.ts +79 -1
  125. package/src/daemon/conversation-agent-loop-handlers.ts +20 -0
  126. package/src/daemon/conversation-agent-loop.ts +158 -65
  127. package/src/daemon/conversation-history.ts +4 -19
  128. package/src/daemon/conversation-lifecycle.ts +8 -14
  129. package/src/daemon/conversation-process.ts +13 -7
  130. package/src/daemon/conversation-runtime-assembly.ts +300 -306
  131. package/src/daemon/conversation-tool-setup.ts +44 -14
  132. package/src/daemon/conversation-workspace.ts +1 -2
  133. package/src/daemon/conversation.ts +18 -0
  134. package/src/daemon/date-context.ts +26 -53
  135. package/src/daemon/first-greeting.ts +1 -1
  136. package/src/daemon/handlers/conversations.ts +4 -7
  137. package/src/daemon/handlers/shared.test.ts +143 -0
  138. package/src/daemon/handlers/shared.ts +63 -5
  139. package/src/daemon/handlers/skills.ts +11 -18
  140. package/src/daemon/lifecycle.ts +199 -157
  141. package/src/daemon/message-types/conversations.ts +25 -6
  142. package/src/daemon/message-types/messages.ts +9 -1
  143. package/src/daemon/message-types/schedules.ts +1 -0
  144. package/src/daemon/message-types/settings.ts +6 -0
  145. package/src/daemon/profiler-run-store.ts +557 -0
  146. package/src/daemon/server.ts +89 -9
  147. package/src/daemon/shutdown-handlers.ts +5 -0
  148. package/src/daemon/tool-side-effects.ts +23 -3
  149. package/src/export/transcript-formatter.ts +148 -0
  150. package/src/filing/filing-service.ts +228 -0
  151. package/src/heartbeat/heartbeat-service.ts +96 -7
  152. package/src/mcp/client.ts +6 -0
  153. package/src/mcp/mcp-oauth-provider.ts +149 -27
  154. package/src/memory/admin.ts +33 -32
  155. package/src/memory/app-store.ts +69 -0
  156. package/src/memory/conversation-bootstrap.ts +1 -1
  157. package/src/memory/conversation-crud.ts +136 -107
  158. package/src/memory/conversation-group-migration.ts +1 -1
  159. package/src/memory/conversation-queries.ts +58 -12
  160. package/src/memory/conversation-title-service.ts +1 -0
  161. package/src/memory/db-init.ts +182 -376
  162. package/src/memory/graph/bootstrap.ts +75 -66
  163. package/src/memory/graph/capability-seed.ts +167 -15
  164. package/src/memory/graph/consolidation.ts +38 -4
  165. package/src/memory/graph/conversation-graph-memory.ts +133 -104
  166. package/src/memory/graph/extraction-job.ts +9 -4
  167. package/src/memory/graph/extraction.ts +66 -23
  168. package/src/memory/graph/graph-memory-state-store.ts +37 -0
  169. package/src/memory/graph/graph-search.ts +29 -15
  170. package/src/memory/graph/injection.ts +38 -8
  171. package/src/memory/graph/inspect.ts +12 -3
  172. package/src/memory/graph/retriever.ts +365 -262
  173. package/src/memory/graph/store.test.ts +48 -0
  174. package/src/memory/graph/store.ts +150 -11
  175. package/src/memory/graph/tool-handlers.ts +84 -209
  176. package/src/memory/graph/tools.ts +8 -52
  177. package/src/memory/graph/types.ts +24 -0
  178. package/src/memory/job-handlers/cleanup.ts +44 -1
  179. package/src/memory/jobs-store.ts +70 -60
  180. package/src/memory/jobs-worker.ts +44 -28
  181. package/src/memory/llm-request-log-store.ts +96 -12
  182. package/src/memory/memory-recall-log-store.ts +49 -5
  183. package/src/memory/migrations/203-drop-memory-items-tables.ts +33 -1
  184. package/src/memory/migrations/206-memory-graph-node-edits.ts +19 -0
  185. package/src/memory/migrations/206-scrub-corrupted-image-attachments.ts +131 -0
  186. package/src/memory/migrations/207-conversation-graph-memory-state.ts +20 -0
  187. package/src/memory/migrations/208-conversations-last-message-at.ts +35 -0
  188. package/src/memory/migrations/209-strip-thinking-from-consolidated.ts +85 -0
  189. package/src/memory/migrations/210-schedule-reuse-conversation.ts +13 -0
  190. package/src/memory/migrations/211-memory-recall-logs-query-context.ts +21 -0
  191. package/src/memory/migrations/212-llm-request-logs-created-at-index.ts +19 -0
  192. package/src/memory/migrations/index.ts +8 -0
  193. package/src/memory/migrations/registry.ts +8 -0
  194. package/src/memory/schema/conversations.ts +14 -0
  195. package/src/memory/schema/infrastructure.ts +8 -1
  196. package/src/memory/schema/memory-core.ts +0 -51
  197. package/src/memory/schema/memory-graph.ts +15 -0
  198. package/src/memory/task-memory-cleanup.ts +30 -11
  199. package/src/notifications/copy-composer.ts +86 -0
  200. package/src/notifications/decision-engine.ts +35 -0
  201. package/src/permissions/checker.ts +12 -1
  202. package/src/permissions/permission-mode-store.ts +180 -0
  203. package/src/permissions/permission-mode.ts +31 -0
  204. package/src/permissions/workspace-policy.ts +9 -0
  205. package/src/prompts/system-prompt.ts +59 -7
  206. package/src/prompts/templates/BOOTSTRAP-REFERENCE.md +100 -0
  207. package/src/prompts/templates/BOOTSTRAP.md +70 -165
  208. package/src/prompts/templates/HEARTBEAT.md +3 -1
  209. package/src/prompts/templates/SOUL.md +25 -4
  210. package/src/prompts/templates/UPDATES.md +8 -0
  211. package/src/providers/anthropic/client.ts +107 -219
  212. package/src/runtime/auth/route-policy.ts +23 -0
  213. package/src/runtime/http-server.ts +32 -2
  214. package/src/runtime/http-types.ts +12 -1
  215. package/src/runtime/migrations/vbundle-builder.ts +389 -3
  216. package/src/runtime/migrations/vbundle-importer.ts +8 -6
  217. package/src/runtime/routes/__tests__/user-route-dispatcher.test.ts +378 -0
  218. package/src/runtime/routes/app-management-routes.ts +1 -11
  219. package/src/runtime/routes/approval-strategies/guardian-callback-strategy.ts +26 -0
  220. package/src/runtime/routes/archive-utils.ts +29 -0
  221. package/src/runtime/routes/avatar-routes.ts +2 -9
  222. package/src/runtime/routes/btw-routes.ts +14 -1
  223. package/src/runtime/routes/conversation-analysis-routes.ts +173 -0
  224. package/src/runtime/routes/conversation-management-routes.ts +1 -14
  225. package/src/runtime/routes/conversation-query-routes.ts +49 -3
  226. package/src/runtime/routes/conversation-routes.ts +264 -44
  227. package/src/runtime/routes/heartbeat-routes.ts +4 -10
  228. package/src/runtime/routes/identity-routes.ts +53 -18
  229. package/src/runtime/routes/llm-context-normalization.ts +14 -10
  230. package/src/runtime/routes/log-export-routes.ts +23 -275
  231. package/src/runtime/routes/memory-item-routes.test.ts +168 -233
  232. package/src/runtime/routes/migration-routes.ts +18 -7
  233. package/src/runtime/routes/profiler-routes.ts +350 -0
  234. package/src/runtime/routes/schedule-routes.ts +27 -12
  235. package/src/runtime/routes/settings-routes.ts +95 -8
  236. package/src/runtime/routes/subagents-routes.ts +28 -7
  237. package/src/runtime/routes/user-route-dispatcher.ts +223 -0
  238. package/src/runtime/routes/user-routes.ts +41 -0
  239. package/src/runtime/routes/workspace-routes.ts +0 -1
  240. package/src/schedule/schedule-store.ts +30 -0
  241. package/src/schedule/scheduler.ts +45 -18
  242. package/src/skills/catalog-install.ts +10 -2
  243. package/src/skills/managed-store.ts +2 -2
  244. package/src/skills/skill-memory.ts +1 -293
  245. package/src/subagent/index.ts +13 -3
  246. package/src/subagent/manager.ts +308 -29
  247. package/src/subagent/types.ts +68 -0
  248. package/src/tasks/task-runner.ts +4 -4
  249. package/src/tools/apps/executors.ts +29 -4
  250. package/src/tools/filesystem/list.ts +93 -0
  251. package/src/tools/permission-checker.ts +78 -0
  252. package/src/tools/registry.ts +4 -0
  253. package/src/tools/schedule/create.ts +3 -0
  254. package/src/tools/schedule/list.ts +1 -0
  255. package/src/tools/schedule/update.ts +6 -0
  256. package/src/tools/shared/filesystem/errors.ts +5 -0
  257. package/src/tools/shared/filesystem/file-ops-service.ts +90 -2
  258. package/src/tools/shared/filesystem/types.ts +17 -0
  259. package/src/tools/shared/shell-output.ts +31 -2
  260. package/src/tools/subagent/abort.ts +12 -2
  261. package/src/tools/subagent/message.ts +9 -2
  262. package/src/tools/subagent/notify-parent.ts +79 -0
  263. package/src/tools/subagent/read.ts +29 -8
  264. package/src/tools/subagent/resolve.ts +21 -0
  265. package/src/tools/subagent/spawn.ts +2 -0
  266. package/src/tools/subagent/status.ts +11 -1
  267. package/src/tools/system/avatar-generator.ts +3 -3
  268. package/src/tools/system/register.ts +23 -0
  269. package/src/tools/system/set-permission-mode.ts +103 -0
  270. package/src/tools/terminal/parser.ts +30 -5
  271. package/src/tools/terminal/safe-env.ts +16 -1
  272. package/src/tools/tool-manifest.ts +6 -0
  273. package/src/tools/types.ts +2 -0
  274. package/src/util/logger.ts +1 -1
  275. package/src/util/platform.ts +50 -17
  276. package/src/workspace/migrations/023-move-config-files-to-workspace.ts +2 -2
  277. package/src/workspace/migrations/024-move-runtime-files-to-workspace.ts +2 -2
  278. package/src/workspace/migrations/028-recover-conversations-from-disk-view.ts +270 -0
  279. package/src/workspace/migrations/029-seed-pkb.ts +84 -0
  280. package/src/workspace/migrations/registry.ts +4 -0
  281. package/src/workspace/top-level-renderer.ts +5 -9
  282. package/src/__tests__/cli-memory.test.ts +0 -377
  283. package/src/__tests__/clipboard.test.ts +0 -88
  284. package/src/cli/cli-memory.ts +0 -179
  285. package/src/util/clipboard.ts +0 -34
@@ -0,0 +1,387 @@
1
+ import { existsSync, mkdirSync, rmSync, writeFileSync } from "node:fs";
2
+ import { join } from "node:path";
3
+ import { beforeEach, describe, expect, mock, test } from "bun:test";
4
+
5
+ // ---------------------------------------------------------------------------
6
+ // Mocks — must come before any imports that depend on them
7
+ // ---------------------------------------------------------------------------
8
+
9
+ const testDir = process.env.VELLUM_WORKSPACE_DIR!;
10
+ const workspaceDir = testDir;
11
+ const conversationsDir = join(workspaceDir, "conversations");
12
+ mkdirSync(conversationsDir, { recursive: true });
13
+
14
+ mock.module("../util/logger.js", () => ({
15
+ getLogger: () =>
16
+ new Proxy({} as Record<string, unknown>, {
17
+ get: () => () => {},
18
+ }),
19
+ }));
20
+
21
+ mock.module("../config/loader.js", () => ({
22
+ getConfig: () => ({
23
+ ui: {},
24
+ model: "test",
25
+ provider: "test",
26
+ memory: { enabled: false },
27
+ rateLimit: { maxRequestsPerMinute: 0 },
28
+ }),
29
+ }));
30
+
31
+ // ---------------------------------------------------------------------------
32
+ // Imports — after mocks
33
+ // ---------------------------------------------------------------------------
34
+
35
+ import { getDb, initializeDb } from "../memory/db.js";
36
+ import { conversations, messages } from "../memory/schema.js";
37
+ import { recoverConversationsFromDiskViewMigration } from "../workspace/migrations/028-recover-conversations-from-disk-view.js";
38
+
39
+ initializeDb();
40
+
41
+ // ---------------------------------------------------------------------------
42
+ // Helpers
43
+ // ---------------------------------------------------------------------------
44
+
45
+ function resetTables() {
46
+ const db = getDb();
47
+ db.run("DELETE FROM messages");
48
+ db.run("DELETE FROM conversations");
49
+ }
50
+
51
+ function resetConversationsDir() {
52
+ rmSync(conversationsDir, { recursive: true, force: true });
53
+ mkdirSync(conversationsDir, { recursive: true });
54
+ }
55
+
56
+ function createDiskViewDir(
57
+ id: string,
58
+ meta: Record<string, unknown>,
59
+ messagesJsonl?: string,
60
+ ): string {
61
+ const createdAt =
62
+ typeof meta.createdAt === "string" ? meta.createdAt : new Date().toISOString();
63
+ const timestamp = createdAt.replace(/:/g, "-");
64
+ const dirName = `${timestamp}_${id}`;
65
+ const dirPath = join(conversationsDir, dirName);
66
+ mkdirSync(dirPath, { recursive: true });
67
+ writeFileSync(join(dirPath, "meta.json"), JSON.stringify(meta, null, 2) + "\n");
68
+ if (messagesJsonl !== undefined) {
69
+ writeFileSync(join(dirPath, "messages.jsonl"), messagesJsonl);
70
+ }
71
+ return dirPath;
72
+ }
73
+
74
+ // ---------------------------------------------------------------------------
75
+ // Tests
76
+ // ---------------------------------------------------------------------------
77
+
78
+ describe("028-recover-conversations-from-disk-view migration", () => {
79
+ beforeEach(() => {
80
+ resetTables();
81
+ resetConversationsDir();
82
+ });
83
+
84
+ test("recovers conversation with messages", () => {
85
+ const id = "conv-028-basic";
86
+ const createdAt = "2026-03-18T14:23:00.000Z";
87
+ const updatedAt = "2026-03-18T14:25:00.000Z";
88
+
89
+ const userLine = JSON.stringify({
90
+ role: "user",
91
+ ts: "2026-03-18T14:23:30.000Z",
92
+ content: "Hello, world",
93
+ });
94
+ const assistantLine = JSON.stringify({
95
+ role: "assistant",
96
+ ts: "2026-03-18T14:24:00.000Z",
97
+ content: "Hi there!",
98
+ });
99
+
100
+ createDiskViewDir(
101
+ id,
102
+ { id, title: "Basic Recovery", type: "standard", channel: "desktop", createdAt, updatedAt },
103
+ userLine + "\n" + assistantLine + "\n",
104
+ );
105
+
106
+ recoverConversationsFromDiskViewMigration.run(workspaceDir);
107
+
108
+ const db = getDb();
109
+ const convRows = db.select().from(conversations).all();
110
+ expect(convRows).toHaveLength(1);
111
+ expect(convRows[0].id).toBe(id);
112
+ expect(convRows[0].title).toBe("Basic Recovery");
113
+ expect(convRows[0].conversationType).toBe("standard");
114
+ expect(convRows[0].createdAt).toBe(Date.parse(createdAt));
115
+ expect(convRows[0].updatedAt).toBe(Date.parse(updatedAt));
116
+
117
+ const msgRows = db.select().from(messages).all();
118
+ expect(msgRows).toHaveLength(2);
119
+
120
+ const userMsg = msgRows.find((m) => m.role === "user")!;
121
+ expect(userMsg).toBeDefined();
122
+ const userContent = JSON.parse(userMsg.content);
123
+ expect(userContent).toEqual([{ type: "text", text: "Hello, world" }]);
124
+ expect(userMsg.createdAt).toBe(Date.parse("2026-03-18T14:23:30.000Z"));
125
+
126
+ const assistantMsg = msgRows.find((m) => m.role === "assistant")!;
127
+ expect(assistantMsg).toBeDefined();
128
+ const assistantContent = JSON.parse(assistantMsg.content);
129
+ expect(assistantContent).toEqual([{ type: "text", text: "Hi there!" }]);
130
+ expect(assistantMsg.createdAt).toBe(Date.parse("2026-03-18T14:24:00.000Z"));
131
+ });
132
+
133
+ test("handles toolCalls and toolResults", () => {
134
+ const id = "conv-028-tools";
135
+ const createdAt = "2026-03-18T15:00:00.000Z";
136
+
137
+ const toolCallLine = JSON.stringify({
138
+ role: "assistant",
139
+ ts: "2026-03-18T15:00:10.000Z",
140
+ toolCalls: [{ name: "bash", input: { command: "ls" } }],
141
+ });
142
+ const toolResultLine = JSON.stringify({
143
+ role: "user",
144
+ ts: "2026-03-18T15:00:20.000Z",
145
+ toolResults: [{ content: "file.txt" }],
146
+ });
147
+
148
+ createDiskViewDir(
149
+ id,
150
+ { id, title: "Tool Test", type: "standard", createdAt, updatedAt: createdAt },
151
+ toolCallLine + "\n" + toolResultLine + "\n",
152
+ );
153
+
154
+ recoverConversationsFromDiskViewMigration.run(workspaceDir);
155
+
156
+ const db = getDb();
157
+ const msgRows = db.select().from(messages).all();
158
+ expect(msgRows).toHaveLength(2);
159
+
160
+ const assistantMsg = msgRows.find((m) => m.role === "assistant")!;
161
+ const assistantContent = JSON.parse(assistantMsg.content);
162
+ expect(assistantContent).toHaveLength(1);
163
+ expect(assistantContent[0].type).toBe("tool_use");
164
+ expect(assistantContent[0].name).toBe("bash");
165
+ expect(assistantContent[0].input).toEqual({ command: "ls" });
166
+ // tool_use blocks get a random UUID id — just check it's a string
167
+ expect(typeof assistantContent[0].id).toBe("string");
168
+
169
+ const userMsg = msgRows.find((m) => m.role === "user")!;
170
+ const userContent = JSON.parse(userMsg.content);
171
+ expect(userContent).toHaveLength(1);
172
+ expect(userContent[0].type).toBe("tool_result");
173
+ expect(userContent[0].content).toBe("file.txt");
174
+ expect(userContent[0].tool_use_id).toBe("");
175
+ });
176
+
177
+ test("handles mixed content + toolCalls on the same message", () => {
178
+ const id = "conv-028-mixed";
179
+ const createdAt = "2026-03-18T15:30:00.000Z";
180
+
181
+ const mixedLine = JSON.stringify({
182
+ role: "assistant",
183
+ ts: "2026-03-18T15:30:10.000Z",
184
+ content: "Let me check that",
185
+ toolCalls: [{ name: "bash", input: { command: "ls" } }],
186
+ });
187
+
188
+ createDiskViewDir(
189
+ id,
190
+ { id, title: "Mixed Test", type: "standard", createdAt, updatedAt: createdAt },
191
+ mixedLine + "\n",
192
+ );
193
+
194
+ recoverConversationsFromDiskViewMigration.run(workspaceDir);
195
+
196
+ const db = getDb();
197
+ const msgRows = db.select().from(messages).all();
198
+ expect(msgRows).toHaveLength(1);
199
+
200
+ const assistantMsg = msgRows[0];
201
+ expect(assistantMsg.role).toBe("assistant");
202
+
203
+ const contentBlocks = JSON.parse(assistantMsg.content);
204
+ expect(contentBlocks).toHaveLength(2);
205
+
206
+ expect(contentBlocks[0].type).toBe("text");
207
+ expect(contentBlocks[0].text).toBe("Let me check that");
208
+
209
+ expect(contentBlocks[1].type).toBe("tool_use");
210
+ expect(contentBlocks[1].name).toBe("bash");
211
+ expect(contentBlocks[1].input).toEqual({ command: "ls" });
212
+ expect(typeof contentBlocks[1].id).toBe("string");
213
+ });
214
+
215
+ test("skips existing conversations", () => {
216
+ const id = "conv-028-existing";
217
+ const createdAt = "2026-03-18T16:00:00.000Z";
218
+ const createdAtMs = Date.parse(createdAt);
219
+
220
+ // Pre-insert the conversation in the DB
221
+ const db = getDb();
222
+ db.insert(conversations)
223
+ .values({
224
+ id,
225
+ title: "Already Here",
226
+ createdAt: createdAtMs,
227
+ updatedAt: createdAtMs,
228
+ conversationType: "standard",
229
+ source: "user",
230
+ memoryScopeId: "default",
231
+ })
232
+ .run();
233
+
234
+ // Create matching disk-view dir with a message
235
+ createDiskViewDir(
236
+ id,
237
+ { id, title: "Already Here", type: "standard", createdAt, updatedAt: createdAt },
238
+ JSON.stringify({ role: "user", ts: createdAt, content: "Should not be imported" }) + "\n",
239
+ );
240
+
241
+ recoverConversationsFromDiskViewMigration.run(workspaceDir);
242
+
243
+ // Verify no duplication: still 1 conversation, 0 messages (the disk-view message was not imported)
244
+ const convRows = db.select().from(conversations).all();
245
+ expect(convRows).toHaveLength(1);
246
+ expect(convRows[0].title).toBe("Already Here");
247
+
248
+ const msgRows = db.select().from(messages).all();
249
+ expect(msgRows).toHaveLength(0);
250
+ });
251
+
252
+ test("idempotent — running twice produces same result", () => {
253
+ const id = "conv-028-idem";
254
+ const createdAt = "2026-03-18T17:00:00.000Z";
255
+
256
+ createDiskViewDir(
257
+ id,
258
+ { id, title: "Idempotency Test", type: "standard", createdAt, updatedAt: createdAt },
259
+ JSON.stringify({ role: "user", ts: createdAt, content: "First message" }) + "\n" +
260
+ JSON.stringify({ role: "assistant", ts: "2026-03-18T17:01:00.000Z", content: "Reply" }) + "\n",
261
+ );
262
+
263
+ recoverConversationsFromDiskViewMigration.run(workspaceDir);
264
+
265
+ const db = getDb();
266
+ const convCountAfterFirst = db.select().from(conversations).all().length;
267
+ const msgCountAfterFirst = db.select().from(messages).all().length;
268
+ expect(convCountAfterFirst).toBe(1);
269
+ expect(msgCountAfterFirst).toBe(2);
270
+
271
+ // Run again
272
+ recoverConversationsFromDiskViewMigration.run(workspaceDir);
273
+
274
+ const convCountAfterSecond = db.select().from(conversations).all().length;
275
+ const msgCountAfterSecond = db.select().from(messages).all().length;
276
+ expect(convCountAfterSecond).toBe(convCountAfterFirst);
277
+ expect(msgCountAfterSecond).toBe(msgCountAfterFirst);
278
+ });
279
+
280
+ test("handles missing messages.jsonl", () => {
281
+ const id = "conv-028-no-messages";
282
+ const createdAt = "2026-03-18T18:00:00.000Z";
283
+
284
+ // Create dir with only meta.json — no messages.jsonl
285
+ createDiskViewDir(
286
+ id,
287
+ { id, title: "No Messages", type: "standard", createdAt, updatedAt: createdAt },
288
+ );
289
+
290
+ recoverConversationsFromDiskViewMigration.run(workspaceDir);
291
+
292
+ const db = getDb();
293
+ const convRows = db.select().from(conversations).all();
294
+ expect(convRows).toHaveLength(1);
295
+ expect(convRows[0].id).toBe(id);
296
+ expect(convRows[0].title).toBe("No Messages");
297
+
298
+ const msgRows = db.select().from(messages).all();
299
+ expect(msgRows).toHaveLength(0);
300
+ });
301
+
302
+ test("handles malformed JSONL lines", () => {
303
+ const id = "conv-028-malformed-jsonl";
304
+ const createdAt = "2026-03-18T19:00:00.000Z";
305
+
306
+ const validLine = JSON.stringify({ role: "user", ts: createdAt, content: "Valid" });
307
+ const invalidLine = "{ this is not valid json }}}";
308
+
309
+ createDiskViewDir(
310
+ id,
311
+ { id, title: "Malformed JSONL", type: "standard", createdAt, updatedAt: createdAt },
312
+ validLine + "\n" + invalidLine + "\n",
313
+ );
314
+
315
+ recoverConversationsFromDiskViewMigration.run(workspaceDir);
316
+
317
+ const db = getDb();
318
+ const convRows = db.select().from(conversations).all();
319
+ expect(convRows).toHaveLength(1);
320
+
321
+ // Only the valid line should produce a message row
322
+ const msgRows = db.select().from(messages).all();
323
+ expect(msgRows).toHaveLength(1);
324
+ expect(msgRows[0].role).toBe("user");
325
+ const content = JSON.parse(msgRows[0].content);
326
+ expect(content).toEqual([{ type: "text", text: "Valid" }]);
327
+ });
328
+
329
+ test("handles malformed meta.json", () => {
330
+ const id = "conv-028-malformed-meta";
331
+ const createdAt = "2026-03-18T20:00:00.000Z";
332
+ const timestamp = createdAt.replace(/:/g, "-");
333
+ const dirName = `${timestamp}_${id}`;
334
+ const dirPath = join(conversationsDir, dirName);
335
+ mkdirSync(dirPath, { recursive: true });
336
+
337
+ // Write broken JSON directly
338
+ writeFileSync(join(dirPath, "meta.json"), "{ broken json");
339
+
340
+ // Migration should complete without error
341
+ recoverConversationsFromDiskViewMigration.run(workspaceDir);
342
+
343
+ const db = getDb();
344
+ const convRows = db.select().from(conversations).all();
345
+ expect(convRows).toHaveLength(0);
346
+ });
347
+
348
+ test("no-op when conversations dir missing", () => {
349
+ // Remove the conversations dir entirely
350
+ rmSync(conversationsDir, { recursive: true, force: true });
351
+ expect(existsSync(conversationsDir)).toBe(false);
352
+
353
+ // Migration should complete without error
354
+ recoverConversationsFromDiskViewMigration.run(workspaceDir);
355
+
356
+ // No conversations should exist since we can't access the DB rows through a missing dir
357
+ const db = getDb();
358
+ const convRows = db.select().from(conversations).all();
359
+ expect(convRows).toHaveLength(0);
360
+ });
361
+
362
+ test("processes multiple directories", () => {
363
+ const ids = ["conv-028-multi-a", "conv-028-multi-b", "conv-028-multi-c"];
364
+ const baseTime = Date.parse("2026-03-18T21:00:00.000Z");
365
+
366
+ for (let i = 0; i < ids.length; i++) {
367
+ const ts = new Date(baseTime + i * 60_000).toISOString();
368
+ createDiskViewDir(
369
+ ids[i],
370
+ { id: ids[i], title: `Multi ${i + 1}`, type: "standard", createdAt: ts, updatedAt: ts },
371
+ JSON.stringify({ role: "user", ts, content: `Message ${i + 1}` }) + "\n",
372
+ );
373
+ }
374
+
375
+ recoverConversationsFromDiskViewMigration.run(workspaceDir);
376
+
377
+ const db = getDb();
378
+ const convRows = db.select().from(conversations).all();
379
+ expect(convRows).toHaveLength(3);
380
+
381
+ const recoveredIds = convRows.map((c) => c.id).sort();
382
+ expect(recoveredIds).toEqual([...ids].sort());
383
+
384
+ const msgRows = db.select().from(messages).all();
385
+ expect(msgRows).toHaveLength(3);
386
+ });
387
+ });
package/src/agent/loop.ts CHANGED
@@ -31,6 +31,8 @@ export interface AgentLoopConfig {
31
31
  | { type: "tool"; name: string };
32
32
  /** Minimum interval (ms) between consecutive LLM calls to prevent spin when tools return instantly */
33
33
  minTurnIntervalMs?: number;
34
+ /** Override the default prompt cache TTL sent to the provider (e.g. "5m" for short-lived subagents). */
35
+ cacheTtl?: "5m" | "1h";
34
36
  }
35
37
 
36
38
  export interface CheckpointInfo {
@@ -252,6 +254,10 @@ export class AgentLoop {
252
254
  providerConfig.tool_choice = this.config.toolChoice;
253
255
  }
254
256
 
257
+ if (this.config.cacheTtl) {
258
+ providerConfig.cacheTtl = this.config.cacheTtl;
259
+ }
260
+
255
261
  const preLlmResult = await getHookManager().trigger("pre-llm-call", {
256
262
  systemPrompt: turnSystemPrompt,
257
263
  messages: history,
@@ -13,6 +13,7 @@
13
13
 
14
14
  import { answerCall } from "../calls/call-domain.js";
15
15
  import { getGatewayInternalBaseUrl } from "../config/env.js";
16
+ import { findContactChannel } from "../contacts/contact-store.js";
16
17
  import { upsertContactChannel } from "../contacts/contacts-write.js";
17
18
  import {
18
19
  type CanonicalGuardianRequest,
@@ -396,6 +397,25 @@ const accessRequestResolver: GuardianRequestResolver = {
396
397
  const desktopDeliverUrl = resolveDeliverCallbackUrlForChannel(channel);
397
398
  const desktopBearerToken = mintDaemonDeliveryToken();
398
399
 
400
+ // Resolve display names from the contacts database for enriched payloads
401
+ const requesterContactResult = requesterExternalUserId
402
+ ? findContactChannel({
403
+ channelType: channel,
404
+ externalUserId: requesterExternalUserId,
405
+ })
406
+ : null;
407
+ const requesterDisplayName =
408
+ requesterContactResult?.contact.displayName ?? null;
409
+
410
+ const decidedByContactResult = decidedByExternalUserId
411
+ ? findContactChannel({
412
+ channelType: channel,
413
+ externalUserId: decidedByExternalUserId,
414
+ })
415
+ : null;
416
+ const decidedByDisplayName =
417
+ decidedByContactResult?.contact.displayName ?? null;
418
+
399
419
  if (decision.action === "reject") {
400
420
  log.info(
401
421
  { event: "resolver_access_request_denied", requestId: request.id },
@@ -435,6 +455,8 @@ const accessRequestResolver: GuardianRequestResolver = {
435
455
  requesterExternalUserId,
436
456
  requesterChatId,
437
457
  decidedByExternalUserId,
458
+ requesterDisplayName,
459
+ decidedByDisplayName,
438
460
  decision: "denied" as const,
439
461
  };
440
462
 
@@ -726,6 +748,8 @@ const accessRequestResolver: GuardianRequestResolver = {
726
748
  sourceChannel: channel,
727
749
  requesterExternalUserId,
728
750
  requesterChatId,
751
+ requesterDisplayName,
752
+ decidedByDisplayName,
729
753
  verificationSessionId: session.sessionId,
730
754
  },
731
755
  dedupeKey: `trusted-contact:verification-sent:${session.sessionId}`,
@@ -3,7 +3,7 @@ import { mkdirSync, renameSync, writeFileSync } from "node:fs";
3
3
  import { join } from "node:path";
4
4
 
5
5
  import { getLogger } from "../util/logger.js";
6
- import { getWorkspaceDir } from "../util/platform.js";
6
+ import { AVATAR_IMAGE_FILENAME, getAvatarDir } from "../util/platform.js";
7
7
  import { renderCharacterAscii } from "./ascii-renderer.js";
8
8
  import { getCharacterComponents } from "./character-components.js";
9
9
  import { renderCharacterPng } from "./png-renderer.js";
@@ -64,7 +64,7 @@ function writeAvatarFiles(
64
64
  pngBuffer: Buffer,
65
65
  asciiArt: string | null,
66
66
  ): boolean {
67
- const pngPath = join(avatarDir, "avatar-image.png");
67
+ const pngPath = join(avatarDir, AVATAR_IMAGE_FILENAME);
68
68
  const pngTmp = `${pngPath}.${randomUUID()}.tmp`;
69
69
  writeFileSync(pngTmp, pngBuffer);
70
70
  renameSync(pngTmp, pngPath);
@@ -144,7 +144,7 @@ export function writeTraitsAndRenderAvatar(
144
144
  };
145
145
  }
146
146
 
147
- const avatarDir = join(getWorkspaceDir(), "data", "avatar");
147
+ const avatarDir = getAvatarDir();
148
148
  const traitsPath = join(avatarDir, "character-traits.json");
149
149
 
150
150
  try {
@@ -0,0 +1,29 @@
1
+ /**
2
+ * CLI test utility — run an assistant CLI command via the real program,
3
+ * capturing stdout.
4
+ */
5
+ export async function runAssistantCommand(...args: string[]): Promise<string> {
6
+ const { buildCliProgram } = await import("../program.js");
7
+ const program = buildCliProgram();
8
+ program.exitOverride();
9
+ program.configureOutput({ writeErr: () => {}, writeOut: () => {} });
10
+
11
+ const chunks: string[] = [];
12
+ const originalWrite = process.stdout.write;
13
+ process.stdout.write = ((chunk: string | Uint8Array) => {
14
+ chunks.push(
15
+ typeof chunk === "string" ? chunk : new TextDecoder().decode(chunk),
16
+ );
17
+ return true;
18
+ }) as typeof process.stdout.write;
19
+
20
+ try {
21
+ await program.parseAsync(["node", "assistant", ...args]);
22
+ } catch {
23
+ /* commander exit override throws */
24
+ } finally {
25
+ process.stdout.write = originalWrite;
26
+ }
27
+
28
+ return chunks.join("");
29
+ }