@vellumai/assistant 0.10.7-dev.202607100720.fba456e → 0.10.7-dev.202607101106.c87f4e3

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 (58) hide show
  1. package/openapi.yaml +0 -4
  2. package/package.json +1 -1
  3. package/src/__tests__/compaction-trail-store.test.ts +2 -1
  4. package/src/__tests__/conversation-app-control-instantiation.test.ts +20 -3
  5. package/src/__tests__/conversation-runtime-assembly.test.ts +2 -0
  6. package/src/__tests__/conversation-skill-tools.test.ts +31 -6
  7. package/src/__tests__/list-messages-queued.test.ts +9 -3
  8. package/src/__tests__/llm-request-log-agent-loop-exit-reason.test.ts +9 -9
  9. package/src/__tests__/llm-request-log-call-site.test.ts +5 -5
  10. package/src/__tests__/llm-request-log-disabled-writes.test.ts +91 -0
  11. package/src/__tests__/media-generate-image.test.ts +20 -7
  12. package/src/__tests__/platform-bash-auto-approve.test.ts +19 -2
  13. package/src/__tests__/registry.test.ts +18 -0
  14. package/src/__tests__/require-fresh-approval.test.ts +15 -0
  15. package/src/__tests__/skill-projection-feature-flag.test.ts +35 -8
  16. package/src/__tests__/skill-projection.benchmark.test.ts +7 -2
  17. package/src/__tests__/subagent-detail.test.ts +1 -0
  18. package/src/__tests__/summarize-boundary.test.ts +1 -0
  19. package/src/__tests__/tool-approval-handler.test.ts +1 -0
  20. package/src/__tests__/tool-execution-pipeline.benchmark.test.ts +1 -0
  21. package/src/__tests__/tool-executor-lifecycle-events.test.ts +21 -17
  22. package/src/__tests__/tool-executor.test.ts +36 -18
  23. package/src/__tests__/tool-grant-request-escalation.test.ts +1 -0
  24. package/src/__tests__/trusted-contact-inline-approval-integration.test.ts +1 -0
  25. package/src/__tests__/verification-control-plane-policy.test.ts +13 -0
  26. package/src/api/responses/conversation-message.ts +0 -15
  27. package/src/config/schemas/__tests__/llm-request-logs.test.ts +33 -5
  28. package/src/config/schemas/llm-request-logs.ts +78 -27
  29. package/src/daemon/conversation-skill-tools.ts +27 -9
  30. package/src/export/__tests__/transcript-formatter.test.ts +5 -0
  31. package/src/permissions/checker.test.ts +3 -6
  32. package/src/permissions/checker.ts +2 -2
  33. package/src/persistence/__tests__/message-content-file.test.ts +150 -0
  34. package/src/persistence/__tests__/slow-query-log.test.ts +125 -0
  35. package/src/persistence/conversation-crud.ts +11 -1
  36. package/src/persistence/llm-request-log-store.ts +36 -2
  37. package/src/persistence/message-content-file.ts +190 -0
  38. package/src/persistence/migrations/324-message-finalized-column.ts +26 -0
  39. package/src/persistence/migrations/__tests__/297-move-llm-request-logs.test.ts +2 -2
  40. package/src/persistence/schema/conversations.ts +15 -0
  41. package/src/persistence/slow-query-log.ts +58 -21
  42. package/src/persistence/steps.ts +2 -0
  43. package/src/runtime/routes/__tests__/conversation-compaction-routes.test.ts +31 -2
  44. package/src/runtime/routes/__tests__/conversation-query-routes.test.ts +67 -1
  45. package/src/runtime/routes/conversation-compaction-routes.ts +5 -0
  46. package/src/runtime/routes/conversation-query-routes.ts +5 -0
  47. package/src/runtime/routes/conversation-routes.ts +2 -8
  48. package/src/runtime/routes/errors.ts +17 -0
  49. package/src/runtime/routes/llm-request-logs-access.ts +19 -0
  50. package/src/tasks/tool-sanitizer.ts +3 -1
  51. package/src/telemetry/turn-trace-store.test.ts +4 -0
  52. package/src/telemetry/turn-trace-store.ts +1 -4
  53. package/src/tools/registry.ts +89 -11
  54. package/src/tools/run-standalone.ts +5 -13
  55. package/src/tools/workspace-tools/loader.ts +24 -8
  56. package/src/util/sqlite-query-label.ts +51 -0
  57. package/src/util/sqlite-retry.ts +14 -2
  58. package/src/workflows/capabilities.ts +12 -4
package/openapi.yaml CHANGED
@@ -17727,10 +17727,6 @@ paths:
17727
17727
  enum:
17728
17728
  - user
17729
17729
  - assistant
17730
- content:
17731
- type: string
17732
- deprecated: true
17733
- description: "Deprecated: superseded by contentBlocks. Flat plain-text body (joined text segments)."
17734
17730
  timestamp:
17735
17731
  type: string
17736
17732
  attachments:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vellumai/assistant",
3
- "version": "0.10.7-dev.202607100720.fba456e",
3
+ "version": "0.10.7-dev.202607101106.c87f4e3",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "exports": {
@@ -60,6 +60,7 @@ function insertLogAt(
60
60
  callSite: "mainAgent" | "compactionAgent" | null,
61
61
  requestPayload = "{}",
62
62
  ): string {
63
+ // Logging is enabled in these tests, so the write always returns an id.
63
64
  const id = recordRequestLog(
64
65
  conversationId,
65
66
  requestPayload,
@@ -67,7 +68,7 @@ function insertLogAt(
67
68
  undefined,
68
69
  "anthropic",
69
70
  callSite ?? undefined,
70
- );
71
+ )!;
71
72
  // Use the Drizzle update builder rather than `db.run("UPDATE … ?")` —
72
73
  // the drizzle wrapper doesn't accept positional parameters the same
73
74
  // way `bun:sqlite` does, and a silent no-op there manifests as zero
@@ -51,7 +51,9 @@ mock.module("../util/logger.js", () => ({
51
51
  let appControlFlagEnabled = false;
52
52
  mock.module("../config/assistant-feature-flags.js", () => ({
53
53
  isAssistantFeatureFlagEnabled: (key: string) => {
54
- if (key === "app-control") return appControlFlagEnabled;
54
+ if (key === "app-control") {
55
+ return appControlFlagEnabled;
56
+ }
55
57
  return true;
56
58
  },
57
59
  loadDefaultsRegistry: () => ({}),
@@ -143,7 +145,20 @@ mock.module("../tools/registry.js", () => ({
143
145
  let found: Tool | undefined;
144
146
  for (const tools of mockRegisteredTools.values()) {
145
147
  for (const tool of tools) {
146
- if (tool.name === name) found = tool;
148
+ if (tool.name === name) {
149
+ found = tool;
150
+ }
151
+ }
152
+ }
153
+ return found;
154
+ },
155
+ resolveTool: (name: string) => {
156
+ let found: Tool | undefined;
157
+ for (const tools of mockRegisteredTools.values()) {
158
+ for (const tool of tools) {
159
+ if (tool.name === name) {
160
+ found = tool;
161
+ }
147
162
  }
148
163
  }
149
164
  return found;
@@ -158,7 +173,9 @@ mock.module("../tools/registry.js", () => ({
158
173
  let ownerSkillId: string | undefined;
159
174
  for (const [skillId, tools] of mockRegisteredTools.entries()) {
160
175
  for (const tool of tools) {
161
- if (tool.name === name) ownerSkillId = skillId;
176
+ if (tool.name === name) {
177
+ ownerSkillId = skillId;
178
+ }
162
179
  }
163
180
  }
164
181
  return ownerSkillId === undefined
@@ -3220,6 +3220,7 @@ describe("Slack channel chronological rendering — multi-thread", () => {
3220
3220
  createdAt: opts.createdAt,
3221
3221
  metadata: Object.keys(outer).length > 0 ? JSON.stringify(outer) : null,
3222
3222
  clientMessageId: null,
3223
+ finalized: 1,
3223
3224
  };
3224
3225
  }
3225
3226
 
@@ -3241,6 +3242,7 @@ describe("Slack channel chronological rendering — multi-thread", () => {
3241
3242
  createdAt: opts.createdAt,
3242
3243
  metadata: Object.keys(outer).length > 0 ? JSON.stringify(outer) : null,
3243
3244
  clientMessageId: null,
3245
+ finalized: 1,
3244
3246
  };
3245
3247
  }
3246
3248
 
@@ -57,7 +57,9 @@ mock.module("../config/skills.js", () => ({
57
57
  ? skills
58
58
  : skills.filter((skill) => {
59
59
  const owner = skill.owner;
60
- if (owner?.kind !== "plugin") return true;
60
+ if (owner?.kind !== "plugin") {
61
+ return true;
62
+ }
61
63
  return effectiveEnabledPluginSet.has(owner.id);
62
64
  }),
63
65
  }));
@@ -82,10 +84,16 @@ mock.module("../skills/active-skill-tools.js", () => {
82
84
  const entries: Array<{ id: string; version?: string }> = [];
83
85
  for (const msg of messages) {
84
86
  for (const block of msg.content) {
85
- if (block.type !== "tool_result") continue;
86
- if (!skillLoadUseIds.has(block.tool_use_id)) continue;
87
+ if (block.type !== "tool_result") {
88
+ continue;
89
+ }
90
+ if (!skillLoadUseIds.has(block.tool_use_id)) {
91
+ continue;
92
+ }
87
93
  const text = block.content;
88
- if (!text) continue;
94
+ if (!text) {
95
+ continue;
96
+ }
89
97
  for (const match of text.matchAll(re)) {
90
98
  if (!seen.has(match[1])) {
91
99
  seen.add(match[1]);
@@ -170,7 +178,22 @@ mock.module("../tools/registry.js", () => ({
170
178
  let found: Tool | undefined;
171
179
  for (const tools of mockRegisteredTools.values()) {
172
180
  for (const tool of tools) {
173
- if (tool.name === name) found = tool;
181
+ if (tool.name === name) {
182
+ found = tool;
183
+ }
184
+ }
185
+ }
186
+ return found;
187
+ },
188
+ // Sync peek mirrors getTool — the skill-tool projection path reads via
189
+ // resolveTool synchronously.
190
+ resolveTool: (name: string): Tool | undefined => {
191
+ let found: Tool | undefined;
192
+ for (const tools of mockRegisteredTools.values()) {
193
+ for (const tool of tools) {
194
+ if (tool.name === name) {
195
+ found = tool;
196
+ }
174
197
  }
175
198
  }
176
199
  return found;
@@ -184,7 +207,9 @@ mock.module("../tools/registry.js", () => ({
184
207
  let ownerSkillId: string | undefined;
185
208
  for (const [skillId, tools] of mockRegisteredTools.entries()) {
186
209
  for (const tool of tools) {
187
- if (tool.name === name) ownerSkillId = skillId;
210
+ if (tool.name === name) {
211
+ ownerSkillId = skillId;
212
+ }
188
213
  }
189
214
  }
190
215
  return ownerSkillId === undefined
@@ -54,7 +54,7 @@ function resetTables() {
54
54
  interface MessagePayload {
55
55
  id: string;
56
56
  role: string;
57
- content?: string;
57
+ contentBlocks?: Array<{ type: string; text?: string }>;
58
58
  clientMessageId?: string;
59
59
  queueStatus?: "queued" | "processing";
60
60
  queuePosition?: number;
@@ -123,7 +123,10 @@ describe("handleListMessages in-memory queue", () => {
123
123
  expect(first.queuePosition).toBe(1);
124
124
  expect(first.role).toBe("user");
125
125
  expect(first.id).toBe("req-queued-1");
126
- expect(first.content).toBe("please also do this");
126
+ expect(first.contentBlocks?.[0]).toEqual({
127
+ type: "text",
128
+ text: "please also do this",
129
+ });
127
130
  expect(first.clientMessageId).toBe("nonce-queued");
128
131
 
129
132
  const second = body.messages[2];
@@ -148,7 +151,10 @@ describe("handleListMessages in-memory queue", () => {
148
151
  const body = response as { messages: MessagePayload[] };
149
152
 
150
153
  expect(body.messages).toHaveLength(1);
151
- expect(body.messages[0].content).toBe("what the user actually typed");
154
+ expect(body.messages[0].contentBlocks?.[0]).toEqual({
155
+ type: "text",
156
+ text: "what the user actually typed",
157
+ });
152
158
  });
153
159
 
154
160
  test("renders queued attachments with a derived kind", async () => {
@@ -46,7 +46,7 @@ describe("setAgentLoopExitReasonOnLatestLog", () => {
46
46
 
47
47
  test("recordRequestLog leaves agentLoopExitReason NULL", () => {
48
48
  const id = recordRequestLog("conv-1", '{"req":1}', '{"res":1}');
49
- const row = getRequestLogById(id);
49
+ const row = getRequestLogById(id!);
50
50
  expect(row).not.toBeNull();
51
51
  expect(row!.agentLoopExitReason).toBeNull();
52
52
  });
@@ -61,8 +61,8 @@ describe("setAgentLoopExitReasonOnLatestLog", () => {
61
61
 
62
62
  setAgentLoopExitReasonOnLatestLog("conv-1", "no_tool_calls");
63
63
 
64
- expect(getRequestLogById(first)?.agentLoopExitReason).toBeNull();
65
- expect(getRequestLogById(second)?.agentLoopExitReason).toBe(
64
+ expect(getRequestLogById(first!)?.agentLoopExitReason).toBeNull();
65
+ expect(getRequestLogById(second!)?.agentLoopExitReason).toBe(
66
66
  "no_tool_calls",
67
67
  );
68
68
  });
@@ -74,10 +74,10 @@ describe("setAgentLoopExitReasonOnLatestLog", () => {
74
74
 
75
75
  setAgentLoopExitReasonOnLatestLog("conv-a", "yield_to_user");
76
76
 
77
- expect(getRequestLogById(a)?.agentLoopExitReason).toBe("yield_to_user");
77
+ expect(getRequestLogById(a!)?.agentLoopExitReason).toBe("yield_to_user");
78
78
  // conv-b is later overall but belongs to a different conversation —
79
79
  // must stay NULL.
80
- expect(getRequestLogById(b)?.agentLoopExitReason).toBeNull();
80
+ expect(getRequestLogById(b!)?.agentLoopExitReason).toBeNull();
81
81
  });
82
82
 
83
83
  test("no-op when conversation has no logs", () => {
@@ -90,12 +90,12 @@ describe("setAgentLoopExitReasonOnLatestLog", () => {
90
90
  // Previous run: completes, lands a log, gets stamped.
91
91
  const prev = recordRequestLog("conv-1", '{"prev_req":1}', '{"prev_res":1}');
92
92
  setAgentLoopExitReasonOnLatestLog("conv-1", "no_tool_calls");
93
- expect(getRequestLogById(prev)?.agentLoopExitReason).toBe("no_tool_calls");
93
+ expect(getRequestLogById(prev!)?.agentLoopExitReason).toBe("no_tool_calls");
94
94
 
95
95
  // Current run aborts pre-call (or similar) before any LLM call lands.
96
96
  // The helper must NOT overwrite the previous run's row.
97
97
  setAgentLoopExitReasonOnLatestLog("conv-1", "aborted_pre_call");
98
- expect(getRequestLogById(prev)?.agentLoopExitReason).toBe("no_tool_calls");
98
+ expect(getRequestLogById(prev!)?.agentLoopExitReason).toBe("no_tool_calls");
99
99
  });
100
100
 
101
101
  test("stamps the current run's newest row even when a prior row is already stamped", () => {
@@ -112,8 +112,8 @@ describe("setAgentLoopExitReasonOnLatestLog", () => {
112
112
  );
113
113
  setAgentLoopExitReasonOnLatestLog("conv-1", "yield_to_user");
114
114
 
115
- expect(getRequestLogById(prev)?.agentLoopExitReason).toBe("no_tool_calls");
116
- expect(getRequestLogById(current)?.agentLoopExitReason).toBe(
115
+ expect(getRequestLogById(prev!)?.agentLoopExitReason).toBe("no_tool_calls");
116
+ expect(getRequestLogById(current!)?.agentLoopExitReason).toBe(
117
117
  "yield_to_user",
118
118
  );
119
119
  });
@@ -52,14 +52,14 @@ describe("recordRequestLog call_site stamping", () => {
52
52
  "anthropic",
53
53
  "mainAgent",
54
54
  );
55
- const row = getRequestLogById(id);
55
+ const row = getRequestLogById(id!);
56
56
  expect(row).not.toBeNull();
57
57
  expect(row!.callSite).toBe("mainAgent");
58
58
  });
59
59
 
60
60
  test("leaves callSite NULL when omitted (backward compat)", () => {
61
61
  const id = recordRequestLog("conv-1", '{"req":1}', '{"res":1}');
62
- const row = getRequestLogById(id);
62
+ const row = getRequestLogById(id!);
63
63
  expect(row).not.toBeNull();
64
64
  expect(row!.callSite).toBeNull();
65
65
  });
@@ -73,7 +73,7 @@ describe("recordRequestLog call_site stamping", () => {
73
73
  "anthropic",
74
74
  "compactionAgent",
75
75
  );
76
- expect(getRequestLogById(id)?.callSite).toBe("compactionAgent");
76
+ expect(getRequestLogById(id!)?.callSite).toBe("compactionAgent");
77
77
  });
78
78
 
79
79
  test("two rows in the same conversation can carry different callSites", () => {
@@ -93,8 +93,8 @@ describe("recordRequestLog call_site stamping", () => {
93
93
  "anthropic",
94
94
  "compactionAgent",
95
95
  );
96
- expect(getRequestLogById(mainId)?.callSite).toBe("mainAgent");
97
- expect(getRequestLogById(compactId)?.callSite).toBe("compactionAgent");
96
+ expect(getRequestLogById(mainId!)?.callSite).toBe("mainAgent");
97
+ expect(getRequestLogById(compactId!)?.callSite).toBe("compactionAgent");
98
98
  });
99
99
  });
100
100
 
@@ -0,0 +1,91 @@
1
+ /**
2
+ * Tests the master switch for LLM request logging (`llmRequestLogs.enabled`).
3
+ * When logging is off, the store's insert paths (`recordRequestLog`,
4
+ * `recordSyntheticAgentErrorMessageLog`) must skip the write entirely — no
5
+ * prompt/completion payload lands on disk — and return `null`. The read-side
6
+ * 4xx is exercised separately at the route layer.
7
+ */
8
+ import { afterAll, beforeEach, describe, expect, mock, test } from "bun:test";
9
+
10
+ mock.module("../util/logger.js", () => ({
11
+ getLogger: () =>
12
+ new Proxy({} as Record<string, unknown>, {
13
+ get: () => () => {},
14
+ }),
15
+ }));
16
+
17
+ // Mutable so each test toggles the flag the store reads via `getConfigReadOnly`.
18
+ let enabled = true;
19
+ mock.module("../config/loader.js", () => ({
20
+ getConfig: () => ({
21
+ llmRequestLogs: { readSource: "local", enabled },
22
+ }),
23
+ getConfigReadOnly: () => ({
24
+ llmRequestLogs: { readSource: "local", enabled },
25
+ }),
26
+ }));
27
+
28
+ // `mock.module()` persists process-wide; reset so other files don't inherit a
29
+ // stale value.
30
+ afterAll(() => {
31
+ enabled = true;
32
+ });
33
+
34
+ import { getLogsDb } from "../persistence/db-connection.js";
35
+ import { initializeDb } from "../persistence/db-init.js";
36
+ import {
37
+ getRequestLogById,
38
+ getRequestLogsByConversationId,
39
+ recordRequestLog,
40
+ recordSyntheticAgentErrorMessageLog,
41
+ } from "../persistence/llm-request-log-store.js";
42
+ import { llmRequestLogs } from "../persistence/schema/index.js";
43
+
44
+ await initializeDb();
45
+
46
+ function resetLogs(): void {
47
+ getLogsDb()!.delete(llmRequestLogs).run();
48
+ }
49
+
50
+ describe("llmRequestLogs.enabled write gate", () => {
51
+ beforeEach(() => {
52
+ resetLogs();
53
+ enabled = true;
54
+ });
55
+
56
+ test("recordRequestLog writes normally when logging is enabled", () => {
57
+ const id = recordRequestLog("conv-1", '{"req":1}', '{"res":1}');
58
+ expect(id).not.toBeNull();
59
+ expect(getRequestLogById(id!)).not.toBeNull();
60
+ });
61
+
62
+ test("recordRequestLog skips the write when logging is disabled", () => {
63
+ enabled = false;
64
+ const id = recordRequestLog("conv-1", '{"req":1}', '{"res":1}');
65
+ expect(id).toBeNull();
66
+ expect(getRequestLogsByConversationId("conv-1")).toEqual([]);
67
+ });
68
+
69
+ test("recordSyntheticAgentErrorMessageLog skips the write when disabled", () => {
70
+ enabled = false;
71
+ const id = recordSyntheticAgentErrorMessageLog({
72
+ conversationId: "conv-2",
73
+ messageId: "msg-2",
74
+ exitReason: "budget_yield_unrecovered",
75
+ noticeText: "Out of budget.",
76
+ preparedRequest: null,
77
+ createdAt: Date.now(),
78
+ });
79
+ expect(id).toBeNull();
80
+ expect(getRequestLogsByConversationId("conv-2")).toEqual([]);
81
+ });
82
+
83
+ test("re-enabling logging restores writes", () => {
84
+ enabled = false;
85
+ expect(recordRequestLog("conv-3", '{"req":1}', '{"res":1}')).toBeNull();
86
+ enabled = true;
87
+ const id = recordRequestLog("conv-3", '{"req":2}', '{"res":2}');
88
+ expect(id).not.toBeNull();
89
+ expect(getRequestLogsByConversationId("conv-3")).toHaveLength(1);
90
+ });
91
+ });
@@ -44,13 +44,21 @@ mock.module("../config/loader.js", () => ({
44
44
 
45
45
  mock.module("../security/secure-keys.js", () => ({
46
46
  getSecureKeyAsync: async (account: string) => {
47
- if (account === "gemini") return mockGeminiKey;
48
- if (account === "openai") return mockOpenAIKey;
47
+ if (account === "gemini") {
48
+ return mockGeminiKey;
49
+ }
50
+ if (account === "openai") {
51
+ return mockOpenAIKey;
52
+ }
49
53
  return undefined;
50
54
  },
51
55
  getProviderKeyAsync: async (provider: string) => {
52
- if (provider === "gemini") return mockGeminiKey;
53
- if (provider === "openai") return mockOpenAIKey;
56
+ if (provider === "gemini") {
57
+ return mockGeminiKey;
58
+ }
59
+ if (provider === "openai") {
60
+ return mockOpenAIKey;
61
+ }
54
62
  return undefined;
55
63
  },
56
64
  }));
@@ -63,13 +71,16 @@ mock.module("../media/image-service.js", () => ({
63
71
  ) => {
64
72
  lastGenerateProvider = provider;
65
73
  lastGenerateCredentials = credentials;
66
- if (mockGenerateError) throw mockGenerateError;
74
+ if (mockGenerateError) {
75
+ throw mockGenerateError;
76
+ }
67
77
  return mockGenerateResult;
68
78
  },
69
79
  mapImageGenError: (provider: unknown, error: unknown) => {
70
80
  const providerLabel = provider === "openai" ? "OpenAI" : "Gemini";
71
- if (error instanceof Error)
81
+ if (error instanceof Error) {
72
82
  return `Mock ${providerLabel} error: ${error.message}`;
83
+ }
73
84
  return `Mock ${providerLabel} unknown error`;
74
85
  },
75
86
  }));
@@ -394,7 +405,9 @@ describe("image-studio skill script wrapper", () => {
394
405
  expect(result.content).toContain("Generated 1 image");
395
406
  } finally {
396
407
  const { unlink } = await import("fs/promises");
397
- if (await Bun.file(tmpPath).exists()) await unlink(tmpPath);
408
+ if (await Bun.file(tmpPath).exists()) {
409
+ await unlink(tmpPath);
410
+ }
398
411
  }
399
412
  });
400
413
 
@@ -85,7 +85,9 @@ mock.module("../permissions/checker.js", () => ({
85
85
  isDynamicSkillLoadInvocation: () => false,
86
86
  classifyRisk: async () => ({ level: riskOverride }),
87
87
  check: async () => {
88
- if (checkResultOverride) return checkResultOverride;
88
+ if (checkResultOverride) {
89
+ return checkResultOverride;
90
+ }
89
91
  return { decision: "allow", reason: "allowed" };
90
92
  },
91
93
  generateAllowlistOptions: () => [
@@ -106,7 +108,22 @@ mock.module("../telemetry/tool-usage-store.js", () => ({
106
108
 
107
109
  mock.module("../tools/registry.js", () => ({
108
110
  getTool: (name: string) => {
109
- if (name === "unknown_tool") return undefined;
111
+ if (name === "unknown_tool") {
112
+ return undefined;
113
+ }
114
+ return {
115
+ name,
116
+ description: "test tool",
117
+ category: "shell",
118
+ defaultRiskLevel: "medium",
119
+ input_schema: {},
120
+ execute: async () => fakeToolResult,
121
+ };
122
+ },
123
+ resolveTool: (name: string) => {
124
+ if (name === "unknown_tool") {
125
+ return undefined;
126
+ }
110
127
  return {
111
128
  name,
112
129
  description: "test tool",
@@ -17,6 +17,7 @@ import {
17
17
  registerSkillTools,
18
18
  registerTool,
19
19
  registerWorkspaceTools,
20
+ resolveTool,
20
21
  unregisterSkillTools,
21
22
  unregisterWorkspaceTool,
22
23
  } from "../tools/registry.js";
@@ -109,6 +110,23 @@ describe("tool registry dynamic-tools tools", () => {
109
110
  });
110
111
  });
111
112
 
113
+ describe("resolveTool lazy initialization", () => {
114
+ test("await resolveTool initializes a cold registry, then sync getTool sees it", async () => {
115
+ // Empty the registry and clear the cached init promise.
116
+ __clearRegistryForTesting();
117
+ // A synchronous peek sees nothing yet — the registry is cold.
118
+ expect(getTool("file_read")).toBeUndefined();
119
+
120
+ // The async getter awaits initialization (like getHooksFor awaits its
121
+ // reconcile), so it resolves the built-in instead of a spurious undefined.
122
+ const tool = await resolveTool("file_read");
123
+ expect(tool?.name).toBe("file_read");
124
+
125
+ // After the ensure, the synchronous hot-path read sees it too.
126
+ expect(getTool("file_read")?.name).toBe("file_read");
127
+ });
128
+ });
129
+
112
130
  describe("tool manifest", () => {
113
131
  test("explicit tools list includes memory tools", () => {
114
132
  const names = explicitTools.map((t) => t.name);
@@ -157,6 +157,21 @@ mock.module("../tools/registry.js", () => ({
157
157
  execute: async () => fakeToolResult,
158
158
  };
159
159
  },
160
+ resolveTool: (name: string) => {
161
+ if (name === "unknown_tool") {
162
+ return undefined;
163
+ }
164
+ const isGmailTool = name.startsWith("gmail_");
165
+ return {
166
+ name,
167
+ description: "test tool",
168
+ category: isGmailTool ? "gmail" : "credential-execution",
169
+ defaultRiskLevel: "high",
170
+ executionTarget: isGmailTool ? ("host" as const) : undefined,
171
+ input_schema: {},
172
+ execute: async () => fakeToolResult,
173
+ };
174
+ },
160
175
  getAllTools: () => [],
161
176
  }));
162
177
 
@@ -55,7 +55,9 @@ let _mockOverrides: Record<string, boolean> = {};
55
55
  mock.module("../config/assistant-feature-flags.js", () => ({
56
56
  isAssistantFeatureFlagEnabled: (key: string, _config: unknown): boolean => {
57
57
  const explicit = _mockOverrides[key];
58
- if (typeof explicit === "boolean") return explicit;
58
+ if (typeof explicit === "boolean") {
59
+ return explicit;
60
+ }
59
61
  return false; // undeclared flags default to disabled
60
62
  },
61
63
  clearFeatureFlagOverridesCache: () => {
@@ -84,15 +86,23 @@ mock.module("../skills/active-skill-tools.js", () => {
84
86
  const entries: Array<{ id: string; version?: string }> = [];
85
87
  for (const msg of messages) {
86
88
  for (const block of msg.content) {
87
- if (block.type !== "tool_result") continue;
88
- if (!skillLoadUseIds.has(block.tool_use_id)) continue;
89
+ if (block.type !== "tool_result") {
90
+ continue;
91
+ }
92
+ if (!skillLoadUseIds.has(block.tool_use_id)) {
93
+ continue;
94
+ }
89
95
  const text = block.content;
90
- if (!text) continue;
96
+ if (!text) {
97
+ continue;
98
+ }
91
99
  for (const m of text.matchAll(re)) {
92
100
  if (!seen.has(m[1])) {
93
101
  seen.add(m[1]);
94
102
  const entry: { id: string; version?: string } = { id: m[1] };
95
- if (m[2]) entry.version = m[2];
103
+ if (m[2]) {
104
+ entry.version = m[2];
105
+ }
96
106
  entries.push(entry);
97
107
  }
98
108
  }
@@ -111,7 +121,9 @@ mock.module("../skills/tool-manifest.js", () => ({
111
121
  const parts = filePath.split("/");
112
122
  const skillId = parts[parts.length - 2];
113
123
  const manifest = mockManifests[skillId];
114
- if (!manifest) throw new Error(`Mock: no manifest for skill "${skillId}"`);
124
+ if (!manifest) {
125
+ throw new Error(`Mock: no manifest for skill "${skillId}"`);
126
+ }
115
127
  return manifest;
116
128
  },
117
129
  }));
@@ -162,7 +174,20 @@ mock.module("../tools/registry.js", () => ({
162
174
  let found: Tool | undefined;
163
175
  for (const tools of mockRegisteredTools.values()) {
164
176
  for (const tool of tools) {
165
- if (tool.name === name) found = tool;
177
+ if (tool.name === name) {
178
+ found = tool;
179
+ }
180
+ }
181
+ }
182
+ return found;
183
+ },
184
+ resolveTool: (name: string): Tool | undefined => {
185
+ let found: Tool | undefined;
186
+ for (const tools of mockRegisteredTools.values()) {
187
+ for (const tool of tools) {
188
+ if (tool.name === name) {
189
+ found = tool;
190
+ }
166
191
  }
167
192
  }
168
193
  return found;
@@ -177,7 +202,9 @@ mock.module("../tools/registry.js", () => ({
177
202
  let ownerSkillId: string | undefined;
178
203
  for (const [skillId, tools] of mockRegisteredTools.entries()) {
179
204
  for (const tool of tools) {
180
- if (tool.name === name) ownerSkillId = skillId;
205
+ if (tool.name === name) {
206
+ ownerSkillId = skillId;
207
+ }
181
208
  }
182
209
  }
183
210
  return ownerSkillId === undefined
@@ -175,15 +175,20 @@ mock.module("../tools/registry.js", () => ({
175
175
  skillId: string,
176
176
  tools: Array<{ name: string; [k: string]: unknown }>,
177
177
  ) => {
178
- for (const t of tools) benchmarkRegistry.set(t.name, { tool: t, skillId });
178
+ for (const t of tools) {
179
+ benchmarkRegistry.set(t.name, { tool: t, skillId });
180
+ }
179
181
  return tools;
180
182
  },
181
183
  unregisterSkillTools: (skillId: string) => {
182
184
  for (const [name, entry] of benchmarkRegistry) {
183
- if (entry.skillId === skillId) benchmarkRegistry.delete(name);
185
+ if (entry.skillId === skillId) {
186
+ benchmarkRegistry.delete(name);
187
+ }
184
188
  }
185
189
  },
186
190
  getTool: (name: string) => benchmarkRegistry.get(name)?.tool,
191
+ resolveTool: (name: string) => benchmarkRegistry.get(name)?.tool,
187
192
  getToolOwner: (name: string) => {
188
193
  const entry = benchmarkRegistry.get(name);
189
194
  return entry ? { kind: "skill" as const, id: entry.skillId } : undefined;
@@ -19,6 +19,7 @@ function msg(role: string, content: unknown[]): MessageRow {
19
19
  createdAt: Date.now(),
20
20
  metadata: null,
21
21
  clientMessageId: null,
22
+ finalized: 1,
22
23
  };
23
24
  }
24
25
 
@@ -13,6 +13,7 @@ function row(id: string, role: string, content: string): MessageRow {
13
13
  createdAt: 0,
14
14
  metadata: null,
15
15
  clientMessageId: null,
16
+ finalized: 1,
16
17
  };
17
18
  }
18
19
 
@@ -32,6 +32,7 @@ const fakeTool = {
32
32
 
33
33
  mock.module("../tools/registry.js", () => ({
34
34
  getTool: (name: string) => (name === "bash" ? fakeTool : undefined),
35
+ resolveTool: (name: string) => (name === "bash" ? fakeTool : undefined),
35
36
  getAllTools: () => [fakeTool],
36
37
  }));
37
38
 
@@ -52,6 +52,7 @@ mock.module("../config/skills.js", () => ({
52
52
 
53
53
  mock.module("../tools/registry.js", () => ({
54
54
  getTool: (name: string) => localRegistry.get(name),
55
+ resolveTool: (name: string) => localRegistry.get(name),
55
56
  getAllTools: () => Array.from(localRegistry.values()),
56
57
  registerTool: (tool: import("../tools/types.js").Tool) => {
57
58
  localRegistry.set(tool.name, tool);