@vellumai/assistant 0.8.2 → 0.8.3

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 (231) hide show
  1. package/ARCHITECTURE.md +11 -12
  2. package/docker-entrypoint.sh +13 -1
  3. package/docker-init-apt-root.sh +79 -6
  4. package/openapi.yaml +336 -21
  5. package/package.json +1 -1
  6. package/src/__tests__/agent-loop-exit-reason.test.ts +272 -0
  7. package/src/__tests__/agent-loop-provider-error-recording.test.ts +195 -0
  8. package/src/__tests__/compactor-tail-resolution.test.ts +107 -1
  9. package/src/__tests__/config-get-vision-flag.test.ts +136 -0
  10. package/src/__tests__/config-loader-backfill.test.ts +115 -18
  11. package/src/__tests__/context-token-estimator.test.ts +30 -65
  12. package/src/__tests__/conversation-agent-loop.test.ts +57 -1
  13. package/src/__tests__/conversation-media-retry.test.ts +19 -8
  14. package/src/__tests__/conversation-runtime-assembly.test.ts +26 -4
  15. package/src/__tests__/date-context.test.ts +45 -0
  16. package/src/__tests__/external-plugin-loader.test.ts +91 -19
  17. package/src/__tests__/guardian-action-no-hardcoded-copy.test.ts +0 -1
  18. package/src/__tests__/guardian-dispatch.test.ts +1 -0
  19. package/src/__tests__/heartbeat-service.test.ts +24 -164
  20. package/src/__tests__/helpers/channel-test-adapter.ts +0 -2
  21. package/src/__tests__/host-app-control-proxy.test.ts +241 -0
  22. package/src/__tests__/host-proxy-preactivation.test.ts +200 -13
  23. package/src/__tests__/injector-background-turn.test.ts +153 -0
  24. package/src/__tests__/injector-chain.test.ts +5 -0
  25. package/src/__tests__/lifecycle-memory-v2-seed.test.ts +9 -2
  26. package/src/__tests__/llm-callsite-catalog.test.ts +25 -0
  27. package/src/__tests__/llm-catalog-parity.test.ts +3 -0
  28. package/src/__tests__/llm-request-log-agent-loop-exit-reason.test.ts +116 -0
  29. package/src/__tests__/llm-request-log-error-payload.test.ts +138 -0
  30. package/src/__tests__/llm-request-log-source-clickhouse.test.ts +2 -0
  31. package/src/__tests__/llm-resolver.test.ts +255 -2
  32. package/src/__tests__/managed-profile-guard.test.ts +10 -0
  33. package/src/__tests__/notification-decision-fallback.test.ts +0 -91
  34. package/src/__tests__/notification-decision-strategy.test.ts +14 -31
  35. package/src/__tests__/notification-deep-link.test.ts +15 -0
  36. package/src/__tests__/notification-guardian-path.test.ts +1 -2
  37. package/src/__tests__/notification-platform-adapter.test.ts +5 -4
  38. package/src/__tests__/notification-telegram-adapter.test.ts +1 -0
  39. package/src/__tests__/notification-vellum-adapter.test.ts +113 -0
  40. package/src/__tests__/openai-provider.test.ts +218 -3
  41. package/src/__tests__/openai-responses-cutover-guard.test.ts +3 -3
  42. package/src/__tests__/openrouter-provider-only.test.ts +51 -3
  43. package/src/__tests__/openrouter-token-estimation.test.ts +34 -25
  44. package/src/__tests__/platform-proxy-context.test.ts +6 -1
  45. package/src/__tests__/plugin-tool-contribution.test.ts +3 -3
  46. package/src/__tests__/plugin-types.test.ts +2 -2
  47. package/src/__tests__/provider-catalog-visibility.test.ts +16 -0
  48. package/src/__tests__/provider-platform-proxy-integration.test.ts +27 -25
  49. package/src/__tests__/secret-routes-platform-proxy.test.ts +1 -1
  50. package/src/__tests__/system-prompt.test.ts +6 -73
  51. package/src/__tests__/workspace-migration-087-memory-router-balanced-profile.test.ts +228 -0
  52. package/src/a2a/__tests__/agent-card.test.ts +98 -0
  53. package/src/a2a/__tests__/e2e-a2a-channel.test.ts +597 -0
  54. package/src/a2a/__tests__/protocol-helpers.test.ts +113 -0
  55. package/src/a2a/__tests__/task-store.test.ts +246 -0
  56. package/src/a2a/agent-card.ts +58 -0
  57. package/src/a2a/feature-gate.ts +8 -0
  58. package/src/a2a/protocol-constants.ts +21 -0
  59. package/src/a2a/protocol-errors.ts +50 -0
  60. package/src/a2a/protocol-types.ts +162 -0
  61. package/src/a2a/task-store.ts +168 -0
  62. package/src/agent/loop.ts +167 -18
  63. package/src/channels/config.ts +9 -0
  64. package/src/channels/types.ts +14 -0
  65. package/src/cli/{__tests__ → commands/__tests__}/notifications.test.ts +201 -28
  66. package/src/cli/commands/__tests__/schedules.test.ts +469 -0
  67. package/src/cli/commands/notifications.ts +65 -35
  68. package/src/cli/commands/plugins.ts +67 -0
  69. package/src/cli/commands/schedules.ts +297 -5
  70. package/src/cli/lib/__tests__/search-plugins.test.ts +261 -0
  71. package/src/cli/lib/install-from-github.ts +8 -9
  72. package/src/cli/lib/search-plugins.ts +163 -0
  73. package/src/cli/program.ts +14 -0
  74. package/src/config/assistant-feature-flags.ts +24 -54
  75. package/src/config/bundled-skills/app-builder/SKILL.md +117 -1
  76. package/src/config/bundled-skills/phone-calls/SKILL.md +1 -1
  77. package/src/config/call-site-defaults.ts +105 -0
  78. package/src/config/feature-flag-registry.json +21 -29
  79. package/src/config/llm-resolver.ts +52 -1
  80. package/src/config/schema.ts +2 -0
  81. package/src/config/schemas/__tests__/memory-v2.test.ts +3 -3
  82. package/src/config/schemas/channels.ts +9 -0
  83. package/src/config/schemas/conversations.ts +10 -0
  84. package/src/config/schemas/heartbeat.ts +14 -0
  85. package/src/config/schemas/llm.ts +1 -3
  86. package/src/config/schemas/memory-retrospective.ts +1 -1
  87. package/src/config/schemas/memory-v2.ts +4 -4
  88. package/src/config/schemas/memory.ts +3 -1
  89. package/src/config/seed-inference-profiles.ts +99 -29
  90. package/src/context/compactor.ts +72 -12
  91. package/src/context/token-estimator.ts +32 -34
  92. package/src/daemon/__tests__/conversation-lifecycle-auto-analyze.test.ts +3 -22
  93. package/src/daemon/conversation-agent-loop-handlers.ts +78 -0
  94. package/src/daemon/conversation-agent-loop.ts +29 -2
  95. package/src/daemon/conversation-runtime-assembly.ts +9 -0
  96. package/src/daemon/conversation.ts +0 -7
  97. package/src/daemon/date-context.ts +40 -0
  98. package/src/daemon/guardian-action-generators.ts +1 -125
  99. package/src/daemon/handlers/__tests__/config-a2a-complete.test.ts +248 -0
  100. package/src/daemon/handlers/__tests__/config-a2a-invite.test.ts +154 -0
  101. package/src/daemon/handlers/__tests__/config-a2a-redeem.test.ts +133 -0
  102. package/src/daemon/handlers/__tests__/config-a2a.test.ts +95 -0
  103. package/src/daemon/handlers/config-a2a.ts +289 -0
  104. package/src/daemon/handlers/conversations.ts +1 -0
  105. package/src/daemon/host-app-control-proxy.ts +69 -18
  106. package/src/daemon/host-proxy-preactivation.ts +85 -18
  107. package/src/daemon/lifecycle.ts +49 -61
  108. package/src/daemon/memory-v2-startup.ts +49 -13
  109. package/src/daemon/message-types/notifications.ts +21 -0
  110. package/src/daemon/pkb-reminder-builder.test.ts +10 -53
  111. package/src/daemon/pkb-reminder-builder.ts +4 -19
  112. package/src/daemon/process-message.ts +3 -0
  113. package/src/daemon/skill-memory-refresh.ts +5 -1
  114. package/src/daemon/wake-target-adapter.ts +2 -0
  115. package/src/export/__tests__/transcript-formatter.test.ts +121 -0
  116. package/src/export/transcript-formatter.ts +54 -20
  117. package/src/heartbeat/__tests__/heartbeat-service.test.ts +44 -0
  118. package/src/heartbeat/heartbeat-service.ts +34 -191
  119. package/src/home/__tests__/feed-types.test.ts +40 -0
  120. package/src/home/feed-types.ts +14 -2
  121. package/src/ipc/cli-client.ts +147 -45
  122. package/src/memory/__tests__/conversation-queries.test.ts +220 -0
  123. package/src/memory/__tests__/memory-retrospective-enqueue.test.ts +2 -50
  124. package/src/memory/__tests__/memory-retrospective-job.test.ts +87 -4
  125. package/src/memory/conversation-queries.ts +87 -1
  126. package/src/memory/conversation-title-service.ts +26 -4
  127. package/src/memory/db-init.ts +6 -0
  128. package/src/memory/graph/__tests__/conversation-graph-memory-v2-routing.test.ts +84 -3
  129. package/src/memory/graph/conversation-graph-memory.ts +18 -6
  130. package/src/memory/graph/tools.ts +6 -37
  131. package/src/memory/invite-store.ts +53 -0
  132. package/src/memory/llm-request-log-source-clickhouse.ts +7 -2
  133. package/src/memory/llm-request-log-store.ts +92 -1
  134. package/src/memory/memory-retrospective-enqueue.ts +1 -20
  135. package/src/memory/memory-retrospective-job.ts +33 -6
  136. package/src/memory/migrations/250-provider-connection-base-url-and-models.ts +28 -0
  137. package/src/memory/migrations/251-a2a-tasks.ts +49 -0
  138. package/src/memory/migrations/252-llm-request-log-agent-loop-exit-reason.ts +32 -0
  139. package/src/memory/migrations/index.ts +3 -0
  140. package/src/memory/migrations/registry.ts +8 -0
  141. package/src/memory/schema/a2a.ts +15 -0
  142. package/src/memory/schema/index.ts +1 -0
  143. package/src/memory/schema/inference.ts +2 -0
  144. package/src/memory/schema/infrastructure.ts +1 -0
  145. package/src/memory/v2/__tests__/activation-store.test.ts +25 -23
  146. package/src/memory/v2/__tests__/cli-command-store.test.ts +404 -0
  147. package/src/memory/v2/__tests__/frontmatter-sweep.test.ts +25 -4
  148. package/src/memory/v2/__tests__/injection.test.ts +190 -3
  149. package/src/memory/v2/__tests__/static-context.test.ts +12 -1
  150. package/src/memory/v2/activation-store.ts +14 -16
  151. package/src/memory/v2/cli-command-content.ts +19 -0
  152. package/src/memory/v2/cli-command-store.ts +304 -0
  153. package/src/memory/v2/frontmatter-sweep.ts +7 -1
  154. package/src/memory/v2/injection.ts +49 -20
  155. package/src/memory/v2/page-index.ts +38 -13
  156. package/src/memory/v2/static-context.ts +4 -4
  157. package/src/memory/v2/types.ts +23 -0
  158. package/src/messaging/providers/a2a/__tests__/deliver.test.ts +274 -0
  159. package/src/messaging/providers/a2a/deliver.ts +156 -0
  160. package/src/messaging/providers/gmail/client.ts +9 -2
  161. package/src/messaging/providers/index.ts +11 -2
  162. package/src/notifications/__tests__/broadcaster.test.ts +203 -0
  163. package/src/notifications/__tests__/decision-engine.test.ts +283 -0
  164. package/src/notifications/__tests__/deterministic-checks.test.ts +286 -0
  165. package/src/notifications/__tests__/emit-signal-home-feed.test.ts +1 -0
  166. package/src/notifications/__tests__/home-feed-side-effect.test.ts +430 -7
  167. package/src/notifications/adapters/macos.ts +12 -2
  168. package/src/notifications/broadcaster.ts +29 -4
  169. package/src/notifications/copy-composer.ts +17 -64
  170. package/src/notifications/decision-engine.ts +111 -44
  171. package/src/notifications/deterministic-checks.ts +96 -0
  172. package/src/notifications/emit-signal.ts +1 -0
  173. package/src/notifications/home-feed-side-effect.ts +85 -6
  174. package/src/notifications/signal.ts +0 -4
  175. package/src/notifications/types.ts +8 -0
  176. package/src/oauth/platform-connection.test.ts +43 -3
  177. package/src/oauth/platform-connection.ts +13 -4
  178. package/src/plugins/defaults/injectors.ts +38 -19
  179. package/src/plugins/external-plugin-loader.ts +82 -10
  180. package/src/plugins/types.ts +16 -7
  181. package/src/prompts/__tests__/system-prompt.test.ts +6 -51
  182. package/src/prompts/__tests__/task-progress-hint-section.test.ts +4 -8
  183. package/src/prompts/system-prompt.ts +0 -8
  184. package/src/prompts/templates/BOOTSTRAP.md +5 -5
  185. package/src/prompts/templates/system-sections.ts +0 -9
  186. package/src/providers/__tests__/inference.test.ts +2 -0
  187. package/src/providers/call-site-routing.ts +24 -6
  188. package/src/providers/connection-resolution.ts +63 -13
  189. package/src/providers/inference/__tests__/adapter-factory-openai-compatible.test.ts +74 -0
  190. package/src/providers/inference/__tests__/connections-openai-compatible.test.ts +175 -0
  191. package/src/providers/inference/__tests__/connections-status-label.test.ts +15 -0
  192. package/src/providers/inference/adapter-factory.ts +9 -20
  193. package/src/providers/inference/auth.ts +12 -0
  194. package/src/providers/inference/backfill.ts +14 -1
  195. package/src/providers/inference/connections.ts +85 -5
  196. package/src/providers/inference/resolve-auth.ts +2 -0
  197. package/src/providers/model-catalog.ts +199 -244
  198. package/src/providers/model-intents.ts +3 -3
  199. package/src/providers/openai/__tests__/chat-completions-provider-reasoning.test.ts +235 -0
  200. package/src/providers/openai/chat-completions-provider.ts +159 -6
  201. package/src/providers/openrouter/client.ts +42 -4
  202. package/src/providers/platform-proxy/constants.ts +3 -4
  203. package/src/providers/provider-catalog-visibility.ts +3 -1
  204. package/src/providers/provider-send-message.ts +27 -12
  205. package/src/providers/registry.ts +30 -1
  206. package/src/runtime/agent-wake.ts +61 -1
  207. package/src/runtime/auth/route-policy.ts +13 -0
  208. package/src/runtime/http-server.ts +7 -16
  209. package/src/runtime/http-types.ts +0 -47
  210. package/src/runtime/routes/__tests__/consolidation-routes.test.ts +258 -0
  211. package/src/runtime/routes/__tests__/conversation-query-routes.test.ts +66 -4
  212. package/src/runtime/routes/__tests__/inference-provider-connection-routes.test.ts +275 -44
  213. package/src/runtime/routes/__tests__/llm-call-sites-routes.test.ts +12 -0
  214. package/src/runtime/routes/channel-availability-routes.ts +5 -0
  215. package/src/runtime/routes/consolidation-routes.ts +100 -0
  216. package/src/runtime/routes/conversation-query-routes.ts +70 -11
  217. package/src/runtime/routes/conversation-routes.ts +7 -0
  218. package/src/runtime/routes/index.ts +2 -0
  219. package/src/runtime/routes/inference-provider-connection-routes.ts +134 -1
  220. package/src/runtime/routes/integrations/a2a.ts +235 -0
  221. package/src/runtime/routes/llm-call-sites-routes.ts +11 -1
  222. package/src/runtime/routes/subagents-routes.ts +41 -0
  223. package/src/subagent/manager.ts +2 -0
  224. package/src/tools/memory/register.ts +1 -9
  225. package/src/tools/registry.ts +2 -2
  226. package/src/tools/types.ts +37 -2
  227. package/src/workspace/migrations/087-memory-router-balanced-profile.ts +91 -0
  228. package/src/workspace/migrations/registry.ts +2 -0
  229. package/src/__tests__/guardian-action-conversation-turn.test.ts +0 -441
  230. package/src/memory/graph/__tests__/remember-description.test.ts +0 -55
  231. package/src/runtime/guardian-action-conversation-turn.ts +0 -99
@@ -4,6 +4,7 @@ import { UiConfigSchema } from "../config/schemas/platform.js";
4
4
  import {
5
5
  canonicalizeTimeZone,
6
6
  extractUserTimeZoneFromRecall,
7
+ formatLocalTimestamp,
7
8
  formatTurnTimestamp,
8
9
  resolveTurnTimezoneContext,
9
10
  } from "../daemon/date-context.js";
@@ -321,3 +322,47 @@ describe("formatTurnTimestamp", () => {
321
322
  expect(result).not.toContain("24:");
322
323
  });
323
324
  });
325
+
326
+ // ---------------------------------------------------------------------------
327
+ // formatLocalTimestamp
328
+ // ---------------------------------------------------------------------------
329
+
330
+ describe("formatLocalTimestamp", () => {
331
+ // 2026-05-11T17:30:00Z is 10:30 PDT (UTC-7, DST in effect).
332
+ const utcMidday = Date.parse("2026-05-11T17:30:00Z");
333
+
334
+ test("defaults to UTC when no timeZone is provided", () => {
335
+ expect(formatLocalTimestamp(utcMidday)).toBe("2026-05-11 17:30:00");
336
+ });
337
+
338
+ test("explicit UTC matches the no-timezone default", () => {
339
+ expect(formatLocalTimestamp(utcMidday, "UTC")).toBe("2026-05-11 17:30:00");
340
+ });
341
+
342
+ test("renders in America/Los_Angeles when configured", () => {
343
+ expect(formatLocalTimestamp(utcMidday, "America/Los_Angeles")).toBe(
344
+ "2026-05-11 10:30:00",
345
+ );
346
+ });
347
+
348
+ test("renders in Asia/Tokyo when configured", () => {
349
+ // UTC+9 — 17:30 UTC is 02:30 next-day local.
350
+ expect(formatLocalTimestamp(utcMidday, "Asia/Tokyo")).toBe(
351
+ "2026-05-12 02:30:00",
352
+ );
353
+ });
354
+
355
+ test("handles a DST spring-forward boundary correctly", () => {
356
+ // 2026-03-08 06:30:00Z is 01:30 EST. The 02:00 → 03:00 spring-forward
357
+ // in America/New_York happens at 07:00 UTC. 07:30 UTC is 03:30 EDT.
358
+ const beforeJump = Date.parse("2026-03-08T06:30:00Z");
359
+ const afterJump = Date.parse("2026-03-08T07:30:00Z");
360
+
361
+ expect(formatLocalTimestamp(beforeJump, "America/New_York")).toBe(
362
+ "2026-03-08 01:30:00",
363
+ );
364
+ expect(formatLocalTimestamp(afterJump, "America/New_York")).toBe(
365
+ "2026-03-08 03:30:00",
366
+ );
367
+ });
368
+ });
@@ -223,9 +223,9 @@ describe("loadExternalPlugin — hooks", () => {
223
223
  );
224
224
  expect(typeof registered?.hooks?.init).toBe("function");
225
225
  await registered?.hooks?.init?.({} as never);
226
- expect(
227
- (globalThis as Record<string, unknown>).__externalInitCalled,
228
- ).toBe(true);
226
+ expect((globalThis as Record<string, unknown>).__externalInitCalled).toBe(
227
+ true,
228
+ );
229
229
  delete (globalThis as Record<string, unknown>).__externalInitCalled;
230
230
  });
231
231
 
@@ -290,9 +290,9 @@ describe("loadExternalPlugin — hooks", () => {
290
290
  expect(Object.keys(registered?.hooks ?? {})).toEqual(["init"]);
291
291
  expect(typeof registered?.hooks?.init).toBe("function");
292
292
  await registered?.hooks?.init?.({} as never);
293
- expect((globalThis as Record<string, unknown>).__externalDtsInitCalled).toBe(
294
- true,
295
- );
293
+ expect(
294
+ (globalThis as Record<string, unknown>).__externalDtsInitCalled,
295
+ ).toBe(true);
296
296
  delete (globalThis as Record<string, unknown>).__externalDtsInitCalled;
297
297
  });
298
298
 
@@ -345,7 +345,6 @@ describe("loadExternalPlugin — tools", () => {
345
345
  dir,
346
346
  "tools/alpha.ts",
347
347
  `export default {
348
- name: "two_tools_alpha",
349
348
  description: "alpha",
350
349
  defaultRiskLevel: "low" as const,
351
350
  input_schema: { type: "object", properties: {}, required: [] },
@@ -357,7 +356,6 @@ describe("loadExternalPlugin — tools", () => {
357
356
  dir,
358
357
  "tools/beta.ts",
359
358
  `export default {
360
- name: "two_tools_beta",
361
359
  description: "beta",
362
360
  defaultRiskLevel: "low" as const,
363
361
  input_schema: { type: "object", properties: {}, required: [] },
@@ -374,7 +372,7 @@ describe("loadExternalPlugin — tools", () => {
374
372
  const names = (registered?.tools ?? []).map(
375
373
  (t) => (t as { name: string }).name,
376
374
  );
377
- expect(names).toEqual(["two_tools_alpha", "two_tools_beta"]);
375
+ expect(names).toEqual(["alpha", "beta"]);
378
376
  });
379
377
 
380
378
  test("plugin.tools is undefined when tools/ is absent", async () => {
@@ -403,18 +401,95 @@ describe("loadExternalPlugin — tools", () => {
403
401
  expect(registeredNames()).toHaveLength(0);
404
402
  });
405
403
 
406
- test("a tool default export missing string name is logged and skipped", async () => {
407
- const dir = freshPluginDir("tool-no-name");
408
- writePackageJson(dir, { name: "tool-no-name", version: "0.1.0" });
404
+ test("a tool default export with missing fields loads with documented defaults", async () => {
405
+ const dir = freshPluginDir("tool-with-defaults");
406
+ writePackageJson(dir, { name: "tool-with-defaults", version: "0.1.0" });
407
+ // The default export is a bare empty object — no description,
408
+ // defaultRiskLevel, input_schema, or execute. The loader must fill
409
+ // each slot with its documented default and still register the plugin.
410
+ writeSurfaceFile(dir, "tools/empty.ts", `export default {};\n`);
411
+
412
+ await loadExternalPlugin(dir);
413
+
414
+ const registered = getRegisteredPlugins().find(
415
+ (p) => p.manifest.name === "tool-with-defaults",
416
+ );
417
+ expect(registered).toBeDefined();
418
+ const tools = (registered?.tools ?? []) as Array<{
419
+ name: string;
420
+ description: string;
421
+ defaultRiskLevel: string;
422
+ input_schema: Record<string, unknown>;
423
+ execute: (
424
+ input: Record<string, unknown>,
425
+ context: unknown,
426
+ ) => Promise<{ content: string; isError: boolean }>;
427
+ }>;
428
+ expect(tools).toHaveLength(1);
429
+ const empty = tools[0]!;
430
+ expect(empty.name).toBe("empty");
431
+ expect(empty.description).toBe("");
432
+ expect(empty.defaultRiskLevel).toBe("medium");
433
+ expect(empty.input_schema).toEqual({
434
+ type: "object",
435
+ properties: {},
436
+ additionalProperties: false,
437
+ });
438
+ expect(typeof empty.execute).toBe("function");
439
+
440
+ const result = await empty.execute({}, {} as unknown);
441
+ expect(result.isError).toBe(true);
442
+ expect(result.content).toContain("empty");
443
+ expect(result.content).toContain("no execute implementation");
444
+ });
445
+
446
+ test("a partial tool default export merges author fields with defaults", async () => {
447
+ const dir = freshPluginDir("tool-partial-defaults");
448
+ writePackageJson(dir, {
449
+ name: "tool-partial-defaults",
450
+ version: "0.1.0",
451
+ });
452
+ // Author supplies only description + execute; the loader must default
453
+ // defaultRiskLevel and input_schema while keeping the author's fields.
409
454
  writeSurfaceFile(
410
455
  dir,
411
- "tools/nameless.ts",
412
- `export default { description: "missing name" };\n`,
456
+ "tools/partial.ts",
457
+ `export default {
458
+ description: "custom description",
459
+ async execute() {
460
+ return { content: "ran", isError: false };
461
+ },
462
+ };
463
+ `,
413
464
  );
414
465
 
415
466
  await loadExternalPlugin(dir);
416
467
 
417
- expect(registeredNames()).toHaveLength(0);
468
+ const registered = getRegisteredPlugins().find(
469
+ (p) => p.manifest.name === "tool-partial-defaults",
470
+ );
471
+ const tool = (registered?.tools ?? [])[0] as
472
+ | {
473
+ description: string;
474
+ defaultRiskLevel: string;
475
+ input_schema: object;
476
+ execute: (
477
+ input: Record<string, unknown>,
478
+ context: unknown,
479
+ ) => Promise<{ content: string; isError: boolean }>;
480
+ }
481
+ | undefined;
482
+ expect(tool).toBeDefined();
483
+ expect(tool?.description).toBe("custom description");
484
+ expect(tool?.defaultRiskLevel).toBe("medium");
485
+ expect(tool?.input_schema).toEqual({
486
+ type: "object",
487
+ properties: {},
488
+ additionalProperties: false,
489
+ });
490
+ const result = await tool!.execute({}, {} as unknown);
491
+ expect(result.isError).toBe(false);
492
+ expect(result.content).toBe("ran");
418
493
  });
419
494
  });
420
495
 
@@ -474,9 +549,6 @@ describe("loadExternalPlugin — end-to-end @vellumai/simple-memory", () => {
474
549
  const toolNames = (registered?.tools ?? [])
475
550
  .map((t) => (t as { name: string }).name)
476
551
  .sort();
477
- expect(toolNames).toEqual([
478
- "simple_memory_recall",
479
- "simple_memory_remember",
480
- ]);
552
+ expect(toolNames).toEqual(["recall", "remember"]);
481
553
  });
482
554
  });
@@ -20,7 +20,6 @@ const SCANNED_FILES = [
20
20
  "calls/call-controller.ts",
21
21
  "calls/guardian-action-sweep.ts",
22
22
  "runtime/routes/inbound-message-handler.ts",
23
- "runtime/guardian-action-conversation-turn.ts",
24
23
  "daemon/conversation-process.ts",
25
24
  ];
26
25
 
@@ -295,6 +295,7 @@ describe("guardian-dispatch", () => {
295
295
  conversationId: "conv-from-thread-created",
296
296
  title: "Guardian alert",
297
297
  sourceEventName: "guardian.question",
298
+ silent: false,
298
299
  };
299
300
  mockEmitResult = {
300
301
  signalId: "sig-4",
@@ -43,6 +43,7 @@ let mockConfig = {
43
43
  timezone: null as string | null,
44
44
  activeHoursStart: undefined as number | undefined,
45
45
  activeHoursEnd: undefined as number | undefined,
46
+ disposition: "Default disposition text mentioning notifications skill.",
46
47
  },
47
48
  };
48
49
 
@@ -386,6 +387,7 @@ describe("HeartbeatService", () => {
386
387
  timezone: null,
387
388
  activeHoursStart: undefined,
388
389
  activeHoursEnd: undefined,
390
+ disposition: "Default disposition text mentioning notifications skill.",
389
391
  },
390
392
  };
391
393
  });
@@ -418,8 +420,7 @@ describe("HeartbeatService", () => {
418
420
  expect(processMessageCalls[0].conversationId).toBe("conv-1");
419
421
  expect(processMessageCalls[0].content).toContain("<heartbeat-checklist>");
420
422
  expect(processMessageCalls[0].content).toContain("<heartbeat-disposition>");
421
- expect(processMessageCalls[0].content).toContain("HEARTBEAT_OK");
422
- expect(processMessageCalls[0].content).toContain("HEARTBEAT_ALERT");
423
+ expect(processMessageCalls[0].content).toContain("notifications skill");
423
424
  });
424
425
 
425
426
  test("HEARTBEAT.md content is embedded in prompt when file exists", async () => {
@@ -750,30 +751,13 @@ describe("HeartbeatService", () => {
750
751
  });
751
752
  });
752
753
 
753
- test("HEARTBEAT_ALERT emits a notification signal and surfaces the conversation", async () => {
754
+ test("conversation surfaces to the sidebar on every successful run", async () => {
754
755
  const conversationCreatedCalls: Array<{
755
756
  conversationId: string;
756
757
  title: string;
757
758
  }> = [];
758
759
  const service = createService({
759
760
  onConversationCreated: (info) => conversationCreatedCalls.push(info),
760
- processMessage: async (...args: unknown[]) => {
761
- const conversationId = args[0] as string;
762
- mockStoredMessages.push({
763
- id: "assistant-alert-1",
764
- conversationId,
765
- role: "assistant",
766
- content: JSON.stringify([
767
- {
768
- type: "text",
769
- text: "The first heartbeat found a concrete follow-up for the guardian.\nHEARTBEAT_ALERT",
770
- },
771
- ]),
772
- createdAt: Date.now(),
773
- metadata: null,
774
- });
775
- return { messageId: "user-heartbeat-1" };
776
- },
777
761
  });
778
762
 
779
763
  await service.runOnce();
@@ -782,113 +766,6 @@ describe("HeartbeatService", () => {
782
766
  expect(conversationCreatedCalls).toEqual([
783
767
  { conversationId: "conv-1", title: "Heartbeat" },
784
768
  ]);
785
- expect(emittedNotificationSignals).toHaveLength(1);
786
- expect(emittedNotificationSignals[0]).toMatchObject({
787
- sourceEventName: "heartbeat.alert",
788
- sourceChannel: "watcher",
789
- sourceContextId: "mock-run-id",
790
- dedupeKey: "heartbeat:alert:mock-run-id",
791
- attentionHints: {
792
- requiresAction: true,
793
- urgency: "medium",
794
- isAsyncBackground: true,
795
- visibleInSourceNow: false,
796
- },
797
- conversationAffinityHint: { vellum: "conv-1" },
798
- conversationMetadata: {
799
- source: "heartbeat",
800
- groupId: "system:background",
801
- },
802
- });
803
- expect(emittedNotificationSignals[0].contextPayload.summary).toBe(
804
- "The first heartbeat found a concrete follow-up for the guardian.",
805
- );
806
- expect(emittedNotificationSignals[0].contextPayload.messageId).toBe(
807
- "assistant-alert-1",
808
- );
809
- expect(
810
- emittedNotificationSignals[0].contextPayload.sourceInterface,
811
- ).toBeUndefined();
812
- });
813
-
814
- test("HEARTBEAT_OK stays silent", async () => {
815
- const conversationCreatedCalls: Array<{
816
- conversationId: string;
817
- title: string;
818
- }> = [];
819
- const service = createService({
820
- onConversationCreated: (info) => conversationCreatedCalls.push(info),
821
- processMessage: async (...args: unknown[]) => {
822
- const conversationId = args[0] as string;
823
- mockStoredMessages.push({
824
- id: "assistant-ok-1",
825
- conversationId,
826
- role: "assistant",
827
- content: JSON.stringify([
828
- {
829
- type: "text",
830
- text: "Everything looks good.\nHEARTBEAT_OK",
831
- },
832
- ]),
833
- createdAt: Date.now(),
834
- metadata: null,
835
- });
836
- return { messageId: "user-heartbeat-1" };
837
- },
838
- });
839
-
840
- await service.runOnce();
841
- await new Promise((resolve) => setTimeout(resolve, 0));
842
-
843
- // The conversation surfaces to the sidebar via the runner's bootstrap
844
- // callback for *every* heartbeat — "silent OK" means no notification
845
- // signal is emitted, not that the conversation is hidden.
846
- expect(conversationCreatedCalls).toHaveLength(1);
847
- expect(emittedNotificationSignals).toHaveLength(0);
848
- });
849
-
850
- test("HEARTBEAT_OK stays silent when earlier content mentions HEARTBEAT_ALERT", async () => {
851
- const conversationCreatedCalls: Array<{
852
- conversationId: string;
853
- title: string;
854
- }> = [];
855
- const service = createService({
856
- onConversationCreated: (info) => conversationCreatedCalls.push(info),
857
- processMessage: async (...args: unknown[]) => {
858
- const conversationId = args[0] as string;
859
- mockStoredMessages.push({
860
- id: "assistant-ok-2",
861
- conversationId,
862
- role: "assistant",
863
- content: JSON.stringify([
864
- {
865
- type: "thinking",
866
- thinking:
867
- "I should decide between HEARTBEAT_ALERT and HEARTBEAT_OK.",
868
- },
869
- {
870
- type: "tool_result",
871
- content: "Tool output mentions HEARTBEAT_ALERT.",
872
- },
873
- {
874
- type: "text",
875
- text: "I considered HEARTBEAT_ALERT, but there is nothing useful to surface.\nHEARTBEAT_OK",
876
- },
877
- ]),
878
- createdAt: Date.now(),
879
- metadata: null,
880
- });
881
- return { messageId: "user-heartbeat-1" };
882
- },
883
- });
884
-
885
- await service.runOnce();
886
- await new Promise((resolve) => setTimeout(resolve, 0));
887
-
888
- // Conversation surfaces via the runner bootstrap, but no notification
889
- // is emitted since the disposition is OK.
890
- expect(conversationCreatedCalls).toHaveLength(1);
891
- expect(emittedNotificationSignals).toHaveLength(0);
892
769
  });
893
770
 
894
771
  test("end-to-end: llm.callSites.heartbeatAgent.speed resolves to 'fast'", async () => {
@@ -1524,43 +1401,6 @@ describe("HeartbeatService", () => {
1524
1401
  });
1525
1402
  });
1526
1403
 
1527
- test("CAS false suppresses success surfacing", async () => {
1528
- mockCompleteHeartbeatRun.mockImplementation(() => false);
1529
-
1530
- const conversationCreatedCalls: Array<{
1531
- conversationId: string;
1532
- title: string;
1533
- }> = [];
1534
- const service = createService({
1535
- onConversationCreated: (info) => conversationCreatedCalls.push(info),
1536
- processMessage: async (...args: unknown[]) => {
1537
- const conversationId = args[0] as string;
1538
- mockStoredMessages.push({
1539
- id: "assistant-alert-1",
1540
- conversationId,
1541
- role: "assistant",
1542
- content: JSON.stringify([
1543
- {
1544
- type: "text",
1545
- text: "Something worth surfacing.\nHEARTBEAT_ALERT",
1546
- },
1547
- ]),
1548
- createdAt: Date.now(),
1549
- metadata: null,
1550
- });
1551
- return { messageId: "msg-1" };
1552
- },
1553
- });
1554
- await service.runOnce();
1555
- await new Promise((resolve) => setTimeout(resolve, 0));
1556
-
1557
- // The bootstrap-time surface fires regardless of CAS (it happens
1558
- // before completeHeartbeatRun). CAS-false suppresses the alert
1559
- // notification emit but not the sidebar entry.
1560
- expect(conversationCreatedCalls).toHaveLength(1);
1561
- expect(emittedNotificationSignals).toHaveLength(0);
1562
- });
1563
-
1564
1404
  test("CAS false suppresses failure alerter and feed event", async () => {
1565
1405
  mockCompleteHeartbeatRun.mockImplementation(() => false);
1566
1406
 
@@ -1836,4 +1676,24 @@ describe("HeartbeatService", () => {
1836
1676
  expect(processMessageCalls[0].content).not.toContain("<early-heartbeat>");
1837
1677
  });
1838
1678
  });
1679
+
1680
+ describe("configurable disposition", () => {
1681
+ test("injects the configured disposition inside <heartbeat-disposition>", () => {
1682
+ mockConfig.heartbeat.disposition = "Marker text from config.";
1683
+ const service = createService();
1684
+ const { prompt } = service.buildPrompt("- Check things", [], 10);
1685
+
1686
+ expect(prompt).toContain(
1687
+ "<heartbeat-disposition>\nMarker text from config.\n</heartbeat-disposition>",
1688
+ );
1689
+ });
1690
+
1691
+ test("omits the block when disposition is empty", () => {
1692
+ mockConfig.heartbeat.disposition = "";
1693
+ const service = createService();
1694
+ const { prompt } = service.buildPrompt("- Check things", [], 10);
1695
+
1696
+ expect(prompt).not.toContain("<heartbeat-disposition>");
1697
+ });
1698
+ });
1839
1699
  });
@@ -65,7 +65,6 @@ import type {
65
65
  ApprovalConversationGenerator,
66
66
  ApprovalCopyGenerator,
67
67
  GuardianActionCopyGenerator,
68
- GuardianFollowUpConversationGenerator,
69
68
  MessageProcessor,
70
69
  } from "../../runtime/http-types.js";
71
70
  import {
@@ -113,7 +112,6 @@ export async function handleChannelInbound(
113
112
  _approvalCopyGenerator?: ApprovalCopyGenerator,
114
113
  _approvalConversationGenerator?: ApprovalConversationGenerator,
115
114
  _guardianActionCopyGenerator?: GuardianActionCopyGenerator,
116
- _guardianFollowUpConversationGenerator?: GuardianFollowUpConversationGenerator,
117
115
  ): Promise<Response> {
118
116
  const body = await req.json();
119
117
  return wrapHandler(() => _handleChannelInbound({ body }));