@vellumai/assistant 0.8.11 → 0.8.12-staging.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (244) hide show
  1. package/ARCHITECTURE.md +15 -17
  2. package/README.md +0 -6
  3. package/bun.lock +6 -122
  4. package/node_modules/@vellumai/gateway-client/bun.lock +1 -0
  5. package/node_modules/@vellumai/gateway-client/package.json +3 -1
  6. package/node_modules/@vellumai/gateway-client/src/__tests__/gateway-client.test.ts +1 -1
  7. package/node_modules/@vellumai/gateway-client/src/gateway-ipc-contracts.ts +87 -0
  8. package/node_modules/@vellumai/gateway-client/src/index.ts +3 -5
  9. package/openapi.yaml +633 -4
  10. package/package.json +1 -3
  11. package/src/__tests__/adaptive-thinking-repair.test.ts +185 -0
  12. package/src/__tests__/agent-loop-compaction-events.test.ts +7 -6
  13. package/src/__tests__/anthropic-provider.test.ts +129 -0
  14. package/src/__tests__/background-workers-disk-pressure.test.ts +4 -1
  15. package/src/__tests__/btw-routes.test.ts +7 -34
  16. package/src/__tests__/checker.test.ts +6 -12
  17. package/src/__tests__/config-loader-backfill.test.ts +4 -2
  18. package/src/__tests__/config-loader-quarantine-notice.test.ts +167 -0
  19. package/src/__tests__/config-watcher.test.ts +2 -2
  20. package/src/__tests__/conversation-agent-loop-inference-profile.test.ts +1 -1
  21. package/src/__tests__/conversation-error.test.ts +2 -6
  22. package/src/__tests__/conversation-history-web-search.test.ts +8 -0
  23. package/src/__tests__/conversation-title-service.test.ts +2 -1
  24. package/src/__tests__/credential-security-invariants.test.ts +1 -1
  25. package/src/__tests__/disk-pressure-tools.test.ts +1 -1
  26. package/src/__tests__/exploration-drift-hook.test.ts +692 -0
  27. package/src/__tests__/filing-service.test.ts +8 -3
  28. package/src/__tests__/guardian-action-store.test.ts +0 -167
  29. package/src/__tests__/handlers-skills-memory-v2-reseed.test.ts +1 -1
  30. package/src/__tests__/heartbeat-disk-pressure.test.ts +4 -1
  31. package/src/__tests__/heartbeat-service.test.ts +5 -2
  32. package/src/__tests__/identity-intro-cache.test.ts +12 -5
  33. package/src/__tests__/identity-routes.test.ts +16 -57
  34. package/src/__tests__/injector-chain.test.ts +8 -3
  35. package/src/__tests__/injector-config-quarantine-notice.test.ts +115 -0
  36. package/src/__tests__/llm-catalog-parity.test.ts +16 -0
  37. package/src/__tests__/llm-usage-store.test.ts +11 -0
  38. package/src/__tests__/log-export-workspace.test.ts +468 -3
  39. package/src/__tests__/memory-v2-static-injector.test.ts +22 -0
  40. package/src/__tests__/model-intents.test.ts +1 -1
  41. package/src/__tests__/oauth-cli.test.ts +19 -8
  42. package/src/__tests__/openai-provider.test.ts +34 -0
  43. package/src/__tests__/prechat-onboarding-contract.test.ts +0 -1
  44. package/src/__tests__/recurrence-engine.test.ts +45 -0
  45. package/src/__tests__/schedule-routes.test.ts +34 -0
  46. package/src/__tests__/scheduler-disk-pressure.test.ts +1 -1
  47. package/src/__tests__/script-proxy-conversation-manager.test.ts +10 -5
  48. package/src/__tests__/secret-fixtures.ts +20 -0
  49. package/src/__tests__/skill-tool-factory.test.ts +49 -0
  50. package/src/__tests__/subagent-role-registry.test.ts +24 -1
  51. package/src/__tests__/subagent-tools.test.ts +1 -0
  52. package/src/__tests__/system-prompt.test.ts +109 -11
  53. package/src/__tests__/tool-approval-handler.test.ts +85 -0
  54. package/src/__tests__/tool-audit-listener.test.ts +86 -0
  55. package/src/__tests__/tool-error-hook.test.ts +1 -0
  56. package/src/__tests__/tool-result-spool.test.ts +337 -0
  57. package/src/__tests__/tool-result-truncate-hook.test.ts +1 -0
  58. package/src/__tests__/validate-input.test.ts +95 -1
  59. package/src/__tests__/workspace-migration-098-remove-stale-updates-bulletin-file.test.ts +65 -0
  60. package/src/__tests__/workspace-migration-099-disable-cache-one-shot-callsites.test.ts +139 -0
  61. package/src/__tests__/workspace-migration-100-upgrade-quality-profile-to-fable-5.test.ts +174 -0
  62. package/src/__tests__/workspace-migration-101-upgrade-balanced-economy-to-minimax-m3.test.ts +162 -0
  63. package/src/__tests__/workspace-release-notes-feature-flag-guard.test.ts +45 -95
  64. package/src/acp/__tests__/agent-process.test.ts +315 -2
  65. package/src/acp/__tests__/prepare-agent-env.test.ts +79 -5
  66. package/src/acp/agent-process.ts +163 -34
  67. package/src/acp/prepare-agent-env.ts +55 -15
  68. package/src/agent/loop.ts +81 -24
  69. package/src/api/events/usage-progress.ts +28 -0
  70. package/src/api/index.ts +6 -0
  71. package/src/background-wake/wake-intent-hooks.test.ts +2 -0
  72. package/src/bundler/app-bundler.ts +25 -42
  73. package/src/bundler/app-compiler.ts +8 -0
  74. package/src/calls/call-controller.ts +1 -1
  75. package/src/cli/commands/plugins.ts +248 -15
  76. package/src/cli/lib/__tests__/inspect-plugin.test.ts +318 -0
  77. package/src/cli/lib/__tests__/install-from-github.test.ts +16 -9
  78. package/src/cli/lib/__tests__/plugin-artifact.test.ts +183 -0
  79. package/src/cli/lib/__tests__/plugin-details.test.ts +158 -0
  80. package/src/cli/lib/__tests__/plugin-fingerprint.test.ts +245 -0
  81. package/src/cli/lib/__tests__/upgrade-plugin.test.ts +307 -0
  82. package/src/cli/lib/inspect-plugin.ts +252 -0
  83. package/src/cli/lib/install-from-github.ts +214 -21
  84. package/src/cli/lib/list-installed-plugins.ts +17 -6
  85. package/src/cli/lib/plugin-artifact.ts +103 -0
  86. package/src/cli/lib/plugin-details.ts +18 -1
  87. package/src/cli/lib/plugin-fingerprint.ts +197 -0
  88. package/src/cli/lib/upgrade-plugin.ts +225 -0
  89. package/src/config/bundled-skills/subagent/SKILL.md +2 -0
  90. package/src/config/bundled-skills/subagent/TOOLS.json +8 -2
  91. package/src/config/call-site-defaults.ts +13 -2
  92. package/src/config/feature-flag-registry.json +8 -16
  93. package/src/config/loader.ts +52 -59
  94. package/src/config/schema.ts +0 -2
  95. package/src/config/schemas/__tests__/memory-v2.test.ts +1 -0
  96. package/src/config/schemas/__tests__/memory-v3.test.ts +10 -0
  97. package/src/config/schemas/llm.ts +10 -0
  98. package/src/config/schemas/memory-v2.ts +13 -0
  99. package/src/config/schemas/memory-v3.ts +92 -0
  100. package/src/config/seed-inference-profiles.ts +4 -8
  101. package/src/context/post-turn-tool-result-truncation.ts +32 -18
  102. package/src/context/tool-result-spool.ts +104 -0
  103. package/src/credential-execution/feature-gates.ts +0 -1
  104. package/src/daemon/conversation-agent-loop-handlers.ts +41 -16
  105. package/src/daemon/conversation-error.ts +6 -15
  106. package/src/daemon/conversation.ts +9 -0
  107. package/src/daemon/disk-pressure-policy.ts +0 -1
  108. package/src/daemon/lifecycle.ts +1 -20
  109. package/src/daemon/message-types/conversations.ts +2 -15
  110. package/src/daemon/trust-context.ts +1 -1
  111. package/src/events/tool-audit-listener.ts +40 -9
  112. package/src/heartbeat/__tests__/heartbeat-service.test.ts +1 -1
  113. package/src/home/__tests__/home-content-refresh.test.ts +114 -0
  114. package/src/home/__tests__/suggested-prompts.test.ts +86 -5
  115. package/src/home/home-content-refresh.ts +43 -31
  116. package/src/home/home-greeting-cache.ts +8 -1
  117. package/src/home/home-greeting.ts +13 -9
  118. package/src/home/suggested-prompts.ts +77 -24
  119. package/src/ipc/routes/trust-rules.test.ts +66 -72
  120. package/src/media/image-credentials.ts +2 -2
  121. package/src/memory/__tests__/compaction-log-store-clickhouse.test.ts +432 -0
  122. package/src/memory/{compaction-log-writer-clickhouse.ts → compaction-log-store-clickhouse.ts} +264 -55
  123. package/src/memory/conversation-attention-store.ts +1 -0
  124. package/src/memory/conversation-bootstrap.ts +18 -9
  125. package/src/memory/conversation-crud.ts +12 -2
  126. package/src/memory/conversation-title-service.ts +53 -9
  127. package/src/memory/delivery-channels.ts +0 -69
  128. package/src/memory/graph/extraction-job.ts +0 -15
  129. package/src/memory/guardian-action-store.ts +1 -376
  130. package/src/memory/llm-usage-store.ts +5 -1
  131. package/src/memory/migrations/181-rename-thread-starters-checkpoints.ts +2 -2
  132. package/src/memory/v2/__tests__/consolidation-job.test.ts +183 -2
  133. package/src/memory/v2/__tests__/injection.test.ts +70 -0
  134. package/src/memory/v2/__tests__/static-context.test.ts +12 -0
  135. package/src/memory/v2/consolidation-job.ts +93 -9
  136. package/src/memory/v2/injection.ts +53 -0
  137. package/src/memory/v2/prompts/consolidation.ts +1 -0
  138. package/src/memory/v2/static-context.ts +13 -1
  139. package/src/memory/v2/sweep-job.ts +1 -1
  140. package/src/memory/v2/types.ts +5 -0
  141. package/src/plugin-api/types.ts +7 -0
  142. package/src/plugins/defaults/exploration-drift/hooks/post-tool-use.ts +300 -0
  143. package/src/plugins/defaults/exploration-drift/package.json +15 -0
  144. package/src/plugins/defaults/index.ts +25 -0
  145. package/src/plugins/defaults/memory-retrieval/injectors.ts +132 -4
  146. package/src/plugins/defaults/memory-v3-shadow/__tests__/card.test.ts +92 -0
  147. package/src/plugins/defaults/memory-v3-shadow/__tests__/carry-integration.test.ts +2 -1
  148. package/src/plugins/defaults/memory-v3-shadow/__tests__/fresh-set.test.ts +52 -0
  149. package/src/plugins/defaults/memory-v3-shadow/__tests__/injection.test.ts +1 -0
  150. package/src/plugins/defaults/memory-v3-shadow/__tests__/live-integration.test.ts +2 -1
  151. package/src/plugins/defaults/memory-v3-shadow/__tests__/orchestrate.test.ts +136 -5
  152. package/src/plugins/defaults/memory-v3-shadow/__tests__/pool-select.test.ts +17 -0
  153. package/src/plugins/defaults/memory-v3-shadow/__tests__/selection-log-store.test.ts +6 -0
  154. package/src/plugins/defaults/memory-v3-shadow/__tests__/shadow-integration.test.ts +5 -1
  155. package/src/plugins/defaults/memory-v3-shadow/__tests__/shadow-plugin.test.ts +68 -4
  156. package/src/plugins/defaults/memory-v3-shadow/card.ts +49 -5
  157. package/src/plugins/defaults/memory-v3-shadow/fresh-set.ts +59 -0
  158. package/src/plugins/defaults/memory-v3-shadow/injector.ts +4 -2
  159. package/src/plugins/defaults/memory-v3-shadow/learned-edges.test.ts +169 -0
  160. package/src/plugins/defaults/memory-v3-shadow/learned-edges.ts +178 -0
  161. package/src/plugins/defaults/memory-v3-shadow/orchestrate.ts +115 -26
  162. package/src/plugins/defaults/memory-v3-shadow/pool-select.ts +13 -9
  163. package/src/plugins/defaults/memory-v3-shadow/shadow-plugin.ts +144 -22
  164. package/src/plugins/defaults/memory-v3-shadow/types.ts +24 -6
  165. package/src/plugins/defaults/title-generate/hooks/stop.ts +13 -0
  166. package/src/plugins/defaults/title-generate/hooks/user-prompt-submit.ts +16 -0
  167. package/src/prompts/cache-boundary.ts +17 -0
  168. package/src/prompts/sections.ts +50 -17
  169. package/src/prompts/system-prompt.ts +12 -4
  170. package/src/prompts/templates/system-sections.ts +22 -0
  171. package/src/providers/__tests__/unparseable-tool-args.test.ts +53 -0
  172. package/src/providers/anthropic/client.ts +74 -28
  173. package/src/providers/gemini/client.ts +5 -1
  174. package/src/providers/minimax/client.ts +9 -0
  175. package/src/providers/model-catalog.ts +28 -0
  176. package/src/providers/model-intents.ts +3 -3
  177. package/src/providers/openai/chat-completions-provider.ts +4 -2
  178. package/src/providers/openai/responses-provider.ts +7 -2
  179. package/src/providers/retry.ts +8 -0
  180. package/src/providers/types.ts +11 -0
  181. package/src/providers/unparseable-tool-args.ts +56 -0
  182. package/src/runtime/AGENTS.md +6 -0
  183. package/src/runtime/__tests__/agent-wake.test.ts +2 -2
  184. package/src/runtime/agent-wake.ts +5 -5
  185. package/src/runtime/background-job-runner.ts +2 -2
  186. package/src/runtime/migrations/__tests__/vbundle-legacy-user-md.test.ts +150 -3
  187. package/src/runtime/migrations/vbundle-import-analyzer.ts +29 -6
  188. package/src/runtime/migrations/vbundle-import-policy.ts +23 -0
  189. package/src/runtime/migrations/vbundle-importer.ts +9 -4
  190. package/src/runtime/migrations/vbundle-streaming-importer.ts +8 -3
  191. package/src/runtime/pre-first-message-gate.ts +1 -1
  192. package/src/runtime/routes/__tests__/conversation-compaction-routes.test.ts +241 -0
  193. package/src/runtime/routes/__tests__/conversation-query-routes.test.ts +132 -0
  194. package/src/runtime/routes/__tests__/gateway-log-routes.test.ts +97 -185
  195. package/src/runtime/routes/__tests__/home-feed-routes.test.ts +17 -0
  196. package/src/runtime/routes/__tests__/plugins-routes.test.ts +348 -0
  197. package/src/runtime/routes/__tests__/task-routes.test.ts +3 -3
  198. package/src/runtime/routes/btw-routes.ts +0 -14
  199. package/src/runtime/routes/conversation-compaction-routes.ts +86 -19
  200. package/src/runtime/routes/conversation-list-routes.ts +77 -5
  201. package/src/runtime/routes/conversation-management-routes.ts +54 -0
  202. package/src/runtime/routes/conversation-query-routes.ts +79 -4
  203. package/src/runtime/routes/gateway-log-routes.ts +14 -64
  204. package/src/runtime/routes/home-feed-routes.ts +10 -0
  205. package/src/runtime/routes/identity-intro-cache.ts +1 -1
  206. package/src/runtime/routes/identity-routes.ts +76 -20
  207. package/src/runtime/routes/inbound-message-handler.ts +0 -36
  208. package/src/runtime/routes/log-export-routes.ts +143 -96
  209. package/src/runtime/routes/plugins-routes.ts +380 -0
  210. package/src/runtime/routes/redact-staged-export.ts +259 -0
  211. package/src/runtime/routes/schedule-routes.ts +19 -2
  212. package/src/runtime/routes/trust-rules-routes.ts +14 -67
  213. package/src/schedule/recurrence-engine.ts +34 -0
  214. package/src/schedule/scheduler.ts +1 -0
  215. package/src/security/redact-json.ts +61 -0
  216. package/src/skills/validate-input.ts +41 -1
  217. package/src/subagent/types.ts +26 -1
  218. package/src/telemetry/types.ts +15 -1
  219. package/src/telemetry/usage-telemetry-reporter.test.ts +6 -1
  220. package/src/telemetry/usage-telemetry-reporter.ts +1 -0
  221. package/src/tools/apps/executors.ts +1 -1
  222. package/src/tools/skills/skill-tool-factory.ts +19 -8
  223. package/src/tools/tool-approval-handler.ts +31 -0
  224. package/src/usage/types.ts +8 -1
  225. package/src/util/platform.ts +16 -0
  226. package/src/watcher/engine.ts +1 -0
  227. package/src/workspace/adaptive-thinking-repair.ts +113 -0
  228. package/src/workspace/migrations/097-enable-adaptive-thinking-managed-profiles.ts +70 -67
  229. package/src/workspace/migrations/098-remove-stale-updates-bulletin-file.ts +31 -0
  230. package/src/workspace/migrations/099-disable-cache-one-shot-callsites.ts +81 -0
  231. package/src/workspace/migrations/100-upgrade-quality-profile-to-fable-5.ts +86 -0
  232. package/src/workspace/migrations/101-upgrade-balanced-economy-to-minimax-m3.ts +70 -0
  233. package/src/workspace/migrations/registry.ts +8 -0
  234. package/src/__tests__/config-loader-quarantine-bulletin.test.ts +0 -202
  235. package/src/__tests__/conversation-starters-cadence.test.ts +0 -161
  236. package/src/__tests__/guardian-action-followup-executor.test.ts +0 -322
  237. package/src/__tests__/guardian-action-followup-store.test.ts +0 -373
  238. package/src/__tests__/guardian-action-late-reply.test.ts +0 -1083
  239. package/src/__tests__/update-bulletin-job.test.ts +0 -292
  240. package/src/config/schemas/updates.ts +0 -14
  241. package/src/memory/__tests__/compaction-log-writer-clickhouse.test.ts +0 -227
  242. package/src/memory/conversation-starters-cadence.ts +0 -78
  243. package/src/prompts/update-bulletin-job.ts +0 -180
  244. package/src/runtime/guardian-action-followup-executor.ts +0 -306
@@ -12,6 +12,10 @@ import { INTERNAL_GUARDIAN_TRUST_CONTEXT } from "../../daemon/trust-context.js";
12
12
  import { bootstrapConversation } from "../../memory/conversation-bootstrap.js";
13
13
  import { getConversation } from "../../memory/conversation-crud.js";
14
14
  import { getUsageCostForConversationWindow } from "../../memory/llm-usage-store.js";
15
+ import {
16
+ describeRRuleExpression,
17
+ isSingleFireRRule,
18
+ } from "../../schedule/recurrence-engine.js";
15
19
  import { normalizeScheduleSyntax } from "../../schedule/recurrence-types.js";
16
20
  import {
17
21
  runScript,
@@ -136,7 +140,20 @@ function getCadenceDescription(
136
140
  if (job.syntax === "cron") {
137
141
  return describeCronExpression(job.cronExpression);
138
142
  }
139
- return job.expression ?? "";
143
+ return describeRRuleExpression(job.cronExpression);
144
+ }
145
+
146
+ /**
147
+ * Presentation-layer one-shot flag. A COUNT=1 rrule fires exactly once and
148
+ * should read as one-time in clients, even though the scheduler internally
149
+ * treats expression-backed jobs as recurring (retry policy, conversation
150
+ * reuse). Do not feed this back into scheduler logic.
151
+ */
152
+ function isOneShotForDisplay(
153
+ job: Pick<ScheduleJob, "syntax" | "cronExpression">,
154
+ ): boolean {
155
+ if (job.cronExpression == null) return true;
156
+ return job.syntax === "rrule" && isSingleFireRRule(job.cronExpression);
140
157
  }
141
158
 
142
159
  function handleListSchedules(queryParams: Record<string, string>) {
@@ -182,7 +199,7 @@ function handleListSchedules(queryParams: Record<string, string>) {
182
199
  routingIntent: j.routingIntent,
183
200
  reuseConversation: j.reuseConversation,
184
201
  wakeConversationId: j.wakeConversationId,
185
- isOneShot: j.cronExpression == null,
202
+ isOneShot: isOneShotForDisplay(j),
186
203
  };
187
204
  }),
188
205
  };
@@ -1,83 +1,30 @@
1
1
  /**
2
- * Trust rule listing route — gateway HTTP proxy.
2
+ * Trust rule listing route — gateway IPC proxy.
3
3
  *
4
- * The handler makes a single HTTP call to the gateway's trust-rules REST API
5
- * and surfaces the body's `.error` message on non-OK responses.
4
+ * The handler calls the gateway over the local IPC socket because trust rule
5
+ * storage is gateway-owned in Docker mode.
6
6
  */
7
- import { z } from "zod";
7
+ import {
8
+ TrustRulesListIpcParamsSchema,
9
+ type TrustRulesListIpcResponse,
10
+ TrustRulesListIpcResponseSchema,
11
+ } from "@vellumai/gateway-client/gateway-ipc-contracts";
8
12
 
9
- import { getGatewayInternalBaseUrl } from "../../config/env.js";
13
+ import { ipcCallPersistent } from "../../ipc/gateway-client.js";
10
14
  import { ACTOR_PRINCIPALS } from "../auth/route-policy.js";
11
15
  import type { RouteDefinition, RouteHandlerArgs } from "./types.js";
12
16
 
13
- // ── Shared helper ───────────────────────────────────────────────────────
14
-
15
- async function gatewayFetch(
16
- path: string,
17
- init?: RequestInit,
18
- ): Promise<unknown> {
19
- const base = getGatewayInternalBaseUrl();
20
- const res = await fetch(`${base}${path}`, init);
21
- if (!res.ok) {
22
- let message = `Gateway request failed (${res.status})`;
23
- try {
24
- const body = (await res.json()) as { error?: unknown };
25
- if (typeof body.error === "string") {
26
- message = body.error;
27
- }
28
- } catch {
29
- // ignore JSON parse failures
30
- }
31
- throw new Error(message);
32
- }
33
- return res.json();
34
- }
35
-
36
- // ── Schemas ─────────────────────────────────────────────────────────────
37
-
38
- const TrustRulesListParams = z
39
- .object({
40
- tool: z.string().optional(),
41
- origin: z.string().optional(),
42
- include_all: z.boolean().optional(),
43
- })
44
- .strict();
45
-
46
- const TrustRuleSchema = z.object({
47
- id: z.string(),
48
- tool: z.string(),
49
- pattern: z.string(),
50
- risk: z.enum(["low", "medium", "high"]),
51
- description: z.string(),
52
- origin: z.enum(["default", "user_defined"]),
53
- userModified: z.boolean(),
54
- deleted: z.boolean(),
55
- createdAt: z.string(),
56
- updatedAt: z.string(),
57
- });
58
-
59
- const TrustRulesListResponseSchema = z.object({
60
- rules: z.array(TrustRuleSchema),
61
- });
62
- type TrustRulesListResponse = z.infer<typeof TrustRulesListResponseSchema>;
63
-
64
17
  // ── Handlers ────────────────────────────────────────────────────────────
65
18
 
66
19
  async function handleList({
67
20
  queryParams = {},
68
21
  body = {},
69
- }: RouteHandlerArgs): Promise<TrustRulesListResponse> {
22
+ }: RouteHandlerArgs): Promise<TrustRulesListIpcResponse> {
70
23
  // HTTP GET delivers filters via queryParams; CLI IPC puts them in body.
71
24
  const source = Object.keys(queryParams).length > 0 ? queryParams : body;
72
- const p = TrustRulesListParams.parse(source);
73
- const qs = new URLSearchParams();
74
- if (p.tool) qs.set("tool", p.tool);
75
- if (p.origin) qs.set("origin", p.origin);
76
- if (p.include_all) qs.set("include_all", "true");
77
- const query = qs.toString();
78
- return gatewayFetch(
79
- `/v1/trust-rules${query ? `?${query}` : ""}`,
80
- ) as Promise<TrustRulesListResponse>;
25
+ const p = TrustRulesListIpcParamsSchema.parse(source);
26
+ const result = await ipcCallPersistent("trust_rules_list", p);
27
+ return TrustRulesListIpcResponseSchema.parse(result);
81
28
  }
82
29
 
83
30
  // ── Route definitions ───────────────────────────────────────────────────
@@ -96,7 +43,7 @@ export const ROUTES: RouteDefinition[] = [
96
43
  description:
97
44
  "List trust rules, optionally filtered by tool, origin, or include_all.",
98
45
  tags: ["trust-rules"],
99
- responseBody: TrustRulesListResponseSchema,
46
+ responseBody: TrustRulesListIpcResponseSchema,
100
47
  queryParams: [
101
48
  { name: "tool", description: "Filter by tool name" },
102
49
  { name: "origin", description: "Filter by origin" },
@@ -135,6 +135,40 @@ export function isValidScheduleExpression(spec: ScheduleSpec): boolean {
135
135
  }
136
136
  }
137
137
 
138
+ /**
139
+ * Detect whether an RRULE expression fires exactly once — a single RRULE
140
+ * with COUNT=1 and no set constructs. Such schedules are semantically
141
+ * one-shots even though they carry a recurrence expression.
142
+ */
143
+ export function isSingleFireRRule(expression: string): boolean {
144
+ try {
145
+ const normalized = normalizeRruleExpression(expression);
146
+ if (hasSetConstructs(normalized)) return false;
147
+ const rule = rrulestr(normalized);
148
+ return !(rule instanceof RRuleSet) && rule.options.count === 1;
149
+ } catch {
150
+ return false;
151
+ }
152
+ }
153
+
154
+ /**
155
+ * Human-readable description of an RRULE expression for display surfaces.
156
+ * Single-fire rules read as "One-time"; rules the library cannot express
157
+ * fall back to "Custom recurrence" rather than leaking raw iCalendar text.
158
+ */
159
+ export function describeRRuleExpression(expression: string): string {
160
+ if (isSingleFireRRule(expression)) return "One-time";
161
+ try {
162
+ const normalized = normalizeRruleExpression(expression);
163
+ if (hasSetConstructs(normalized)) return "Custom recurrence";
164
+ const text = rrulestr(normalized).toText();
165
+ if (!text) return "Custom recurrence";
166
+ return text.charAt(0).toUpperCase() + text.slice(1);
167
+ } catch {
168
+ return "Custom recurrence";
169
+ }
170
+ }
171
+
138
172
  /**
139
173
  * Compute the next run timestamp (epoch ms) for a schedule expression.
140
174
  * Throws if no future runs exist.
@@ -619,6 +619,7 @@ export async function runScheduleDueWorkOnce(
619
619
  jobName: `schedule:${job.id}`,
620
620
  source: "schedule",
621
621
  prompt: job.message,
622
+ systemHint: `Schedule: ${job.name}`,
622
623
  trustContext: { sourceChannel: "vellum", trustClass: "guardian" },
623
624
  callSite: "mainAgent",
624
625
  timeoutMs: SCHEDULE_TALK_TIMEOUT_MS,
@@ -0,0 +1,61 @@
1
+ /**
2
+ * JSON-aware secret redaction: walk a parsed JSON value and run
3
+ * `redactSecrets()` over every string leaf (object keys included).
4
+ *
5
+ * The redaction marker (`<redacted type="..." />`) contains double quotes,
6
+ * so splicing it into an already-serialized JSON document corrupts any
7
+ * string it lands inside. Callers that need to keep serialized JSON valid
8
+ * must redact the string leaves of the parsed value and re-stringify,
9
+ * rather than redacting the serialized text.
10
+ *
11
+ * Shared by the export-time staged-file sweep (`redact-staged-export.ts`)
12
+ * and the tool-audit listener (`tool-audit-listener.ts`). The hot-path pino
13
+ * log serializer (`util/log-redact.ts`) intentionally does not use this
14
+ * module.
15
+ */
16
+
17
+ import { redactSecrets } from "./secret-scanner.js";
18
+
19
+ export interface RedactedJsonValue {
20
+ /** Redacted copy of the input (the input itself is never mutated). */
21
+ value: unknown;
22
+ /** True when at least one string leaf was changed by redaction. */
23
+ changed: boolean;
24
+ }
25
+
26
+ interface RedactionState {
27
+ changed: boolean;
28
+ }
29
+
30
+ /**
31
+ * Redact secrets in every string leaf (keys included) of a parsed JSON
32
+ * value. Returns the redacted copy plus a `changed` flag so callers can
33
+ * keep unchanged content byte-identical (no gratuitous re-serialization).
34
+ */
35
+ export function redactJsonStringLeaves(value: unknown): RedactedJsonValue {
36
+ const state: RedactionState = { changed: false };
37
+ return { value: walk(value, state), changed: state.changed };
38
+ }
39
+
40
+ function walk(value: unknown, state: RedactionState): unknown {
41
+ if (typeof value === "string") {
42
+ return redactString(value, state);
43
+ }
44
+ if (Array.isArray(value)) {
45
+ return value.map((item) => walk(item, state));
46
+ }
47
+ if (value !== null && typeof value === "object") {
48
+ return Object.fromEntries(
49
+ Object.entries(value).map(
50
+ ([key, val]) => [redactString(key, state), walk(val, state)] as const,
51
+ ),
52
+ );
53
+ }
54
+ return value;
55
+ }
56
+
57
+ function redactString(value: string, state: RedactionState): string {
58
+ const redacted = redactSecrets(value);
59
+ if (redacted !== value) state.changed = true;
60
+ return redacted;
61
+ }
@@ -66,6 +66,42 @@ function quoteList(values: readonly string[]): string {
66
66
  return values.map((v) => `"${v}"`).join(", ");
67
67
  }
68
68
 
69
+ /**
70
+ * Coerce string-encoded booleans (`"true"`/`"false"`) to real booleans for
71
+ * properties the schema declares as `type: "boolean"`.
72
+ *
73
+ * Some providers' models serialize booleans as JSON strings. Rejecting those
74
+ * loses the caller's intent: the model's typical recovery is to drop the field
75
+ * and retry, at which point the field's default silently inverts what it asked
76
+ * for (e.g. `app_create` with `auto_open: "false"` → retry omits the field →
77
+ * default `true` opens a half-built app). Accepting the unambiguous string
78
+ * forms preserves intent.
79
+ *
80
+ * Pure: returns a new object when a coercion applies, otherwise returns
81
+ * `input` unchanged. Never mutates `input` or `schema`.
82
+ */
83
+ export function coerceStringBooleans(
84
+ input: Record<string, unknown>,
85
+ schema: Record<string, unknown> | undefined,
86
+ ): Record<string, unknown> {
87
+ if (!schema) return input;
88
+ const properties = schema.properties;
89
+ if (!isPlainObject(properties)) return input;
90
+
91
+ let coerced: Record<string, unknown> | undefined;
92
+ for (const [key, rawSubSchema] of Object.entries(properties)) {
93
+ if (!isPlainObject(rawSubSchema)) continue;
94
+ if (rawSubSchema.type !== "boolean") continue;
95
+ const value = input[key];
96
+ if (typeof value !== "string") continue;
97
+ const normalized = value.trim().toLowerCase();
98
+ if (normalized !== "true" && normalized !== "false") continue;
99
+ coerced ??= { ...input };
100
+ coerced[key] = normalized === "true";
101
+ }
102
+ return coerced ?? input;
103
+ }
104
+
69
105
  /**
70
106
  * Validate a tool input object against the (optional) JSON-schema definition
71
107
  * declared on the tool entry. Returns `{ ok: true }` if the input is valid (or
@@ -121,7 +157,11 @@ export function validateInputAgainstSchema(
121
157
  if (typeof declaredType === "string" && SUPPORTED_TYPES.has(declaredType)) {
122
158
  const type = declaredType as SupportedType;
123
159
  if (!matchesType(value, type)) {
124
- errors.push(`${key} must be ${typeArticle(type)} ${type}`);
160
+ errors.push(
161
+ type === "boolean" && typeof value === "string"
162
+ ? `${key} must be a boolean — pass true or false as a JSON boolean, not a string`
163
+ : `${key} must be ${typeArticle(type)} ${type}`,
164
+ );
125
165
  // No point checking enum/items if the base type is wrong.
126
166
  continue;
127
167
  }
@@ -105,7 +105,12 @@ export const SUBAGENT_LIMITS = {
105
105
 
106
106
  // ── Roles ───────────────────────────────────────────────────────────────
107
107
 
108
- export type SubagentRole = "general" | "researcher" | "coder" | "planner";
108
+ export type SubagentRole =
109
+ | "general"
110
+ | "researcher"
111
+ | "coder"
112
+ | "planner"
113
+ | "investigator";
109
114
 
110
115
  export interface SubagentRoleConfig {
111
116
  /**
@@ -167,4 +172,24 @@ export const SUBAGENT_ROLE_REGISTRY: Record<SubagentRole, SubagentRoleConfig> =
167
172
  systemPromptPreamble:
168
173
  "You are an analysis-focused subagent with read-only access. Read files, search the web, and synthesize findings. You cannot write files or run shell commands.",
169
174
  },
175
+ investigator: {
176
+ allowedTools: [
177
+ "bash",
178
+ "file_read",
179
+ "file_list",
180
+ "web_search",
181
+ "web_fetch",
182
+ "recall",
183
+ "notify_parent",
184
+ ],
185
+ skillIds: [],
186
+ systemPromptPreamble: [
187
+ "You are an investigation-focused subagent for root-cause analysis: debugging, log forensics, and tracing behavior across code.",
188
+ "Your shell access is for read-only investigation (grep, find, reading files and logs) — do not modify files or system state.",
189
+ "Working method: read whole files instead of many small line-range slices; prefer broad searches (e.g. grep -rn across a directory) over one-symbol-at-a-time queries.",
190
+ "Send notify_parent (urgency 'important') as soon as each finding is confirmed, so progress survives interruption.",
191
+ "Your final message must be a compact root-cause report with these sections: Symptom, Root cause, Evidence (file:line references), Suggested fix, Open questions.",
192
+ "If you approach context limits, stop investigating and produce the report from what you have — a partial report delivered is worth more than a complete investigation lost.",
193
+ ].join(" "),
194
+ },
170
195
  };
@@ -55,7 +55,12 @@ export interface ModelTelemetryEventBase extends TelemetryEventBase {
55
55
  inference_profile_source: UsageAttributionProfileSource | null;
56
56
  }
57
57
 
58
- /** LLM usage event — one per provider API call. */
58
+ /**
59
+ * LLM usage event — one per persisted usage row. The main agent loop
60
+ * persists a single row per turn with token totals summed across every
61
+ * provider API call in the loop (`llm_call_count` carries the call count);
62
+ * auxiliary call sites persist one row per call.
63
+ */
59
64
  export interface LlmUsageTelemetryEvent extends TelemetryEventBase {
60
65
  type: "llm_usage";
61
66
  /**
@@ -85,6 +90,15 @@ export interface LlmUsageTelemetryEvent extends TelemetryEventBase {
85
90
  output_tokens: number;
86
91
  cache_creation_input_tokens: number | null;
87
92
  cache_read_input_tokens: number | null;
93
+ /**
94
+ * Number of provider API calls aggregated into this event. The main
95
+ * agent loop persists one usage row per turn with token totals summed
96
+ * across every call in the loop, so this is how downstream consumers
97
+ * recover per-call averages (effective tokens ÷ calls). Auxiliary call
98
+ * sites record exactly 1. Null for rows persisted before daemon
99
+ * migration `200-usage-llm-call-count`; consumers treat null as 1.
100
+ */
101
+ llm_call_count: number | null;
88
102
  /**
89
103
  * The provider's untouched `usage` block. Anthropic surfaces a TTL
90
104
  * breakdown under `cache_creation.ephemeral_{5m,1h}_input_tokens`;
@@ -183,7 +183,7 @@ initializeDb();
183
183
 
184
184
  let eventIdCounter = 0;
185
185
 
186
- // The reporter consumes `UnreportedUsageEvent` (UsageEvent + the two
186
+ // The reporter consumes `UnreportedUsageEvent` (UsageEvent + the
187
187
  // JOIN-computed fields `conversationType` and `turnIndex`). Build that
188
188
  // shape directly so the mock matches `queryUnreportedUsageEvents`'
189
189
  // return type exactly.
@@ -218,6 +218,7 @@ function makeUsageEvent(
218
218
  assistantVersion: "test-app-version",
219
219
  conversationType: "standard",
220
220
  turnIndex: 1,
221
+ llmCallCount: 1,
221
222
  ...overrides,
222
223
  };
223
224
  }
@@ -507,6 +508,7 @@ describe("UsageTelemetryReporter", () => {
507
508
  callSite: "compactionAgent",
508
509
  inferenceProfile: "quality-optimized",
509
510
  inferenceProfileSource: "conversation",
511
+ llmCallCount: 3,
510
512
  createdAt: 1700000099000,
511
513
  });
512
514
  mockQueryUnreportedUsageEvents.mockReturnValue([event]);
@@ -538,6 +540,7 @@ describe("UsageTelemetryReporter", () => {
538
540
  expect(e.output_tokens).toBe(100);
539
541
  expect(e.cache_creation_input_tokens).toBe(20);
540
542
  expect(e.cache_read_input_tokens).toBe(15);
543
+ expect(e.llm_call_count).toBe(3);
541
544
  expect(e.actor).toBe("context_compactor");
542
545
  expect(e.llm_call_site).toBe("compactionAgent");
543
546
  expect(e.inference_profile).toBe("quality-optimized");
@@ -599,6 +602,7 @@ describe("UsageTelemetryReporter", () => {
599
602
  callSite: null,
600
603
  inferenceProfile: null,
601
604
  inferenceProfileSource: null,
605
+ llmCallCount: null,
602
606
  });
603
607
  mockQueryUnreportedUsageEvents.mockReturnValue([event]);
604
608
  mockFetch.mockImplementation(() =>
@@ -617,6 +621,7 @@ describe("UsageTelemetryReporter", () => {
617
621
  llm_call_site: null,
618
622
  inference_profile: null,
619
623
  inference_profile_source: null,
624
+ llm_call_count: null,
620
625
  });
621
626
  });
622
627
 
@@ -379,6 +379,7 @@ export class UsageTelemetryReporter {
379
379
  output_tokens: e.outputTokens,
380
380
  cache_creation_input_tokens: e.cacheCreationInputTokens ?? null,
381
381
  cache_read_input_tokens: e.cacheReadInputTokens ?? null,
382
+ llm_call_count: e.llmCallCount,
382
383
  raw_usage: e.rawUsage,
383
384
  actor: e.actor,
384
385
  llm_call_site: e.callSite,
@@ -548,7 +548,7 @@ export async function executeAppGenerateIcon(
548
548
  return {
549
549
  content: JSON.stringify({
550
550
  error:
551
- "Icon generation failed. Make sure a Gemini API key is configured in Settings.",
551
+ "Icon generation failed. Make sure a Gemini API key is configured in Settings → Models & Services.",
552
552
  }),
553
553
  isError: true,
554
554
  };
@@ -1,6 +1,9 @@
1
1
  import type { SkillToolEntry } from "../../config/skills.js";
2
2
  import { RiskLevel } from "../../permissions/types.js";
3
- import { validateInputAgainstSchema } from "../../skills/validate-input.js";
3
+ import {
4
+ coerceStringBooleans,
5
+ validateInputAgainstSchema,
6
+ } from "../../skills/validate-input.js";
4
7
  import type {
5
8
  ExecutionTarget,
6
9
  Tool,
@@ -42,10 +45,12 @@ export function createSkillTool(
42
45
  input: Record<string, unknown>,
43
46
  context: ToolContext,
44
47
  ): Promise<ToolExecutionResult> {
48
+ const schema = entry.input_schema as Record<string, unknown> | undefined;
49
+ const coercedInput = coerceStringBooleans(input, schema);
45
50
  const validation = validateInputAgainstSchema(
46
51
  entry.name,
47
- input,
48
- entry.input_schema as Record<string, unknown> | undefined,
52
+ coercedInput,
53
+ schema,
49
54
  );
50
55
  if (!validation.ok) {
51
56
  return {
@@ -54,11 +59,17 @@ export function createSkillTool(
54
59
  };
55
60
  }
56
61
 
57
- return runSkillToolScript(skillDir, entry.executor, input, context, {
58
- target: entry.execution_target,
59
- expectedSkillVersionHash: versionHash,
60
- bundled,
61
- });
62
+ return runSkillToolScript(
63
+ skillDir,
64
+ entry.executor,
65
+ coercedInput,
66
+ context,
67
+ {
68
+ target: entry.execution_target,
69
+ expectedSkillVersionHash: versionHash,
70
+ bundled,
71
+ },
72
+ );
62
73
  },
63
74
  };
64
75
  }
@@ -5,6 +5,10 @@ import {
5
5
  getCanonicalGuardianRequest,
6
6
  updateCanonicalGuardianRequest,
7
7
  } from "../memory/canonical-guardian-store.js";
8
+ import {
9
+ isUnparseableToolArgs,
10
+ unparseableToolArgsMessage,
11
+ } from "../providers/unparseable-tool-args.js";
8
12
  import { isUntrustedTrustClass } from "../runtime/actor-trust-resolver.js";
9
13
  import { createOrReuseToolGrantRequest } from "../runtime/tool-grant-request-helper.js";
10
14
  import { redactSecrets } from "../security/secret-scanner.js";
@@ -241,6 +245,33 @@ export class ToolApprovalHandler {
241
245
  };
242
246
  }
243
247
 
248
+ // Reject tool calls whose arguments failed JSON parsing in the provider
249
+ // layer (wrapped under the `_raw` marker). Executing with the marker
250
+ // object would feed garbage input to the tool — and worse, a tool that
251
+ // tolerates missing fields can "succeed" (e.g. ui_show creating a
252
+ // typeless surface), so the model never learns its arguments were
253
+ // mangled. Fail loudly instead so the model retries.
254
+ if (isUnparseableToolArgs(input)) {
255
+ const msg = unparseableToolArgsMessage(name, input._raw);
256
+ const durationMs = Date.now() - startTime;
257
+ emitLifecycleEvent({
258
+ type: "error",
259
+ toolName: name,
260
+ executionTarget,
261
+ input,
262
+ workingDir: context.workingDir,
263
+ conversationId: context.conversationId,
264
+ requestId: context.requestId,
265
+ riskLevel,
266
+ decision: "error",
267
+ durationMs,
268
+ errorMessage: msg,
269
+ isExpected: true,
270
+ errorCategory: "tool_failure",
271
+ });
272
+ return { allowed: false, result: { content: msg, isError: true } };
273
+ }
274
+
244
275
  // Reject tool invocations targeting guardian control-plane endpoints from non-guardian actors.
245
276
  const guardianCheck = enforceVerificationControlPlanePolicy(
246
277
  name,
@@ -62,7 +62,7 @@ export interface UsageEventInput {
62
62
  inferenceProfile?: string | null;
63
63
  inferenceProfileSource?: UsageAttributionProfileSource | null;
64
64
  /** Number of actual LLM API calls represented by this event (defaults to 1). */
65
- llmCallCount?: number;
65
+ llmCallCount?: number | null;
66
66
  }
67
67
 
68
68
  /**
@@ -85,6 +85,13 @@ export interface UsageEvent extends UsageEventInput {
85
85
  inferenceProfileSource: UsageAttributionProfileSource | null;
86
86
  estimatedCostUsd: number | null;
87
87
  pricingStatus: "priced" | "unpriced";
88
+ /**
89
+ * Number of provider API calls aggregated into this event. The main agent
90
+ * loop persists one row per turn with this set to the number of calls in
91
+ * the loop; auxiliary call sites persist 1. `null` only for rows persisted
92
+ * before migration `200-usage-llm-call-count` ran.
93
+ */
94
+ llmCallCount: number | null;
88
95
  /**
89
96
  * Version of the assistant binary at the moment this event was
90
97
  * RECORDED, captured by `recordUsageEvent` and persisted with the
@@ -79,6 +79,22 @@ export function getDataDir(): string {
79
79
  return join(getWorkspaceDir(), "data");
80
80
  }
81
81
 
82
+ /**
83
+ * Returns the path to the config-quarantine notice sentinel
84
+ * (`<workspace>/data/config-quarantine-notice.json`).
85
+ *
86
+ * Written by the config loader when a corrupt `config.json` is quarantined and
87
+ * read by the per-turn `config-quarantine-notice` injector. Lives under the
88
+ * internal data dir (runtime state, config-free to resolve) rather than the
89
+ * user-facing workspace root because it is daemon-written bookkeeping, not a
90
+ * file the user edits. The path resolves without loading config, so it is safe
91
+ * to call during early-boot config load before the DB or `getConfig().dataDir`
92
+ * exist.
93
+ */
94
+ export function getConfigQuarantineNoticePath(): string {
95
+ return join(getDataDir(), "config-quarantine-notice.json");
96
+ }
97
+
82
98
  /**
83
99
  * Returns the embedding models directory (~/.vellum/workspace/embedding-models).
84
100
  * Downloaded embedding runtime (onnxruntime-node, transformers bundle, model weights)
@@ -251,6 +251,7 @@ export async function runWatchersOnce(
251
251
  // The seed lives in the sandwich messages; processMessage runs
252
252
  // with an empty prompt so we don't double-inject the action prompt.
253
253
  prompt: "",
254
+ systemHint: `Watcher: ${watcher.name}`,
254
255
  trustContext: { sourceChannel: "vellum", trustClass: "guardian" },
255
256
  callSite: "mainAgent",
256
257
  timeoutMs: WATCHER_JOB_TIMEOUT_MS,