@vellumai/assistant 0.10.3-dev.202606282129.2e2ccad → 0.10.3-dev.202606282329.3fa6b86

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 (71) hide show
  1. package/openapi.yaml +0 -6
  2. package/package.json +1 -1
  3. package/src/__tests__/plugin-import-boundary-guard.test.ts +423 -0
  4. package/src/__tests__/voice-quality.test.ts +38 -12
  5. package/src/api/responses/memory-v3-selection-log.ts +4 -6
  6. package/src/background-wake/background-wake-routes.test.ts +53 -23
  7. package/src/background-wake/wake-intent-hooks.test.ts +5 -4
  8. package/src/calls/tts-call-strategy.ts +4 -3
  9. package/src/calls/voice-quality.ts +47 -59
  10. package/src/config/feature-flag-registry.json +0 -8
  11. package/src/daemon/lifecycle.ts +2 -4
  12. package/src/heartbeat/heartbeat-service.ts +5 -0
  13. package/src/memory/db-async-query.ts +1 -329
  14. package/src/memory/db-connection.ts +1 -238
  15. package/src/memory/db-init.ts +1 -268
  16. package/src/memory/db-maintenance.ts +1 -158
  17. package/src/memory/db-singleton.ts +1 -105
  18. package/src/memory/jobs-worker.ts +2 -6
  19. package/src/memory/schema/conversations.ts +1 -236
  20. package/src/memory/schema/inference.ts +1 -28
  21. package/src/memory/schema/oauth.ts +1 -97
  22. package/src/memory/schema.ts +1 -1
  23. package/src/memory/v2/__tests__/consolidation-job.test.ts +17 -20
  24. package/src/memory/v2/consolidation-job.ts +10 -21
  25. package/src/persistence/db-async-query.ts +329 -0
  26. package/src/persistence/db-connection.ts +238 -0
  27. package/src/persistence/db-init.ts +270 -0
  28. package/src/persistence/db-maintenance.ts +158 -0
  29. package/src/persistence/db-singleton.ts +105 -0
  30. package/src/persistence/schema/conversations.ts +236 -0
  31. package/src/persistence/schema/inference.ts +28 -0
  32. package/src/persistence/schema/oauth.ts +97 -0
  33. package/src/plugins/defaults/compaction/compact.ts +2 -1
  34. package/src/plugins/defaults/compaction/context-overflow-reducer.ts +2 -1
  35. package/src/plugins/defaults/compaction/window-manager.ts +8 -7
  36. package/src/plugins/defaults/empty-response/hooks/post-model-call.ts +6 -2
  37. package/src/plugins/defaults/exploration-drift/hooks/post-tool-use.ts +5 -3
  38. package/src/plugins/defaults/history-repair/terminal.ts +1 -1
  39. package/src/plugins/defaults/image-recovery/recover.ts +5 -2
  40. package/src/plugins/defaults/memory-retrieval/injectors.ts +2 -1
  41. package/src/plugins/defaults/memory-retrieval/tail-reinjection-strip.ts +2 -1
  42. package/src/plugins/defaults/memory-v3-shadow/__tests__/injection.test.ts +2 -36
  43. package/src/plugins/defaults/memory-v3-shadow/__tests__/maintain-job.test.ts +20 -28
  44. package/src/plugins/defaults/memory-v3-shadow/__tests__/selection-log-store.test.ts +3 -10
  45. package/src/plugins/defaults/memory-v3-shadow/__tests__/shadow-integration.test.ts +14 -15
  46. package/src/plugins/defaults/memory-v3-shadow/__tests__/shadow-plugin.test.ts +36 -95
  47. package/src/plugins/defaults/memory-v3-shadow/injector.ts +19 -46
  48. package/src/plugins/defaults/memory-v3-shadow/maintain-job.ts +1 -6
  49. package/src/plugins/defaults/memory-v3-shadow/pool-select.ts +6 -6
  50. package/src/plugins/defaults/memory-v3-shadow/prune.ts +2 -1
  51. package/src/plugins/defaults/memory-v3-shadow/selection-log-store.ts +0 -4
  52. package/src/plugins/defaults/memory-v3-shadow/shadow-plugin.ts +9 -43
  53. package/src/plugins/defaults/surface-completion-nudge/hooks/post-model-call.ts +6 -2
  54. package/src/plugins/defaults/task-progress-nudge/hooks/post-tool-use.ts +6 -2
  55. package/src/plugins/defaults/title-generate/hooks/stop.ts +1 -2
  56. package/src/plugins/defaults/tool-error/hooks/post-tool-use.ts +5 -3
  57. package/src/runtime/routes/background-wake-routes.ts +19 -5
  58. package/src/schedule/scheduler.ts +5 -0
  59. package/src/background-wake/runtime-registry.ts +0 -24
  60. /package/src/{memory → persistence}/schema/a2a.ts +0 -0
  61. /package/src/{memory → persistence}/schema/acp.ts +0 -0
  62. /package/src/{memory → persistence}/schema/bookmarks.ts +0 -0
  63. /package/src/{memory → persistence}/schema/calls.ts +0 -0
  64. /package/src/{memory → persistence}/schema/contacts.ts +0 -0
  65. /package/src/{memory → persistence}/schema/guardian.ts +0 -0
  66. /package/src/{memory → persistence}/schema/index.ts +0 -0
  67. /package/src/{memory → persistence}/schema/infrastructure.ts +0 -0
  68. /package/src/{memory → persistence}/schema/memory-core.ts +0 -0
  69. /package/src/{memory → persistence}/schema/memory-graph.ts +0 -0
  70. /package/src/{memory → persistence}/schema/notifications.ts +0 -0
  71. /package/src/{memory → persistence}/schema/tasks.ts +0 -0
package/openapi.yaml CHANGED
@@ -8659,8 +8659,6 @@ paths:
8659
8659
  type: number
8660
8660
  live:
8661
8661
  type: boolean
8662
- shadow:
8663
- type: boolean
8664
8662
  selections:
8665
8663
  type: array
8666
8664
  items:
@@ -8690,7 +8688,6 @@ paths:
8690
8688
  required:
8691
8689
  - turn
8692
8690
  - live
8693
- - shadow
8694
8691
  - selections
8695
8692
  - injectedText
8696
8693
  additionalProperties: false
@@ -18266,8 +18263,6 @@ paths:
18266
18263
  type: number
18267
18264
  live:
18268
18265
  type: boolean
18269
- shadow:
18270
- type: boolean
18271
18266
  selections:
18272
18267
  type: array
18273
18268
  items:
@@ -18297,7 +18292,6 @@ paths:
18297
18292
  required:
18298
18293
  - turn
18299
18294
  - live
18300
- - shadow
18301
18295
  - selections
18302
18296
  - injectedText
18303
18297
  additionalProperties: false
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vellumai/assistant",
3
- "version": "0.10.3-dev.202606282129.2e2ccad",
3
+ "version": "0.10.3-dev.202606282329.3fa6b86",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "exports": {
@@ -0,0 +1,423 @@
1
+ import { readFileSync } from "node:fs";
2
+ import { dirname, join, relative, resolve, sep } from "node:path";
3
+ import { describe, expect, test } from "bun:test";
4
+
5
+ import { Glob } from "bun";
6
+
7
+ /**
8
+ * Guard tests for the plugin import boundary. Each immediate subdirectory of
9
+ * `assistant/src/plugins/defaults/` is one self-contained plugin. A plugin's
10
+ * production code may import only:
11
+ * 1. from `@vellumai/plugin-api` (the public plugin API surface), or
12
+ * 2. from within its own plugin directory.
13
+ *
14
+ * Anything else is an "escaping import": a relative import resolving outside
15
+ * the plugin's own directory (reaching into assistant host code, a sibling
16
+ * plugin, or `plugins/types.js`), or a bare specifier other than
17
+ * `@vellumai/plugin-api` (node:*, bun:*, or a third-party package).
18
+ *
19
+ * Two guards run here:
20
+ * - A baseline RATCHET ({@link BASELINE}) that fails when a NEW escaping
21
+ * import appears for a plugin (and when a baseline entry goes stale), so
22
+ * coupling to host internals cannot silently grow.
23
+ * - An ANTI-BACKSLIDE check that forbids importing, from `providers/types.js`
24
+ * or `config/schemas/llm.js`, any symbol that `@vellumai/plugin-api`
25
+ * already re-exports — locking in the migration that moved those type
26
+ * imports onto the public API.
27
+ *
28
+ * Tests run from `assistant/`, so paths are resolved against `process.cwd()`.
29
+ * Test files (`*.test.ts`, `__tests__/`) are out of scope — the boundary we
30
+ * guard is the shipped plugin runtime, and plugin tests legitimately import
31
+ * test infrastructure.
32
+ */
33
+
34
+ /** `assistant/src/plugins/defaults`, relative to the `assistant/` cwd. */
35
+ const DEFAULTS_REL = join("src", "plugins", "defaults");
36
+ const DEFAULTS_ABS = join(process.cwd(), DEFAULTS_REL);
37
+
38
+ /**
39
+ * Allowed escaping imports per plugin: plugin name → sorted list of distinct
40
+ * specifiers it reaches for outside itself. These are host-internal couplings
41
+ * with no `@vellumai/plugin-api` equivalent (memory/daemon/context/config
42
+ * internals, runtime builtins, third-party packages). `providers/types.js`
43
+ * appears only for `ToolDefinition` (a `@vellumai/skill-host-contracts` type
44
+ * distinct from plugin-api's), which the anti-backslide guard enforces.
45
+ *
46
+ * Regenerate after an intentional change with:
47
+ * UPDATE_PLUGIN_IMPORT_BASELINE=1 bun test src/__tests__/plugin-import-boundary-guard.test.ts
48
+ * and paste the printed object here, explaining the new coupling in your PR.
49
+ */
50
+ const BASELINE: Record<string, readonly string[]> = {
51
+ compaction: [
52
+ "../../../config/loader.js",
53
+ "../../../config/schemas/compaction.js",
54
+ "../../../config/schemas/inference.js",
55
+ "../../../config/types.js",
56
+ "../../../context/compactor.js",
57
+ "../../../context/token-estimator.js",
58
+ "../../../context/tool-result-truncation.js",
59
+ "../../../daemon/conversation-media-retry.js",
60
+ "../../../daemon/conversation-registry.js",
61
+ "../../../daemon/conversation-runtime-assembly.js",
62
+ "../../../providers/types.js",
63
+ "../../../runtime/actor-trust-resolver.js",
64
+ "../../../util/logger.js",
65
+ "../../types.js",
66
+ ],
67
+ "exploration-drift": ["../../../../subagent/index.js"],
68
+ "image-fallback": [
69
+ "../../../../providers/provider-send-message.js",
70
+ "node:crypto",
71
+ "node:fs",
72
+ "node:path",
73
+ ],
74
+ "image-recovery": [
75
+ "../../../agent/image-optimize.js",
76
+ "../../../context/image-dimensions.js",
77
+ "../../../memory/conversation-crud.js",
78
+ "../../../util/logger.js",
79
+ ],
80
+ "max-tokens-continue": ["../../../../agent/loop.js"],
81
+ "memory-retrieval": [
82
+ "../../../../config/loader.js",
83
+ "../../../../config/memory-v3-gate.js",
84
+ "../../../../daemon/conversation-registry.js",
85
+ "../../../../daemon/conversation-runtime-assembly.js",
86
+ "../../../../daemon/message-types/memory.js",
87
+ "../../../../daemon/trust-context.js",
88
+ "../../../../memory/conversation-crud.js",
89
+ "../../../../memory/graph/conversation-graph-memory.js",
90
+ "../../../../memory/memory-recall-log-store.js",
91
+ "../../../../runtime/assistant-event-hub.js",
92
+ "../../../config/loader.js",
93
+ "../../../context/strip-injections.js",
94
+ "../../../daemon/conversation-registry.js",
95
+ "../../../daemon/conversation-runtime-assembly.js",
96
+ "../../../daemon/conversation-workspace.js",
97
+ "../../../daemon/now-scratchpad.js",
98
+ "../../../daemon/pkb-context-tracker.js",
99
+ "../../../daemon/pkb-reminder-builder.js",
100
+ "../../../daemon/trust-context.js",
101
+ "../../../documents/document-comments-store.js",
102
+ "../../../memory/graph/conversation-graph-memory.js",
103
+ "../../../memory/pkb/autoinject.js",
104
+ "../../../memory/pkb/context.js",
105
+ "../../../memory/pkb/pkb-search.js",
106
+ "../../../memory/pkb/types.js",
107
+ "../../../memory/v2/static-context.js",
108
+ "../../../runtime/capabilities.js",
109
+ "../../../types.js",
110
+ "../../../util/logger.js",
111
+ "../../../util/platform.js",
112
+ "../../memory-v3-shadow/ever-injected-store.js",
113
+ "../../types.js",
114
+ "../memory-v3-shadow/injector.js",
115
+ "node:fs",
116
+ "node:path",
117
+ ],
118
+ "memory-v3-shadow": [
119
+ "../../../api/responses/memory-v3-selection-log.js",
120
+ "../../../config/loader.js",
121
+ "../../../config/memory-v3-gate.js",
122
+ "../../../config/schema.js",
123
+ "../../../config/skills.js",
124
+ "../../../config/types.js",
125
+ "../../../daemon/conversation-error.js",
126
+ "../../../daemon/conversation-notices.js",
127
+ "../../../daemon/conversation-registry.js",
128
+ "../../../daemon/trust-context.js",
129
+ "../../../memory/checkpoints.js",
130
+ "../../../memory/conversation-crud.js",
131
+ "../../../memory/db-connection.js",
132
+ "../../../memory/embedding-backend.js",
133
+ "../../../memory/embedding-billing-breaker.js",
134
+ "../../../memory/embedding-cache.js",
135
+ "../../../memory/embedding-types.js",
136
+ "../../../memory/jobs-store.js",
137
+ "../../../memory/memory-marker.js",
138
+ "../../../memory/message-content.js",
139
+ "../../../memory/prompt-override.js",
140
+ "../../../memory/qdrant-client.js",
141
+ "../../../memory/v2/cli-command-store.js",
142
+ "../../../memory/v2/injected-block-slugs.js",
143
+ "../../../memory/v2/page-index.js",
144
+ "../../../memory/v2/page-store.js",
145
+ "../../../memory/v2/skill-store.js",
146
+ "../../../providers/cache-control.js",
147
+ "../../../providers/provider-send-message.js",
148
+ "../../../providers/types.js",
149
+ "../../../skills/frontmatter.js",
150
+ "../../../util/log-redact.js",
151
+ "../../../util/logger.js",
152
+ "../../../util/platform.js",
153
+ "../../../util/strip-comment-lines.js",
154
+ "../../../util/truncate.js",
155
+ "../../types.js",
156
+ "@qdrant/js-client-rest",
157
+ "node:crypto",
158
+ "node:fs",
159
+ "node:path",
160
+ "uuid",
161
+ "zod",
162
+ ],
163
+ "task-progress-nudge": [
164
+ "../../../../daemon/conversation-registry.js",
165
+ "../../../../providers/weak-open-model.js",
166
+ "../../../../subagent/index.js",
167
+ ],
168
+ "title-generate": [
169
+ "../../../../config/loader.js",
170
+ "../../../../memory/conversation-crud.js",
171
+ "../../../../memory/conversation-title-service.js",
172
+ ],
173
+ };
174
+
175
+ /**
176
+ * Symbols that `@vellumai/plugin-api` re-exports from `providers/types.js`. A
177
+ * plugin must import these from the public API, not the host module. NOT
178
+ * listed (and therefore allowed from `providers/types.js`): `ToolDefinition`,
179
+ * which `providers/types.js` re-exports from `@vellumai/skill-host-contracts`
180
+ * — a different type than plugin-api's own `ToolDefinition`.
181
+ */
182
+ const PLUGIN_API_PROVIDER_TYPES: ReadonlySet<string> = new Set([
183
+ "ContentBlock",
184
+ "FileContent",
185
+ "ImageContent",
186
+ "Message",
187
+ "Provider",
188
+ "ProviderEvent",
189
+ "ProviderResponse",
190
+ "RedactedThinkingContent",
191
+ "SendMessageConfig",
192
+ "SendMessageOptions",
193
+ "ServerToolUseContent",
194
+ "TextContent",
195
+ "ThinkingContent",
196
+ "ToolResultContent",
197
+ "ToolUseContent",
198
+ "WebSearchToolResultContent",
199
+ ]);
200
+
201
+ /** Symbols `@vellumai/plugin-api` re-exports from `config/schemas/llm.js`. */
202
+ const PLUGIN_API_LLM_TYPES: ReadonlySet<string> = new Set(["LLMCallSite"]);
203
+
204
+ /** Matches `import ... from "X"`, `export ... from "X"`, `import("X")`,
205
+ * `require("X")`, and side-effect `import "X"` — including multi-line forms,
206
+ * since the between-keyword-and-`from` span never contains a quote. */
207
+ function importSpecifierRegex(): RegExp {
208
+ return /(?:import|export)\b[^'"]*?from\s*['"]([^'"]+)['"]|(?:import|require)\(\s*['"]([^'"]+)['"]\s*\)|^\s*import\s+['"]([^'"]+)['"]/gm;
209
+ }
210
+
211
+ interface PluginFile {
212
+ /** Plugin directory name (first path segment under `defaults/`). */
213
+ plugin: string;
214
+ /** Path relative to the `assistant/` cwd, for messages. */
215
+ relPath: string;
216
+ /** Absolute path on disk. */
217
+ absPath: string;
218
+ source: string;
219
+ }
220
+
221
+ /** Production plugin files: one per plugin subdirectory, tests excluded. */
222
+ function collectPluginFiles(): PluginFile[] {
223
+ const files: PluginFile[] = [];
224
+ for (const ext of ["ts", "tsx"]) {
225
+ for (const rel of new Glob(`${DEFAULTS_REL}/**/*.${ext}`).scanSync({
226
+ cwd: process.cwd(),
227
+ })) {
228
+ const norm = rel.split("/").join(sep);
229
+ if (norm.endsWith(".test.ts") || norm.endsWith(".test.tsx")) continue;
230
+ if (norm.split(sep).includes("__tests__")) continue;
231
+ const underDefaults = relative(DEFAULTS_REL, norm);
232
+ const segments = underDefaults.split(sep);
233
+ // A file directly under defaults/ (e.g. index.ts) is the registry, not a
234
+ // plugin — it has no plugin directory segment.
235
+ if (segments.length < 2) continue;
236
+ const absPath = join(process.cwd(), norm);
237
+ files.push({
238
+ plugin: segments[0]!,
239
+ relPath: norm,
240
+ absPath,
241
+ source: readFileSync(absPath, "utf-8"),
242
+ });
243
+ }
244
+ }
245
+ return files;
246
+ }
247
+
248
+ interface Escape {
249
+ plugin: string;
250
+ specifier: string;
251
+ relPath: string;
252
+ }
253
+
254
+ /** Every escaping import across the production plugin files. */
255
+ function collectEscapes(files: PluginFile[]): Escape[] {
256
+ const escapes: Escape[] = [];
257
+ for (const file of files) {
258
+ const pluginRoot = join(DEFAULTS_ABS, file.plugin);
259
+ const regex = importSpecifierRegex();
260
+ let match: RegExpExecArray | null;
261
+ while ((match = regex.exec(file.source)) !== null) {
262
+ const specifier = match[1] ?? match[2] ?? match[3];
263
+ if (!specifier || specifier === "@vellumai/plugin-api") continue;
264
+ if (specifier.startsWith(".")) {
265
+ const resolved = resolve(dirname(file.absPath), specifier);
266
+ // Stays inside the plugin's own directory → allowed.
267
+ if (!relative(pluginRoot, resolved).startsWith("..")) continue;
268
+ }
269
+ escapes.push({ plugin: file.plugin, specifier, relPath: file.relPath });
270
+ }
271
+ }
272
+ return escapes;
273
+ }
274
+
275
+ /** Distinct escaping specifiers per plugin, sorted — the ratchet's view. */
276
+ function escapesByPlugin(escapes: Escape[]): Map<string, string[]> {
277
+ const sets = new Map<string, Set<string>>();
278
+ for (const e of escapes) {
279
+ let set = sets.get(e.plugin);
280
+ if (!set) sets.set(e.plugin, (set = new Set()));
281
+ set.add(e.specifier);
282
+ }
283
+ return new Map(
284
+ [...sets].map(([plugin, set]) => [plugin, [...set].sort()] as const),
285
+ );
286
+ }
287
+
288
+ /** Parse the named-import symbols out of a `{ ... }` import clause. */
289
+ function parseNamedSymbols(clause: string): string[] {
290
+ return clause
291
+ .split(",")
292
+ .map(
293
+ (part) =>
294
+ part
295
+ .trim()
296
+ .replace(/^type\s+/, "")
297
+ .split(/\s+as\s+/)[0]!,
298
+ )
299
+ .map((name) => name.trim())
300
+ .filter((name) => name.length > 0);
301
+ }
302
+
303
+ /** Symbols a plugin file imports from a host module whose specifier ends with
304
+ * `hostPathSuffix` (e.g. `providers/types.js`), across multi-line clauses. */
305
+ function symbolsImportedFrom(source: string, hostPathSuffix: string): string[] {
306
+ const escaped = hostPathSuffix.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
307
+ const regex = new RegExp(
308
+ String.raw`import\s+(?:type\s+)?\{([^}]*)\}\s*from\s*['"]([^'"]*` +
309
+ escaped +
310
+ String.raw`)['"]`,
311
+ "g",
312
+ );
313
+ const symbols: string[] = [];
314
+ let match: RegExpExecArray | null;
315
+ while ((match = regex.exec(source)) !== null) {
316
+ symbols.push(...parseNamedSymbols(match[1]!));
317
+ }
318
+ return symbols;
319
+ }
320
+
321
+ describe("plugin import boundary", () => {
322
+ const files = collectPluginFiles();
323
+ const escapes = collectEscapes(files);
324
+ const byPlugin = escapesByPlugin(escapes);
325
+
326
+ test("no new escaping import beyond the committed baseline", () => {
327
+ if (process.env.UPDATE_PLUGIN_IMPORT_BASELINE === "1") {
328
+ const regenerated: Record<string, string[]> = {};
329
+ for (const plugin of [...byPlugin.keys()].sort()) {
330
+ regenerated[plugin] = byPlugin.get(plugin)!;
331
+ }
332
+
333
+ console.log(
334
+ "Regenerated plugin import baseline — paste into BASELINE:\n" +
335
+ JSON.stringify(regenerated, null, 2),
336
+ );
337
+ }
338
+
339
+ const exampleFile = new Map<string, string>();
340
+ for (const e of escapes) {
341
+ exampleFile.set(`${e.plugin}${e.specifier}`, e.relPath);
342
+ }
343
+
344
+ const plugins = new Set([...byPlugin.keys(), ...Object.keys(BASELINE)]);
345
+ const added: string[] = [];
346
+ const stale: string[] = [];
347
+ for (const plugin of [...plugins].sort()) {
348
+ const found = new Set(byPlugin.get(plugin) ?? []);
349
+ const allowed = new Set(BASELINE[plugin] ?? []);
350
+ for (const spec of [...found].sort()) {
351
+ if (!allowed.has(spec)) {
352
+ const where = exampleFile.get(`${plugin}${spec}`);
353
+ added.push(` - ${plugin}: "${spec}" (e.g. ${where})`);
354
+ }
355
+ }
356
+ for (const spec of [...allowed].sort()) {
357
+ if (!found.has(spec)) stale.push(` - ${plugin}: "${spec}"`);
358
+ }
359
+ }
360
+
361
+ const problems: string[] = [];
362
+ if (added.length > 0) {
363
+ problems.push(
364
+ "New escaping imports (a plugin reached outside itself for something",
365
+ "not in its baseline):",
366
+ ...added,
367
+ "",
368
+ "If @vellumai/plugin-api already exports this symbol, import it from",
369
+ "there. If this is genuinely host-internal with no plugin-api",
370
+ "equivalent, regenerate the baseline with",
371
+ "UPDATE_PLUGIN_IMPORT_BASELINE=1 and explain the new coupling in your PR.",
372
+ );
373
+ }
374
+ if (stale.length > 0) {
375
+ if (problems.length > 0) problems.push("");
376
+ problems.push(
377
+ "Stale baseline entries (no longer imported — tighten the baseline):",
378
+ ...stale,
379
+ "",
380
+ "Regenerate with UPDATE_PLUGIN_IMPORT_BASELINE=1 to drop them.",
381
+ );
382
+ }
383
+
384
+ expect(problems, problems.join("\n")).toEqual([]);
385
+ });
386
+
387
+ test("plugins import plugin-api-provided types from @vellumai/plugin-api, not host modules", () => {
388
+ const violations: string[] = [];
389
+ for (const file of files) {
390
+ for (const symbol of symbolsImportedFrom(
391
+ file.source,
392
+ "providers/types.js",
393
+ )) {
394
+ if (PLUGIN_API_PROVIDER_TYPES.has(symbol)) {
395
+ violations.push(
396
+ ` - ${file.relPath}: "${symbol}" from providers/types.js`,
397
+ );
398
+ }
399
+ }
400
+ for (const symbol of symbolsImportedFrom(
401
+ file.source,
402
+ "config/schemas/llm.js",
403
+ )) {
404
+ if (PLUGIN_API_LLM_TYPES.has(symbol)) {
405
+ violations.push(
406
+ ` - ${file.relPath}: "${symbol}" from config/schemas/llm.js`,
407
+ );
408
+ }
409
+ }
410
+ }
411
+
412
+ const message = [
413
+ "Plugin files import symbols from host modules that @vellumai/plugin-api",
414
+ "already re-exports. Import them from @vellumai/plugin-api instead:",
415
+ ...violations,
416
+ "",
417
+ "(ToolDefinition from providers/types.js is allowed — it is a distinct",
418
+ "@vellumai/skill-host-contracts type, not plugin-api's ToolDefinition.)",
419
+ ].join("\n");
420
+
421
+ expect(violations, message).toEqual([]);
422
+ });
423
+ });
@@ -378,8 +378,34 @@ describe("resolveVoiceQualityProfile", () => {
378
378
  },
379
379
  };
380
380
  const profile = resolveVoiceQualityProfile();
381
- expect(profile.ttsProvider).toBe("Google");
382
- expect(profile.voice).toBe("");
381
+ expect(profile.ttsProvider).toBe("ElevenLabs");
382
+ expect(profile.voice).toBe(DEFAULT_ELEVENLABS_VOICE_ID);
383
+ });
384
+
385
+ test("synthesized-play TwiML carries a non-empty native fallback voice (64106 regression)", () => {
386
+ // Regression for the dropped-call bug: when a synthesized-play provider's
387
+ // mid-call synthesis fails, the controller falls back to native token TTS.
388
+ // If the TwiML voice is empty, Twilio rejects the turn with error 64106
389
+ // ("TTS provider rejected the request due to invalid parameters") and the
390
+ // call drops. The profile must expose a real ElevenLabs fallback voice.
391
+ mockConfig = {
392
+ calls: {
393
+ voice: { language: "en-US", transcriptionProvider: "Deepgram" },
394
+ },
395
+ services: {
396
+ tts: {
397
+ provider: "fish-audio",
398
+ providers: {
399
+ elevenlabs: { voiceId: "fallback-voice-xyz" },
400
+ "fish-audio": { referenceId: "ref-123" },
401
+ },
402
+ },
403
+ },
404
+ };
405
+ const profile = resolveVoiceQualityProfile();
406
+ expect(profile.ttsProvider).toBe("ElevenLabs");
407
+ expect(profile.voice).toBe("fallback-voice-xyz");
408
+ expect(profile.voice).not.toBe("");
383
409
  });
384
410
 
385
411
  test("preserves language setting for synthesized providers", () => {
@@ -429,8 +455,8 @@ describe("resolveVoiceQualityProfile", () => {
429
455
  };
430
456
  const profile = resolveVoiceQualityProfile();
431
457
  // Should resolve to fish-audio from canonical config
432
- expect(profile.ttsProvider).toBe("Google");
433
- expect(profile.voice).toBe("");
458
+ expect(profile.ttsProvider).toBe("ElevenLabs");
459
+ expect(profile.voice).toBe(DEFAULT_ELEVENLABS_VOICE_ID);
434
460
  });
435
461
 
436
462
  // -- Strategy-based behavior (explicit call strategy) -------------------
@@ -457,8 +483,8 @@ describe("resolveVoiceQualityProfile", () => {
457
483
  },
458
484
  };
459
485
  const profile = resolveVoiceQualityProfile();
460
- expect(profile.ttsProvider).toBe("Google");
461
- expect(profile.voice).toBe("");
486
+ expect(profile.ttsProvider).toBe("ElevenLabs");
487
+ expect(profile.voice).toBe(DEFAULT_ELEVENLABS_VOICE_ID);
462
488
  });
463
489
 
464
490
  test("native-twilio strategy delegates voice-spec to registered builder", () => {
@@ -567,10 +593,10 @@ describe("resolveVoiceQualityProfile", () => {
567
593
  },
568
594
  };
569
595
  const profile = resolveVoiceQualityProfile();
570
- // Deepgram is synthesized-play in the catalog, so Twilio gets the
571
- // placeholder ttsProvider and an empty voice string.
572
- expect(profile.ttsProvider).toBe("Google");
573
- expect(profile.voice).toBe("");
596
+ // Deepgram is synthesized-play in the catalog. The TwiML still carries the
597
+ // ElevenLabs fallback voice so a synthesis failure can speak natively.
598
+ expect(profile.ttsProvider).toBe("ElevenLabs");
599
+ expect(profile.voice).toBe(DEFAULT_ELEVENLABS_VOICE_ID);
574
600
  });
575
601
 
576
602
  test("Deepgram preserves language setting on synthesized-play path", () => {
@@ -594,7 +620,7 @@ describe("resolveVoiceQualityProfile", () => {
594
620
  };
595
621
  const profile = resolveVoiceQualityProfile();
596
622
  expect(profile.language).toBe("fr-FR");
597
- expect(profile.ttsProvider).toBe("Google");
598
- expect(profile.voice).toBe("");
623
+ expect(profile.ttsProvider).toBe("ElevenLabs");
624
+ expect(profile.voice).toBe(DEFAULT_ELEVENLABS_VOICE_ID);
599
625
  });
600
626
  });
@@ -9,11 +9,10 @@
9
9
  *
10
10
  * Returned as part of `LlmContextResponse` — see `./llm-context-response.ts`.
11
11
  *
12
- * `live` / `shadow` reflect the CURRENT `memory-v3-live` / `memory-v3-shadow`
13
- * flag state, not the per-turn history (the selection rows don't record which
14
- * mode was active when they were written). When `live` is true the rendered
15
- * `injectedText` reflects the live selection; when only `shadow` is true it is
16
- * the block that WOULD have been injected.
12
+ * `live` reflects the CURRENT `memory.v3.live` config state, not the per-turn
13
+ * history (the selection rows don't record whether v3 was live when they were
14
+ * written). The rendered `injectedText` is the `<memory>` block for the logged
15
+ * selection.
17
16
  *
18
17
  * `injectedText` is inspector-only, not a verbatim record of the live cards +
19
18
  * spotlight. It re-renders each selection's matched section — resolved from the
@@ -58,7 +57,6 @@ export type MemoryV3SelectionRow = z.infer<typeof MemoryV3SelectionRowSchema>;
58
57
  export const MemoryV3SelectionLogSchema = z.object({
59
58
  turn: z.number(),
60
59
  live: z.boolean(),
61
- shadow: z.boolean(),
62
60
  selections: z.array(MemoryV3SelectionRowSchema),
63
61
  injectedText: z.string(),
64
62
  });