@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,328 @@
1
+ /**
2
+ * Tests for identity/health route handlers, focusing on profiler metadata
3
+ * in /v1/health and /v1/healthz responses.
4
+ *
5
+ * Proves:
6
+ * - Backward compatibility: health endpoints return expected shape when
7
+ * profiler mode is off (no env vars).
8
+ * - Profiler payload: when profiler env vars are set, the response includes
9
+ * a `profiler` object with the expected structure and budget state.
10
+ * - Artifact detection: when run manifests and Bun summary files exist,
11
+ * the response correctly reports artifact counts and lastCompletedRun.
12
+ */
13
+ import { existsSync, mkdirSync, rmSync, writeFileSync } from "node:fs";
14
+ import { join } from "node:path";
15
+ import { afterEach, beforeEach, describe, expect, mock, test } from "bun:test";
16
+
17
+ // Silence logger before any imports that use it
18
+ mock.module("../util/logger.js", () => ({
19
+ getLogger: () =>
20
+ new Proxy({} as Record<string, unknown>, {
21
+ get: () => () => {},
22
+ }),
23
+ }));
24
+
25
+ import { handleDetailedHealth } from "../runtime/routes/identity-routes.js";
26
+ import { getWorkspaceDir } from "../util/platform.js";
27
+
28
+ // ── Env helpers ─────────────────────────────────────────────────────────
29
+
30
+ let savedEnv: Record<string, string | undefined>;
31
+
32
+ const PROFILER_ENV_KEYS = [
33
+ "VELLUM_PROFILER_RUN_ID",
34
+ "VELLUM_PROFILER_MODE",
35
+ "VELLUM_PROFILER_MAX_BYTES",
36
+ "VELLUM_PROFILER_MAX_RUNS",
37
+ "VELLUM_PROFILER_MIN_FREE_MB",
38
+ ] as const;
39
+
40
+ function clearProfilerEnv(): void {
41
+ for (const key of PROFILER_ENV_KEYS) {
42
+ delete process.env[key];
43
+ }
44
+ }
45
+
46
+ function setProfilerEnv(
47
+ mode: string,
48
+ runId: string,
49
+ opts?: { maxBytes?: number; maxRuns?: number; minFreeMb?: number },
50
+ ): void {
51
+ process.env.VELLUM_PROFILER_RUN_ID = runId;
52
+ process.env.VELLUM_PROFILER_MODE = mode;
53
+ if (opts?.maxBytes !== undefined) {
54
+ process.env.VELLUM_PROFILER_MAX_BYTES = String(opts.maxBytes);
55
+ }
56
+ if (opts?.maxRuns !== undefined) {
57
+ process.env.VELLUM_PROFILER_MAX_RUNS = String(opts.maxRuns);
58
+ }
59
+ if (opts?.minFreeMb !== undefined) {
60
+ process.env.VELLUM_PROFILER_MIN_FREE_MB = String(opts.minFreeMb);
61
+ }
62
+ }
63
+
64
+ // ── Filesystem helpers ──────────────────────────────────────────────────
65
+
66
+ function ensureProfilerRunDir(runId: string): string {
67
+ const wsDir = getWorkspaceDir();
68
+ const runDir = join(wsDir, "data", "profiler", "runs", runId);
69
+ mkdirSync(runDir, { recursive: true });
70
+ return runDir;
71
+ }
72
+
73
+ function writeRunManifest(
74
+ runId: string,
75
+ manifest: {
76
+ status: "active" | "completed";
77
+ createdAt?: string;
78
+ updatedAt?: string;
79
+ completedAt?: string;
80
+ totalBytes?: number;
81
+ },
82
+ ): void {
83
+ const runDir = ensureProfilerRunDir(runId);
84
+ const m: Record<string, unknown> = {
85
+ runId,
86
+ status: manifest.status,
87
+ createdAt: manifest.createdAt ?? new Date().toISOString(),
88
+ updatedAt: manifest.updatedAt ?? new Date().toISOString(),
89
+ totalBytes: manifest.totalBytes ?? 0,
90
+ };
91
+ if (manifest.completedAt) {
92
+ m.completedAt = manifest.completedAt;
93
+ }
94
+ writeFileSync(join(runDir, "manifest.json"), JSON.stringify(m, null, 2));
95
+ }
96
+
97
+ function writeArtifactFile(
98
+ runId: string,
99
+ filename: string,
100
+ sizeBytes: number,
101
+ ): void {
102
+ const runDir = ensureProfilerRunDir(runId);
103
+ writeFileSync(join(runDir, filename), Buffer.alloc(sizeBytes));
104
+ }
105
+
106
+ // ── Setup / teardown ────────────────────────────────────────────────────
107
+
108
+ beforeEach(() => {
109
+ savedEnv = {};
110
+ for (const key of PROFILER_ENV_KEYS) {
111
+ savedEnv[key] = process.env[key];
112
+ }
113
+ clearProfilerEnv();
114
+
115
+ // Clean up any profiler run directories from previous tests so
116
+ // rescanRuns() doesn't pick up stale state in the shared workspace.
117
+ const profilerRunsDir = join(getWorkspaceDir(), "data", "profiler", "runs");
118
+ if (existsSync(profilerRunsDir)) {
119
+ rmSync(profilerRunsDir, { recursive: true, force: true });
120
+ }
121
+ });
122
+
123
+ afterEach(() => {
124
+ for (const [key, value] of Object.entries(savedEnv)) {
125
+ if (value === undefined) {
126
+ delete process.env[key];
127
+ } else {
128
+ process.env[key] = value;
129
+ }
130
+ }
131
+ });
132
+
133
+ // ── Tests ───────────────────────────────────────────────────────────────
134
+
135
+ describe("identity routes — health endpoint", () => {
136
+ describe("backward compatibility (profiler disabled)", () => {
137
+ test("/v1/health returns expected shape without profiler key when env vars are absent", async () => {
138
+ const res = handleDetailedHealth();
139
+ expect(res.status).toBe(200);
140
+
141
+ const body = (await res.json()) as Record<string, unknown>;
142
+ expect(body.status).toBe("healthy");
143
+ expect(body.timestamp).toBeDefined();
144
+ expect(body.version).toBeDefined();
145
+ expect(body.disk).toBeDefined();
146
+ expect(body.memory).toBeDefined();
147
+ expect(body.cpu).toBeDefined();
148
+ expect(body.migrations).toBeDefined();
149
+
150
+ // Profiler should either be absent or show enabled: false
151
+ if ("profiler" in body) {
152
+ const profiler = body.profiler as Record<string, unknown>;
153
+ expect(profiler.enabled).toBe(false);
154
+ expect(profiler.mode).toBeNull();
155
+ expect(profiler.runId).toBeNull();
156
+ expect(profiler.budget).toBeNull();
157
+ }
158
+ });
159
+
160
+ test("/v1/healthz returns the same shape as /v1/health", async () => {
161
+ // Both endpoints call handleDetailedHealth, so the shape must match
162
+ const res = handleDetailedHealth();
163
+ const body = (await res.json()) as Record<string, unknown>;
164
+
165
+ expect(body.status).toBe("healthy");
166
+ expect(body.timestamp).toBeDefined();
167
+ expect(body.migrations).toBeDefined();
168
+ });
169
+ });
170
+
171
+ describe("profiler payload (profiler enabled)", () => {
172
+ test("returns profiler object with enabled=true when env vars are set", async () => {
173
+ setProfilerEnv("cpu", "run-health-test-1", {
174
+ maxBytes: 10_000_000,
175
+ minFreeMb: 10,
176
+ });
177
+ ensureProfilerRunDir("run-health-test-1");
178
+
179
+ const res = handleDetailedHealth();
180
+ const body = (await res.json()) as Record<string, unknown>;
181
+
182
+ expect(body.profiler).toBeDefined();
183
+ const profiler = body.profiler as Record<string, unknown>;
184
+ expect(profiler.enabled).toBe(true);
185
+ expect(profiler.mode).toBe("cpu");
186
+ expect(profiler.runId).toBe("run-health-test-1");
187
+ expect(profiler.runDir).toContain("run-health-test-1");
188
+ expect(typeof profiler.totalBytes).toBe("number");
189
+ expect(typeof profiler.artifactCount).toBe("number");
190
+ });
191
+
192
+ test("includes budget block with expected fields", async () => {
193
+ setProfilerEnv("heap", "run-budget-test", {
194
+ maxBytes: 50_000_000,
195
+ minFreeMb: 100,
196
+ });
197
+ ensureProfilerRunDir("run-budget-test");
198
+
199
+ const res = handleDetailedHealth();
200
+ const body = (await res.json()) as Record<string, unknown>;
201
+ const profiler = body.profiler as Record<string, unknown>;
202
+ const budget = profiler.budget as Record<string, unknown>;
203
+
204
+ expect(budget).toBeDefined();
205
+ expect(budget.maxBytes).toBe(50_000_000);
206
+ expect(typeof budget.remainingBytes).toBe("number");
207
+ expect(budget.minFreeMb).toBe(100);
208
+ expect(typeof budget.freeMb).toBe("number");
209
+ expect(typeof budget.overBudget).toBe("boolean");
210
+ });
211
+
212
+ test("reports artifact count from .cpuprofile files", async () => {
213
+ setProfilerEnv("cpu", "run-artifact-count", {
214
+ maxBytes: 100_000_000,
215
+ minFreeMb: 0,
216
+ });
217
+ writeArtifactFile("run-artifact-count", "profile-1.cpuprofile", 1024);
218
+ writeArtifactFile("run-artifact-count", "profile-2.cpuprofile", 2048);
219
+ // Non-artifact file should not count
220
+ writeArtifactFile("run-artifact-count", "log.txt", 512);
221
+
222
+ const res = handleDetailedHealth();
223
+ const body = (await res.json()) as Record<string, unknown>;
224
+ const profiler = body.profiler as Record<string, unknown>;
225
+
226
+ expect(profiler.artifactCount).toBe(2);
227
+ });
228
+
229
+ test("detects over-budget state when total bytes exceed maxBytes", async () => {
230
+ setProfilerEnv("cpu+heap", "run-over-budget", {
231
+ maxBytes: 100, // Very small budget
232
+ minFreeMb: 0,
233
+ });
234
+ // Write a file larger than the budget
235
+ writeArtifactFile("run-over-budget", "big.cpuprofile", 5000);
236
+
237
+ const res = handleDetailedHealth();
238
+ const body = (await res.json()) as Record<string, unknown>;
239
+ const profiler = body.profiler as Record<string, unknown>;
240
+ const budget = profiler.budget as Record<string, unknown>;
241
+
242
+ expect(budget.overBudget).toBe(true);
243
+ expect(budget.remainingBytes).toBe(0);
244
+ });
245
+ });
246
+
247
+ describe("lastCompletedRun", () => {
248
+ test("returns null when no completed runs exist", async () => {
249
+ setProfilerEnv("cpu", "run-no-completed", {
250
+ maxBytes: 100_000_000,
251
+ minFreeMb: 0,
252
+ });
253
+ ensureProfilerRunDir("run-no-completed");
254
+
255
+ const res = handleDetailedHealth();
256
+ const body = (await res.json()) as Record<string, unknown>;
257
+ const profiler = body.profiler as Record<string, unknown>;
258
+
259
+ expect(profiler.lastCompletedRun).toBeNull();
260
+ });
261
+
262
+ test("returns completed run summary with artifact count and hasSummaries", async () => {
263
+ setProfilerEnv("cpu", "active-run-xyz", {
264
+ maxBytes: 100_000_000,
265
+ minFreeMb: 0,
266
+ });
267
+ ensureProfilerRunDir("active-run-xyz");
268
+
269
+ // Create a completed run with artifacts and a summary file
270
+ const completedId = "completed-run-abc";
271
+ const expectedCompletedAt = "2025-06-01T00:30:00Z";
272
+ writeRunManifest(completedId, {
273
+ status: "completed",
274
+ createdAt: "2025-06-01T00:00:00Z",
275
+ updatedAt: "2025-06-01T01:00:00Z",
276
+ completedAt: expectedCompletedAt,
277
+ totalBytes: 4096,
278
+ });
279
+ writeArtifactFile(completedId, "profile.cpuprofile", 3072);
280
+ writeArtifactFile(completedId, "summary.md", 256);
281
+
282
+ const res = handleDetailedHealth();
283
+ const body = (await res.json()) as Record<string, unknown>;
284
+ const profiler = body.profiler as Record<string, unknown>;
285
+ const last = profiler.lastCompletedRun as Record<string, unknown>;
286
+
287
+ expect(last).toBeDefined();
288
+ expect(last.runId).toBe(completedId);
289
+ expect(last.artifactCount).toBe(1); // Only .cpuprofile counts
290
+ expect(last.hasSummaries).toBe(true);
291
+ expect(typeof last.totalBytes).toBe("number");
292
+ // completedAt should reflect the manifest's completedAt value,
293
+ // not the current time or updatedAt.
294
+ expect(last.completedAt).toBe(expectedCompletedAt);
295
+ });
296
+
297
+ test("selects the most recent completed run when multiple exist", async () => {
298
+ setProfilerEnv("heap", "active-multi", {
299
+ maxBytes: 100_000_000,
300
+ maxRuns: 100,
301
+ minFreeMb: 0,
302
+ });
303
+ ensureProfilerRunDir("active-multi");
304
+
305
+ writeRunManifest("older-completed", {
306
+ status: "completed",
307
+ createdAt: "2025-01-01T00:00:00Z",
308
+ updatedAt: "2025-01-01T01:00:00Z",
309
+ });
310
+ writeArtifactFile("older-completed", "old.heapsnapshot", 512);
311
+
312
+ writeRunManifest("newer-completed", {
313
+ status: "completed",
314
+ createdAt: "2025-06-15T00:00:00Z",
315
+ updatedAt: "2025-06-15T01:00:00Z",
316
+ });
317
+ writeArtifactFile("newer-completed", "new.heapsnapshot", 1024);
318
+
319
+ const res = handleDetailedHealth();
320
+ const body = (await res.json()) as Record<string, unknown>;
321
+ const profiler = body.profiler as Record<string, unknown>;
322
+ const last = profiler.lastCompletedRun as Record<string, unknown>;
323
+
324
+ expect(last).toBeDefined();
325
+ expect(last.runId).toBe("newer-completed");
326
+ });
327
+ });
328
+ });
@@ -125,6 +125,18 @@ describe("assembleInjectionBlock", () => {
125
125
  expect(result).toContain("[skill]");
126
126
  expect(result).toContain("→ use skill_load to activate");
127
127
  });
128
+
129
+ test("assembleInjectionBlock omits skill_load suffix for CLI commands", () => {
130
+ const node = makeScoredNode({
131
+ type: "procedural",
132
+ content:
133
+ 'cli:bash\nThe "assistant bash" CLI command is available. Execute a shell command.',
134
+ });
135
+ const result = assembleInjectionBlock([node]);
136
+ expect(result).not.toContain("[skill]");
137
+ expect(result).not.toContain("skill_load to activate");
138
+ expect(result).toContain("CLI command is available");
139
+ });
128
140
  });
129
141
 
130
142
  describe("assembleContextBlock — procedural nodes", () => {
@@ -139,6 +151,18 @@ describe("assembleContextBlock — procedural nodes", () => {
139
151
  expect(result).toContain("use skill_load to activate");
140
152
  });
141
153
 
154
+ test("omits skill_load suffix for CLI commands", () => {
155
+ const node = makeScoredNode({
156
+ type: "procedural",
157
+ content:
158
+ 'cli:bash\nThe "assistant bash" CLI command is available. Execute a shell command.',
159
+ });
160
+ const result = assembleContextBlock([node]);
161
+ expect(result).toContain("### Skills You Can Use");
162
+ expect(result).not.toContain("skill_load to activate");
163
+ expect(result).toContain("CLI command is available");
164
+ });
165
+
142
166
  test("strips skill: prefix from old-format content", () => {
143
167
  const node = makeScoredNode({
144
168
  type: "procedural",
@@ -32,7 +32,7 @@ const mockInstallExternalSkill = mock(
32
32
  );
33
33
  const mockGetCatalog = mock(async () => []);
34
34
  const mockInstallSkillLocally = mock(async () => {});
35
- const mockSeedCatalogSkillMemories = mock(() => {});
35
+ const mockSeedSkillGraphNodes = mock(() => {});
36
36
  const mockEnsureSkillEntry = mock(
37
37
  (_raw: Record<string, unknown>, _id: string) => ({
38
38
  enabled: false,
@@ -115,9 +115,10 @@ mock.module("../skills/managed-store.js", () => ({
115
115
  removeSkillsIndexEntry: () => {},
116
116
  validateManagedSkillId: () => null,
117
117
  }));
118
- mock.module("../skills/skill-memory.js", () => ({
119
- deleteSkillCapabilityMemory: () => {},
120
- seedCatalogSkillMemories: mockSeedCatalogSkillMemories,
118
+ mock.module("../memory/graph/capability-seed.js", () => ({
119
+ deleteSkillCapabilityNode: () => {},
120
+ seedSkillGraphNodes: mockSeedSkillGraphNodes,
121
+ seedUninstalledCatalogSkillMemories: async () => {},
121
122
  }));
122
123
  mock.module("../util/platform.js", () => ({
123
124
  getWorkspaceSkillsDir: () => "/tmp/test-skills",
@@ -158,7 +159,7 @@ describe("installSkill routing", () => {
158
159
  mockInstallExternalSkill.mockReset();
159
160
  mockGetCatalog.mockReset();
160
161
  mockInstallSkillLocally.mockReset();
161
- mockSeedCatalogSkillMemories.mockReset();
162
+ mockSeedSkillGraphNodes.mockReset();
162
163
  mockEnsureSkillEntry.mockReset();
163
164
 
164
165
  // Defaults
@@ -167,7 +168,7 @@ describe("installSkill routing", () => {
167
168
  mockInstallExternalSkill.mockResolvedValue(undefined);
168
169
  mockGetCatalog.mockResolvedValue([]);
169
170
  mockInstallSkillLocally.mockResolvedValue(undefined);
170
- mockSeedCatalogSkillMemories.mockReturnValue(undefined);
171
+ mockSeedSkillGraphNodes.mockReturnValue(undefined);
171
172
  mockEnsureSkillEntry.mockReturnValue({ enabled: false });
172
173
  });
173
174
 
@@ -37,15 +37,15 @@ describe("claimMemoryJobs with Qdrant circuit breaker", () => {
37
37
 
38
38
  test("claims embed jobs when circuit breaker is closed (healthy)", () => {
39
39
  enqueueMemoryJob("embed_segment", { segmentId: "seg-1" });
40
- enqueueMemoryJob("embed_item", { itemId: "item-1" });
41
- enqueueMemoryJob("extract_items", { conversationId: "conv-1" });
40
+ enqueueMemoryJob("embed_graph_node", { nodeId: "node-1" });
41
+ enqueueMemoryJob("graph_extract", { conversationId: "conv-1" });
42
42
 
43
43
  const claimed = claimMemoryJobs(10);
44
44
  const types = claimed.map((j) => j.type);
45
45
 
46
46
  expect(types).toContain("embed_segment");
47
- expect(types).toContain("embed_item");
48
- expect(types).toContain("extract_items");
47
+ expect(types).toContain("embed_graph_node");
48
+ expect(types).toContain("graph_extract");
49
49
  expect(claimed).toHaveLength(3);
50
50
  });
51
51
 
@@ -62,9 +62,9 @@ describe("claimMemoryJobs with Qdrant circuit breaker", () => {
62
62
  }
63
63
 
64
64
  enqueueMemoryJob("embed_segment", { segmentId: "seg-1" });
65
- enqueueMemoryJob("embed_item", { itemId: "item-1" });
65
+ enqueueMemoryJob("embed_graph_node", { nodeId: "node-1" });
66
66
  enqueueMemoryJob("embed_summary", { summaryId: "sum-1" });
67
- enqueueMemoryJob("extract_items", { conversationId: "conv-1" });
67
+ enqueueMemoryJob("graph_extract", { conversationId: "conv-1" });
68
68
  enqueueMemoryJob("build_conversation_summary", {
69
69
  conversationId: "conv-1",
70
70
  });
@@ -73,10 +73,10 @@ describe("claimMemoryJobs with Qdrant circuit breaker", () => {
73
73
  const types = claimed.map((j) => j.type);
74
74
 
75
75
  // Only non-embed jobs should be claimed
76
- expect(types).toContain("extract_items");
76
+ expect(types).toContain("graph_extract");
77
77
  expect(types).toContain("build_conversation_summary");
78
78
  expect(types).not.toContain("embed_segment");
79
- expect(types).not.toContain("embed_item");
79
+ expect(types).not.toContain("embed_graph_node");
80
80
  expect(types).not.toContain("embed_summary");
81
81
  expect(claimed).toHaveLength(2);
82
82
  });
@@ -95,7 +95,7 @@ describe("claimMemoryJobs with Qdrant circuit breaker", () => {
95
95
 
96
96
  // Verify embed jobs are skipped while open
97
97
  enqueueMemoryJob("embed_segment", { segmentId: "seg-1" });
98
- enqueueMemoryJob("extract_items", { conversationId: "conv-1" });
98
+ enqueueMemoryJob("graph_extract", { conversationId: "conv-1" });
99
99
 
100
100
  const claimedWhileOpen = claimMemoryJobs(10);
101
101
  expect(claimedWhileOpen.map((j) => j.type)).not.toContain("embed_segment");
@@ -104,21 +104,22 @@ describe("claimMemoryJobs with Qdrant circuit breaker", () => {
104
104
  _resetQdrantBreaker();
105
105
 
106
106
  // Re-enqueue an embed job (the previous one is now "running")
107
- enqueueMemoryJob("embed_item", { itemId: "item-2" });
107
+ enqueueMemoryJob("embed_graph_node", { nodeId: "node-2" });
108
108
 
109
109
  const claimedAfterClose = claimMemoryJobs(10);
110
110
  const types = claimedAfterClose.map((j) => j.type);
111
111
 
112
- expect(types).toContain("embed_item");
112
+ expect(types).toContain("embed_graph_node");
113
113
  });
114
114
 
115
115
  test("all embed job types are skipped when breaker is open", async () => {
116
116
  const embedTypes: MemoryJobType[] = [
117
117
  "embed_segment",
118
- "embed_item",
119
118
  "embed_summary",
120
119
  "embed_media",
121
120
  "embed_attachment",
121
+ "embed_graph_node",
122
+ "graph_trigger_embed",
122
123
  ];
123
124
 
124
125
  // Trip the circuit breaker
@@ -137,13 +138,13 @@ describe("claimMemoryJobs with Qdrant circuit breaker", () => {
137
138
  enqueueMemoryJob(type, { id: `test-${type}` });
138
139
  }
139
140
  // Also enqueue a non-embed job
140
- enqueueMemoryJob("extract_entities", { conversationId: "conv-1" });
141
+ enqueueMemoryJob("graph_consolidate", { conversationId: "conv-1" });
141
142
 
142
143
  const claimed = claimMemoryJobs(20);
143
144
  const types = claimed.map((j) => j.type);
144
145
 
145
146
  // Only the non-embed job should be claimed
146
147
  expect(claimed).toHaveLength(1);
147
- expect(types).toEqual(["extract_entities"]);
148
+ expect(types).toEqual(["graph_consolidate"]);
148
149
  });
149
150
  });