@mycadet/assistant 0.12.10 → 0.13.0

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 (180) hide show
  1. package/node_modules/@mycadet/ces-client/node_modules/@mycadet/service-contracts/src/__tests__/ingress.test.ts +10 -10
  2. package/node_modules/@mycadet/ces-client/node_modules/@mycadet/service-contracts/src/ingress.ts +3 -3
  3. package/node_modules/@mycadet/ces-client/src/http-credentials.ts +18 -1
  4. package/node_modules/@mycadet/gateway-client/node_modules/@mycadet/service-contracts/src/__tests__/ingress.test.ts +10 -10
  5. package/node_modules/@mycadet/gateway-client/node_modules/@mycadet/service-contracts/src/ingress.ts +3 -3
  6. package/node_modules/@mycadet/gateway-client/src/gateway-ipc-contracts.ts +0 -20
  7. package/node_modules/@mycadet/service-contracts/src/__tests__/ingress.test.ts +10 -10
  8. package/node_modules/@mycadet/service-contracts/src/ingress.ts +3 -3
  9. package/openapi.yaml +2 -708
  10. package/package.json +2 -3
  11. package/scripts/sync-web-search-catalog.ts +1 -2
  12. package/src/__tests__/cloud-profile-guard.test.ts +41 -5
  13. package/src/__tests__/config-schema.test.ts +6 -38
  14. package/src/__tests__/conversation-queue.test.ts +45 -6
  15. package/src/__tests__/db-readiness-http-gate.test.ts +0 -6
  16. package/src/__tests__/persist-onboarding-artifacts.test.ts +0 -1
  17. package/src/__tests__/persona-resolver.test.ts +0 -51
  18. package/src/__tests__/provider-cloud-proxy-integration.test.ts +2 -110
  19. package/src/__tests__/run-due-schedules.test.ts +0 -4
  20. package/src/__tests__/schedule-store.test.ts +0 -12
  21. package/src/__tests__/stt-stream-session.test.ts +4 -4
  22. package/src/__tests__/telephony-tts-guard.test.ts +2 -50
  23. package/src/__tests__/voice-config-update.test.ts +12 -167
  24. package/src/__tests__/web-search-catalog-parity.test.ts +0 -2
  25. package/src/__tests__/workspace-migration-130-speech-mode-to-provider.test.ts +9 -7
  26. package/src/__tests__/workspace-migration-132-web-search-mode-to-provider.test.ts +6 -6
  27. package/src/calls/__tests__/voice-session-bridge.test.ts +1 -4
  28. package/src/calls/resolve-call-tts-provider.ts +6 -25
  29. package/src/calls/telephony-credential-preflight.ts +0 -10
  30. package/src/calls/telephony-tts-capability.ts +1 -17
  31. package/src/cli/commands/tts.ts +1 -2
  32. package/src/cli/index.help.ts +0 -2
  33. package/src/cli/program.ts +0 -2
  34. package/src/config/bundled-skills/settings/tools/voice-config-update.ts +4 -42
  35. package/src/config/schemas/__tests__/stt.test.ts +10 -14
  36. package/src/config/schemas/llm.ts +3 -5
  37. package/src/config/schemas/services.ts +5 -6
  38. package/src/config/schemas/stt.ts +1 -3
  39. package/src/config/schemas/tts.ts +1 -30
  40. package/src/daemon/daemon-readiness.ts +4 -29
  41. package/src/daemon/lifecycle.ts +0 -31
  42. package/src/daemon/providers-setup.ts +0 -8
  43. package/src/heartbeat/__tests__/heartbeat-service.test.ts +0 -1
  44. package/src/heartbeat/heartbeat-service.ts +1 -66
  45. package/src/ipc/__tests__/db-readiness-ipc-gate.test.ts +1 -9
  46. package/src/live-voice/__tests__/live-voice-credential-preflight.test.ts +0 -44
  47. package/src/live-voice/__tests__/live-voice-flux-turn-end.test.ts +4 -6
  48. package/src/live-voice/__tests__/live-voice-stt.test.ts +1 -1
  49. package/src/live-voice/live-voice-credential-preflight.ts +9 -33
  50. package/src/live-voice/live-voice-session.ts +10 -6
  51. package/src/live-voice/live-voice-tts.ts +1 -8
  52. package/src/plugins/defaults/memory/worker.ts +3 -3
  53. package/src/prompts/persona-resolver.ts +0 -35
  54. package/src/providers/__tests__/search-provider-catalog.test.ts +5 -8
  55. package/src/providers/__tests__/voice-error-copy.test.ts +9 -10
  56. package/src/providers/search-provider-catalog.ts +7 -39
  57. package/src/providers/speech-to-text/__tests__/provider-catalog.test.ts +3 -47
  58. package/src/providers/speech-to-text/__tests__/resolve.test.ts +6 -372
  59. package/src/providers/speech-to-text/provider-catalog.ts +2 -73
  60. package/src/providers/speech-to-text/resolve.ts +3 -120
  61. package/src/runtime/http-router-types.ts +1 -3
  62. package/src/runtime/http-server.ts +1 -1
  63. package/src/runtime/routes/__tests__/conversation-query-routes.test.ts +13 -11
  64. package/src/runtime/routes/__tests__/tts-routes.test.ts +2 -6
  65. package/src/runtime/routes/conversation-query-routes.ts +0 -96
  66. package/src/runtime/routes/inbound-stages/transcribe-audio.test.ts +7 -7
  67. package/src/runtime/routes/index.ts +0 -4
  68. package/src/runtime/routes/live-voice-routes.test.ts +4 -11
  69. package/src/runtime/routes/live-voice-routes.ts +8 -11
  70. package/src/runtime/routes/secret-routes.ts +0 -6
  71. package/src/runtime/routes/tts-routes.ts +0 -38
  72. package/src/schedule/__tests__/plugin-schedule-reconciler.test.ts +1 -7
  73. package/src/schedule/__tests__/worker-mcp-readiness.test.ts +0 -4
  74. package/src/schedule/schedule-store.ts +0 -7
  75. package/src/schedule/scheduler.ts +0 -5
  76. package/src/schedule/worker.ts +1 -1
  77. package/src/stt/__tests__/daemon-batch-transcriber.test.ts +2 -10
  78. package/src/stt/daemon-batch-transcriber.ts +4 -38
  79. package/src/stt/types.ts +1 -9
  80. package/src/tools/network/__tests__/web-search.test.ts +3 -250
  81. package/src/tools/network/web-search.ts +4 -165
  82. package/src/tts/__tests__/provider-resolution.test.ts +0 -5
  83. package/src/tts/provider-catalog.ts +0 -4
  84. package/src/tts/types.ts +0 -1
  85. package/src/__tests__/cloud-speech-defaults.test.ts +0 -330
  86. package/src/__tests__/migration-cross-version-compatibility.test.ts +0 -1708
  87. package/src/__tests__/migration-export-http.test.ts +0 -557
  88. package/src/__tests__/migration-export-streaming.test.ts +0 -350
  89. package/src/__tests__/migration-export-to-gcs.test.ts +0 -706
  90. package/src/__tests__/migration-import-commit-http.test.ts +0 -1176
  91. package/src/__tests__/migration-import-from-gcs.test.ts +0 -534
  92. package/src/__tests__/migration-import-from-path.test.ts +0 -324
  93. package/src/__tests__/migration-import-from-url.test.ts +0 -588
  94. package/src/__tests__/migration-import-preflight-http.test.ts +0 -739
  95. package/src/__tests__/migration-jobs-status.test.ts +0 -139
  96. package/src/__tests__/migration-parity-persistence.test.ts +0 -1891
  97. package/src/__tests__/migration-transport.test.ts +0 -1089
  98. package/src/__tests__/migration-validate-http.test.ts +0 -634
  99. package/src/__tests__/migration-wizard.test.ts +0 -1395
  100. package/src/__tests__/rebind-secrets-screen.test.ts +0 -876
  101. package/src/__tests__/sanitize-config-for-transfer.test.ts +0 -259
  102. package/src/__tests__/transfer-progress-screen.test.ts +0 -1217
  103. package/src/__tests__/validation-results-screen.test.ts +0 -1130
  104. package/src/__tests__/vbundle-pax-and-symlink.test.ts +0 -147
  105. package/src/background-wake/background-wake-routes.test.ts +0 -906
  106. package/src/background-wake/cloud-client.test.ts +0 -328
  107. package/src/background-wake/cloud-client.ts +0 -196
  108. package/src/background-wake/next-wake.test.ts +0 -335
  109. package/src/background-wake/next-wake.ts +0 -188
  110. package/src/background-wake/publisher.ts +0 -93
  111. package/src/background-wake/wake-intent-hooks.test.ts +0 -234
  112. package/src/cli/commands/__tests__/backup.test.ts +0 -363
  113. package/src/cli/commands/backup.help.ts +0 -180
  114. package/src/cli/commands/backup.ts +0 -398
  115. package/src/cloud/cloud-speech.test.ts +0 -335
  116. package/src/cloud/cloud-speech.ts +0 -300
  117. package/src/config/cloud-speech-defaults.ts +0 -300
  118. package/src/config/sanitize-for-transfer.ts +0 -111
  119. package/src/live-voice/__tests__/live-voice-cloud-speech-fallback.test.ts +0 -250
  120. package/src/providers/speech-to-text/__tests__/cadet-cloud-realtime.test.ts +0 -457
  121. package/src/providers/speech-to-text/__tests__/cadet-cloud.test.ts +0 -271
  122. package/src/providers/speech-to-text/__tests__/cadet-speech-relay-connection.test.ts +0 -126
  123. package/src/providers/speech-to-text/cadet-cloud-flux-realtime.ts +0 -309
  124. package/src/providers/speech-to-text/cadet-cloud-realtime.ts +0 -315
  125. package/src/providers/speech-to-text/cadet-cloud.ts +0 -108
  126. package/src/providers/speech-to-text/cadet-speech-relay-connection.ts +0 -176
  127. package/src/runtime/migrations/__tests__/gcs-signed-url.test.ts +0 -254
  128. package/src/runtime/migrations/__tests__/job-registry.test.ts +0 -346
  129. package/src/runtime/migrations/__tests__/origin-mode.test.ts +0 -62
  130. package/src/runtime/migrations/__tests__/rebind-secrets-credentials.test.ts +0 -184
  131. package/src/runtime/migrations/__tests__/staged-import-path.test.ts +0 -110
  132. package/src/runtime/migrations/__tests__/v1-test-helpers.ts +0 -112
  133. package/src/runtime/migrations/__tests__/vbundle-builder-credentials.test.ts +0 -285
  134. package/src/runtime/migrations/__tests__/vbundle-builder-fd-leak.test.ts +0 -126
  135. package/src/runtime/migrations/__tests__/vbundle-builder-v1-shape.test.ts +0 -253
  136. package/src/runtime/migrations/__tests__/vbundle-import-credentials.test.ts +0 -208
  137. package/src/runtime/migrations/__tests__/vbundle-import-parity.test.ts +0 -485
  138. package/src/runtime/migrations/__tests__/vbundle-import-policy.test.ts +0 -349
  139. package/src/runtime/migrations/__tests__/vbundle-import-version-compat.test.ts +0 -196
  140. package/src/runtime/migrations/__tests__/vbundle-legacy-user-md.test.ts +0 -551
  141. package/src/runtime/migrations/__tests__/vbundle-metadata-merge-integration.test.ts +0 -428
  142. package/src/runtime/migrations/__tests__/vbundle-metadata-merge.test.ts +0 -220
  143. package/src/runtime/migrations/__tests__/vbundle-streaming-importer.test.ts +0 -1823
  144. package/src/runtime/migrations/__tests__/vbundle-streaming-validator.test.ts +0 -598
  145. package/src/runtime/migrations/__tests__/vbundle-symlink-importer.test.ts +0 -463
  146. package/src/runtime/migrations/__tests__/vbundle-symlink-streaming-importer.test.ts +0 -538
  147. package/src/runtime/migrations/__tests__/vbundle-symlink-streaming.test.ts +0 -515
  148. package/src/runtime/migrations/__tests__/vbundle-symlink-tar.test.ts +0 -453
  149. package/src/runtime/migrations/__tests__/vbundle-symlink-walker.test.ts +0 -319
  150. package/src/runtime/migrations/__tests__/vbundle-tar-stream.test.ts +0 -240
  151. package/src/runtime/migrations/__tests__/vbundle-validator-v1-schema.test.ts +0 -473
  152. package/src/runtime/migrations/gcs-signed-url.ts +0 -190
  153. package/src/runtime/migrations/job-registry.ts +0 -297
  154. package/src/runtime/migrations/migration-transport.ts +0 -612
  155. package/src/runtime/migrations/migration-wizard.ts +0 -747
  156. package/src/runtime/migrations/origin-mode.ts +0 -42
  157. package/src/runtime/migrations/rebind-secrets-screen.ts +0 -412
  158. package/src/runtime/migrations/staged-import-path.ts +0 -116
  159. package/src/runtime/migrations/transfer-progress-screen.ts +0 -323
  160. package/src/runtime/migrations/validation-results-screen.ts +0 -469
  161. package/src/runtime/migrations/vbundle-builder.ts +0 -1382
  162. package/src/runtime/migrations/vbundle-import-analyzer.ts +0 -405
  163. package/src/runtime/migrations/vbundle-import-policy.ts +0 -241
  164. package/src/runtime/migrations/vbundle-importer.ts +0 -826
  165. package/src/runtime/migrations/vbundle-metadata-merge.ts +0 -113
  166. package/src/runtime/migrations/vbundle-streaming-importer.ts +0 -2844
  167. package/src/runtime/migrations/vbundle-streaming-validator.ts +0 -426
  168. package/src/runtime/migrations/vbundle-tar-stream.ts +0 -244
  169. package/src/runtime/migrations/vbundle-validator.ts +0 -724
  170. package/src/runtime/routes/__tests__/migration-cadet-metadata-reconcile.test.ts +0 -227
  171. package/src/runtime/routes/__tests__/migration-export-secrets-redacted.test.ts +0 -54
  172. package/src/runtime/routes/__tests__/migration-import-allowed-hosts-env.test.ts +0 -70
  173. package/src/runtime/routes/__tests__/migration-import-credential-filter.test.ts +0 -257
  174. package/src/runtime/routes/background-wake-routes.ts +0 -387
  175. package/src/runtime/routes/migration-routes.ts +0 -2584
  176. package/src/tools/network/__tests__/cloud-search-proxy.test.ts +0 -282
  177. package/src/tools/network/cloud-search-proxy.ts +0 -211
  178. package/src/tts/__tests__/cadet-provider.test.ts +0 -253
  179. package/src/tts/providers/cadet-provider.ts +0 -244
  180. package/src/types/tar-stream.d.ts +0 -66
@@ -1,271 +0,0 @@
1
- import { beforeEach, describe, expect, mock, test } from "bun:test";
2
-
3
- import { SttError } from "../../../stt/types.js";
4
- import { describeSttFailure } from "../../voice-error-copy.js";
5
-
6
- let mockTranscribeResult: unknown;
7
- let mockClient: { cloudAssistantId: string } | null = null;
8
-
9
- mock.module("../../../cloud/cloud-speech.js", () => ({
10
- managedSpeechTranscribe: async () => mockTranscribeResult,
11
- }));
12
-
13
- mock.module("../../../cloud/client.js", () => ({
14
- CadetCloudClient: {
15
- create: async () => mockClient,
16
- },
17
- }));
18
-
19
- import {
20
- cadetCloudSpeechAvailable,
21
- cadetCloudTranscribe,
22
- sttErrorFromCloudSpeech,
23
- } from "../cadet-cloud.js";
24
-
25
- describe("cadetCloudSpeechAvailable", () => {
26
- test("true with a connected platform client", async () => {
27
- mockClient = { cloudAssistantId: "asst-1" };
28
- expect(await cadetCloudSpeechAvailable()).toBe(true);
29
- });
30
-
31
- test("false without a client or without an assistant ID", async () => {
32
- mockClient = null;
33
- expect(await cadetCloudSpeechAvailable()).toBe(false);
34
- mockClient = { cloudAssistantId: "" };
35
- expect(await cadetCloudSpeechAvailable()).toBe(false);
36
- });
37
- });
38
-
39
- describe("cadetCloudTranscribe", () => {
40
- beforeEach(() => {
41
- mockClient = { cloudAssistantId: "asst-1" };
42
- });
43
-
44
- test("returns the transcription text", async () => {
45
- mockTranscribeResult = {
46
- ok: true,
47
- value: { text: "hello", durationSeconds: 2 },
48
- };
49
- expect(await cadetCloudTranscribe(Buffer.from([1]), "audio/wav")).toEqual({
50
- text: "hello",
51
- });
52
- });
53
-
54
- test("throws a normalized SttError on failure", async () => {
55
- mockTranscribeResult = {
56
- ok: false,
57
- kind: "platform-error",
58
- status: 429,
59
- message: "slow down",
60
- };
61
- await expect(
62
- cadetCloudTranscribe(Buffer.from([1]), "audio/wav"),
63
- ).rejects.toMatchObject({ name: "SttError", category: "rate-limit" });
64
- });
65
- });
66
-
67
- describe("sttErrorFromCloudSpeech", () => {
68
- const cases: Array<{
69
- name: string;
70
- failure: Record<string, unknown>;
71
- category: string;
72
- userFacing: boolean;
73
- messageIncludes?: string;
74
- messageExcludes?: string[];
75
- }> = [
76
- {
77
- name: "unavailable → auth, user-facing reconnect copy",
78
- failure: { ok: false, kind: "unavailable", message: "no connection" },
79
- category: "auth",
80
- userFacing: true,
81
- messageIncludes: "no connection",
82
- },
83
- {
84
- name: "insufficient_balance → provider-error, user-facing credit copy",
85
- failure: {
86
- ok: false,
87
- kind: "platform-error",
88
- status: 402,
89
- code: "insufficient_balance",
90
- message: "balance",
91
- },
92
- category: "provider-error",
93
- userFacing: true,
94
- messageIncludes: "Cadet credits",
95
- },
96
- // Platform-supplied `detail` present: real remediation, surfaced verbatim.
97
- {
98
- name: "401 with platform detail → auth, user-facing verbatim",
99
- failure: {
100
- ok: false,
101
- kind: "platform-error",
102
- status: 401,
103
- detail: "Your session expired.",
104
- message: "Your session expired.",
105
- },
106
- category: "auth",
107
- userFacing: true,
108
- messageIncludes: "session expired",
109
- },
110
- {
111
- name: "429 with platform detail → rate-limit, user-facing verbatim",
112
- failure: {
113
- ok: false,
114
- kind: "platform-error",
115
- status: 429,
116
- detail: "Too many requests, retry in 30s.",
117
- message: "Too many requests, retry in 30s.",
118
- },
119
- category: "rate-limit",
120
- userFacing: true,
121
- messageIncludes: "retry in 30s",
122
- },
123
- {
124
- name: "413 with platform detail → invalid-audio, user-facing verbatim",
125
- failure: {
126
- ok: false,
127
- kind: "platform-error",
128
- status: 413,
129
- detail: "Audio exceeds the 25 MB limit.",
130
- message: "Audio exceeds the 25 MB limit.",
131
- },
132
- category: "invalid-audio",
133
- userFacing: true,
134
- messageIncludes: "25 MB",
135
- },
136
- // No platform `detail`: the generic "(platform returned N)" fallback stays
137
- // non-user-facing so describeSttFailure emits friendly category copy
138
- // instead of leaking a raw HTTP status — except a bare auth status, whose
139
- // friendly copy would be the wrong BYOK "check your API key" guidance, so it
140
- // gets a sanitized cloud-connection message surfaced verbatim.
141
- {
142
- name: "401 without detail → auth, user-facing sanitized reconnect copy",
143
- failure: {
144
- ok: false,
145
- kind: "platform-error",
146
- status: 401,
147
- message: "Cloud speech transcription failed (platform returned 401).",
148
- },
149
- category: "auth",
150
- userFacing: true,
151
- messageIncludes: "reconnect your Cadet account",
152
- messageExcludes: ["API key", "platform returned"],
153
- },
154
- {
155
- name: "403 without detail → auth, user-facing sanitized reconnect copy",
156
- failure: {
157
- ok: false,
158
- kind: "platform-error",
159
- status: 403,
160
- message: "Cloud speech transcription failed (platform returned 403).",
161
- },
162
- category: "auth",
163
- userFacing: true,
164
- messageIncludes: "reconnect your Cadet account",
165
- messageExcludes: ["API key", "platform returned"],
166
- },
167
- {
168
- name: "429 without detail → rate-limit, not user-facing",
169
- failure: {
170
- ok: false,
171
- kind: "platform-error",
172
- status: 429,
173
- message: "Cloud speech transcription failed (platform returned 429).",
174
- },
175
- category: "rate-limit",
176
- userFacing: false,
177
- },
178
- {
179
- name: "413 without detail → invalid-audio, not user-facing",
180
- failure: {
181
- ok: false,
182
- kind: "platform-error",
183
- status: 413,
184
- message: "Cloud speech transcription failed (platform returned 413).",
185
- },
186
- category: "invalid-audio",
187
- userFacing: false,
188
- },
189
- {
190
- name: "502 without detail → provider-error, not user-facing",
191
- failure: {
192
- ok: false,
193
- kind: "platform-error",
194
- status: 502,
195
- message: "Cloud speech transcription failed (platform returned 502).",
196
- },
197
- category: "provider-error",
198
- userFacing: false,
199
- },
200
- ];
201
-
202
- for (const {
203
- name,
204
- failure,
205
- category,
206
- userFacing,
207
- messageIncludes,
208
- messageExcludes,
209
- } of cases) {
210
- test(name, () => {
211
- const err = sttErrorFromCloudSpeech(
212
- failure as Parameters<typeof sttErrorFromCloudSpeech>[0],
213
- );
214
- expect(err).toBeInstanceOf(SttError);
215
- expect(err.category).toBe(category as SttError["category"]);
216
- // User-facing when the failure carries real remediation (or a sanitized
217
- // managed-auth message); a bare non-auth status stays non-user-facing for
218
- // friendly category rewrite.
219
- expect(err.userFacing).toBe(userFacing);
220
- if (messageIncludes) {
221
- expect(err.message).toContain(messageIncludes);
222
- }
223
- for (const excluded of messageExcludes ?? []) {
224
- expect(err.message).not.toContain(excluded);
225
- }
226
- });
227
- }
228
- });
229
-
230
- describe("describeSttFailure over managed STT failures", () => {
231
- test("surfaces a cloud-supplied detail verbatim", () => {
232
- const err = sttErrorFromCloudSpeech({
233
- ok: false,
234
- kind: "platform-error",
235
- status: 429,
236
- detail: "Too many requests, retry in 30s.",
237
- message: "Too many requests, retry in 30s.",
238
- });
239
- expect(describeSttFailure(err, "cadet")).toBe(
240
- "Too many requests, retry in 30s.",
241
- );
242
- });
243
-
244
- test("replaces a bare HTTP-status fallback with friendly category copy", () => {
245
- const err = sttErrorFromCloudSpeech({
246
- ok: false,
247
- kind: "platform-error",
248
- status: 429,
249
- message: "Cloud speech transcription failed (platform returned 429).",
250
- });
251
- const copy = describeSttFailure(err, "cadet");
252
- expect(copy).not.toContain("platform returned 429");
253
- expect(copy).toContain("rate-limiting");
254
- });
255
-
256
- test("bare auth failure gets sanitized reconnect copy, not the BYOK rewrite", () => {
257
- const err = sttErrorFromCloudSpeech({
258
- ok: false,
259
- kind: "platform-error",
260
- status: 401,
261
- message: "Cloud speech transcription failed (platform returned 401).",
262
- });
263
- const copy = describeSttFailure(err, "cadet");
264
- // Managed STT holds no local key: the "check your API key in Settings →
265
- // Voice" guidance is wrong, and the raw HTTP status must not leak.
266
- expect(copy).toContain("reconnect your Cadet account");
267
- expect(copy).not.toContain("API key");
268
- expect(copy).not.toContain("Settings → Voice");
269
- expect(copy).not.toContain("platform returned 401");
270
- });
271
- });
@@ -1,126 +0,0 @@
1
- import { afterEach, beforeEach, describe, expect, mock, test } from "bun:test";
2
-
3
- let mockMintResult: string | null = "minted-token";
4
-
5
- mock.module("../../../runtime/auth/token-service.js", () => ({
6
- mintToken: (params: { aud: string; sub: string }) => {
7
- if (mockMintResult === null) {
8
- throw new Error("signing key unavailable");
9
- }
10
- return `${mockMintResult}:${params.aud}:${params.sub}`;
11
- },
12
- }));
13
-
14
- import {
15
- mapTunnelError,
16
- probeTunnelRejection,
17
- resolveSpeechRelayConnection,
18
- } from "../cadet-speech-relay-connection.js";
19
-
20
- describe("resolveSpeechRelayConnection", () => {
21
- const originalEnv = process.env.GATEWAY_INTERNAL_URL;
22
-
23
- beforeEach(() => {
24
- mockMintResult = "minted-token";
25
- delete process.env.GATEWAY_INTERNAL_URL;
26
- });
27
-
28
- afterEach(() => {
29
- if (originalEnv === undefined) {
30
- delete process.env.GATEWAY_INTERNAL_URL;
31
- } else {
32
- process.env.GATEWAY_INTERNAL_URL = originalEnv;
33
- }
34
- });
35
-
36
- test("resolves the gateway origin and a per-dial token minter", async () => {
37
- process.env.GATEWAY_INTERNAL_URL = "http://gateway:7822/";
38
-
39
- const connection = await resolveSpeechRelayConnection();
40
- expect(connection).not.toBeNull();
41
- expect(connection!.httpBaseUrl).toBe("http://gateway:7822");
42
- expect(connection!.wsBaseUrl).toBe("ws://gateway:7822");
43
- // The token targets the gateway audience with the daemon service sub.
44
- expect(connection!.mintServiceToken()).toBe(
45
- "minted-token:cadet-gateway:svc:daemon:self",
46
- );
47
- });
48
-
49
- test("returns null when the signing key is unavailable", async () => {
50
- mockMintResult = null;
51
- expect(await resolveSpeechRelayConnection()).toBeNull();
52
- });
53
- });
54
-
55
- describe("mapTunnelError", () => {
56
- test("maps the relay contract's codes onto categories", () => {
57
- expect(mapTunnelError({ code: "invalid_key" })).toMatchObject({
58
- category: "auth",
59
- message: expect.stringContaining("TUNNEL_BASE_URL"),
60
- });
61
- expect(
62
- mapTunnelError({ code: "missing_platform_connection" }),
63
- ).toMatchObject({
64
- category: "auth",
65
- message: expect.stringContaining("cloud connect"),
66
- });
67
- expect(mapTunnelError({ code: "invalid_token" })).toMatchObject({
68
- category: "auth",
69
- message: expect.stringContaining("restart the assistant"),
70
- });
71
- expect(mapTunnelError({ code: "insufficient_balance" })).toMatchObject({
72
- category: "provider-error",
73
- message: expect.stringContaining("credits"),
74
- });
75
- expect(mapTunnelError({ code: "provider_unreachable" }).category).toBe(
76
- "provider-error",
77
- );
78
- expect(mapTunnelError({ code: "upstream_error" }).category).toBe(
79
- "provider-error",
80
- );
81
- expect(
82
- mapTunnelError({ code: "novel_code", detail: "what" }),
83
- ).toMatchObject({
84
- category: "provider-error",
85
- message: expect.stringContaining("novel_code"),
86
- });
87
- });
88
- });
89
-
90
- describe("probeTunnelRejection", () => {
91
- const originalFetch = globalThis.fetch;
92
-
93
- afterEach(() => {
94
- globalThis.fetch = originalFetch;
95
- });
96
-
97
- test("returns the JSON rejection body", async () => {
98
- globalThis.fetch = (async () =>
99
- new Response(JSON.stringify({ code: "invalid_key", detail: "nope" }), {
100
- status: 401,
101
- })) as unknown as typeof fetch;
102
-
103
- expect(await probeTunnelRejection("http://gateway.test/x")).toEqual({
104
- code: "invalid_key",
105
- detail: "nope",
106
- });
107
- });
108
-
109
- test("returns null on the gateway's gate-passed 426", async () => {
110
- globalThis.fetch = (async () =>
111
- new Response(
112
- JSON.stringify({ code: "upgrade_required", detail: "connect via WS" }),
113
- { status: 426 },
114
- )) as unknown as typeof fetch;
115
-
116
- expect(await probeTunnelRejection("http://gateway.test/x")).toBeNull();
117
- });
118
-
119
- test("returns null when the probe itself fails", async () => {
120
- globalThis.fetch = (async () => {
121
- throw new Error("refused");
122
- }) as unknown as typeof fetch;
123
-
124
- expect(await probeTunnelRejection("http://gateway.test/x")).toBeNull();
125
- });
126
- });
@@ -1,309 +0,0 @@
1
- /**
2
- * Cloud Flux STT through the gateway's speech relay
3
- * (gateway -> tunnel -> Deepgram Flux; tunnel contact is gateway-only).
4
- *
5
- * Composes {@link DeepgramFluxRealtimeTranscriber} against the relay's
6
- * managed STT v2 endpoint with `contract=flux`, so Flux's own turn events
7
- * arrive verbatim. Without that opt-in tunnel translates them into the
8
- * released v1 Deepgram dialect, which drops `EagerEndOfTurn` and
9
- * `TurnResumed` entirely and leaves live voice with no provider turn end to
10
- * act on.
11
- *
12
- * This wrapper owns what is relay-specific, mirroring
13
- * `CadetManagedRealtimeTranscriber` on the v1 path:
14
- *
15
- * - Dialing the gateway (`/v2/speech/stt/stream`, `?key=` carries a
16
- * daemon-minted service token, no `model` param since tunnel derives and
17
- * prices the Flux model from `language`).
18
- * - Surfacing `tunnel_error` control frames as categorized SttErrors instead
19
- * of generic socket failures.
20
- * - Transparent re-dial on tunnel's 30-minute session cap, so one live-voice
21
- * session can outlive it.
22
- *
23
- * There is deliberately no `finalizeUtterance`: Flux commits turns itself,
24
- * and the interface makes the method optional so live voice feature-detects
25
- * its absence and never asks for a flush.
26
- */
27
-
28
- import {
29
- type StreamingTranscriber,
30
- SttError,
31
- type SttStreamServerEvent,
32
- } from "../../stt/types.js";
33
- import { getLogger } from "../../util/logger.js";
34
- import {
35
- mapTunnelError,
36
- probeTunnelRejection,
37
- type SpeechRelayConnection,
38
- type TunnelErrorInfo,
39
- } from "./cadet-speech-relay-connection.js";
40
- import { DeepgramFluxRealtimeTranscriber } from "./deepgram-flux-realtime.js";
41
-
42
- const log = getLogger("cadet-cloud-flux-realtime");
43
-
44
- /** Managed STT v2: tunnel resolves this to Deepgram's Flux `/v2/listen`. */
45
- const STT_STREAM_PATH = "/v2/speech/stt/stream";
46
-
47
- export interface CadetManagedFluxRealtimeOptions {
48
- /** Audio sample rate in Hz (default: 16000). */
49
- sampleRate?: number;
50
- /**
51
- * BCP-47 language code (or `"multi"`), forwarded to the relay. This is
52
- * how tunnel picks between `flux-general-en` and `flux-general-multi`, so
53
- * it is the only lever over the Flux model from this side.
54
- */
55
- language?: string;
56
- }
57
-
58
- export class CadetManagedFluxRealtimeTranscriber implements StreamingTranscriber {
59
- readonly providerId = "cadet-flux" as const;
60
- readonly boundaryId = "daemon-streaming" as const;
61
-
62
- private readonly connection: SpeechRelayConnection;
63
- private readonly options: CadetManagedFluxRealtimeOptions;
64
-
65
- private onEvent: ((event: SttStreamServerEvent) => void) | null = null;
66
- /** The current relay session; null while re-dialing after a session cap. */
67
- private inner: DeepgramFluxRealtimeTranscriber | null = null;
68
- /**
69
- * The most recent `tunnel_error` frame from the current session. Set just
70
- * before tunnel closes the socket; consumed when the inner adapter turns
71
- * that close into an error event.
72
- */
73
- private pendingRelayError: TunnelErrorInfo | null = null;
74
- /**
75
- * Set when the current session hit tunnel's duration cap: the swap to a
76
- * fresh session is deferred to the capped session's `closed` event so its
77
- * close cleanup drains through first.
78
- */
79
- private redialOnClose = false;
80
- private stopping = false;
81
- private closedEmitted = false;
82
- // Consumers see one stream across relay reconnects, with one turn sequence.
83
- private turnIndexOffset = 0;
84
- private latestTurnIndex = -1;
85
-
86
- constructor(
87
- connection: SpeechRelayConnection,
88
- options: CadetManagedFluxRealtimeOptions = {},
89
- ) {
90
- this.connection = connection;
91
- this.options = options;
92
- }
93
-
94
- async start(onEvent: (event: SttStreamServerEvent) => void): Promise<void> {
95
- if (this.onEvent) {
96
- throw new Error(
97
- "CadetManagedFluxRealtimeTranscriber: start() called twice",
98
- );
99
- }
100
- this.onEvent = onEvent;
101
- try {
102
- const inner = await this.dial();
103
- if (this.stopping) {
104
- // stop() raced the initial dial: it saw no inner to tear down and
105
- // already emitted closed, so the freshly opened relay session must
106
- // not be kept (a leaked session keeps tunnel metering).
107
- inner.stop();
108
- return;
109
- }
110
- this.inner = inner;
111
- } catch (err) {
112
- // A failed WebSocket upgrade exposes no HTTP details; replay the
113
- // request as a plain GET to recover the relay's {code, detail}
114
- // rejection.
115
- const rejection = await probeTunnelRejection(this.probeUrl());
116
- if (rejection) {
117
- const mapped = mapTunnelError(rejection);
118
- throw new SttError(mapped.category, mapped.message, {
119
- userFacing: true,
120
- });
121
- }
122
- throw err;
123
- }
124
- }
125
-
126
- sendAudio(audio: Buffer, mimeType: string): void {
127
- // Audio arriving during a session-cap re-dial (sub-second) is dropped,
128
- // mirroring the inner adapter's backpressure-drop semantics.
129
- this.inner?.sendAudio(audio, mimeType);
130
- }
131
-
132
- stop(): void {
133
- if (this.stopping) {
134
- return;
135
- }
136
- this.stopping = true;
137
- if (this.inner) {
138
- this.inner.stop();
139
- return;
140
- }
141
- // Mid-redial stop: there is no session to drain.
142
- this.emitClosedOnce();
143
- }
144
-
145
- // -- Relay session management ------------------------------------------
146
-
147
- private async dial(): Promise<DeepgramFluxRealtimeTranscriber> {
148
- this.pendingRelayError = null;
149
- // Fresh per dial: a session-cap re-dial happens ~30 minutes in, long
150
- // after any previously minted token expired.
151
- const inner = new DeepgramFluxRealtimeTranscriber(
152
- this.connection.mintServiceToken(),
153
- {
154
- baseUrl: this.connection.wsBaseUrl,
155
- path: STT_STREAM_PATH,
156
- queryAuth: true,
157
- omitModelParam: true,
158
- nativeContract: true,
159
- ...(this.options.sampleRate !== undefined
160
- ? { sampleRate: this.options.sampleRate }
161
- : {}),
162
- ...(this.options.language ? { language: this.options.language } : {}),
163
- onUnhandledFrame: (frame) => this.handleRelayFrame(frame),
164
- },
165
- );
166
- await inner.start((event) => this.handleInnerEvent(inner, event));
167
- return inner;
168
- }
169
-
170
- private probeUrl(): string {
171
- const key = encodeURIComponent(this.connection.mintServiceToken());
172
- return `${this.connection.httpBaseUrl}${STT_STREAM_PATH}?key=${key}`;
173
- }
174
-
175
- private handleRelayFrame(frame: Record<string, unknown>): void {
176
- if (frame.type !== "tunnel_error" || typeof frame.code !== "string") {
177
- return;
178
- }
179
- this.pendingRelayError = {
180
- code: frame.code,
181
- ...(typeof frame.detail === "string" ? { detail: frame.detail } : {}),
182
- };
183
- log.info(
184
- { code: frame.code, detail: frame.detail },
185
- "Received tunnel_error frame on managed Flux relay",
186
- );
187
- }
188
-
189
- private handleInnerEvent(
190
- source: DeepgramFluxRealtimeTranscriber,
191
- event: SttStreamServerEvent,
192
- ): void {
193
- // Events from a superseded session (the one replaced by a re-dial) must
194
- // not leak through: its close/error was already accounted for.
195
- if (source !== this.inner) {
196
- return;
197
- }
198
-
199
- if (event.type === "error") {
200
- const relayError = this.pendingRelayError;
201
- if (relayError?.code === "session_duration_exceeded") {
202
- // Tunnel's 30-minute cap, not a failure. Swallow the error but keep
203
- // the capped session current so its close cleanup drains first; the
204
- // re-dial happens on its `closed` event.
205
- this.redialOnClose = !this.stopping;
206
- return;
207
- }
208
- if (relayError) {
209
- const mapped = mapTunnelError(relayError);
210
- this.emit({
211
- type: "error",
212
- category: mapped.category,
213
- message: mapped.message,
214
- });
215
- return;
216
- }
217
- this.emit(event);
218
- return;
219
- }
220
-
221
- if (event.type === "closed") {
222
- if (this.redialOnClose && !this.stopping) {
223
- this.redialOnClose = false;
224
- this.beginRedial();
225
- return;
226
- }
227
- this.emitClosedOnce();
228
- return;
229
- }
230
-
231
- if (
232
- (event.type === "turn-start" || event.type === "turn-end") &&
233
- event.turnIndex !== undefined
234
- ) {
235
- const turnIndex = this.turnIndexOffset + event.turnIndex;
236
- this.latestTurnIndex = Math.max(this.latestTurnIndex, turnIndex);
237
- this.emit({ ...event, turnIndex });
238
- return;
239
- }
240
- this.emit(event);
241
- }
242
-
243
- /**
244
- * Replace the capped session with a fresh one. Detached: audio sent during
245
- * the swap is dropped (see {@link sendAudio}); a dial failure is a real
246
- * error and ends the stream.
247
- */
248
- private beginRedial(): void {
249
- log.info("Cloud Flux relay session hit tunnel's duration cap, re-dialing");
250
- this.inner = null;
251
- this.turnIndexOffset = this.latestTurnIndex + 1;
252
- void (async () => {
253
- try {
254
- const inner = await this.dial();
255
- if (this.stopping) {
256
- inner.stop();
257
- return;
258
- }
259
- this.inner = inner;
260
- log.info("Cloud Flux relay re-dial complete; session continues");
261
- } catch (err) {
262
- if (this.stopping) {
263
- return;
264
- }
265
- const rejection = await probeTunnelRejection(this.probeUrl());
266
- if (rejection) {
267
- const mapped = mapTunnelError(rejection);
268
- this.emit({
269
- type: "error",
270
- category: mapped.category,
271
- message: mapped.message,
272
- });
273
- } else {
274
- this.emit({
275
- type: "error",
276
- category: "provider-error",
277
- message: `Cloud Flux relay re-dial failed after the session cap: ${
278
- err instanceof Error ? err.message : String(err)
279
- }`,
280
- });
281
- }
282
- this.emitClosedOnce();
283
- }
284
- })();
285
- }
286
-
287
- // -- Event emission -----------------------------------------------------
288
-
289
- private emit(event: SttStreamServerEvent): void {
290
- if (!this.onEvent || this.closedEmitted) {
291
- return;
292
- }
293
- if (event.type === "closed") {
294
- this.closedEmitted = true;
295
- }
296
- try {
297
- this.onEvent(event);
298
- } catch (err) {
299
- log.warn(
300
- { error: err },
301
- "Listener error in Cadet Cloud Flux realtime adapter",
302
- );
303
- }
304
- }
305
-
306
- private emitClosedOnce(): void {
307
- this.emit({ type: "closed" });
308
- }
309
- }