@vellumai/assistant 0.8.11 → 0.8.12-staging.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 (219) 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 +126 -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-usage-store.test.ts +11 -0
  37. package/src/__tests__/memory-v2-static-injector.test.ts +22 -0
  38. package/src/__tests__/model-intents.test.ts +1 -1
  39. package/src/__tests__/oauth-cli.test.ts +19 -8
  40. package/src/__tests__/openai-provider.test.ts +34 -0
  41. package/src/__tests__/prechat-onboarding-contract.test.ts +0 -1
  42. package/src/__tests__/recurrence-engine.test.ts +45 -0
  43. package/src/__tests__/schedule-routes.test.ts +34 -0
  44. package/src/__tests__/scheduler-disk-pressure.test.ts +1 -1
  45. package/src/__tests__/script-proxy-conversation-manager.test.ts +10 -5
  46. package/src/__tests__/skill-tool-factory.test.ts +49 -0
  47. package/src/__tests__/subagent-role-registry.test.ts +24 -1
  48. package/src/__tests__/subagent-tools.test.ts +1 -0
  49. package/src/__tests__/system-prompt.test.ts +109 -11
  50. package/src/__tests__/tool-error-hook.test.ts +1 -0
  51. package/src/__tests__/tool-result-spool.test.ts +337 -0
  52. package/src/__tests__/tool-result-truncate-hook.test.ts +1 -0
  53. package/src/__tests__/validate-input.test.ts +95 -1
  54. package/src/__tests__/workspace-migration-098-remove-stale-updates-bulletin-file.test.ts +65 -0
  55. package/src/__tests__/workspace-migration-099-disable-cache-one-shot-callsites.test.ts +139 -0
  56. package/src/__tests__/workspace-release-notes-feature-flag-guard.test.ts +45 -95
  57. package/src/agent/loop.ts +81 -24
  58. package/src/api/events/usage-progress.ts +28 -0
  59. package/src/api/index.ts +6 -0
  60. package/src/background-wake/wake-intent-hooks.test.ts +2 -0
  61. package/src/bundler/app-bundler.ts +25 -42
  62. package/src/calls/call-controller.ts +1 -1
  63. package/src/cli/commands/plugins.ts +248 -15
  64. package/src/cli/lib/__tests__/inspect-plugin.test.ts +318 -0
  65. package/src/cli/lib/__tests__/install-from-github.test.ts +16 -9
  66. package/src/cli/lib/__tests__/plugin-artifact.test.ts +183 -0
  67. package/src/cli/lib/__tests__/plugin-details.test.ts +158 -0
  68. package/src/cli/lib/__tests__/plugin-fingerprint.test.ts +245 -0
  69. package/src/cli/lib/__tests__/upgrade-plugin.test.ts +301 -0
  70. package/src/cli/lib/inspect-plugin.ts +252 -0
  71. package/src/cli/lib/install-from-github.ts +214 -21
  72. package/src/cli/lib/list-installed-plugins.ts +17 -6
  73. package/src/cli/lib/plugin-artifact.ts +103 -0
  74. package/src/cli/lib/plugin-details.ts +18 -1
  75. package/src/cli/lib/plugin-fingerprint.ts +197 -0
  76. package/src/cli/lib/upgrade-plugin.ts +219 -0
  77. package/src/config/bundled-skills/subagent/SKILL.md +2 -0
  78. package/src/config/bundled-skills/subagent/TOOLS.json +8 -2
  79. package/src/config/call-site-defaults.ts +13 -2
  80. package/src/config/feature-flag-registry.json +8 -16
  81. package/src/config/loader.ts +52 -59
  82. package/src/config/schema.ts +0 -2
  83. package/src/config/schemas/__tests__/memory-v2.test.ts +1 -0
  84. package/src/config/schemas/__tests__/memory-v3.test.ts +10 -0
  85. package/src/config/schemas/llm.ts +10 -0
  86. package/src/config/schemas/memory-v2.ts +13 -0
  87. package/src/config/schemas/memory-v3.ts +92 -0
  88. package/src/context/post-turn-tool-result-truncation.ts +32 -18
  89. package/src/context/tool-result-spool.ts +104 -0
  90. package/src/credential-execution/feature-gates.ts +0 -1
  91. package/src/daemon/conversation-agent-loop-handlers.ts +41 -16
  92. package/src/daemon/conversation-error.ts +6 -15
  93. package/src/daemon/conversation.ts +9 -0
  94. package/src/daemon/disk-pressure-policy.ts +0 -1
  95. package/src/daemon/lifecycle.ts +1 -20
  96. package/src/daemon/message-types/conversations.ts +2 -15
  97. package/src/daemon/trust-context.ts +1 -1
  98. package/src/heartbeat/__tests__/heartbeat-service.test.ts +1 -1
  99. package/src/home/__tests__/home-content-refresh.test.ts +114 -0
  100. package/src/home/__tests__/suggested-prompts.test.ts +86 -5
  101. package/src/home/home-content-refresh.ts +43 -31
  102. package/src/home/home-greeting-cache.ts +8 -1
  103. package/src/home/home-greeting.ts +13 -9
  104. package/src/home/suggested-prompts.ts +77 -24
  105. package/src/ipc/routes/trust-rules.test.ts +66 -72
  106. package/src/media/image-credentials.ts +2 -2
  107. package/src/memory/__tests__/compaction-log-store-clickhouse.test.ts +432 -0
  108. package/src/memory/{compaction-log-writer-clickhouse.ts → compaction-log-store-clickhouse.ts} +264 -55
  109. package/src/memory/conversation-attention-store.ts +1 -0
  110. package/src/memory/conversation-bootstrap.ts +18 -9
  111. package/src/memory/conversation-crud.ts +12 -2
  112. package/src/memory/conversation-title-service.ts +53 -9
  113. package/src/memory/delivery-channels.ts +0 -69
  114. package/src/memory/graph/extraction-job.ts +0 -15
  115. package/src/memory/guardian-action-store.ts +1 -376
  116. package/src/memory/llm-usage-store.ts +5 -1
  117. package/src/memory/migrations/181-rename-thread-starters-checkpoints.ts +2 -2
  118. package/src/memory/v2/__tests__/consolidation-job.test.ts +183 -2
  119. package/src/memory/v2/__tests__/injection.test.ts +70 -0
  120. package/src/memory/v2/__tests__/static-context.test.ts +12 -0
  121. package/src/memory/v2/consolidation-job.ts +93 -9
  122. package/src/memory/v2/injection.ts +53 -0
  123. package/src/memory/v2/prompts/consolidation.ts +1 -0
  124. package/src/memory/v2/static-context.ts +13 -1
  125. package/src/memory/v2/sweep-job.ts +1 -1
  126. package/src/memory/v2/types.ts +5 -0
  127. package/src/plugin-api/types.ts +7 -0
  128. package/src/plugins/defaults/exploration-drift/hooks/post-tool-use.ts +300 -0
  129. package/src/plugins/defaults/exploration-drift/package.json +15 -0
  130. package/src/plugins/defaults/index.ts +25 -0
  131. package/src/plugins/defaults/memory-retrieval/injectors.ts +132 -4
  132. package/src/plugins/defaults/memory-v3-shadow/__tests__/card.test.ts +92 -0
  133. package/src/plugins/defaults/memory-v3-shadow/__tests__/carry-integration.test.ts +2 -1
  134. package/src/plugins/defaults/memory-v3-shadow/__tests__/fresh-set.test.ts +52 -0
  135. package/src/plugins/defaults/memory-v3-shadow/__tests__/injection.test.ts +1 -0
  136. package/src/plugins/defaults/memory-v3-shadow/__tests__/live-integration.test.ts +2 -1
  137. package/src/plugins/defaults/memory-v3-shadow/__tests__/orchestrate.test.ts +136 -5
  138. package/src/plugins/defaults/memory-v3-shadow/__tests__/pool-select.test.ts +17 -0
  139. package/src/plugins/defaults/memory-v3-shadow/__tests__/selection-log-store.test.ts +6 -0
  140. package/src/plugins/defaults/memory-v3-shadow/__tests__/shadow-integration.test.ts +5 -1
  141. package/src/plugins/defaults/memory-v3-shadow/__tests__/shadow-plugin.test.ts +68 -4
  142. package/src/plugins/defaults/memory-v3-shadow/card.ts +49 -5
  143. package/src/plugins/defaults/memory-v3-shadow/fresh-set.ts +59 -0
  144. package/src/plugins/defaults/memory-v3-shadow/injector.ts +4 -2
  145. package/src/plugins/defaults/memory-v3-shadow/learned-edges.test.ts +169 -0
  146. package/src/plugins/defaults/memory-v3-shadow/learned-edges.ts +178 -0
  147. package/src/plugins/defaults/memory-v3-shadow/orchestrate.ts +115 -26
  148. package/src/plugins/defaults/memory-v3-shadow/pool-select.ts +13 -9
  149. package/src/plugins/defaults/memory-v3-shadow/shadow-plugin.ts +144 -22
  150. package/src/plugins/defaults/memory-v3-shadow/types.ts +24 -6
  151. package/src/plugins/defaults/title-generate/hooks/stop.ts +13 -0
  152. package/src/plugins/defaults/title-generate/hooks/user-prompt-submit.ts +16 -0
  153. package/src/prompts/cache-boundary.ts +17 -0
  154. package/src/prompts/sections.ts +50 -17
  155. package/src/prompts/system-prompt.ts +12 -4
  156. package/src/prompts/templates/system-sections.ts +22 -0
  157. package/src/providers/anthropic/client.ts +74 -28
  158. package/src/providers/gemini/client.ts +5 -1
  159. package/src/providers/minimax/client.ts +9 -0
  160. package/src/providers/model-intents.ts +2 -2
  161. package/src/providers/openai/chat-completions-provider.ts +2 -1
  162. package/src/providers/openai/responses-provider.ts +5 -1
  163. package/src/providers/retry.ts +8 -0
  164. package/src/providers/types.ts +11 -0
  165. package/src/runtime/AGENTS.md +6 -0
  166. package/src/runtime/__tests__/agent-wake.test.ts +2 -2
  167. package/src/runtime/agent-wake.ts +5 -5
  168. package/src/runtime/background-job-runner.ts +2 -2
  169. package/src/runtime/migrations/__tests__/vbundle-legacy-user-md.test.ts +150 -3
  170. package/src/runtime/migrations/vbundle-import-analyzer.ts +29 -6
  171. package/src/runtime/migrations/vbundle-import-policy.ts +23 -0
  172. package/src/runtime/migrations/vbundle-importer.ts +9 -4
  173. package/src/runtime/migrations/vbundle-streaming-importer.ts +8 -3
  174. package/src/runtime/pre-first-message-gate.ts +1 -1
  175. package/src/runtime/routes/__tests__/conversation-compaction-routes.test.ts +241 -0
  176. package/src/runtime/routes/__tests__/gateway-log-routes.test.ts +97 -185
  177. package/src/runtime/routes/__tests__/home-feed-routes.test.ts +17 -0
  178. package/src/runtime/routes/__tests__/plugins-routes.test.ts +1 -0
  179. package/src/runtime/routes/__tests__/task-routes.test.ts +3 -3
  180. package/src/runtime/routes/btw-routes.ts +0 -14
  181. package/src/runtime/routes/conversation-compaction-routes.ts +86 -19
  182. package/src/runtime/routes/conversation-list-routes.ts +77 -5
  183. package/src/runtime/routes/conversation-management-routes.ts +54 -0
  184. package/src/runtime/routes/gateway-log-routes.ts +14 -64
  185. package/src/runtime/routes/home-feed-routes.ts +10 -0
  186. package/src/runtime/routes/identity-intro-cache.ts +1 -1
  187. package/src/runtime/routes/identity-routes.ts +76 -20
  188. package/src/runtime/routes/inbound-message-handler.ts +0 -36
  189. package/src/runtime/routes/plugins-routes.ts +21 -0
  190. package/src/runtime/routes/schedule-routes.ts +19 -2
  191. package/src/runtime/routes/trust-rules-routes.ts +14 -67
  192. package/src/schedule/recurrence-engine.ts +34 -0
  193. package/src/schedule/scheduler.ts +1 -0
  194. package/src/skills/validate-input.ts +41 -1
  195. package/src/subagent/types.ts +26 -1
  196. package/src/telemetry/types.ts +15 -1
  197. package/src/telemetry/usage-telemetry-reporter.test.ts +6 -1
  198. package/src/telemetry/usage-telemetry-reporter.ts +1 -0
  199. package/src/tools/apps/executors.ts +1 -1
  200. package/src/tools/skills/skill-tool-factory.ts +19 -8
  201. package/src/usage/types.ts +8 -1
  202. package/src/util/platform.ts +16 -0
  203. package/src/watcher/engine.ts +1 -0
  204. package/src/workspace/adaptive-thinking-repair.ts +113 -0
  205. package/src/workspace/migrations/097-enable-adaptive-thinking-managed-profiles.ts +70 -67
  206. package/src/workspace/migrations/098-remove-stale-updates-bulletin-file.ts +31 -0
  207. package/src/workspace/migrations/099-disable-cache-one-shot-callsites.ts +81 -0
  208. package/src/workspace/migrations/registry.ts +4 -0
  209. package/src/__tests__/config-loader-quarantine-bulletin.test.ts +0 -202
  210. package/src/__tests__/conversation-starters-cadence.test.ts +0 -161
  211. package/src/__tests__/guardian-action-followup-executor.test.ts +0 -322
  212. package/src/__tests__/guardian-action-followup-store.test.ts +0 -373
  213. package/src/__tests__/guardian-action-late-reply.test.ts +0 -1083
  214. package/src/__tests__/update-bulletin-job.test.ts +0 -292
  215. package/src/config/schemas/updates.ts +0 -14
  216. package/src/memory/__tests__/compaction-log-writer-clickhouse.test.ts +0 -227
  217. package/src/memory/conversation-starters-cadence.ts +0 -78
  218. package/src/prompts/update-bulletin-job.ts +0 -180
  219. package/src/runtime/guardian-action-followup-executor.ts +0 -306
@@ -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,
@@ -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
  }
@@ -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,
@@ -0,0 +1,113 @@
1
+ import { existsSync, readFileSync, writeFileSync } from "node:fs";
2
+ import { join } from "node:path";
3
+
4
+ // Enable adaptive thinking on the managed "balanced" and "quality-optimized"
5
+ // profiles.
6
+ //
7
+ // The assistant-side seed defaults (MANAGED_PROFILE_TEMPLATES in
8
+ // seed-inference-profiles.ts) already ship thinking: { enabled: true,
9
+ // streamThinking: true } for both profiles, which normalizes to
10
+ // { type: "adaptive" } on the wire. Off-platform (BYOK) instances pick this
11
+ // up on every boot because the seeder overwrites managed profiles. On-platform
12
+ // instances preserve existing profiles (the platform overlay is authoritative),
13
+ // so instances that were hatched before thinking was enabled in the templates
14
+ // are stuck with thinking disabled or absent.
15
+ //
16
+ // Workspace migration 097 patches the on-disk config once, but it runs before
17
+ // mergeDefaultWorkspaceConfig() which can overwrite the fix with overlay
18
+ // profiles that have thinking disabled or absent. lifecycle.ts calls this
19
+ // repair again after the overlay merge + profile seeding so the fix sticks.
20
+ // The migration keeps its own frozen copy of this logic (migration files are
21
+ // self-contained snapshots and must not be imported from).
22
+ //
23
+ // The repair patches both profiles, adding thinking: { enabled: true,
24
+ // streamThinking: true } where it's missing or explicitly disabled. It skips
25
+ // profiles that:
26
+ // - Don't exist (no profile to patch)
27
+ // - Already have thinking enabled (idempotent)
28
+ // - Are source: "user" (user-created profiles are untouched)
29
+ // - Have a non-managed, non-absent source (unknown origin)
30
+ // - Resolve to a non-Anthropic provider (adaptive thinking is
31
+ // Anthropic-specific). A profile with no explicit provider inherits
32
+ // llm.default.provider, so the check falls back to that — with a
33
+ // completely absent llm.default.provider treated as Anthropic, matching
34
+ // migration 052's own `?? "anthropic"` default. This keeps the legacy
35
+ // non-Anthropic empty `{}` shells seeded by migration 052 off the repair.
36
+
37
+ const ADAPTIVE_THINKING = { enabled: true, streamThinking: true } as const;
38
+ const TARGET_PROFILES = ["balanced", "quality-optimized"] as const;
39
+
40
+ /**
41
+ * Patch managed Anthropic profiles that are missing adaptive thinking.
42
+ *
43
+ * Idempotent: profiles that already have thinking enabled are skipped.
44
+ */
45
+ export function repairAdaptiveThinkingOnManagedProfiles(
46
+ workspaceDir: string,
47
+ ): void {
48
+ const configPath = join(workspaceDir, "config.json");
49
+ if (!existsSync(configPath)) return;
50
+
51
+ let config: Record<string, unknown>;
52
+ try {
53
+ const raw = JSON.parse(readFileSync(configPath, "utf-8"));
54
+ if (!raw || typeof raw !== "object" || Array.isArray(raw)) return;
55
+ config = raw as Record<string, unknown>;
56
+ } catch {
57
+ return;
58
+ }
59
+
60
+ const llm = readObject(config.llm);
61
+ if (llm === null) return;
62
+
63
+ const profiles = readObject(llm.profiles);
64
+ if (profiles === null) return;
65
+
66
+ // Profiles without an explicit provider inherit llm.default.provider at
67
+ // resolution time; an absent llm.default.provider resolves to Anthropic.
68
+ const defaultBlock = readObject(llm.default);
69
+ const defaultProvider =
70
+ typeof defaultBlock?.provider === "string"
71
+ ? defaultBlock.provider
72
+ : "anthropic";
73
+
74
+ let changed = false;
75
+
76
+ for (const name of TARGET_PROFILES) {
77
+ const profile = readObject(profiles[name]);
78
+ if (profile === null) continue;
79
+
80
+ // Only patch managed Anthropic profiles.
81
+ // Legacy profiles created before the `source` metadata field was introduced
82
+ // have source=undefined. Treat these as managed when the profile name is one
83
+ // of the canonical managed names (which TARGET_PROFILES already guarantees)
84
+ // and the effective provider — explicit, or inherited from llm.default — is
85
+ // Anthropic. Explicit `source: "user"` profiles are always skipped.
86
+ if (profile.source === "user") continue;
87
+ if (profile.source !== undefined && profile.source !== "managed") continue;
88
+ const effectiveProvider =
89
+ typeof profile.provider === "string" ? profile.provider : defaultProvider;
90
+ if (effectiveProvider !== "anthropic") continue;
91
+
92
+ // Skip if thinking is already enabled.
93
+ const thinking = readObject(profile.thinking);
94
+ if (thinking !== null && thinking.enabled === true) continue;
95
+
96
+ profile.thinking = { ...ADAPTIVE_THINKING };
97
+ profiles[name] = profile;
98
+ changed = true;
99
+ }
100
+
101
+ if (changed) {
102
+ llm.profiles = profiles;
103
+ config.llm = llm;
104
+ writeFileSync(configPath, JSON.stringify(config, null, 2) + "\n");
105
+ }
106
+ }
107
+
108
+ function readObject(value: unknown): Record<string, unknown> | null {
109
+ if (value === null || typeof value !== "object" || Array.isArray(value)) {
110
+ return null;
111
+ }
112
+ return value as Record<string, unknown>;
113
+ }