@mycadet/assistant 0.12.11 → 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 (176) hide show
  1. package/node_modules/@mycadet/ces-client/node_modules/@mycadet/service-contracts/src/__tests__/ingress.test.ts +2 -2
  2. package/node_modules/@mycadet/ces-client/node_modules/@mycadet/service-contracts/src/ingress.ts +2 -2
  3. package/node_modules/@mycadet/gateway-client/node_modules/@mycadet/service-contracts/src/__tests__/ingress.test.ts +2 -2
  4. package/node_modules/@mycadet/gateway-client/node_modules/@mycadet/service-contracts/src/ingress.ts +2 -2
  5. package/node_modules/@mycadet/gateway-client/src/gateway-ipc-contracts.ts +0 -20
  6. package/node_modules/@mycadet/service-contracts/src/__tests__/ingress.test.ts +2 -2
  7. package/node_modules/@mycadet/service-contracts/src/ingress.ts +2 -2
  8. package/openapi.yaml +2 -708
  9. package/package.json +2 -3
  10. package/scripts/sync-web-search-catalog.ts +1 -2
  11. package/src/__tests__/cloud-profile-guard.test.ts +2 -2
  12. package/src/__tests__/config-schema.test.ts +6 -38
  13. package/src/__tests__/conversation-queue.test.ts +45 -6
  14. package/src/__tests__/db-readiness-http-gate.test.ts +0 -6
  15. package/src/__tests__/persist-onboarding-artifacts.test.ts +0 -1
  16. package/src/__tests__/persona-resolver.test.ts +0 -51
  17. package/src/__tests__/provider-cloud-proxy-integration.test.ts +2 -110
  18. package/src/__tests__/run-due-schedules.test.ts +0 -4
  19. package/src/__tests__/schedule-store.test.ts +0 -12
  20. package/src/__tests__/stt-stream-session.test.ts +4 -4
  21. package/src/__tests__/telephony-tts-guard.test.ts +2 -50
  22. package/src/__tests__/voice-config-update.test.ts +12 -167
  23. package/src/__tests__/web-search-catalog-parity.test.ts +0 -2
  24. package/src/__tests__/workspace-migration-130-speech-mode-to-provider.test.ts +9 -7
  25. package/src/__tests__/workspace-migration-132-web-search-mode-to-provider.test.ts +6 -6
  26. package/src/calls/__tests__/voice-session-bridge.test.ts +1 -4
  27. package/src/calls/resolve-call-tts-provider.ts +6 -25
  28. package/src/calls/telephony-credential-preflight.ts +0 -10
  29. package/src/calls/telephony-tts-capability.ts +1 -17
  30. package/src/cli/commands/tts.ts +1 -2
  31. package/src/cli/index.help.ts +0 -2
  32. package/src/cli/program.ts +0 -2
  33. package/src/config/bundled-skills/settings/tools/voice-config-update.ts +4 -42
  34. package/src/config/schemas/__tests__/stt.test.ts +10 -14
  35. package/src/config/schemas/services.ts +5 -6
  36. package/src/config/schemas/stt.ts +1 -3
  37. package/src/config/schemas/tts.ts +1 -30
  38. package/src/daemon/daemon-readiness.ts +4 -29
  39. package/src/daemon/lifecycle.ts +0 -31
  40. package/src/daemon/providers-setup.ts +0 -8
  41. package/src/heartbeat/__tests__/heartbeat-service.test.ts +0 -1
  42. package/src/heartbeat/heartbeat-service.ts +1 -66
  43. package/src/ipc/__tests__/db-readiness-ipc-gate.test.ts +1 -9
  44. package/src/live-voice/__tests__/live-voice-credential-preflight.test.ts +0 -44
  45. package/src/live-voice/__tests__/live-voice-flux-turn-end.test.ts +4 -6
  46. package/src/live-voice/__tests__/live-voice-stt.test.ts +1 -1
  47. package/src/live-voice/live-voice-credential-preflight.ts +9 -33
  48. package/src/live-voice/live-voice-session.ts +10 -6
  49. package/src/live-voice/live-voice-tts.ts +1 -8
  50. package/src/plugins/defaults/memory/worker.ts +3 -3
  51. package/src/prompts/persona-resolver.ts +0 -35
  52. package/src/providers/__tests__/search-provider-catalog.test.ts +5 -8
  53. package/src/providers/__tests__/voice-error-copy.test.ts +9 -10
  54. package/src/providers/search-provider-catalog.ts +7 -39
  55. package/src/providers/speech-to-text/__tests__/provider-catalog.test.ts +3 -47
  56. package/src/providers/speech-to-text/__tests__/resolve.test.ts +6 -372
  57. package/src/providers/speech-to-text/provider-catalog.ts +2 -73
  58. package/src/providers/speech-to-text/resolve.ts +3 -120
  59. package/src/runtime/http-router-types.ts +1 -3
  60. package/src/runtime/http-server.ts +1 -1
  61. package/src/runtime/routes/__tests__/tts-routes.test.ts +2 -6
  62. package/src/runtime/routes/inbound-stages/transcribe-audio.test.ts +7 -7
  63. package/src/runtime/routes/index.ts +0 -4
  64. package/src/runtime/routes/live-voice-routes.test.ts +4 -11
  65. package/src/runtime/routes/live-voice-routes.ts +8 -11
  66. package/src/runtime/routes/secret-routes.ts +0 -6
  67. package/src/runtime/routes/tts-routes.ts +0 -38
  68. package/src/schedule/__tests__/plugin-schedule-reconciler.test.ts +1 -7
  69. package/src/schedule/__tests__/worker-mcp-readiness.test.ts +0 -4
  70. package/src/schedule/schedule-store.ts +0 -7
  71. package/src/schedule/scheduler.ts +0 -5
  72. package/src/schedule/worker.ts +1 -1
  73. package/src/stt/__tests__/daemon-batch-transcriber.test.ts +2 -10
  74. package/src/stt/daemon-batch-transcriber.ts +4 -38
  75. package/src/stt/types.ts +1 -9
  76. package/src/tools/network/__tests__/web-search.test.ts +3 -250
  77. package/src/tools/network/web-search.ts +4 -165
  78. package/src/tts/__tests__/provider-resolution.test.ts +0 -5
  79. package/src/tts/provider-catalog.ts +0 -4
  80. package/src/tts/types.ts +0 -1
  81. package/src/__tests__/cloud-speech-defaults.test.ts +0 -330
  82. package/src/__tests__/migration-cross-version-compatibility.test.ts +0 -1708
  83. package/src/__tests__/migration-export-http.test.ts +0 -557
  84. package/src/__tests__/migration-export-streaming.test.ts +0 -350
  85. package/src/__tests__/migration-export-to-gcs.test.ts +0 -706
  86. package/src/__tests__/migration-import-commit-http.test.ts +0 -1176
  87. package/src/__tests__/migration-import-from-gcs.test.ts +0 -534
  88. package/src/__tests__/migration-import-from-path.test.ts +0 -324
  89. package/src/__tests__/migration-import-from-url.test.ts +0 -588
  90. package/src/__tests__/migration-import-preflight-http.test.ts +0 -739
  91. package/src/__tests__/migration-jobs-status.test.ts +0 -139
  92. package/src/__tests__/migration-parity-persistence.test.ts +0 -1891
  93. package/src/__tests__/migration-transport.test.ts +0 -1089
  94. package/src/__tests__/migration-validate-http.test.ts +0 -634
  95. package/src/__tests__/migration-wizard.test.ts +0 -1395
  96. package/src/__tests__/rebind-secrets-screen.test.ts +0 -876
  97. package/src/__tests__/sanitize-config-for-transfer.test.ts +0 -259
  98. package/src/__tests__/transfer-progress-screen.test.ts +0 -1217
  99. package/src/__tests__/validation-results-screen.test.ts +0 -1130
  100. package/src/__tests__/vbundle-pax-and-symlink.test.ts +0 -147
  101. package/src/background-wake/background-wake-routes.test.ts +0 -906
  102. package/src/background-wake/cloud-client.test.ts +0 -328
  103. package/src/background-wake/cloud-client.ts +0 -196
  104. package/src/background-wake/next-wake.test.ts +0 -335
  105. package/src/background-wake/next-wake.ts +0 -188
  106. package/src/background-wake/publisher.ts +0 -93
  107. package/src/background-wake/wake-intent-hooks.test.ts +0 -234
  108. package/src/cli/commands/__tests__/backup.test.ts +0 -363
  109. package/src/cli/commands/backup.help.ts +0 -180
  110. package/src/cli/commands/backup.ts +0 -398
  111. package/src/cloud/cloud-speech.test.ts +0 -335
  112. package/src/cloud/cloud-speech.ts +0 -300
  113. package/src/config/cloud-speech-defaults.ts +0 -300
  114. package/src/config/sanitize-for-transfer.ts +0 -111
  115. package/src/live-voice/__tests__/live-voice-cloud-speech-fallback.test.ts +0 -250
  116. package/src/providers/speech-to-text/__tests__/cadet-cloud-realtime.test.ts +0 -457
  117. package/src/providers/speech-to-text/__tests__/cadet-cloud.test.ts +0 -271
  118. package/src/providers/speech-to-text/__tests__/cadet-speech-relay-connection.test.ts +0 -126
  119. package/src/providers/speech-to-text/cadet-cloud-flux-realtime.ts +0 -309
  120. package/src/providers/speech-to-text/cadet-cloud-realtime.ts +0 -315
  121. package/src/providers/speech-to-text/cadet-cloud.ts +0 -108
  122. package/src/providers/speech-to-text/cadet-speech-relay-connection.ts +0 -176
  123. package/src/runtime/migrations/__tests__/gcs-signed-url.test.ts +0 -254
  124. package/src/runtime/migrations/__tests__/job-registry.test.ts +0 -346
  125. package/src/runtime/migrations/__tests__/origin-mode.test.ts +0 -62
  126. package/src/runtime/migrations/__tests__/rebind-secrets-credentials.test.ts +0 -184
  127. package/src/runtime/migrations/__tests__/staged-import-path.test.ts +0 -110
  128. package/src/runtime/migrations/__tests__/v1-test-helpers.ts +0 -112
  129. package/src/runtime/migrations/__tests__/vbundle-builder-credentials.test.ts +0 -285
  130. package/src/runtime/migrations/__tests__/vbundle-builder-fd-leak.test.ts +0 -126
  131. package/src/runtime/migrations/__tests__/vbundle-builder-v1-shape.test.ts +0 -253
  132. package/src/runtime/migrations/__tests__/vbundle-import-credentials.test.ts +0 -208
  133. package/src/runtime/migrations/__tests__/vbundle-import-parity.test.ts +0 -485
  134. package/src/runtime/migrations/__tests__/vbundle-import-policy.test.ts +0 -349
  135. package/src/runtime/migrations/__tests__/vbundle-import-version-compat.test.ts +0 -196
  136. package/src/runtime/migrations/__tests__/vbundle-legacy-user-md.test.ts +0 -551
  137. package/src/runtime/migrations/__tests__/vbundle-metadata-merge-integration.test.ts +0 -428
  138. package/src/runtime/migrations/__tests__/vbundle-metadata-merge.test.ts +0 -220
  139. package/src/runtime/migrations/__tests__/vbundle-streaming-importer.test.ts +0 -1823
  140. package/src/runtime/migrations/__tests__/vbundle-streaming-validator.test.ts +0 -598
  141. package/src/runtime/migrations/__tests__/vbundle-symlink-importer.test.ts +0 -463
  142. package/src/runtime/migrations/__tests__/vbundle-symlink-streaming-importer.test.ts +0 -538
  143. package/src/runtime/migrations/__tests__/vbundle-symlink-streaming.test.ts +0 -515
  144. package/src/runtime/migrations/__tests__/vbundle-symlink-tar.test.ts +0 -453
  145. package/src/runtime/migrations/__tests__/vbundle-symlink-walker.test.ts +0 -319
  146. package/src/runtime/migrations/__tests__/vbundle-tar-stream.test.ts +0 -240
  147. package/src/runtime/migrations/__tests__/vbundle-validator-v1-schema.test.ts +0 -473
  148. package/src/runtime/migrations/gcs-signed-url.ts +0 -190
  149. package/src/runtime/migrations/job-registry.ts +0 -297
  150. package/src/runtime/migrations/migration-transport.ts +0 -612
  151. package/src/runtime/migrations/migration-wizard.ts +0 -747
  152. package/src/runtime/migrations/origin-mode.ts +0 -42
  153. package/src/runtime/migrations/rebind-secrets-screen.ts +0 -412
  154. package/src/runtime/migrations/staged-import-path.ts +0 -116
  155. package/src/runtime/migrations/transfer-progress-screen.ts +0 -323
  156. package/src/runtime/migrations/validation-results-screen.ts +0 -469
  157. package/src/runtime/migrations/vbundle-builder.ts +0 -1382
  158. package/src/runtime/migrations/vbundle-import-analyzer.ts +0 -405
  159. package/src/runtime/migrations/vbundle-import-policy.ts +0 -241
  160. package/src/runtime/migrations/vbundle-importer.ts +0 -826
  161. package/src/runtime/migrations/vbundle-metadata-merge.ts +0 -113
  162. package/src/runtime/migrations/vbundle-streaming-importer.ts +0 -2844
  163. package/src/runtime/migrations/vbundle-streaming-validator.ts +0 -426
  164. package/src/runtime/migrations/vbundle-tar-stream.ts +0 -244
  165. package/src/runtime/migrations/vbundle-validator.ts +0 -724
  166. package/src/runtime/routes/__tests__/migration-cadet-metadata-reconcile.test.ts +0 -227
  167. package/src/runtime/routes/__tests__/migration-export-secrets-redacted.test.ts +0 -54
  168. package/src/runtime/routes/__tests__/migration-import-allowed-hosts-env.test.ts +0 -70
  169. package/src/runtime/routes/__tests__/migration-import-credential-filter.test.ts +0 -257
  170. package/src/runtime/routes/background-wake-routes.ts +0 -387
  171. package/src/runtime/routes/migration-routes.ts +0 -2584
  172. package/src/tools/network/__tests__/cloud-search-proxy.test.ts +0 -282
  173. package/src/tools/network/cloud-search-proxy.ts +0 -211
  174. package/src/tts/__tests__/cadet-provider.test.ts +0 -253
  175. package/src/tts/providers/cadet-provider.ts +0 -244
  176. 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
- }