@vellumai/assistant 0.9.0-dev.202606191938.289c607 → 0.9.0-dev.202606192038.d313bf2

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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vellumai/assistant",
3
- "version": "0.9.0-dev.202606191938.289c607",
3
+ "version": "0.9.0-dev.202606192038.d313bf2",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "exports": {
@@ -372,7 +372,9 @@ describe("projectSkillTools", () => {
372
372
  previouslyActiveSkillIds: sessionState,
373
373
  });
374
374
 
375
- // Tool definitions are no longer sent to the LLM — tools are invoked via skill_execute dispatch.
375
+ // Tool definitions are not sent to the LLM here — tools are invoked via
376
+ // skill_execute dispatch; weak-open-model first-class exposure resolves
377
+ // defs from the registry by name in createResolveToolsCallback.
376
378
  expect(result.toolDefinitions).toEqual([]);
377
379
  expect(result.allowedToolNames).toEqual(
378
380
  new Set(["deploy_run", "deploy_status"]),
@@ -0,0 +1,99 @@
1
+ /**
2
+ * Tests for resolveFirstClassSkillDefs: weak open models get loaded skill tools
3
+ * exposed first-class (resolved from the registry by name), capable models do
4
+ * not, and the turn allowlist still gates which defs are included.
5
+ */
6
+
7
+ import { afterEach, beforeEach, describe, expect, test } from "bun:test";
8
+
9
+ import { resolveFirstClassSkillDefs } from "../daemon/conversation-tool-setup.js";
10
+ import { RiskLevel } from "../permissions/types.js";
11
+ import { registerSkillTools, unregisterSkillTools } from "../tools/registry.js";
12
+ import type { Tool } from "../tools/types.js";
13
+
14
+ const MINIMAX = "accounts/fireworks/models/minimax-m3";
15
+ const CLAUDE = "claude-opus-4-8";
16
+
17
+ const SKILL_ID = "first-class-test-skill";
18
+
19
+ function makeSkillTool(name: string): Tool {
20
+ return {
21
+ name,
22
+ description: `Test tool ${name}`,
23
+ category: "testing",
24
+ defaultRiskLevel: RiskLevel.Low,
25
+ executionTarget: "host",
26
+ input_schema: {
27
+ type: "object",
28
+ properties: { content: { type: "string" } },
29
+ required: ["content"],
30
+ },
31
+ execute: async () => ({ content: "", isError: false }),
32
+ };
33
+ }
34
+
35
+ describe("resolveFirstClassSkillDefs", () => {
36
+ beforeEach(() => {
37
+ registerSkillTools(SKILL_ID, [
38
+ makeSkillTool("document_update"),
39
+ makeSkillTool("document_create"),
40
+ ]);
41
+ });
42
+
43
+ afterEach(() => {
44
+ unregisterSkillTools(SKILL_ID);
45
+ });
46
+
47
+ const allowed = new Set(["document_update", "document_create"]);
48
+ const turnAllowed = new Set([
49
+ "document_update",
50
+ "document_create",
51
+ "skill_execute",
52
+ ]);
53
+
54
+ test("exposes loaded skill tools for a weak open model", () => {
55
+ const defs = resolveFirstClassSkillDefs(allowed, turnAllowed, MINIMAX);
56
+ expect(defs.map((d) => d.name).sort()).toEqual([
57
+ "document_create",
58
+ "document_update",
59
+ ]);
60
+ });
61
+
62
+ test("returns nothing for a capable model", () => {
63
+ const defs = resolveFirstClassSkillDefs(allowed, turnAllowed, CLAUDE);
64
+ expect(defs).toEqual([]);
65
+ });
66
+
67
+ test("returns nothing when the model is unknown/absent", () => {
68
+ expect(resolveFirstClassSkillDefs(allowed, turnAllowed, null)).toEqual([]);
69
+ expect(resolveFirstClassSkillDefs(allowed, turnAllowed, undefined)).toEqual(
70
+ [],
71
+ );
72
+ });
73
+
74
+ test("respects the turn allowlist (subagent / exclude gating)", () => {
75
+ // Only document_update is allowed this turn — document_create is filtered.
76
+ const restricted = new Set(["document_update", "skill_execute"]);
77
+ const defs = resolveFirstClassSkillDefs(allowed, restricted, MINIMAX);
78
+ expect(defs.map((d) => d.name)).toEqual(["document_update"]);
79
+ });
80
+
81
+ test("skips names with no registered tool", () => {
82
+ const withGhost = new Set([...allowed, "not_registered"]);
83
+ const turn = new Set([...turnAllowed, "not_registered"]);
84
+ const defs = resolveFirstClassSkillDefs(withGhost, turn, MINIMAX);
85
+ expect(defs.map((d) => d.name).sort()).toEqual([
86
+ "document_create",
87
+ "document_update",
88
+ ]);
89
+ });
90
+
91
+ test("each exposed def carries its real scalar schema (single-escape)", () => {
92
+ const defs = resolveFirstClassSkillDefs(allowed, turnAllowed, MINIMAX);
93
+ const update = defs.find((d) => d.name === "document_update");
94
+ expect(update?.input_schema).toMatchObject({
95
+ properties: { content: { type: "string" } },
96
+ required: ["content"],
97
+ });
98
+ });
99
+ });
@@ -12,7 +12,12 @@ let providerRefreshCalls = 0;
12
12
  const PLATFORM_BASE_URL = "https://platform.example.com";
13
13
  const ASSISTANT_API_KEY_PATH = credentialKey("vellum", "assistant_api_key");
14
14
  const PLATFORM_BASE_URL_PATH = credentialKey("vellum", "platform_base_url");
15
- const MANAGED_PROVIDERS = ["anthropic", "openai", "gemini", "fireworks"] as const;
15
+ const MANAGED_PROVIDERS = [
16
+ "anthropic",
17
+ "openai",
18
+ "gemini",
19
+ "fireworks",
20
+ ] as const;
16
21
 
17
22
  let platformBaseUrlOverride: string | undefined;
18
23
 
@@ -116,6 +121,13 @@ mock.module("../util/logger.js", () => ({
116
121
  }),
117
122
  }));
118
123
 
124
+ // `handleAddSecret` fires this detached when a managed-proxy credential lands —
125
+ // a v2-memory side effect outside this suite's provider-registry scope. Stub it
126
+ // to a no-op; its behavior is covered by memory-v2-startup.test.ts.
127
+ mock.module("../daemon/memory-v2-startup.js", () => ({
128
+ maybeReseedCapabilitiesAfterManagedCredential: async () => {},
129
+ }));
130
+
119
131
  import {
120
132
  getProviderRoutingSource,
121
133
  initializeProviders,
@@ -199,7 +211,9 @@ describe("secret routes managed proxy registry sync", () => {
199
211
  test("provider API key writes notify live-conversation refresh listeners", async () => {
200
212
  await addApiKey("fireworks", "fw-key");
201
213
 
202
- expect(secureKeyStore[credentialKey("fireworks", "api_key")]).toBe("fw-key");
214
+ expect(secureKeyStore[credentialKey("fireworks", "api_key")]).toBe(
215
+ "fw-key",
216
+ );
203
217
  expect(providerRefreshCalls).toBe(1);
204
218
 
205
219
  await deleteApiKey("fireworks");
@@ -255,6 +255,24 @@ describe("createSkillTool — unknown parameter validation", () => {
255
255
  expect(result.isError).toBe(false);
256
256
  });
257
257
 
258
+ test("strips the harness `activity` field before validation", async () => {
259
+ const hash = computeSkillVersionHash(tempDir);
260
+ const tool = createSkillTool(
261
+ makeEntry({ executor: "echo.ts" }),
262
+ tempDir,
263
+ hash,
264
+ );
265
+
266
+ // A first-class skill-tool call may carry `activity` (the harness progress
267
+ // field). It must not be rejected as an unknown parameter.
268
+ const result = await tool.execute(
269
+ { query: "hello", activity: "Doing the thing" },
270
+ makeContext(),
271
+ );
272
+
273
+ expect(result.isError).toBe(false);
274
+ });
275
+
258
276
  test("allows empty input when schema has no required fields", async () => {
259
277
  const hash = computeSkillVersionHash(tempDir);
260
278
  const tool = createSkillTool(
@@ -360,7 +360,10 @@ export function projectSkillTools(
360
360
  return { toolDefinitions: [], allowedToolNames: new Set() };
361
361
  }
362
362
 
363
- // Tool definitions are no longer sent to the LLM — tools are invoked via skill_execute dispatch.
363
+ // Tool definitions are not sent to the LLM here — tools are invoked via
364
+ // skill_execute dispatch (capable models), and the weak-open-model
365
+ // first-class exposure resolves the registered defs from the registry in
366
+ // createResolveToolsCallback by name.
364
367
  const allToolNames = new Set<string>();
365
368
  const successfulEntries = new Map<string, string>();
366
369
  // Track skills already unregistered in the version-change branch so the
@@ -19,6 +19,7 @@ import type { PermissionPrompter } from "../permissions/prompter.js";
19
19
  import type { SecretPrompter } from "../permissions/secret-prompter.js";
20
20
  import { advisorEnabledForProfile } from "../plugins/defaults/advisor/advisor-gate.js";
21
21
  import type { Message, ToolDefinition } from "../providers/types.js";
22
+ import { isWeakOpenModel } from "../providers/weak-open-model.js";
22
23
  import { assistantEventHub } from "../runtime/assistant-event-hub.js";
23
24
  import { registerConversationSender } from "../tools/browser/browser-screencast.js";
24
25
  import type { ToolExecutor } from "../tools/executor.js";
@@ -681,6 +682,36 @@ export function isToolActiveForContext(
681
682
  return true;
682
683
  }
683
684
 
685
+ /**
686
+ * Resolve the loaded skill tools to expose first-class to weak open models.
687
+ *
688
+ * Weak open models (MiniMax, Kimi, DeepSeek, GLM) fail to serialize the nested
689
+ * `skill_execute` envelope — a large value double-escaped as a JSON string in
690
+ * `input` — and either drop it or emit it bare. Exposing each loaded skill
691
+ * tool first-class lets the model fill the tool's own scalar params directly
692
+ * (single-escape). Capable models keep the envelope-only contract: this returns
693
+ * `[]` for them, so their wire surface is unchanged.
694
+ *
695
+ * Defs are resolved from the registry by name; `skillToolNames` come from the
696
+ * projection (skill tools only), filtered by `turnAllowed` so subagent
697
+ * allowlists and the tool exclude list still apply. The generic `skill_execute`
698
+ * tool remains available as a fallback for both model tiers.
699
+ */
700
+ export function resolveFirstClassSkillDefs(
701
+ skillToolNames: Iterable<string>,
702
+ turnAllowed: ReadonlySet<string>,
703
+ resolvedModel: string | null | undefined,
704
+ ): ToolDefinition[] {
705
+ if (!isWeakOpenModel(resolvedModel)) return [];
706
+ const defs: ToolDefinition[] = [];
707
+ for (const name of skillToolNames) {
708
+ if (!turnAllowed.has(name)) continue;
709
+ const tool = getTool(name);
710
+ if (tool) defs.push(tool);
711
+ }
712
+ return defs;
713
+ }
714
+
684
715
  /**
685
716
  * Build a resolveTools callback that merges base tool definitions with
686
717
  * dynamically projected skill tools on each agent turn. Also updates
@@ -810,7 +841,30 @@ export function createResolveToolsCallback(
810
841
  }
811
842
 
812
843
  ctx.allowedToolNames = turnAllowed;
813
- const baseDefs = injectActivityField(allBaseDefs, ACTIVITY_SKIP_SET);
844
+
845
+ // Weak open models fail to serialize the nested `skill_execute` envelope
846
+ // (a large value double-escaped as a JSON string in `input`), so expose the
847
+ // loaded skill tools first-class — their scalar params are emitted directly
848
+ // (single-escape). The defs are resolved from the registry by name; the
849
+ // names are already in `turnAllowed`. The generic `skill_execute` stays
850
+ // available as a fallback, and capable models keep the envelope-only
851
+ // contract (this branch is skipped for them).
852
+ const resolvedModel = resolveConversationAttribution({
853
+ conversationId: ctx.conversationId ?? "",
854
+ currentCallSite: ctx.currentCallSite,
855
+ currentTurnOverrideProfile: ctx.currentTurnOverrideProfile,
856
+ })?.resolvedModel;
857
+ const baseDefs = [
858
+ ...injectActivityField(allBaseDefs, ACTIVITY_SKIP_SET),
859
+ ...injectActivityField(
860
+ resolveFirstClassSkillDefs(
861
+ projection.allowedToolNames,
862
+ turnAllowed,
863
+ resolvedModel,
864
+ ),
865
+ ACTIVITY_SKIP_SET,
866
+ ),
867
+ ];
814
868
 
815
869
  const config = getConfig();
816
870
  if (
@@ -0,0 +1,131 @@
1
+ /**
2
+ * Tests for `maybeReseedCapabilitiesAfterManagedCredential` in
3
+ * `memory-v2-startup.ts`.
4
+ *
5
+ * The secrets route calls this when a managed-proxy credential lands, to close
6
+ * the first-boot race where the daemon's startup capability seed (skills + CLI
7
+ * commands) runs before the platform provisions the managed embedding
8
+ * credential — the seed's embed throws and the synthetic capability pages never
9
+ * reach the page index. The reseed must fire only when v2 memory is enabled AND
10
+ * the managed-proxy prerequisites are now satisfied, so self-hosted / BYOK
11
+ * assistants (no managed proxy) are never made to run a doomed embed. When v3 is
12
+ * live it then enqueues a `memory_v3_maintain` job so v3 picks up the capability
13
+ * pages immediately instead of waiting out the 6h maintain backstop.
14
+ *
15
+ * Dynamic-imported collaborators are mocked at module scope; `bun:test`
16
+ * isolates `mock.module` per test file.
17
+ */
18
+ import { afterEach, describe, expect, mock, test } from "bun:test";
19
+
20
+ import { makeMockLogger } from "../__tests__/helpers/mock-logger.js";
21
+ import type { AssistantConfig } from "../config/schema.js";
22
+
23
+ const proxyState = { prereqs: true };
24
+ const v3State = { live: true };
25
+ const seedSkill = mock(async () => {});
26
+ const seedCli = mock(async () => {});
27
+ const enqueueJob = mock(
28
+ (_type: string, _payload: Record<string, unknown>) => 1,
29
+ );
30
+
31
+ mock.module("../util/logger.js", () => ({
32
+ getLogger: () => makeMockLogger(),
33
+ }));
34
+
35
+ mock.module("../providers/platform-proxy/context.js", () => ({
36
+ hasManagedProxyPrereqs: async () => proxyState.prereqs,
37
+ }));
38
+
39
+ mock.module("../config/memory-v3-gate.js", () => ({
40
+ isMemoryV3Live: () => v3State.live,
41
+ }));
42
+
43
+ mock.module("../memory/jobs-store.js", () => ({
44
+ enqueueMemoryJob: enqueueJob,
45
+ }));
46
+
47
+ mock.module("../memory/v2/skill-store.js", () => ({
48
+ seedV2SkillEntries: seedSkill,
49
+ }));
50
+
51
+ mock.module("../memory/v2/cli-command-store.js", () => ({
52
+ seedV2CliCommandEntries: seedCli,
53
+ }));
54
+
55
+ const { maybeReseedCapabilitiesAfterManagedCredential } =
56
+ await import("./memory-v2-startup.js");
57
+
58
+ function configWithV2(enabled: boolean): AssistantConfig {
59
+ return { memory: { v2: { enabled } } } as unknown as AssistantConfig;
60
+ }
61
+
62
+ afterEach(() => {
63
+ seedSkill.mockClear();
64
+ seedCli.mockClear();
65
+ enqueueJob.mockClear();
66
+ proxyState.prereqs = true;
67
+ v3State.live = true;
68
+ });
69
+
70
+ describe("maybeReseedCapabilitiesAfterManagedCredential", () => {
71
+ test("reseeds both skill and CLI entries when v2 is enabled and managed-proxy prereqs are satisfied", async () => {
72
+ proxyState.prereqs = true;
73
+
74
+ await maybeReseedCapabilitiesAfterManagedCredential(configWithV2(true));
75
+
76
+ expect(seedSkill).toHaveBeenCalledTimes(1);
77
+ expect(seedCli).toHaveBeenCalledTimes(1);
78
+ });
79
+
80
+ test("enqueues a v3 maintain pass after reseeding when v3 is live", async () => {
81
+ proxyState.prereqs = true;
82
+ v3State.live = true;
83
+
84
+ await maybeReseedCapabilitiesAfterManagedCredential(configWithV2(true));
85
+
86
+ expect(enqueueJob).toHaveBeenCalledTimes(1);
87
+ expect(enqueueJob).toHaveBeenCalledWith("memory_v3_maintain", {});
88
+ });
89
+
90
+ test("reseeds but does not enqueue a v3 maintain pass when v3 is not live", async () => {
91
+ proxyState.prereqs = true;
92
+ v3State.live = false;
93
+
94
+ await maybeReseedCapabilitiesAfterManagedCredential(configWithV2(true));
95
+
96
+ expect(seedSkill).toHaveBeenCalledTimes(1);
97
+ expect(seedCli).toHaveBeenCalledTimes(1);
98
+ expect(enqueueJob).not.toHaveBeenCalled();
99
+ });
100
+
101
+ test("no-op when v2 memory is disabled", async () => {
102
+ await maybeReseedCapabilitiesAfterManagedCredential(configWithV2(false));
103
+
104
+ expect(seedSkill).not.toHaveBeenCalled();
105
+ expect(seedCli).not.toHaveBeenCalled();
106
+ expect(enqueueJob).not.toHaveBeenCalled();
107
+ });
108
+
109
+ test("no-op for non-managed assistants (managed-proxy prereqs not satisfied)", async () => {
110
+ proxyState.prereqs = false;
111
+
112
+ await maybeReseedCapabilitiesAfterManagedCredential(configWithV2(true));
113
+
114
+ expect(seedSkill).not.toHaveBeenCalled();
115
+ expect(seedCli).not.toHaveBeenCalled();
116
+ expect(enqueueJob).not.toHaveBeenCalled();
117
+ });
118
+
119
+ test("swallows a seed failure and still reseeds the other catalog", async () => {
120
+ proxyState.prereqs = true;
121
+ seedSkill.mockImplementationOnce(async () => {
122
+ throw new Error('Embedding backend "gemini" is not configured');
123
+ });
124
+
125
+ // Must not reject — the helper contains each seed's failure so a doomed
126
+ // embed never propagates back to the credential-store caller.
127
+ await maybeReseedCapabilitiesAfterManagedCredential(configWithV2(true));
128
+
129
+ expect(seedCli).toHaveBeenCalledTimes(1);
130
+ });
131
+ });
@@ -3,7 +3,8 @@
3
3
  // ---------------------------------------------------------------------------
4
4
  //
5
5
  // Small focused module that holds the gating + dispatch logic for v2-specific
6
- // startup work invoked from `lifecycle.ts`. Lives in its own file so the unit
6
+ // startup work invoked from `lifecycle.ts` (and, for the post-credential
7
+ // capability reseed, from the secrets route). Lives in its own file so the unit
7
8
  // test for the gate does not have to mount the entire lifecycle import graph.
8
9
 
9
10
  import type { AssistantConfig } from "../config/schema.js";
@@ -48,6 +49,104 @@ export function maybeSeedMemoryV2CliCommands(config: AssistantConfig): void {
48
49
  .catch((err) => log.warn({ err }, "Failed to seed v2 CLI-command entries"));
49
50
  }
50
51
 
52
+ /**
53
+ * Re-seed the v2 skill and CLI-command capability entries once a managed-proxy
54
+ * credential lands, closing the first-boot race where the daemon's startup seed
55
+ * runs before the platform has provisioned the managed embedding credential.
56
+ *
57
+ * On a brand-new managed assistant the memory worker fires the startup seed
58
+ * (`maybeSeedMemoryV2Skills` / `maybeSeedMemoryV2CliCommands`) seconds after
59
+ * boot, but the platform pushes `vellum:assistant_api_key` (the credential the
60
+ * managed Gemini embedding backend needs) tens of seconds later. The seed's
61
+ * `embedWithBackend` call throws `EmbeddingBackendUnavailableError` before the
62
+ * skill/CLI `entries` cache is replaced, so `listSkillEntries()` /
63
+ * `listCliCommandEntries()` stay empty and the synthetic `skills/<id>` and
64
+ * `cli-commands/<name>` rows never reach the page index — leaving the v3 needle
65
+ * finder lane and always-candidate skill pinning with nothing to surface until
66
+ * the next daemon restart. Re-running the seed when the credential arrives
67
+ * restores the capability pages without a restart.
68
+ *
69
+ * Gated on the managed-proxy prerequisites now being satisfied (both the
70
+ * platform base URL and the assistant API key present) so a non-managed
71
+ * credential write — or a partial update that has not yet completed the pair —
72
+ * does not kick a doomed embed. Idempotent: `seedV2SkillEntries` /
73
+ * `seedV2CliCommandEntries` atomically replace their caches, so a redundant
74
+ * reseed (the startup seed already succeeded) is cheap and harmless. The two
75
+ * catalogs are independent, so they reseed in parallel. Callers invoke this
76
+ * detached (`void`) — it must not block the credential-store response.
77
+ *
78
+ * Reseeding alone only repopulates the shared page index — v3 reads its
79
+ * synthetic capability rows from the v2 stores, but its memoized lanes and its
80
+ * `memory_v3_sections` dense store refresh only on the v3 maintain pass (6-hour
81
+ * backstop). So when v3 is live, enqueue a `memory_v3_maintain` job after the
82
+ * reseed: its capability-reconcile stage embeds the freshly-seeded rows into the
83
+ * dense store and its lane-invalidation stage forces a rebuild against the now-
84
+ * populated index, so v3 surfaces the skill/CLI pages within seconds instead of
85
+ * waiting out the backstop.
86
+ */
87
+ export async function maybeReseedCapabilitiesAfterManagedCredential(
88
+ config: AssistantConfig,
89
+ ): Promise<void> {
90
+ if (!config.memory.v2.enabled) return;
91
+
92
+ const { hasManagedProxyPrereqs } =
93
+ await import("../providers/platform-proxy/context.js");
94
+ if (!(await hasManagedProxyPrereqs())) return;
95
+
96
+ // Skills and CLI commands are independent catalogs sharing the unified
97
+ // collection — reseed in parallel, each contained so one catalog's embed
98
+ // failure does not abort the other or reject the detached caller.
99
+ const catalogs: ReadonlyArray<[label: string, seed: () => Promise<void>]> = [
100
+ [
101
+ "skill",
102
+ async () => {
103
+ const { seedV2SkillEntries } =
104
+ await import("../memory/v2/skill-store.js");
105
+ await seedV2SkillEntries({ throwOnError: true });
106
+ },
107
+ ],
108
+ [
109
+ "CLI-command",
110
+ async () => {
111
+ const { seedV2CliCommandEntries } =
112
+ await import("../memory/v2/cli-command-store.js");
113
+ await seedV2CliCommandEntries({ throwOnError: true });
114
+ },
115
+ ],
116
+ ];
117
+
118
+ await Promise.all(
119
+ catalogs.map(async ([label, seed]) => {
120
+ try {
121
+ await seed();
122
+ log.info(
123
+ `Memory v2 ${label} entries seeded after managed proxy credential update`,
124
+ );
125
+ } catch (err) {
126
+ log.warn(
127
+ { err },
128
+ `Failed to seed v2 ${label} entries after managed proxy credential update`,
129
+ );
130
+ }
131
+ }),
132
+ );
133
+
134
+ // The stores (and the page index) are now populated; when v3 is live, kick a
135
+ // maintain pass so it embeds the capability rows into `memory_v3_sections` and
136
+ // invalidates its lanes immediately rather than waiting out the 6h backstop.
137
+ const { isMemoryV3Live } = await import("../config/memory-v3-gate.js");
138
+ if (!isMemoryV3Live(config)) return;
139
+ try {
140
+ const { enqueueMemoryJob } = await import("../memory/jobs-store.js");
141
+ enqueueMemoryJob("memory_v3_maintain", {});
142
+ } catch (err) {
143
+ log.warn(
144
+ { err },
145
+ "Failed to enqueue memory_v3_maintain after managed proxy credential update",
146
+ );
147
+ }
148
+ }
149
+
51
150
  /**
52
151
  * Build the v2 BM25 corpus stats (per-token document frequencies + avg doc
53
152
  * length), then re-seed the v2 skill entries so any skills written during
@@ -22,6 +22,7 @@ import {
22
22
  invalidateConfigCache,
23
23
  } from "../../config/loader.js";
24
24
  import type { CesClient } from "../../credential-execution/client.js";
25
+ import { maybeReseedCapabilitiesAfterManagedCredential } from "../../daemon/memory-v2-startup.js";
25
26
  import { setSentryOrganizationId, setSentryUserId } from "../../instrument.js";
26
27
  import { clearEmbeddingBackendCache } from "../../memory/embedding-backend.js";
27
28
  import { syncManualTokenConnection } from "../../oauth/manual-token-connection.js";
@@ -296,6 +297,10 @@ async function handleAddSecret({ body }: RouteHandlerArgs) {
296
297
  }
297
298
  if (isManagedProxyCredential(service, field)) {
298
299
  await refreshProvidersAfterSecretChange();
300
+ // Close the first-boot race where the startup capability seed ran before
301
+ // the managed embedding credential was provisioned, leaving skill/CLI
302
+ // pages unseeded until restart. Detached — must not block the response.
303
+ void maybeReseedCapabilitiesAfterManagedCredential(getConfig());
299
304
  if (service === "vellum" && field === "assistant_api_key") {
300
305
  const generation = ++apiKeyGeneration;
301
306
  const deps = getSecretsDeps();
@@ -46,7 +46,12 @@ export function createSkillTool(
46
46
  context: ToolContext,
47
47
  ): Promise<ToolExecutionResult> {
48
48
  const schema = entry.input_schema as Record<string, unknown> | undefined;
49
- const coercedInput = coerceStringBooleans(input, schema);
49
+ // `activity` is a harness field (the skill_execute envelope and
50
+ // first-class skill-tool exposure both surface it for progress display),
51
+ // never an inner tool parameter. Strip it before validation so a model
52
+ // that includes it on a direct call isn't rejected for an unknown field.
53
+ const { activity: _activity, ...rest } = input;
54
+ const coercedInput = coerceStringBooleans(rest, schema);
50
55
  const validation = validateInputAgainstSchema(
51
56
  entry.name,
52
57
  coercedInput,