@vellumai/assistant 0.10.8-dev.202607142125.4cfef21 โ†’ 0.10.8-dev.202607142225.1d4ba6c

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.10.8-dev.202607142125.4cfef21",
3
+ "version": "0.10.8-dev.202607142225.1d4ba6c",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "exports": {
@@ -896,11 +896,12 @@ describe("AssistantConfigSchema", () => {
896
896
  mode: "open-mic",
897
897
  vad: {
898
898
  speechEnergyThreshold: 800,
899
- silenceThresholdMs: 800,
899
+ silenceThresholdMs: 1200,
900
900
  maxTurnDurationMs: 30000,
901
- bargeInMinSpeechMs: 60,
901
+ bargeInMinSpeechMs: 250,
902
902
  },
903
903
  maxSessionDurationSeconds: 1800,
904
+ archiveAudio: false,
904
905
  });
905
906
  });
906
907
 
@@ -15,7 +15,8 @@
15
15
  "description": "Proactive chief-of-staff: a morning digest, propose-then-confirm inbox triage, calendar prep, and a weekly competitor brief.",
16
16
  "category": "productivity",
17
17
  "homepage": "https://github.com/vellum-ai/admin-copilot",
18
- "license": "MIT"
18
+ "license": "MIT",
19
+ "icon": "๐Ÿง‘โ€๐Ÿ’ผ"
19
20
  },
20
21
  {
21
22
  "name": "agent-wrapped",
@@ -28,7 +29,8 @@
28
29
  "category": "creative",
29
30
  "license": "MIT",
30
31
  "submittedBy": "marina",
31
- "homepage": "https://agent-wrapped.com"
32
+ "homepage": "https://agent-wrapped.com",
33
+ "icon": "๐ŸŽ"
32
34
  },
33
35
  {
34
36
  "name": "ai-hero-engineer-kit",
@@ -37,10 +39,11 @@
37
39
  "repo": "marinatrajk/ai-hero-engineer-kit",
38
40
  "ref": "18d33c62b15a3e2cc22dc070723fe3077749dcec"
39
41
  },
40
- "description": "Five engineering skills adapted from Matt Pocock's '5 Agent Skills I Use Every Day' \u2014 plans, PRDs, vertical-slice issues, TDD loop, and codebase deepening in one install.",
42
+ "description": "Five engineering skills adapted from Matt Pocock's '5 Agent Skills I Use Every Day' โ€” plans, PRDs, vertical-slice issues, TDD loop, and codebase deepening in one install.",
41
43
  "category": "developer",
42
44
  "homepage": "https://github.com/marinatrajk/ai-hero-engineer-kit",
43
- "license": "MIT"
45
+ "license": "MIT",
46
+ "icon": "๐Ÿงฐ"
44
47
  },
45
48
  {
46
49
  "name": "amex-perk-reminder",
@@ -52,7 +55,8 @@
52
55
  "description": "Track every credit on the Amex cards you hold (Platinum, Gold, Business Platinum, Delta Gold/Platinum/Reserve) with status-aware reminders before any window closes.",
53
56
  "category": "lifestyle",
54
57
  "homepage": "https://github.com/AnitaKirkovska/amex-perk-reminder",
55
- "license": "MIT"
58
+ "license": "MIT",
59
+ "icon": "๐Ÿ’ณ"
56
60
  },
57
61
  {
58
62
  "name": "astrology-horoscope",
@@ -64,7 +68,8 @@
64
68
  "description": "Western tropical natal chart and transit computation. Local ephemeris (astronomy-engine, no API keys) for planet positions, ascendant/midheaven, whole-sign houses, and aspects, plus a natal-reading skill that interprets the raw chart into a readable report.",
65
69
  "category": "hobby",
66
70
  "homepage": "https://github.com/vellum-ai/astrology-horoscope",
67
- "license": "MIT"
71
+ "license": "MIT",
72
+ "icon": "๐Ÿ”ฎ"
68
73
  },
69
74
  {
70
75
  "name": "caveman",
@@ -77,7 +82,7 @@
77
82
  "category": "productivity",
78
83
  "homepage": "https://github.com/JuliusBrussee/caveman",
79
84
  "license": "MIT",
80
- "icon": "\ud83e\uddb4"
85
+ "icon": "๐Ÿฆด"
81
86
  },
82
87
  {
83
88
  "name": "coffee-aficionado",
@@ -90,7 +95,7 @@
90
95
  "category": "lifestyle",
91
96
  "homepage": "https://github.com/vellum-ai/coffee-aficionado",
92
97
  "license": "MIT",
93
- "icon": "\u2615"
98
+ "icon": "โ˜•"
94
99
  },
95
100
  {
96
101
  "name": "cofounder",
@@ -102,7 +107,8 @@
102
107
  "description": "AI co-founder that remembers your company context, challenges your thinking, and tracks your decisions.",
103
108
  "category": "productivity",
104
109
  "homepage": "https://github.com/vellum-ai/cofounder",
105
- "license": "MIT"
110
+ "license": "MIT",
111
+ "icon": "๐Ÿค"
106
112
  },
107
113
  {
108
114
  "name": "cognee",
@@ -114,7 +120,8 @@
114
120
  },
115
121
  "description": "Cognee knowledge graph memory for Vellum Assistant: session-aware storage, auto-routing recall, and persistent learning across sessions. Supports local mode and Cognee Cloud.",
116
122
  "category": "memory",
117
- "homepage": "https://github.com/topoteretes/cognee-integrations/tree/main/integrations/vellum-assistant"
123
+ "homepage": "https://github.com/topoteretes/cognee-integrations/tree/main/integrations/vellum-assistant",
124
+ "icon": "๐Ÿ•ธ๏ธ"
118
125
  },
119
126
  {
120
127
  "name": "dynamic-notch",
@@ -126,7 +133,8 @@
126
133
  "description": "Put your Vellum assistant inside your MacBook's notch: click to chat, hold Ctrl+Option to talk, and get a floating task bulb while it works, with live-streamed replies and per-sentence voice.",
127
134
  "category": "interface",
128
135
  "homepage": "https://github.com/AnitaKirkovska/dynamic-notch",
129
- "license": "MIT"
136
+ "license": "MIT",
137
+ "icon": "๐Ÿ’ป"
130
138
  },
131
139
  {
132
140
  "name": "family-briefing",
@@ -138,7 +146,8 @@
138
146
  "description": "Daily morning briefing for parents: merges family calendars, triages school emails, and detects scheduling conflicts.",
139
147
  "category": "productivity",
140
148
  "homepage": "https://github.com/AnitaKirkovska/family-briefing",
141
- "license": "MIT"
149
+ "license": "MIT",
150
+ "icon": "๐Ÿ‘จโ€๐Ÿ‘ฉโ€๐Ÿ‘งโ€๐Ÿ‘ฆ"
142
151
  },
143
152
  {
144
153
  "name": "fitness-companion",
@@ -150,7 +159,8 @@
150
159
  "description": "Fitness and nutrition companion. Logs meals, workouts, and weight; looks up nutrition data from OpenFoodFacts; calculates macro targets; and injects daily fitness context into every conversation.",
151
160
  "category": "health",
152
161
  "homepage": "https://github.com/vellum-ai/fitness-companion",
153
- "license": "MIT"
162
+ "license": "MIT",
163
+ "icon": "๐Ÿ’ช"
154
164
  },
155
165
  {
156
166
  "name": "git-workflow",
@@ -162,7 +172,8 @@
162
172
  "description": "Git workflow tools and multi-perspective code review: branch management, push, stash, PR creation/review/merge/checkout, diffs, logs, changelog generation, and parallel subagent code review with confidence scoring.",
163
173
  "category": "developer",
164
174
  "homepage": "https://github.com/vellum-ai/git-workflow",
165
- "license": "MIT"
175
+ "license": "MIT",
176
+ "icon": "๐Ÿ”€"
166
177
  },
167
178
  {
168
179
  "name": "google-drive-search",
@@ -174,7 +185,8 @@
174
185
  "description": "Local full-text search across your Google Drive. Indexes file metadata and document content into SQLite FTS5 for instant, private search.",
175
186
  "category": "productivity",
176
187
  "homepage": "https://github.com/ZeebBoyBlue/google-drive-search",
177
- "license": "Apache-2.0"
188
+ "license": "Apache-2.0",
189
+ "icon": "๐Ÿ”"
178
190
  },
179
191
  {
180
192
  "name": "influencer-marketing",
@@ -186,7 +198,8 @@
186
198
  "description": "Find and research influencers on Instagram, TikTok, and X/Twitter. Includes a strategy builder that walks from campaign brief to research criteria, a research skill that discovers real profiles via browser automation, scores candidates, and exports ranked shortlists, and a dashboard skill that turns the shortlist into an interactive outreach tracker.",
187
199
  "category": "marketing",
188
200
  "homepage": "https://github.com/ZeebBoyBlue/influencer",
189
- "license": "MIT"
201
+ "license": "MIT",
202
+ "icon": "๐Ÿ“ฃ"
190
203
  },
191
204
  {
192
205
  "name": "kitchen-companion",
@@ -198,7 +211,8 @@
198
211
  "description": "Full kitchen companion. Scan your fridge with a photo to stock a pantry inventory and get 3 makeable-now recipes, get alerts before food expires, clip recipes from any URL into a clean format, and generate meal plans with grocery lists from what you actually have.",
199
212
  "category": "lifestyle",
200
213
  "homepage": "https://github.com/vellum-ai/kitchen-companion",
201
- "license": "MIT"
214
+ "license": "MIT",
215
+ "icon": "๐Ÿณ"
202
216
  },
203
217
  {
204
218
  "name": "level-up",
@@ -210,7 +224,8 @@
210
224
  "description": "Surfaces a first-class \"Level Up\" diff card whenever the assistant edits its own skills or plugins, so you can see how it improved itself after the fact.",
211
225
  "category": "developer",
212
226
  "homepage": "https://github.com/vellum-ai/level-up",
213
- "license": "MIT"
227
+ "license": "MIT",
228
+ "icon": "๐Ÿ†™"
214
229
  },
215
230
  {
216
231
  "name": "marketing-expert",
@@ -222,7 +237,8 @@
222
237
  "description": "Acts as a full-stack marketing expert for any business (B2B or B2C): an on-demand persona plus playbook skills (positioning, demand planning, launches, content & copywriting, brand voice, email sequences, SEO & GEO, competitive teardown, board reporting) and deterministic funnel/positioning/GTM/competitive tools.",
223
238
  "category": "marketing",
224
239
  "homepage": "https://github.com/vellum-ai/marketing-expert",
225
- "license": "MIT"
240
+ "license": "MIT",
241
+ "icon": "๐Ÿ“ˆ"
226
242
  },
227
243
  {
228
244
  "name": "model-router",
@@ -234,7 +250,8 @@
234
250
  "description": "Routes each user-facing message to one of your workspace's inference profiles, so model selection can vary per call instead of staying pinned to a single profile.",
235
251
  "category": "productivity",
236
252
  "homepage": "https://github.com/AnitaKirkovska/model-router",
237
- "license": "MIT"
253
+ "license": "MIT",
254
+ "icon": "๐Ÿšฆ"
238
255
  },
239
256
  {
240
257
  "name": "motion-design",
@@ -246,7 +263,8 @@
246
263
  "description": "Teaches the assistant to build polished UI animations with Motion (motion.dev) for React: animated product mockups, chat replays, typing effects, and staggered reveals, with a complete animated chat example included.",
247
264
  "category": "creative",
248
265
  "homepage": "https://github.com/vellum-ai/motion-design",
249
- "license": "MIT"
266
+ "license": "MIT",
267
+ "icon": "๐ŸŽฌ"
250
268
  },
251
269
  {
252
270
  "name": "originkit",
@@ -258,7 +276,8 @@
258
276
  "description": "Browse and import animated UI components from Originkit, a free library of 50 animated components. Browse by category or search term without an API key; fetch component source code (React, Next.js, Vite, Framer) with a free key from originkit.dev.",
259
277
  "category": "productivity",
260
278
  "homepage": "https://github.com/vellum-ai/originkit",
261
- "license": "MIT"
279
+ "license": "MIT",
280
+ "icon": "๐Ÿงฑ"
262
281
  },
263
282
  {
264
283
  "name": "personal-finance",
@@ -270,7 +289,8 @@
270
289
  "description": "Track expenses, income, recurring bills, and savings funds from your assistant. Multi-currency, receipt OCR, Plaid bank sync, and financial summaries. All data stored locally in SQLite.",
271
290
  "category": "productivity",
272
291
  "homepage": "https://github.com/AnitaKirkovska/personal-finance",
273
- "license": "MIT"
292
+ "license": "MIT",
293
+ "icon": "๐Ÿ’ฐ"
274
294
  },
275
295
  {
276
296
  "name": "plant-doctor",
@@ -282,7 +302,8 @@
282
302
  "description": "Diagnose plants from photos. Identifies the species, assesses health with visible evidence, and returns a care plan. Named plants get a persistent checkup history so you can track whether they are improving.",
283
303
  "category": "lifestyle",
284
304
  "homepage": "https://github.com/vellum-ai/plant-doctor",
285
- "license": "MIT"
305
+ "license": "MIT",
306
+ "icon": "๐Ÿชด"
286
307
  },
287
308
  {
288
309
  "name": "polyglot",
@@ -294,7 +315,8 @@
294
315
  "description": "Ambient language acquisition through your assistant. Three graduated modes (practice, ambient, immersive) weave a target language into your real conversations, with SM-2 spaced repetition, error-pattern tracking, and CEFR level history.",
295
316
  "category": "education",
296
317
  "homepage": "https://github.com/vellum-ai/polyglot",
297
- "license": "MIT"
318
+ "license": "MIT",
319
+ "icon": "๐Ÿ—ฃ๏ธ"
298
320
  },
299
321
  {
300
322
  "name": "proactive-slacks",
@@ -306,7 +328,8 @@
306
328
  "description": "Let your assistant reach you on Slack on purpose. Route notifications by topic to the right channel at the right volume, with heartbeat-based change detection so it only pings when something actually changed.",
307
329
  "category": "productivity",
308
330
  "homepage": "https://github.com/AnitaKirkovska/proactive-slacks",
309
- "license": "MIT"
331
+ "license": "MIT",
332
+ "icon": "๐Ÿ’ฌ"
310
333
  },
311
334
  {
312
335
  "name": "reading-pal",
@@ -318,7 +341,8 @@
318
341
  "description": "Your reading life in one plugin. Track your TBR pile, get guilt-tripped about your backlog, find your next read, and sync from Goodreads.",
319
342
  "category": "hobby",
320
343
  "homepage": "https://github.com/vellum-ai/reading-pal",
321
- "license": "MIT"
344
+ "license": "MIT",
345
+ "icon": "๐Ÿ“š"
322
346
  },
323
347
  {
324
348
  "name": "simple-memory",
@@ -330,7 +354,8 @@
330
354
  "description": "Reference memory plugin that exercises the full plugin API with low-cost variants of Vellum's memory defaults.",
331
355
  "category": "memory",
332
356
  "homepage": "https://github.com/vellum-ai/simple-memory",
333
- "license": "MIT"
357
+ "license": "MIT",
358
+ "icon": "๐Ÿง "
334
359
  },
335
360
  {
336
361
  "name": "smb-inbox-brief",
@@ -342,7 +367,8 @@
342
367
  "description": "Find urgent replies, overdue invoices, quotes, and customer follow-ups before they slip. Scans email, logs business items, delivers urgency-ranked daily briefs.",
343
368
  "category": "productivity",
344
369
  "homepage": "https://github.com/AnitaKirkovska/smb-inbox-brief",
345
- "license": "MIT"
370
+ "license": "MIT",
371
+ "icon": "๐Ÿ“ง"
346
372
  },
347
373
  {
348
374
  "name": "tennis-companion",
@@ -354,7 +380,8 @@
354
380
  "description": "Tennis journal, drill sessions, court finder, and progress tracking. Log matches with opponent tendencies, generate 30-minute practice sessions with progressions, find nearby courts via OpenStreetMap, and track win rate by surface and conditions.",
355
381
  "category": "health",
356
382
  "homepage": "https://github.com/vellum-ai/tennis-companion",
357
- "license": "MIT"
383
+ "license": "MIT",
384
+ "icon": "๐ŸŽพ"
358
385
  },
359
386
  {
360
387
  "name": "travel-planner",
@@ -366,7 +393,8 @@
366
393
  "description": "A travel EA for your assistant. Remembers how you travel (carrier, miles, hotel budget, card benefits), keeps per-trip records, watches Gmail for booking changes, syncs your calendar, and emails an EA-style trip brief 48 hours before departure.",
367
394
  "category": "lifestyle",
368
395
  "homepage": "https://github.com/AnitaKirkovska/travel-planner",
369
- "license": "MIT"
396
+ "license": "MIT",
397
+ "icon": "โœˆ๏ธ"
370
398
  },
371
399
  {
372
400
  "name": "vellum-client-qa",
@@ -378,7 +406,8 @@
378
406
  "description": "Cross-platform QA rig for vellum-assistant clients: mock the platform backend, boot the real chat UI, and verify feature branches in headless Chromium (web), an iOS simulator, or Electron with screenshot and video proof.",
379
407
  "category": "developer",
380
408
  "homepage": "https://github.com/vellum-ai/vellum-client-qa",
381
- "license": "MIT"
409
+ "license": "MIT",
410
+ "icon": "๐Ÿงช"
382
411
  },
383
412
  {
384
413
  "name": "writing-coach",
@@ -390,7 +419,8 @@
390
419
  "description": "A writing coach in your pocket. Daily prompts that escalate with your streak, savage-but-constructive draft roasts, query letter critiques, and a word count enforcer that guilt-trips you when you skip.",
391
420
  "category": "hobby",
392
421
  "homepage": "https://github.com/vellum-ai/writing-coach",
393
- "license": "MIT"
422
+ "license": "MIT",
423
+ "icon": "โœ๏ธ"
394
424
  }
395
425
  ]
396
426
  }
@@ -11,9 +11,9 @@ describe("LiveVoiceVadConfigSchema", () => {
11
11
  const parsed = LiveVoiceVadConfigSchema.parse({});
12
12
  expect(parsed).toEqual({
13
13
  speechEnergyThreshold: 800,
14
- silenceThresholdMs: 800,
14
+ silenceThresholdMs: 1200,
15
15
  maxTurnDurationMs: 30_000,
16
- bargeInMinSpeechMs: 60,
16
+ bargeInMinSpeechMs: 250,
17
17
  });
18
18
  });
19
19
 
@@ -64,22 +64,40 @@ describe("LiveVoiceConfigSchema", () => {
64
64
  mode: "open-mic",
65
65
  vad: {
66
66
  speechEnergyThreshold: 800,
67
- silenceThresholdMs: 800,
67
+ silenceThresholdMs: 1200,
68
68
  maxTurnDurationMs: 30_000,
69
- bargeInMinSpeechMs: 60,
69
+ bargeInMinSpeechMs: 250,
70
70
  },
71
71
  maxSessionDurationSeconds: 1800,
72
+ // Off by default: voice turns carry only their transcript, no audio
73
+ // artifacts on the conversation messages (JARVIS-1283).
74
+ archiveAudio: false,
72
75
  });
73
76
  });
74
77
 
78
+ test("archiveAudio can be enabled", () => {
79
+ expect(
80
+ LiveVoiceConfigSchema.parse({ archiveAudio: true }).archiveAudio,
81
+ ).toBe(true);
82
+ });
83
+
84
+ test("rejects a non-boolean archiveAudio", () => {
85
+ const result = LiveVoiceConfigSchema.safeParse({ archiveAudio: "yes" });
86
+ expect(result.success).toBe(false);
87
+ if (!result.success) {
88
+ const msgs = result.error.issues.map((i) => i.message);
89
+ expect(msgs.some((m) => m.includes("liveVoice.archiveAudio"))).toBe(true);
90
+ }
91
+ });
92
+
75
93
  test("accepts overrides", () => {
76
94
  const parsed = LiveVoiceConfigSchema.parse({
77
95
  mode: "ptt",
78
- vad: { silenceThresholdMs: 1200 },
96
+ vad: { silenceThresholdMs: 900 },
79
97
  maxSessionDurationSeconds: 600,
80
98
  });
81
99
  expect(parsed.mode).toBe("ptt");
82
- expect(parsed.vad.silenceThresholdMs).toBe(1200);
100
+ expect(parsed.vad.silenceThresholdMs).toBe(900);
83
101
  // Unspecified vad fields still get defaults
84
102
  expect(parsed.vad.speechEnergyThreshold).toBe(800);
85
103
  expect(parsed.vad.maxTurnDurationMs).toBe(30_000);
@@ -20,9 +20,9 @@ export const LiveVoiceVadConfigSchema = z
20
20
  .number({ error: "liveVoice.vad.silenceThresholdMs must be a number" })
21
21
  .int("liveVoice.vad.silenceThresholdMs must be an integer")
22
22
  .positive("liveVoice.vad.silenceThresholdMs must be a positive integer")
23
- .default(800)
23
+ .default(1200)
24
24
  .describe(
25
- "Trailing silence duration (ms) after speech that ends the user's turn",
25
+ "Trailing silence duration (ms) after speech that ends the user's turn โ€” the default 'pause before reply'. Clients may override it per-session via the start frame.",
26
26
  ),
27
27
  maxTurnDurationMs: z
28
28
  .number({ error: "liveVoice.vad.maxTurnDurationMs must be a number" })
@@ -38,9 +38,9 @@ export const LiveVoiceVadConfigSchema = z
38
38
  .nonnegative(
39
39
  "liveVoice.vad.bargeInMinSpeechMs must be a nonnegative integer",
40
40
  )
41
- .default(60)
41
+ .default(250)
42
42
  .describe(
43
- "Sustained speech (ms) required before speech during assistant playback interrupts it; 0 disables the guard",
43
+ "Sustained speech (ms) required before speech during assistant playback interrupts it โ€” the default 'interrupt sensitivity' (higher = harder to interrupt). 0 disables the guard. Clients may override it per-session via the start frame. Raised from 60 so brief TTS bleed through imperfect echo cancellation no longer self-interrupts the assistant.",
44
44
  ),
45
45
  })
46
46
  .describe(
@@ -68,6 +68,12 @@ export const LiveVoiceConfigSchema = z
68
68
  )
69
69
  .default(1800)
70
70
  .describe("Maximum duration of a single live voice session in seconds"),
71
+ archiveAudio: z
72
+ .boolean({ error: "liveVoice.archiveAudio must be a boolean" })
73
+ .default(false)
74
+ .describe(
75
+ "Persist the recorded user + assistant audio of each voice turn as attachments on the conversation messages. Off by default: voice turns carry only their transcribed text, so no audio-file artifacts land in the conversation history. Enable for playback/debugging.",
76
+ ),
71
77
  })
72
78
  .describe(
73
79
  "Live voice (in-app duplex audio) configuration โ€” mic mode, VAD tuning, and session limits",
@@ -1,14 +1,16 @@
1
- import { describe, expect, mock, test } from "bun:test";
1
+ import { describe, expect, mock, spyOn, test } from "bun:test";
2
2
 
3
3
  import type {
4
4
  VoiceTurnCallbacks,
5
5
  VoiceTurnOptions,
6
6
  } from "../../calls/voice-session-bridge.js";
7
+ import { loadRawConfig, saveRawConfig } from "../../config/loader.js";
7
8
  import type {
8
9
  StreamingTranscriber,
9
10
  SttStreamServerEvent,
10
11
  } from "../../stt/types.js";
11
12
  import type { LiveVoiceAudioArchiveResult } from "../live-voice-archive.js";
13
+ import * as liveVoiceArchive from "../live-voice-archive.js";
12
14
  import {
13
15
  createLiveVoiceSession,
14
16
  type LiveVoiceSessionArchiveAudioInput,
@@ -217,7 +219,9 @@ async function waitFor(
217
219
  message = "Timed out waiting for live voice integration condition",
218
220
  ): Promise<void> {
219
221
  for (let attempt = 0; attempt < 80; attempt += 1) {
220
- if (predicate()) return;
222
+ if (predicate()) {
223
+ return;
224
+ }
221
225
  await new Promise((resolve) => setTimeout(resolve, 5));
222
226
  }
223
227
  throw new Error(message);
@@ -672,3 +676,113 @@ describe("LiveVoiceSession integration smoke harness", () => {
672
676
  ]);
673
677
  });
674
678
  });
679
+
680
+ // ---------------------------------------------------------------------------
681
+ // Audio archiving is config-gated and OFF by default (JARVIS-1283): a voice
682
+ // turn persists only its transcribed text, so no audio-file artifact lands on
683
+ // the conversation messages unless `liveVoice.archiveAudio` is enabled. These
684
+ // exercise the production factory's config resolution (no injected archiver).
685
+ // ---------------------------------------------------------------------------
686
+
687
+ describe("live-voice audio archiving default (JARVIS-1283)", () => {
688
+ function makeSingleTurnLegs() {
689
+ const startVoiceTurn = mock(async (options: VoiceTurnOptions) => {
690
+ options.callbacks?.persisted_user_message_id?.("user-message-123");
691
+ options.callbacks?.assistant_text_delta?.(
692
+ makeTextDelta("Hello from the assistant."),
693
+ );
694
+ options.callbacks?.message_complete?.(makeMessageComplete());
695
+ return { turnId: "bridge-turn-1", abort: mock() };
696
+ });
697
+ const streamTtsAudio = mock(async (options: LiveVoiceTtsOptions) => {
698
+ options.onAudioChunk(makeTtsChunk(`audio:${options.text}`));
699
+ return makeTtsResult(options.text);
700
+ });
701
+ return { startVoiceTurn, streamTtsAudio };
702
+ }
703
+
704
+ async function driveOneTurn(
705
+ session: ReturnType<typeof createLiveVoiceSession>,
706
+ frames: LiveVoiceServerFrame[],
707
+ ) {
708
+ await session.start();
709
+ await session.handleBinaryAudio(new Uint8Array([1, 2, 3, 4]));
710
+ await session.handleClientFrame({ type: "ptt_release" });
711
+ await waitFor(() => frames.some((frame) => frame.type === "tts_done"));
712
+ }
713
+
714
+ test("with no injected archiver and default config, nothing is archived", async () => {
715
+ const { startVoiceTurn, streamTtsAudio } = makeSingleTurnLegs();
716
+ const { context, frames } = createContext();
717
+ // No `archiveAudio` option โ†’ the factory consults config, which defaults
718
+ // off in the test workspace.
719
+ const session = createLiveVoiceSession(context, {
720
+ resolveCredentialReadiness: null,
721
+ resolveTranscriber: mock(async () => new FakeStreamingTranscriber()),
722
+ startVoiceTurn,
723
+ streamTtsAudio,
724
+ metricsClock: createClock(),
725
+ createTurnId: () => "live-turn-1",
726
+ });
727
+
728
+ await driveOneTurn(session, frames);
729
+
730
+ // The turn completed, but no `archived` frame was emitted โ€” no audio
731
+ // attachment was written to either message.
732
+ expect(startVoiceTurn).toHaveBeenCalledTimes(1);
733
+ expect(frames.some((frame) => frame.type === "archived")).toBe(false);
734
+ expect(frameTypes(frames)).not.toContain("archived");
735
+ });
736
+
737
+ test("liveVoice.archiveAudio=true wires the default archiver through the factory", async () => {
738
+ // The real linkers write to the DB; these are role-less inputs, so stub
739
+ // them (synchronously โ€” the linkers are sync) with a valid archived result
740
+ // per role. The point under test is the configโ†’default-archiver wiring, not
741
+ // the DB write, which the injected-archiver tests above already cover.
742
+ const audioInput = {
743
+ sessionId: "session-123",
744
+ turnId: "live-turn-1",
745
+ mimeType: "audio/pcm",
746
+ audio: {
747
+ type: "base64" as const,
748
+ dataBase64: Buffer.from([1, 2, 3, 4]).toString("base64"),
749
+ },
750
+ };
751
+ const userSpy = spyOn(
752
+ liveVoiceArchive,
753
+ "linkLiveVoiceUserUtteranceAudioToMessage",
754
+ ).mockReturnValue(makeArchiveResult({ ...audioInput, role: "user" }));
755
+ const assistantSpy = spyOn(
756
+ liveVoiceArchive,
757
+ "linkLiveVoiceAssistantResponseAudioToMessage",
758
+ ).mockReturnValue(makeArchiveResult({ ...audioInput, role: "assistant" }));
759
+ const originalRaw = loadRawConfig();
760
+ saveRawConfig({ ...originalRaw, liveVoice: { archiveAudio: true } });
761
+
762
+ try {
763
+ const { startVoiceTurn, streamTtsAudio } = makeSingleTurnLegs();
764
+ const { context, frames } = createContext();
765
+ // Still no injected `archiveAudio` โ€” the config default must supply it.
766
+ const session = createLiveVoiceSession(context, {
767
+ resolveCredentialReadiness: null,
768
+ resolveTranscriber: mock(async () => new FakeStreamingTranscriber()),
769
+ startVoiceTurn,
770
+ streamTtsAudio,
771
+ metricsClock: createClock(),
772
+ createTurnId: () => "live-turn-1",
773
+ });
774
+
775
+ await driveOneTurn(session, frames);
776
+
777
+ expect(userSpy).toHaveBeenCalledTimes(1);
778
+ expect(assistantSpy).toHaveBeenCalledTimes(1);
779
+ expect(frames.filter((frame) => frame.type === "archived")).toHaveLength(
780
+ 2,
781
+ );
782
+ } finally {
783
+ saveRawConfig(originalRaw);
784
+ userSpy.mockRestore();
785
+ assistantSpy.mockRestore();
786
+ }
787
+ });
788
+ });