@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
@@ -13,18 +13,18 @@ import { existsSync, readdirSync, readFileSync } from "node:fs";
13
13
  import { join } from "node:path";
14
14
 
15
15
  import { and, asc, ne, sql } from "drizzle-orm";
16
+ import { v4 as uuid } from "uuid";
16
17
 
17
18
  import { getConfig } from "../../config/loader.js";
18
19
  import { getLogger } from "../../util/logger.js";
19
20
  import { getWorkspaceDir } from "../../util/platform.js";
20
21
  import { getMemoryCheckpoint, setMemoryCheckpoint } from "../checkpoints.js";
21
- import { getDb, rawAll } from "../db.js";
22
+ import { getDb, rawAll, rawGet, rawRun } from "../db.js";
22
23
  import { enqueueMemoryJob, hasActiveJobOfType } from "../jobs-store.js";
23
24
  import { initQdrantClient } from "../qdrant-client.js";
24
25
  import { conversations, memoryGraphNodes, memorySegments } from "../schema.js";
25
26
  import { runGraphExtraction } from "./extraction.js";
26
- import { countNodes, createNode } from "./store.js";
27
- import type { NewNode } from "./types.js";
27
+ import { countNodes } from "./store.js";
28
28
 
29
29
  const log = getLogger("graph-bootstrap");
30
30
 
@@ -61,7 +61,7 @@ export interface BootstrapResult {
61
61
  * if interrupted, re-run and it picks up where it left off.
62
62
  */
63
63
  export async function bootstrapFromHistory(
64
- options?: BootstrapOptions
64
+ options?: BootstrapOptions,
65
65
  ): Promise<BootstrapResult> {
66
66
  const start = Date.now();
67
67
  const scopeId = options?.scopeId ?? "default";
@@ -106,7 +106,7 @@ export async function bootstrapFromHistory(
106
106
 
107
107
  log.info(
108
108
  { total: allConversations.length },
109
- "Starting graph bootstrap from historical conversations"
109
+ "Starting graph bootstrap from historical conversations",
110
110
  );
111
111
 
112
112
  // Resume from checkpoint
@@ -148,7 +148,7 @@ export async function bootstrapFromHistory(
148
148
  skipQdrant: true, // Use DB query for candidates (no Qdrant dependency)
149
149
  conversationTimestamp: conv.createdAt, // Use actual conversation time
150
150
  embedInline: true, // Embed synchronously so nodes are searchable immediately
151
- }
151
+ },
152
152
  );
153
153
 
154
154
  result.totalNodesCreated += extractionResult.nodesCreated;
@@ -171,14 +171,14 @@ export async function bootstrapFromHistory(
171
171
  nodes: nodeCount,
172
172
  elapsed: `${((Date.now() - start) / 1000).toFixed(1)}s`,
173
173
  },
174
- "Bootstrap progress"
174
+ "Bootstrap progress",
175
175
  );
176
176
  }
177
177
  } catch (err) {
178
178
  const errMsg = err instanceof Error ? err.message : String(err);
179
179
  log.warn(
180
180
  { conversationId: conv.id, err: errMsg },
181
- "Failed to extract conversation, continuing"
181
+ "Failed to extract conversation, continuing",
182
182
  );
183
183
  result.errors.push({ conversationId: conv.id, error: errMsg });
184
184
 
@@ -199,7 +199,7 @@ export async function bootstrapFromHistory(
199
199
  errors: result.errors.length,
200
200
  elapsedMs: result.elapsedMs,
201
201
  },
202
- "Graph bootstrap complete"
202
+ "Graph bootstrap complete",
203
203
  );
204
204
 
205
205
  return result;
@@ -209,7 +209,7 @@ export async function bootstrapFromHistory(
209
209
  * Also extract from journal files on disk.
210
210
  */
211
211
  export async function bootstrapFromJournal(
212
- scopeId: string = "default"
212
+ scopeId: string = "default",
213
213
  ): Promise<{ extracted: number; errors: number }> {
214
214
  const config = getConfig();
215
215
  const journalDir = join(getWorkspaceDir(), "journal");
@@ -227,7 +227,7 @@ export async function bootstrapFromJournal(
227
227
  (f) =>
228
228
  f.endsWith(".md") &&
229
229
  !f.startsWith(".") &&
230
- f.toLowerCase() !== "readme.md"
230
+ f.toLowerCase() !== "readme.md",
231
231
  );
232
232
  } catch {
233
233
  continue;
@@ -248,7 +248,7 @@ export async function bootstrapFromJournal(
248
248
  } catch (err) {
249
249
  log.warn(
250
250
  { file, slug, err: err instanceof Error ? err.message : String(err) },
251
- "Failed to extract journal entry"
251
+ "Failed to extract journal entry",
252
252
  );
253
253
  errors++;
254
254
  }
@@ -289,8 +289,8 @@ function parseJournalDate(filename: string): number {
289
289
 
290
290
  return new Date(
291
291
  `${year}-${month}-${day}T${String(hours).padStart(2, "0")}:${String(
292
- minutes
293
- ).padStart(2, "0")}:00`
292
+ minutes,
293
+ ).padStart(2, "0")}:00`,
294
294
  ).getTime();
295
295
  }
296
296
 
@@ -320,8 +320,8 @@ export function maybeEnqueueGraphBootstrap(): void {
320
320
  .where(
321
321
  and(
322
322
  ne(memoryGraphNodes.type, "procedural"),
323
- sql`${memoryGraphNodes.fidelity} != 'gone'`
324
- )
323
+ sql`${memoryGraphNodes.fidelity} != 'gone'`,
324
+ ),
325
325
  )
326
326
  .get()?.count ?? 0;
327
327
 
@@ -343,7 +343,7 @@ export function maybeEnqueueGraphBootstrap(): void {
343
343
 
344
344
  log.info(
345
345
  { segmentCount, hasJournalFiles },
346
- "Graph empty with historical data — enqueueing bootstrap"
346
+ "Graph empty with historical data — enqueueing bootstrap",
347
347
  );
348
348
  enqueueMemoryJob("graph_bootstrap", {});
349
349
  }
@@ -379,6 +379,11 @@ const KIND_TO_PREFIX: Record<string, string> = {
379
379
  *
380
380
  * Idempotent: uses a checkpoint to run only once. Skips items whose
381
381
  * sourceKey already exists in the graph.
382
+ *
383
+ * Uses raw SQL for the INSERT to avoid coupling to the evolving Drizzle
384
+ * schema. ORM-based inserts include every column in the schema definition,
385
+ * so adding a column in a later migration would cause this migration to
386
+ * fail with "table has no column named …" on upgrade paths.
382
387
  */
383
388
  export function migrateToolCreatedItems(): void {
384
389
  if (getMemoryCheckpoint(MIGRATE_ITEMS_CHECKPOINT)) return;
@@ -392,12 +397,15 @@ export function migrateToolCreatedItems(): void {
392
397
  `SELECT id, kind, subject, statement, confidence, importance, scope_id, first_seen_at
393
398
  FROM memory_items
394
399
  WHERE kind IN (${placeholders}) AND status = 'active'`,
395
- ...kinds
400
+ ...kinds,
396
401
  );
397
- } catch {
402
+ } catch (err) {
398
403
  // Table may not exist (fresh install) — nothing to migrate
399
- setMemoryCheckpoint(MIGRATE_ITEMS_CHECKPOINT, "done");
400
- return;
404
+ if (err instanceof Error && err.message.includes("no such table")) {
405
+ setMemoryCheckpoint(MIGRATE_ITEMS_CHECKPOINT, "done");
406
+ return;
407
+ }
408
+ throw err;
401
409
  }
402
410
 
403
411
  if (rows.length === 0) {
@@ -405,7 +413,6 @@ export function migrateToolCreatedItems(): void {
405
413
  return;
406
414
  }
407
415
 
408
- const db = getDb();
409
416
  let migrated = 0;
410
417
 
411
418
  for (const row of rows) {
@@ -413,55 +420,57 @@ export function migrateToolCreatedItems(): void {
413
420
  if (!prefix) continue;
414
421
 
415
422
  // Build content in the format the new tools expect
416
- const content =
417
- row.kind === "playbook"
418
- ? `${row.subject}\n${row.statement}`
419
- : `${row.subject}: ${row.statement}`;
423
+ const content = `${row.subject}\n${row.statement}`;
420
424
 
421
425
  // Check if already migrated (sourceKey exists in graph)
422
426
  const sourceKey = `${prefix}${row.id}`;
423
- const existing = db
424
- .select({ id: memoryGraphNodes.id })
425
- .from(memoryGraphNodes)
426
- .where(
427
- sql`${memoryGraphNodes.sourceConversations} LIKE ${
428
- "%" + sourceKey + "%"
429
- }`
430
- )
431
- .get();
427
+ const existing = rawGet<{ id: string }>(
428
+ `SELECT id FROM memory_graph_nodes WHERE source_conversations LIKE ?`,
429
+ `%${sourceKey}%`,
430
+ );
432
431
  if (existing) continue;
433
432
 
434
433
  const now = Date.now();
435
- const node: NewNode = {
434
+ const id = uuid();
435
+ const emotionalCharge = JSON.stringify({
436
+ valence: 0,
437
+ intensity: 0.1,
438
+ decayCurve: "linear",
439
+ decayRate: 0.05,
440
+ originalIntensity: 0.1,
441
+ });
442
+
443
+ rawRun(
444
+ `INSERT INTO memory_graph_nodes (
445
+ id, content, type, created, last_accessed, last_consolidated,
446
+ event_date, emotional_charge, fidelity, confidence, significance,
447
+ stability, reinforcement_count, last_reinforced,
448
+ source_conversations, source_type, narrative_role, part_of_story,
449
+ image_refs, scope_id
450
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
451
+ id,
436
452
  content,
437
- type: "semantic",
438
- created: row.first_seen_at || now,
439
- lastAccessed: now,
440
- lastConsolidated: now,
441
- eventDate: null,
442
- emotionalCharge: {
443
- valence: 0,
444
- intensity: 0.1,
445
- decayCurve: "linear",
446
- decayRate: 0.05,
447
- originalIntensity: 0.1,
448
- },
449
- fidelity: "vivid",
450
- confidence: row.confidence,
451
- significance: row.importance,
452
- stability: 14,
453
- reinforcementCount: 0,
454
- lastReinforced: now,
455
- sourceConversations: [sourceKey],
456
- sourceType: "direct",
457
- narrativeRole: null,
458
- partOfStory: null,
459
- imageRefs: null,
460
- scopeId: row.scope_id || "default",
461
- };
462
-
463
- const created = createNode(node);
464
- enqueueMemoryJob("embed_graph_node", { nodeId: created.id });
453
+ "semantic",
454
+ row.first_seen_at || now,
455
+ now,
456
+ now,
457
+ null,
458
+ emotionalCharge,
459
+ "vivid",
460
+ row.confidence,
461
+ row.importance,
462
+ 14,
463
+ 0,
464
+ now,
465
+ JSON.stringify([sourceKey]),
466
+ "direct",
467
+ null,
468
+ null,
469
+ null,
470
+ row.scope_id || "default",
471
+ );
472
+
473
+ enqueueMemoryJob("embed_graph_node", { nodeId: id });
465
474
  migrated++;
466
475
  }
467
476
 
@@ -470,7 +479,7 @@ export function migrateToolCreatedItems(): void {
470
479
  if (migrated > 0) {
471
480
  log.info(
472
481
  { migrated, total: rows.length },
473
- "Migrated tool-created items to graph nodes"
482
+ "Migrated tool-created items to graph nodes",
474
483
  );
475
484
  }
476
485
  }
@@ -506,7 +515,7 @@ export async function cleanupStaleItemVectors(): Promise<void> {
506
515
  } catch (err) {
507
516
  log.warn(
508
517
  { err: err instanceof Error ? err.message : String(err) },
509
- "Failed to clean up stale item vectors — will retry on next startup"
518
+ "Failed to clean up stale item vectors — will retry on next startup",
510
519
  );
511
520
  }
512
521
  }
@@ -2,17 +2,22 @@
2
2
  // Memory Graph — Capability seeding for skills and CLI commands
3
3
  //
4
4
  // Creates graph nodes for skill/CLI capabilities so they participate in
5
- // semantic retrieval. Mirrors the old memoryItems-based seeding in
6
- // skill-memory.ts and cli-memory.ts.
5
+ // semantic retrieval.
7
6
  // ---------------------------------------------------------------------------
8
7
 
9
- import { and, eq, like } from "drizzle-orm";
8
+ import { and, eq, like, sql } from "drizzle-orm";
10
9
 
11
10
  import { buildCliProgram } from "../../cli/program.js";
11
+ import { isAssistantFeatureFlagEnabled } from "../../config/assistant-feature-flags.js";
12
12
  import { getConfig } from "../../config/loader.js";
13
13
  import { resolveSkillStates } from "../../config/skill-state.js";
14
14
  import { loadSkillCatalog } from "../../config/skills.js";
15
15
  import {
16
+ getCachedCatalogSync,
17
+ getCatalog,
18
+ } from "../../skills/catalog-cache.js";
19
+ import {
20
+ fromCatalogSkill,
16
21
  fromSkillSummary,
17
22
  type SkillCapabilityInput,
18
23
  } from "../../skills/skill-memory.js";
@@ -24,6 +29,9 @@ import { createNode } from "./store.js";
24
29
 
25
30
  const log = getLogger("graph-capability-seed");
26
31
 
32
+ /** Default significance for capability nodes. */
33
+ const CAPABILITY_SIGNIFICANCE = 0.6;
34
+
27
35
  /** Stable prefix for capability node source tracking. */
28
36
  const SKILL_SOURCE_PREFIX = "capability:skill:";
29
37
  const CLI_SOURCE_PREFIX = "capability:cli:";
@@ -107,11 +115,40 @@ export function seedSkillGraphNodes(): void {
107
115
  seenKeys.add(`${SKILL_SOURCE_PREFIX}${summary.id}`);
108
116
  }
109
117
 
110
- // Always prune stale capability nodesthis cleans up nodes from
111
- // previously-seeded uninstalled skills that are no longer refreshed.
112
- // seenKeys already contains all locally-installed-and-enabled skills
113
- // which is sufficient to identify what should be kept.
114
- pruneStaleCapabilities(SKILL_SOURCE_PREFIX, seenKeys);
118
+ // Protect uninstalled catalog skills from pruning they are seeded
119
+ // asynchronously by seedUninstalledCatalogSkillMemories() and should
120
+ // not be marked as "gone" just because they aren't locally installed.
121
+ // When the catalog cache is cold (empty before the async fetch
122
+ // completes), we can only prune locally managed skills; full
123
+ // catalog-based pruning waits until the cache is populated.
124
+ const cachedCatalog = getCachedCatalogSync();
125
+ if (cachedCatalog.length === 0) {
126
+ // Catalog cache is cold — we can't enumerate remote catalog skills, so
127
+ // skip catalog-based pruning to avoid incorrectly marking valid
128
+ // uninstalled catalog nodes as gone. But still prune locally disabled
129
+ // skills so stale capability nodes don't linger after cold start.
130
+ log.info(
131
+ "Catalog cache is cold — pruning only locally disabled skills",
132
+ );
133
+ const disabled = resolved.filter((r) => r.state !== "enabled");
134
+ for (const { summary } of disabled) {
135
+ deleteSkillCapabilityNode(summary.id);
136
+ }
137
+ } else {
138
+ for (const entry of cachedCatalog) {
139
+ const flagKey = entry.metadata?.vellum?.["feature-flag"];
140
+ if (flagKey && !isAssistantFeatureFlagEnabled(flagKey, config))
141
+ continue;
142
+ seenKeys.add(`${SKILL_SOURCE_PREFIX}${entry.id}`);
143
+ }
144
+ pruneStaleCapabilities(SKILL_SOURCE_PREFIX, seenKeys);
145
+ }
146
+
147
+ // Clean up old-format capability nodes (skill:* and cli:*) that use the
148
+ // legacy "{prefix}:{id}\n..." content format. Mark them as gone so they
149
+ // stop appearing as duplicates. Idempotent — once cleaned, subsequent
150
+ // runs find nothing.
151
+ cleanupOldFormatCapabilityNodes();
115
152
  } catch (err) {
116
153
  log.warn({ err }, "Failed to seed skill graph nodes");
117
154
  }
@@ -137,6 +174,34 @@ export function seedCliGraphNodes(): void {
137
174
  }
138
175
  }
139
176
 
177
+ /**
178
+ * Seed capability graph nodes for catalog skills that are not yet installed.
179
+ * This makes uninstalled skills discoverable via memory injection so the LLM
180
+ * can auto-install them via skill_load when relevant.
181
+ * Best-effort: errors are logged but never thrown.
182
+ */
183
+ export async function seedUninstalledCatalogSkillMemories(): Promise<void> {
184
+ try {
185
+ const fullCatalog = await getCatalog();
186
+ if (fullCatalog.length === 0) return;
187
+
188
+ const installedCatalog = loadSkillCatalog();
189
+ const installedIds = new Set(installedCatalog.map((s) => s.id));
190
+
191
+ const config = getConfig();
192
+ for (const entry of fullCatalog) {
193
+ if (installedIds.has(entry.id)) continue;
194
+
195
+ const flagKey = entry.metadata?.vellum?.["feature-flag"];
196
+ if (flagKey && !isAssistantFeatureFlagEnabled(flagKey, config)) continue;
197
+
198
+ upsertSkillCapabilityNode(entry.id, fromCatalogSkill(entry));
199
+ }
200
+ } catch (err) {
201
+ log.warn({ err }, "Failed to seed uninstalled catalog skill memories");
202
+ }
203
+ }
204
+
140
205
  // ---------------------------------------------------------------------------
141
206
  // Internal helpers
142
207
  // ---------------------------------------------------------------------------
@@ -172,7 +237,7 @@ function upsertCapabilityNode(sourceKey: string, content: string): void {
172
237
  .where(
173
238
  and(
174
239
  eq(memoryGraphNodes.scopeId, "default"),
175
- like(memoryGraphNodes.sourceConversations, `%${sourceKey}%`),
240
+ eq(memoryGraphNodes.sourceConversations, JSON.stringify([sourceKey])),
176
241
  ),
177
242
  )
178
243
  .get();
@@ -181,9 +246,15 @@ function upsertCapabilityNode(sourceKey: string, content: string): void {
181
246
 
182
247
  if (existing) {
183
248
  if (existing.content === content && existing.fidelity !== "gone") {
184
- // Same content — just touch lastAccessed
249
+ // Same content — just touch lastAccessed (and backfill lastConsolidated
250
+ // for nodes created before the fix so they don't decay immediately,
251
+ // and backfill significance for nodes created before the raise to 0.6)
252
+ const updates: Record<string, number> = { lastAccessed: now };
253
+ if (existing.lastConsolidated === 0) updates.lastConsolidated = now;
254
+ if (existing.significance < CAPABILITY_SIGNIFICANCE)
255
+ updates.significance = CAPABILITY_SIGNIFICANCE;
185
256
  db.update(memoryGraphNodes)
186
- .set({ lastAccessed: now })
257
+ .set(updates)
187
258
  .where(eq(memoryGraphNodes.id, existing.id))
188
259
  .run();
189
260
  return;
@@ -195,6 +266,7 @@ function upsertCapabilityNode(sourceKey: string, content: string): void {
195
266
  content,
196
267
  fidelity: "vivid",
197
268
  lastAccessed: now,
269
+ ...(existing.lastConsolidated === 0 ? { lastConsolidated: now } : {}),
198
270
  })
199
271
  .where(eq(memoryGraphNodes.id, existing.id))
200
272
  .run();
@@ -208,7 +280,7 @@ function upsertCapabilityNode(sourceKey: string, content: string): void {
208
280
  type: "procedural" as const,
209
281
  created: now,
210
282
  lastAccessed: now,
211
- lastConsolidated: 0,
283
+ lastConsolidated: now,
212
284
  eventDate: null,
213
285
  emotionalCharge: {
214
286
  valence: 0,
@@ -219,7 +291,7 @@ function upsertCapabilityNode(sourceKey: string, content: string): void {
219
291
  },
220
292
  fidelity: "vivid" as const,
221
293
  confidence: 1.0,
222
- significance: 0.3,
294
+ significance: CAPABILITY_SIGNIFICANCE,
223
295
  stability: 1000, // Effectively permanent — never decays
224
296
  reinforcementCount: 0,
225
297
  lastReinforced: now,
@@ -246,7 +318,7 @@ function deleteCapabilityNode(sourceKey: string): void {
246
318
  .where(
247
319
  and(
248
320
  eq(memoryGraphNodes.scopeId, "default"),
249
- like(memoryGraphNodes.sourceConversations, `%${sourceKey}%`),
321
+ eq(memoryGraphNodes.sourceConversations, JSON.stringify([sourceKey])),
250
322
  ),
251
323
  )
252
324
  .get();
@@ -256,6 +328,79 @@ function deleteCapabilityNode(sourceKey: string): void {
256
328
  .set({ fidelity: "gone", lastAccessed: Date.now() })
257
329
  .where(eq(memoryGraphNodes.id, existing.id))
258
330
  .run();
331
+ enqueueMemoryJob("delete_qdrant_vectors", {
332
+ targetType: "graph_node",
333
+ targetId: existing.id,
334
+ });
335
+ }
336
+ }
337
+
338
+ /**
339
+ * Find and soft-delete old-format capability memory nodes (skill:* and cli:*).
340
+ *
341
+ * The legacy system stored content as "skill:{id}\n{statement}" or
342
+ * "cli:{command}\n{statement}". The current system uses prose format.
343
+ * This marks any remaining old-format nodes as gone so they no longer
344
+ * surface in retrieval.
345
+ */
346
+ function cleanupOldFormatCapabilityNodes(): void {
347
+ const db = getDb();
348
+ const now = Date.now();
349
+
350
+ // --- skill:* old-format nodes ---
351
+ const oldFormatNodes = db
352
+ .select()
353
+ .from(memoryGraphNodes)
354
+ .where(
355
+ and(
356
+ eq(memoryGraphNodes.type, "procedural"),
357
+ eq(memoryGraphNodes.scopeId, "default"),
358
+ sql`${memoryGraphNodes.fidelity} != 'gone'`,
359
+ sql`${memoryGraphNodes.content} LIKE 'skill:%'`,
360
+ ),
361
+ )
362
+ .all();
363
+
364
+ for (const node of oldFormatNodes) {
365
+ // Verify this is truly old-format: "skill:{id}\n..."
366
+ if (!/^skill:\S+\n/.test(node.content)) continue;
367
+
368
+ db.update(memoryGraphNodes)
369
+ .set({ fidelity: "gone", lastAccessed: now })
370
+ .where(eq(memoryGraphNodes.id, node.id))
371
+ .run();
372
+ enqueueMemoryJob("delete_qdrant_vectors", {
373
+ targetType: "graph_node",
374
+ targetId: node.id,
375
+ });
376
+ log.info({ nodeId: node.id }, "Cleaned up old-format skill memory node");
377
+ }
378
+
379
+ // --- cli:* old-format nodes ---
380
+ const oldCliNodes = db
381
+ .select()
382
+ .from(memoryGraphNodes)
383
+ .where(
384
+ and(
385
+ eq(memoryGraphNodes.type, "procedural"),
386
+ eq(memoryGraphNodes.scopeId, "default"),
387
+ sql`${memoryGraphNodes.fidelity} != 'gone'`,
388
+ sql`${memoryGraphNodes.content} LIKE 'cli:%'`,
389
+ ),
390
+ )
391
+ .all();
392
+
393
+ for (const node of oldCliNodes) {
394
+ if (!/^cli:\S+\n/.test(node.content)) continue;
395
+ db.update(memoryGraphNodes)
396
+ .set({ fidelity: "gone", lastAccessed: now })
397
+ .where(eq(memoryGraphNodes.id, node.id))
398
+ .run();
399
+ enqueueMemoryJob("delete_qdrant_vectors", {
400
+ targetType: "graph_node",
401
+ targetId: node.id,
402
+ });
403
+ log.info({ nodeId: node.id }, "Cleaned up old-format CLI memory node");
259
404
  }
260
405
  }
261
406
 
@@ -284,11 +429,18 @@ function pruneStaleCapabilities(prefix: string, activeKeys: Set<string>): void {
284
429
  const sources = JSON.parse(row.sourceConversations as string);
285
430
  const key = Array.isArray(sources) ? sources[0] : null;
286
431
  if (key && typeof key === "string" && !activeKeys.has(key)) {
287
- log.info({ sourceKey: key, nodeId: row.id }, "Pruning stale capability graph node");
432
+ log.info(
433
+ { sourceKey: key, nodeId: row.id },
434
+ "Pruning stale capability graph node",
435
+ );
288
436
  db.update(memoryGraphNodes)
289
437
  .set({ fidelity: "gone", lastAccessed: now })
290
438
  .where(eq(memoryGraphNodes.id, row.id))
291
439
  .run();
440
+ enqueueMemoryJob("delete_qdrant_vectors", {
441
+ targetType: "graph_node",
442
+ targetId: row.id,
443
+ });
292
444
  }
293
445
  } catch {
294
446
  // Skip malformed JSON
@@ -19,13 +19,16 @@ import {
19
19
  } from "../../providers/provider-send-message.js";
20
20
  import { BackendUnavailableError } from "../../util/errors.js";
21
21
  import { getLogger } from "../../util/logger.js";
22
+ import { getDb } from "../db.js";
22
23
  import { parseEpochMs } from "./extraction.js";
23
24
  import {
24
25
  createTrigger,
26
+ deduplicateParagraphs,
25
27
  deleteNode,
26
28
  getEdgesForNode,
27
29
  getTriggersForNode,
28
30
  queryNodes,
31
+ recordNodeEdit,
29
32
  updateNode,
30
33
  } from "./store.js";
31
34
  import type { MemoryNode } from "./types.js";
@@ -60,7 +63,11 @@ function buildConsolidationPrompt(
60
63
  ? ` eventDate=${new Date(n.eventDate).toISOString().split("T")[0]}`
61
64
  : "";
62
65
  const imageStr = n.hasImage ? " [has_image]" : "";
63
- return ` [${n.id}] type=${n.type} sig=${n.significance.toFixed(2)} fidelity=${n.fidelity} reinforced=${n.reinforcementCount}x age=${age}d${eventStr}${imageStr}\n ${n.content}`;
66
+ return ` [${n.id}] type=${n.type} sig=${n.significance.toFixed(
67
+ 2,
68
+ )} fidelity=${n.fidelity} reinforced=${
69
+ n.reinforcementCount
70
+ }x age=${age}d${eventStr}${imageStr}\n ${n.content}`;
64
71
  })
65
72
  .join("\n\n");
66
73
 
@@ -194,7 +201,9 @@ function getTopSignificanceNodes(
194
201
  scopeId,
195
202
  fidelityNot: ["gone"],
196
203
  minSignificance: 0.6,
197
- }).filter((n) => !isCapabilityNode(n)).slice(0, n);
204
+ })
205
+ .filter((n) => !isCapabilityNode(n))
206
+ .slice(0, n);
198
207
  }
199
208
 
200
209
  function getDecayedNodes(scopeId: string): MemoryNode[] {
@@ -202,7 +211,10 @@ function getDecayedNodes(scopeId: string): MemoryNode[] {
202
211
  scopeId,
203
212
  limit: 10000,
204
213
  });
205
- return all.filter((n) => (n.fidelity === "faded" || n.fidelity === "gist") && !isCapabilityNode(n));
214
+ return all.filter(
215
+ (n) =>
216
+ (n.fidelity === "faded" || n.fidelity === "gist") && !isCapabilityNode(n),
217
+ );
206
218
  }
207
219
 
208
220
  function getRandomSample(scopeId: string, n: number = 30): MemoryNode[] {
@@ -506,8 +518,30 @@ async function consolidateChunk(
506
518
 
507
519
  if (Object.keys(changes).length > 1) {
508
520
  // more than just lastConsolidated
509
- updateNode(update.id, changes);
521
+
522
+ // Wrap edit recording + node update in a transaction so they are atomic:
523
+ // if updateNode fails, the edit record is rolled back.
524
+ getDb().transaction(() => {
525
+ if (changes.content) {
526
+ const cleanContent = deduplicateParagraphs(changes.content);
527
+ const node = nodeMap.get(update.id);
528
+ if (node && node.content !== cleanContent) {
529
+ recordNodeEdit({
530
+ nodeId: update.id,
531
+ previousContent: node.content,
532
+ newContent: cleanContent,
533
+ source: "consolidation",
534
+ });
535
+ }
536
+ }
537
+
538
+ updateNode(update.id, changes);
539
+ });
510
540
  result.nodesUpdated++;
541
+ // Sync in-memory state with what updateNode actually wrote to the DB
542
+ // (updateNode deduplicates content before persisting)
543
+ if (changes.content)
544
+ changes.content = deduplicateParagraphs(changes.content);
511
545
  const node = nodeMap.get(update.id);
512
546
  if (node) Object.assign(node, changes);
513
547
  }