@vellumai/assistant 0.12.4-staging.1 → 0.12.4-staging.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (51) hide show
  1. package/openapi.yaml +3 -1
  2. package/package.json +1 -1
  3. package/src/__tests__/config-loader-backfill.test.ts +1 -0
  4. package/src/__tests__/default-profile-catalog-fallback.test.ts +1 -0
  5. package/src/__tests__/inference-profile-validation-jev.test.ts +17 -0
  6. package/src/__tests__/llm-catalog-parity.test.ts +4 -0
  7. package/src/__tests__/llm-resolver-override-or-default.test.ts +43 -0
  8. package/src/__tests__/openai-responses-prompt-cache.test.ts +3 -1
  9. package/src/__tests__/plugin-api-model-profiles.test.ts +7 -0
  10. package/src/__tests__/pricing.test.ts +33 -0
  11. package/src/__tests__/skill-load-tool.test.ts +42 -11
  12. package/src/__tests__/skill-tool-factory.test.ts +50 -2
  13. package/src/__tests__/validate-input.test.ts +73 -0
  14. package/src/__tests__/workspace-migration-159-rename-colliding-jev-profile-name.test.ts +197 -0
  15. package/src/api/requests/dictation.ts +2 -2
  16. package/src/calls/__tests__/typesafe-noul.test.ts +66 -0
  17. package/src/calls/typesafe-noul.ts +8 -2
  18. package/src/calls/voice-escalation-judge.ts +5 -4
  19. package/src/config/__tests__/default-profile-catalog.test.ts +33 -1
  20. package/src/config/__tests__/jev-managed-profile.test.ts +82 -0
  21. package/src/config/call-site-defaults.ts +11 -11
  22. package/src/config/default-profile-catalog.ts +88 -18
  23. package/src/config/default-profile-names.ts +35 -0
  24. package/src/config/inference-profile-validation.ts +7 -6
  25. package/src/config/llm-resolver.ts +16 -0
  26. package/src/config/profile-text-generation.test.ts +9 -0
  27. package/src/config/schemas/call-site-catalog.ts +1 -1
  28. package/src/config/schemas/llm.ts +54 -9
  29. package/src/daemon/conversation-slash.ts +2 -2
  30. package/src/daemon/dictation-text-processing.ts +4 -4
  31. package/src/plugin-api/model-profiles.ts +5 -3
  32. package/src/plugins/defaults/memory/substrate/skill-content.ts +1 -1
  33. package/src/plugins/defaults/memory/v3/__tests__/render-injection.test.ts +3 -0
  34. package/src/providers/__tests__/connection-model-compat.test.ts +2 -0
  35. package/src/providers/anthropic/client.ts +2 -2
  36. package/src/providers/model-catalog.ts +257 -4
  37. package/src/providers/openai/codex-models.ts +2 -0
  38. package/src/runtime/routes/__tests__/dictation-processing.test.ts +174 -0
  39. package/src/runtime/routes/__tests__/llm-call-sites-routes.test.ts +16 -1
  40. package/src/runtime/routes/conversation-routes.ts +4 -2
  41. package/src/runtime/routes/diagnostics-routes.ts +66 -229
  42. package/src/runtime/routes/llm-call-sites-routes.ts +19 -10
  43. package/src/skills/validate-input.ts +75 -11
  44. package/src/tools/skills/execute.ts +1 -1
  45. package/src/tools/skills/format-input-schema.ts +15 -0
  46. package/src/tools/skills/load.ts +2 -26
  47. package/src/tools/skills/skill-tool-factory.ts +2 -1
  48. package/src/tools/workflows/manage-workflows.ts +4 -2
  49. package/src/workspace/migrations/159-rename-colliding-jev-profile-name.ts +344 -0
  50. package/src/workspace/migrations/registry.ts +2 -0
  51. package/src/runtime/routes/__tests__/dictation-cleanup-guard.test.ts +0 -107
package/openapi.yaml CHANGED
@@ -12060,7 +12060,9 @@ paths:
12060
12060
  post:
12061
12061
  operationId: dictation_post
12062
12062
  summary: Process dictation
12063
- description: Classify voice input as dictation or action, clean up text, and apply user style preferences.
12063
+ description:
12064
+ Preserve dictated words with explicit dictionary and snippet replacements, classify actions, or edit
12065
+ selected text when instructed.
12064
12066
  tags:
12065
12067
  - diagnostics
12066
12068
  requestBody:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vellumai/assistant",
3
- "version": "0.12.4-staging.1",
3
+ "version": "0.12.4-staging.3",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "exports": {
@@ -1869,6 +1869,7 @@ describe("seedInferenceProfiles BYOK-mode default profiles", () => {
1869
1869
  "quality-optimized-backup",
1870
1870
  "cost-optimized-backup",
1871
1871
  "latency-optimized-backup",
1872
+ "jev-managed",
1872
1873
  "my-custom",
1873
1874
  ]);
1874
1875
  });
@@ -116,6 +116,7 @@ describe("managed-column-only scoping", () => {
116
116
  expect(Object.keys(MANAGED_PROFILE_TEMPLATES)).toEqual([
117
117
  ...DEFAULT_PROFILE_KEYS,
118
118
  ...BACKUP_PROFILE_KEYS,
119
+ "jev-managed",
119
120
  ]);
120
121
  });
121
122
 
@@ -0,0 +1,17 @@
1
+ import { beforeEach, describe, expect, test } from "bun:test";
2
+
3
+ import { validateInferenceProfileKey } from "../config/inference-profile-validation.js";
4
+ import { setConfig } from "./helpers/set-config.js";
5
+
6
+ describe("validateInferenceProfileKey and the managed Jev profile", () => {
7
+ beforeEach(() => {
8
+ setConfig("llm", { profiles: {} });
9
+ });
10
+
11
+ test("a schedule or subagent turn cannot run on the verdict model", () => {
12
+ expect(validateInferenceProfileKey("balanced")).toBeNull();
13
+ expect(validateInferenceProfileKey("jev-managed")).toContain(
14
+ "is not defined in llm.profiles",
15
+ );
16
+ });
17
+ });
@@ -235,6 +235,10 @@ describe("LLM catalog parity: daemon vs client", () => {
235
235
 
236
236
  test("jev-latest opts out of chat text generation", () => {
237
237
  expect(catalogModelSupportsText("typesafe", "jev-latest")).toBe(false);
238
+ // A managed Jev profile is configured as `vellum`; the routing identity
239
+ // owns no catalog models, so the lookup must judge by the catalog owner.
240
+ expect(catalogModelSupportsText("vellum", "jev-latest")).toBe(false);
241
+ expect(catalogModelSupportsText("vellum", "claude-opus-5")).toBe(true);
238
242
  expect(catalogModelSupportsText("anthropic", "claude-opus-4-8")).toBe(true);
239
243
  expect(catalogModelSupportsText("openai-compatible", "local-model")).toBe(
240
244
  true,
@@ -225,6 +225,49 @@ describe("composeCallSiteTweak", () => {
225
225
  });
226
226
 
227
227
  describe("shipped call-site tuning", () => {
228
+ test("the escalation judge uses managed Jev by default", () => {
229
+ const resolved = resolveCallSiteConfig(
230
+ "voiceEscalationJudge",
231
+ LLMSchema.parse({}),
232
+ );
233
+
234
+ expect(resolved.provider).toBe("vellum");
235
+ expect(resolved.provider_connection).toBeUndefined();
236
+ expect(resolved.model).toBe("jev-latest");
237
+ });
238
+
239
+ test("the escalation judge falls through on a BYOK default provider", () => {
240
+ const resolved = resolveCallSiteConfig(
241
+ "voiceEscalationJudge",
242
+ LLMSchema.parse(anthropicDp),
243
+ );
244
+
245
+ expect(resolved.provider).toBe("anthropic");
246
+ expect(resolved.model).not.toBe("jev-latest");
247
+ });
248
+
249
+ test("a BYOK Jev call-site override replaces the managed default", () => {
250
+ const llm = LLMSchema.parse({
251
+ ...anthropicDp,
252
+ profiles: {
253
+ jev: {
254
+ ...completeCustom,
255
+ provider: "typesafe",
256
+ provider_connection: "typesafe-personal",
257
+ model: "jev-latest",
258
+ },
259
+ },
260
+ callSites: {
261
+ voiceEscalationJudge: { profile: "jev" },
262
+ },
263
+ });
264
+ const resolved = resolveCallSiteConfig("voiceEscalationJudge", llm);
265
+
266
+ expect(resolved.provider).toBe("typesafe");
267
+ expect(resolved.provider_connection).toBe("typesafe-personal");
268
+ expect(resolved.model).toBe("jev-latest");
269
+ });
270
+
228
271
  // A workspace entry that only repoints the profile must not cost the call
229
272
  // site the tuning it ships with. `recall` carries a full set: a token
230
273
  // budget, effort, sampling, thinking, and cache posture.
@@ -446,7 +446,7 @@ describe("OpenAIResponsesProvider explicit prompt caching (GPT-5.6+)", () => {
446
446
  expect(JSON.stringify(messages)).not.toContain("prompt_cache_breakpoint");
447
447
  });
448
448
 
449
- test("catalog flags GPT-5.6 and GPT-6 Astra direct-openai rows", () => {
449
+ test("catalog flags GPT-5.6 and GPT-6 direct-openai rows", () => {
450
450
  const openai = PROVIDER_CATALOG.find((p) => p.id === "openai");
451
451
  const flagged = (openai?.models ?? [])
452
452
  .filter((m) => m.supportsPromptCacheBreakpoints)
@@ -457,6 +457,8 @@ describe("OpenAIResponsesProvider explicit prompt caching (GPT-5.6+)", () => {
457
457
  "gpt-5.6-sol",
458
458
  "gpt-5.6-terra",
459
459
  "gpt-6-astra",
460
+ "gpt-6-luna",
461
+ "gpt-6-sol",
460
462
  ]);
461
463
  });
462
464
  });
@@ -40,6 +40,13 @@ describe("getModelProfiles", () => {
40
40
  writeFixtureConfig({});
41
41
  });
42
42
 
43
+ test("omits the managed Jev profile, whose model returns verdicts rather than text", () => {
44
+ writeFixtureConfig({});
45
+ const keys = getModelProfiles().map((p) => p.key);
46
+ expect(keys).toContain("balanced");
47
+ expect(keys).not.toContain("jev-managed");
48
+ });
49
+
43
50
  test("orders profiles by profileOrder then the remaining keys alphabetically", () => {
44
51
  // GIVEN a workspace whose profileOrder names some keys, with a duplicate
45
52
  // ("balanced") and a key that resolves to no profile ("ghost").
@@ -135,6 +135,39 @@ describe("resolvePricing", () => {
135
135
  expect(result.estimatedCostUsd).toBeCloseTo(0.5 + 0.625 + 0.05, 10);
136
136
  });
137
137
 
138
+ test("bills GPT-6 Sol at published short-context rates", () => {
139
+ const result = resolvePricingForUsage("openai", "gpt-6-sol", {
140
+ directInputTokens: 50_000,
141
+ outputTokens: 50_000,
142
+ cacheCreationInputTokens: 50_000,
143
+ cacheReadInputTokens: 50_000,
144
+ anthropicCacheCreation: null,
145
+ });
146
+
147
+ expect(result.pricingStatus).toBe("priced");
148
+ // 0.05M x $2 direct + 0.05M x $2.5 write + 0.05M x $0.2 read
149
+ // + 0.05M x $10 output.
150
+ expect(result.estimatedCostUsd).toBeCloseTo(0.1 + 0.125 + 0.01 + 0.5, 10);
151
+ });
152
+
153
+ test("bills GPT-6 Luna at the long-context tier above 272k", () => {
154
+ const result = resolvePricingForUsage("openai", "gpt-6-luna", {
155
+ directInputTokens: 500_000,
156
+ outputTokens: 1_000_000,
157
+ cacheCreationInputTokens: 500_000,
158
+ cacheReadInputTokens: 1_000_000,
159
+ anthropicCacheCreation: null,
160
+ });
161
+
162
+ expect(result.pricingStatus).toBe("priced");
163
+ // Tier rates: 0.5M x $0.2 direct + 0.5M x $0.25 write + 1M x $0.02 read
164
+ // + 1M x $0.75 output.
165
+ expect(result.estimatedCostUsd).toBeCloseTo(
166
+ 0.1 + 0.125 + 0.02 + 0.75,
167
+ 10,
168
+ );
169
+ });
170
+
138
171
  test("bills GPT-6 Astra at the long-context tier above 272k", () => {
139
172
  const result = resolvePricingForUsage("openai", "gpt-6-astra", {
140
173
  directInputTokens: 500_000,
@@ -1,4 +1,4 @@
1
- import { mkdirSync, symlinkSync, writeFileSync } from "node:fs";
1
+ import { mkdirSync, readFileSync, symlinkSync, writeFileSync } from "node:fs";
2
2
  import { join } from "node:path";
3
3
  import { beforeEach, describe, expect, mock, test } from "bun:test";
4
4
 
@@ -781,16 +781,48 @@ describe("skill_load tool", () => {
781
781
  expect(result.content).toContain("Deploy the application to production");
782
782
  expect(result.content).toContain("Rollback to previous version");
783
783
 
784
- // Should list parameters with types and required/optional markers
785
- expect(result.content).toContain(
786
- "- environment (string, required): Target environment",
784
+ const schemas = [
785
+ ...result.content.matchAll(/```json\n([\s\S]*?)\n```/g),
786
+ ].map((match) => JSON.parse(match[1]));
787
+ expect(schemas[0].required).toEqual(["environment"]);
788
+ expect(schemas[0].properties.environment).toEqual({
789
+ type: "string",
790
+ description: "Target environment",
791
+ });
792
+ expect(schemas[0].properties.force.type).toBe("boolean");
793
+ expect(schemas[1].required).toEqual(["version"]);
794
+ });
795
+
796
+ test("discloses the complete annotation mark schema before execution", async () => {
797
+ const manifest = JSON.parse(
798
+ readFileSync(
799
+ join(
800
+ import.meta.dir,
801
+ "../config/bundled-skills/screen-annotation/TOOLS.json",
802
+ ),
803
+ "utf-8",
804
+ ),
787
805
  );
788
- expect(result.content).toContain(
789
- "- force (boolean, optional): Force deploy even if checks fail",
806
+ writeSkill(
807
+ "annotation-schema",
808
+ "Annotation Schema",
809
+ "Point at a control",
810
+ "Name the control.",
790
811
  );
791
- expect(result.content).toContain(
792
- "- version (string, required): Version to rollback to",
812
+ writeToolsJson("annotation-schema", manifest.tools);
813
+
814
+ const result = await executeSkillLoad(
815
+ { skill: "annotation-schema" },
816
+ "macos",
793
817
  );
818
+ expect(result.isError).toBe(false);
819
+ const schemaMatch = result.content.match(/```json\n([\s\S]*?)\n```/);
820
+ expect(schemaMatch).not.toBeNull();
821
+ const schema = JSON.parse(schemaMatch![1]);
822
+ expect(schema).toEqual(manifest.tools[0].input_schema);
823
+ expect(schema.properties.marks.items.oneOf).toHaveLength(2);
824
+ expect(schema.properties.marks.items.properties.target.minLength).toBe(1);
825
+ expect(schema.properties.marks.items.properties.caption.maxLength).toBe(80);
794
826
  });
795
827
 
796
828
  test("skill without TOOLS.json does not include tool schemas section", async () => {
@@ -862,9 +894,8 @@ describe("skill_load tool", () => {
862
894
  // The child skill's tool schemas should appear (#### level under ### Tools from …)
863
895
  expect(result.content).toContain("#### child_action");
864
896
  expect(result.content).toContain("A child tool action");
865
- expect(result.content).toContain(
866
- "- target (string, required): Action target",
867
- );
897
+ const schemaMatch = result.content.match(/```json\n([\s\S]*?)\n```/);
898
+ expect(JSON.parse(schemaMatch![1]).required).toEqual(["target"]);
868
899
  expect(result.content).toContain(
869
900
  "Use `skill_execute` to call these tools.",
870
901
  );
@@ -4,7 +4,7 @@ import { tmpdir } from "node:os";
4
4
  import { join } from "node:path";
5
5
  import { afterAll, beforeAll, describe, expect, test } from "bun:test";
6
6
 
7
- import type { SkillToolEntry } from "../config/skills.js";
7
+ import { getBundledSkillsDir, type SkillToolEntry } from "../config/skills.js";
8
8
  import { RiskLevel } from "../permissions/types.js";
9
9
  import { computeSkillVersionHash } from "../skills/version-hash.js";
10
10
  import { resolveSubagentRole } from "../subagent/role-resolution.js";
@@ -261,6 +261,54 @@ describe("createSkillTool — unknown parameter validation", () => {
261
261
  expect(result.content).toContain('Unknown parameter "bar"');
262
262
  });
263
263
 
264
+ test.each([
265
+ { description: "Filters" },
266
+ { marks: [{ description: "Filters" }] },
267
+ { marks: [{}] },
268
+ { marks: [{ target: 42 }] },
269
+ { marks: [{ x: 0.1, y: 0.2 }] },
270
+ { marks: [{ target: "Filters", x: 0.1, y: 0.2, width: 0.3, height: 0.4 }] },
271
+ ])(
272
+ "an annotation validation error provides enough schema for one corrected retry: %j",
273
+ async (invalid) => {
274
+ const skillDir = join(getBundledSkillsDir(), "screen-annotation");
275
+ const manifest = JSON.parse(
276
+ readFileSync(join(skillDir, "TOOLS.json"), "utf-8"),
277
+ );
278
+ const entry = manifest.tools.find(
279
+ (tool: SkillToolEntry) => tool.name === "screen_point_at",
280
+ );
281
+ const tool = createSkillTool(
282
+ entry,
283
+ skillDir,
284
+ computeSkillVersionHash(skillDir),
285
+ BUNDLED,
286
+ );
287
+ const calls: unknown[] = [];
288
+ const context = makeContext({
289
+ proxyToolResolver: async (name, input) => {
290
+ calls.push({ name, input });
291
+ return { content: "Drew 1 mark", isError: false };
292
+ },
293
+ });
294
+
295
+ const rejected = await tool.execute(invalid, context);
296
+ expect(rejected.isError).toBe(true);
297
+ expect(calls).toHaveLength(0);
298
+ expect(rejected.content).toContain("skill_load");
299
+ const schemaMatch = rejected.content.match(/```json\n([\s\S]*?)\n```/);
300
+ expect(schemaMatch).not.toBeNull();
301
+ expect(JSON.parse(schemaMatch![1])).toEqual(entry.input_schema);
302
+
303
+ const input = {
304
+ marks: [{ target: "Filters", caption: "Click Filters" }],
305
+ };
306
+ const corrected = await tool.execute(input, context);
307
+ expect(corrected.isError).toBe(false);
308
+ expect(calls).toEqual([{ name: "computer_use_point_at", input }]);
309
+ },
310
+ );
311
+
264
312
  test("allows input with only known parameters", async () => {
265
313
  const hash = computeSkillVersionHash(tempDir);
266
314
  const tool = createSkillTool(
@@ -471,7 +519,7 @@ describe("createSkillTool — required/type/enum validation", () => {
471
519
 
472
520
  expect(result.isError).toBe(true);
473
521
  expect(result.content).toContain(
474
- "auto_open must be a boolean — pass true or false as a JSON boolean, not a string",
522
+ "auto_open must be a boolean: pass true or false as a JSON boolean, not a string",
475
523
  );
476
524
  });
477
525
 
@@ -6,6 +6,79 @@ import {
6
6
  validateInputAgainstSchema,
7
7
  } from "../skills/validate-input.js";
8
8
 
9
+ describe("nested input validation", () => {
10
+ const schema = {
11
+ properties: {
12
+ options: {
13
+ type: "object",
14
+ properties: { label: { type: "string" } },
15
+ required: ["label"],
16
+ },
17
+ },
18
+ };
19
+
20
+ test("reports nested required fields and types with their path", () => {
21
+ expect(
22
+ validateInputAgainstSchema("example", { options: {} }, schema),
23
+ ).toEqual({
24
+ ok: false,
25
+ errors: ["options.label is required"],
26
+ });
27
+ expect(
28
+ validateInputAgainstSchema("example", { options: { label: 42 } }, schema),
29
+ ).toEqual({
30
+ ok: false,
31
+ errors: ["options.label must be a string"],
32
+ });
33
+ });
34
+
35
+ test("preserves open nested objects and only rejects explicitly closed extra fields", () => {
36
+ const input = { options: { label: "Example", extra: true } };
37
+ expect(validateInputAgainstSchema("example", input, schema)).toEqual({
38
+ ok: true,
39
+ });
40
+ expect(
41
+ validateInputAgainstSchema("example", input, {
42
+ properties: {
43
+ options: {
44
+ ...schema.properties.options,
45
+ additionalProperties: false,
46
+ },
47
+ },
48
+ }),
49
+ ).toEqual({
50
+ ok: false,
51
+ errors: ['Unknown parameter "options.extra". Supported: "label"'],
52
+ });
53
+ });
54
+
55
+ test("does not treat a richer oneOf as a presence-only union", () => {
56
+ expect(
57
+ validateInputAgainstSchema(
58
+ "example",
59
+ { options: { label: "Example" } },
60
+ {
61
+ properties: {
62
+ options: {
63
+ ...schema.properties.options,
64
+ oneOf: [
65
+ {
66
+ required: ["label"],
67
+ properties: { label: { const: "Example" } },
68
+ },
69
+ {
70
+ required: ["label"],
71
+ properties: { label: { const: "Other" } },
72
+ },
73
+ ],
74
+ },
75
+ },
76
+ },
77
+ ),
78
+ ).toEqual({ ok: true });
79
+ });
80
+ });
81
+
9
82
  // ---------------------------------------------------------------------------
10
83
  // required
11
84
  // ---------------------------------------------------------------------------
@@ -0,0 +1,197 @@
1
+ import {
2
+ existsSync,
3
+ mkdirSync,
4
+ readFileSync,
5
+ rmSync,
6
+ writeFileSync,
7
+ } from "node:fs";
8
+ import { tmpdir } from "node:os";
9
+ import { join } from "node:path";
10
+ import { Database } from "bun:sqlite";
11
+ import { afterEach, beforeEach, describe, expect, test } from "bun:test";
12
+
13
+ import { WORKSPACE_MIGRATIONS } from "../workspace/migrations/registry.js";
14
+ import { assertNotLiveDb } from "./assert-not-live-db.js";
15
+
16
+ const migration = WORKSPACE_MIGRATIONS.find(
17
+ (entry) => entry.id === "159-rename-colliding-jev-profile-name",
18
+ )!;
19
+
20
+ let workspaceDir: string;
21
+
22
+ function writeConfig(data: Record<string, unknown>): void {
23
+ writeFileSync(
24
+ join(workspaceDir, "config.json"),
25
+ JSON.stringify(data, null, 2) + "\n",
26
+ );
27
+ }
28
+
29
+ function readConfig(): Record<string, any> {
30
+ return JSON.parse(readFileSync(join(workspaceDir, "config.json"), "utf-8"));
31
+ }
32
+
33
+ function seedProfilePinTables(rows: {
34
+ conversations: string[];
35
+ cronJobs: string[];
36
+ }): void {
37
+ mkdirSync(join(workspaceDir, "data", "db"), { recursive: true });
38
+ const db = new Database(join(workspaceDir, "data", "db", "assistant.db"));
39
+ db.run(
40
+ `CREATE TABLE conversations (id TEXT PRIMARY KEY, inference_profile TEXT)`,
41
+ );
42
+ db.run(
43
+ `CREATE TABLE cron_jobs (id TEXT PRIMARY KEY, inference_profile TEXT)`,
44
+ );
45
+ rows.conversations.forEach((profile, index) => {
46
+ db.query(
47
+ `INSERT INTO conversations (id, inference_profile) VALUES (?, ?)`,
48
+ ).run(`conv-${index}`, profile);
49
+ });
50
+ rows.cronJobs.forEach((profile, index) => {
51
+ db.query(`INSERT INTO cron_jobs (id, inference_profile) VALUES (?, ?)`).run(
52
+ `job-${index}`,
53
+ profile,
54
+ );
55
+ });
56
+ db.close();
57
+ }
58
+
59
+ function readPins(table: string): string[] {
60
+ const db = new Database(join(workspaceDir, "data", "db", "assistant.db"));
61
+ try {
62
+ return (
63
+ db
64
+ .query(`SELECT inference_profile AS p FROM ${table} ORDER BY id`)
65
+ .all() as Array<{ p: string }>
66
+ ).map((row) => row.p);
67
+ } finally {
68
+ db.close();
69
+ }
70
+ }
71
+
72
+ beforeEach(() => {
73
+ workspaceDir = join(
74
+ tmpdir(),
75
+ `vellum-migration-159-test-${Date.now()}-${Math.random().toString(36).slice(2)}`,
76
+ );
77
+ mkdirSync(workspaceDir, { recursive: true });
78
+ });
79
+
80
+ afterEach(() => {
81
+ if (existsSync(workspaceDir)) {
82
+ assertNotLiveDb(workspaceDir);
83
+ rmSync(workspaceDir, { recursive: true, force: true });
84
+ }
85
+ });
86
+
87
+ describe("159-rename-colliding-jev-profile-name migration", () => {
88
+ test("is registered under its id with checkpoint retry", () => {
89
+ expect(migration).toBeDefined();
90
+ expect(migration.retryFailedCheckpoint).toBe(true);
91
+ });
92
+
93
+ test("renames a colliding user profile and rewrites every reference", async () => {
94
+ writeConfig({
95
+ llm: {
96
+ defaultProvider: { provider: "vellum" },
97
+ profiles: {
98
+ "jev-managed": {
99
+ source: "user",
100
+ provider: "anthropic",
101
+ model: "claude-opus-4-7",
102
+ label: "Mine",
103
+ },
104
+ scratch: { source: "user", fallbackProfile: "jev-managed" },
105
+ armA: { source: "user", speed: "fast" },
106
+ blend: {
107
+ source: "user",
108
+ mix: [
109
+ { profile: "jev-managed", weight: 1 },
110
+ { profile: "armA", weight: 1 },
111
+ ],
112
+ },
113
+ },
114
+ profileOrder: ["balanced", "jev-managed", "blend"],
115
+ activeProfile: "jev-managed",
116
+ advisorProfile: "jev-managed",
117
+ callSites: { recall: { profile: "jev-managed", maxTokens: 4096 } },
118
+ },
119
+ });
120
+ seedProfilePinTables({
121
+ conversations: ["jev-managed", "balanced"],
122
+ cronJobs: ["jev-managed"],
123
+ });
124
+
125
+ await migration.run(workspaceDir);
126
+
127
+ const llm = readConfig().llm;
128
+ expect(llm.profiles["jev-managed"]).toBeUndefined();
129
+ expect(llm.profiles["jev-managed-custom"]).toEqual({
130
+ source: "user",
131
+ provider: "anthropic",
132
+ model: "claude-opus-4-7",
133
+ label: "Mine",
134
+ });
135
+ expect(Object.keys(llm.profiles)).toEqual([
136
+ "jev-managed-custom",
137
+ "scratch",
138
+ "armA",
139
+ "blend",
140
+ ]);
141
+ expect(llm.activeProfile).toBe("jev-managed-custom");
142
+ expect(llm.advisorProfile).toBe("jev-managed-custom");
143
+ expect(llm.profileOrder).toEqual([
144
+ "balanced",
145
+ "jev-managed-custom",
146
+ "blend",
147
+ ]);
148
+ expect(llm.callSites.recall.profile).toBe("jev-managed-custom");
149
+ expect(llm.profiles.scratch.fallbackProfile).toBe("jev-managed-custom");
150
+ expect(llm.profiles.blend.mix[0].profile).toBe("jev-managed-custom");
151
+ expect(readPins("conversations")).toEqual([
152
+ "jev-managed-custom",
153
+ "balanced",
154
+ ]);
155
+ expect(readPins("cron_jobs")).toEqual(["jev-managed-custom"]);
156
+ });
157
+
158
+ test("keeps suffixing past taken and reserved names", async () => {
159
+ writeConfig({
160
+ llm: {
161
+ profiles: {
162
+ "jev-managed": {
163
+ source: "user",
164
+ provider: "openai",
165
+ model: "gpt-5.5",
166
+ },
167
+ "jev-managed-custom": { source: "user", provider: "openai" },
168
+ },
169
+ },
170
+ });
171
+
172
+ await migration.run(workspaceDir);
173
+
174
+ const keys = Object.keys(readConfig().llm.profiles);
175
+ expect(keys).toEqual(["jev-managed-custom-2", "jev-managed-custom"]);
176
+ });
177
+
178
+ test("leaves a managed stub alone and is idempotent", async () => {
179
+ const original = {
180
+ llm: {
181
+ profiles: {
182
+ "jev-managed": { source: "managed", status: "disabled" },
183
+ balanced: { source: "managed" },
184
+ },
185
+ },
186
+ };
187
+ writeConfig(original);
188
+
189
+ await migration.run(workspaceDir);
190
+ await migration.run(workspaceDir);
191
+
192
+ expect(readConfig()).toEqual(original);
193
+ expect(existsSync(join(workspaceDir, "data", "db", "assistant.db"))).toBe(
194
+ false,
195
+ );
196
+ });
197
+ });
@@ -1,8 +1,8 @@
1
1
  /**
2
2
  * Wire contract for the dictation REST endpoint (`POST /dictation`).
3
3
  *
4
- * The body web/macOS sends when asking the daemon to clean up a raw voice
5
- * transcript and classify it as dictation vs. action. `context` is its own
4
+ * The body web/macOS sends to apply explicit dictation replacements, classify
5
+ * voice input, or edit selected text. `context` is its own
6
6
  * named schema/type so consumers (web client) can import `DictationContext`
7
7
  * directly instead of inferring it from the generated client.
8
8
  *