@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,315 +0,0 @@
1
- /**
2
- * Managed realtime STT through the gateway's speech relay
3
- * (gateway → tunnel → Deepgram; tunnel contact is gateway-only).
4
- *
5
- * Composes {@link DeepgramRealtimeTranscriber} — both relay legs speak
6
- * Deepgram's live wire protocol verbatim, so all frame handling (Results
7
- * parsing, backpressure, finalize/flush serialization) is inherited, not
8
- * duplicated. This wrapper owns what is relay-specific:
9
- *
10
- * - Dialing the gateway (`/v1/speech/stt/stream`, `?key=` carries a
11
- * daemon-minted service token, no `model` param — the model is pinned
12
- * server-side).
13
- * - Surfacing `tunnel_error` control frames (tunnel's own, or synthesized
14
- * by the gateway for upstream failures) as categorized SttErrors
15
- * instead of generic socket failures.
16
- * - Transparent re-dial on tunnel's 30-minute session cap: live-voice
17
- * reuses ONE streaming transcriber across utterance cycles (#37662), so
18
- * a normal conversation can outlive the cap. Tunnel settles the in-flight
19
- * utterance (trailing finals arrive before the close), then this wrapper
20
- * opens a fresh relay session and continues without surfacing an error.
21
- */
22
-
23
- import {
24
- type StreamingTranscriber,
25
- SttError,
26
- type SttStreamServerEvent,
27
- } from "../../stt/types.js";
28
- import { getLogger } from "../../util/logger.js";
29
- import {
30
- mapTunnelError,
31
- probeTunnelRejection,
32
- type SpeechRelayConnection,
33
- type TunnelErrorInfo,
34
- } from "./cadet-speech-relay-connection.js";
35
- import { DeepgramRealtimeTranscriber } from "./deepgram-realtime.js";
36
-
37
- const log = getLogger("cadet-cloud-realtime");
38
-
39
- const STT_STREAM_PATH = "/v1/speech/stt/stream";
40
-
41
- export interface CadetManagedRealtimeOptions {
42
- /** Audio sample rate in Hz (default: 16000). */
43
- sampleRate?: number;
44
- /** BCP-47 language code, forwarded to the relay. */
45
- language?: string;
46
- /**
47
- * Emit `final` events only at utterance boundaries (telephony mode).
48
- * Note: tunnel's param allowlist has no `utterance_end_ms`, so boundaries
49
- * come from Deepgram's endpointing (`speech_final`) alone.
50
- */
51
- utteranceBoundaryFinals?: boolean;
52
- }
53
-
54
- export class CadetManagedRealtimeTranscriber implements StreamingTranscriber {
55
- readonly providerId = "cadet" as const;
56
- readonly boundaryId = "daemon-streaming" as const;
57
-
58
- private readonly connection: SpeechRelayConnection;
59
- private readonly options: CadetManagedRealtimeOptions;
60
-
61
- private onEvent: ((event: SttStreamServerEvent) => void) | null = null;
62
- /** The current relay session; null while re-dialing after a session cap. */
63
- private inner: DeepgramRealtimeTranscriber | null = null;
64
- /**
65
- * The most recent `tunnel_error` frame from the current session. Set just
66
- * before tunnel closes the socket; consumed when the inner adapter turns
67
- * that close into an error event.
68
- */
69
- private pendingRelayError: TunnelErrorInfo | null = null;
70
- /**
71
- * Set when the current session hit tunnel's duration cap: the swap to a
72
- * fresh session is deferred to the capped session's `closed` event so
73
- * its close cleanup (withheld finals, outstanding finalizes) drains
74
- * through first.
75
- */
76
- private redialOnClose = false;
77
- private stopping = false;
78
- private closedEmitted = false;
79
-
80
- constructor(
81
- connection: SpeechRelayConnection,
82
- options: CadetManagedRealtimeOptions = {},
83
- ) {
84
- this.connection = connection;
85
- this.options = options;
86
- }
87
-
88
- async start(onEvent: (event: SttStreamServerEvent) => void): Promise<void> {
89
- if (this.onEvent) {
90
- throw new Error("CadetManagedRealtimeTranscriber: start() called twice");
91
- }
92
- this.onEvent = onEvent;
93
- try {
94
- const inner = await this.dial();
95
- if (this.stopping) {
96
- // stop() raced the initial dial: it saw no inner to tear down and
97
- // already emitted closed, so the freshly opened relay session must
98
- // not be kept (a leaked session keeps tunnel metering).
99
- inner.stop();
100
- return;
101
- }
102
- this.inner = inner;
103
- } catch (err) {
104
- // A failed WebSocket upgrade exposes no HTTP details; replay the
105
- // request as a plain GET to recover the relay's {code, detail}
106
- // rejection (the gateway replays its whole gate on non-upgrade
107
- // requests, including tunnel's own).
108
- const rejection = await probeTunnelRejection(this.probeUrl());
109
- if (rejection) {
110
- // Typed so the stream session preserves the mapped category —
111
- // an auth/setup failure must not surface as a provider outage.
112
- // `userFacing` keeps the mapped remediation (reconnect, top up
113
- // credits) from being rewritten to BYOK "check your API key" copy.
114
- const mapped = mapTunnelError(rejection);
115
- throw new SttError(mapped.category, mapped.message, {
116
- userFacing: true,
117
- });
118
- }
119
- throw err;
120
- }
121
- }
122
-
123
- sendAudio(audio: Buffer, mimeType: string): void {
124
- // ponytail: audio arriving during a session-cap re-dial (sub-second) is
125
- // dropped, mirroring the inner adapter's backpressure-drop semantics;
126
- // buffer-and-replay if the gap ever proves audible.
127
- this.inner?.sendAudio(audio, mimeType);
128
- }
129
-
130
- finalizeUtterance(): void {
131
- if (this.inner) {
132
- this.inner.finalizeUtterance();
133
- return;
134
- }
135
- // No live session (re-dialing or closed) — nothing is buffered
136
- // provider-side, so the flush is trivially complete.
137
- this.emit({ type: "finalized" });
138
- }
139
-
140
- stop(): void {
141
- if (this.stopping) {
142
- return;
143
- }
144
- this.stopping = true;
145
- if (this.inner) {
146
- this.inner.stop();
147
- return;
148
- }
149
- // Mid-redial stop: there is no session to drain.
150
- this.emitClosedOnce();
151
- }
152
-
153
- // ── Relay session management ─────────────────────────────────────────
154
-
155
- private async dial(): Promise<DeepgramRealtimeTranscriber> {
156
- this.pendingRelayError = null;
157
- // Fresh per dial — a session-cap re-dial happens ~30 minutes in, long
158
- // after any previously minted token expired.
159
- const inner = new DeepgramRealtimeTranscriber(
160
- this.connection.mintServiceToken(),
161
- {
162
- baseUrl: this.connection.wsBaseUrl,
163
- path: STT_STREAM_PATH,
164
- queryAuth: true,
165
- omitModelParam: true,
166
- sampleRate: this.options.sampleRate,
167
- ...(this.options.language ? { language: this.options.language } : {}),
168
- ...(this.options.utteranceBoundaryFinals
169
- ? { utteranceBoundaryFinals: true }
170
- : {}),
171
- onUnhandledFrame: (frame) => this.handleRelayFrame(frame),
172
- },
173
- );
174
- await inner.start((event) => this.handleInnerEvent(inner, event));
175
- return inner;
176
- }
177
-
178
- private probeUrl(): string {
179
- const key = encodeURIComponent(this.connection.mintServiceToken());
180
- return `${this.connection.httpBaseUrl}${STT_STREAM_PATH}?key=${key}`;
181
- }
182
-
183
- private handleRelayFrame(frame: Record<string, unknown>): void {
184
- if (frame.type !== "tunnel_error" || typeof frame.code !== "string") {
185
- return;
186
- }
187
- this.pendingRelayError = {
188
- code: frame.code,
189
- ...(typeof frame.detail === "string" ? { detail: frame.detail } : {}),
190
- };
191
- log.info(
192
- { code: frame.code, detail: frame.detail },
193
- "Received tunnel_error frame on managed STT relay",
194
- );
195
- }
196
-
197
- private handleInnerEvent(
198
- source: DeepgramRealtimeTranscriber,
199
- event: SttStreamServerEvent,
200
- ): void {
201
- // Events from a superseded session (the one replaced by a re-dial)
202
- // must not leak through — its close/error was already accounted for.
203
- if (source !== this.inner) {
204
- return;
205
- }
206
-
207
- if (event.type === "error") {
208
- const relayError = this.pendingRelayError;
209
- if (relayError?.code === "session_duration_exceeded") {
210
- // Tunnel's 30-minute cap, not a failure. Swallow the error but keep
211
- // the capped session current: its close cleanup still flushes
212
- // withheld boundary finals and settles outstanding finalizes, and
213
- // those events must drain before the swap. The re-dial happens on
214
- // its `closed` event.
215
- this.redialOnClose = !this.stopping;
216
- return;
217
- }
218
- if (relayError) {
219
- const mapped = mapTunnelError(relayError);
220
- this.emit({
221
- type: "error",
222
- category: mapped.category,
223
- message: mapped.message,
224
- });
225
- return;
226
- }
227
- this.emit(event);
228
- return;
229
- }
230
-
231
- if (event.type === "closed") {
232
- if (this.redialOnClose && !this.stopping) {
233
- this.redialOnClose = false;
234
- this.beginRedial();
235
- return;
236
- }
237
- this.emitClosedOnce();
238
- return;
239
- }
240
-
241
- this.emit(event);
242
- }
243
-
244
- /**
245
- * Replace the capped session with a fresh one. Detached — audio sent
246
- * during the swap is dropped (see {@link sendAudio}); a dial failure is
247
- * a real error and ends the stream.
248
- */
249
- private beginRedial(): void {
250
- log.info(
251
- "Managed STT relay session hit tunnel's duration cap — re-dialing",
252
- );
253
- this.inner = null;
254
- void (async () => {
255
- try {
256
- const inner = await this.dial();
257
- if (this.stopping) {
258
- inner.stop();
259
- return;
260
- }
261
- this.inner = inner;
262
- log.info("Managed STT relay re-dial complete — session continues");
263
- } catch (err) {
264
- if (this.stopping) {
265
- return;
266
- }
267
- // Same diagnosis as start(): a rejected upgrade hides the relay's
268
- // {code, detail} — mid-call credit exhaustion or a revoked
269
- // connection must surface with its mapped message, not a generic
270
- // transport error.
271
- const rejection = await probeTunnelRejection(this.probeUrl());
272
- if (rejection) {
273
- const mapped = mapTunnelError(rejection);
274
- this.emit({
275
- type: "error",
276
- category: mapped.category,
277
- message: mapped.message,
278
- });
279
- } else {
280
- this.emit({
281
- type: "error",
282
- category: "provider-error",
283
- message: `Cloud speech relay re-dial failed after the session cap: ${
284
- err instanceof Error ? err.message : String(err)
285
- }`,
286
- });
287
- }
288
- this.emitClosedOnce();
289
- }
290
- })();
291
- }
292
-
293
- // ── Event emission ───────────────────────────────────────────────────
294
-
295
- private emit(event: SttStreamServerEvent): void {
296
- if (!this.onEvent || this.closedEmitted) {
297
- return;
298
- }
299
- if (event.type === "closed") {
300
- this.closedEmitted = true;
301
- }
302
- try {
303
- this.onEvent(event);
304
- } catch (err) {
305
- log.warn(
306
- { error: err },
307
- "Listener error in Cadet Cloud realtime adapter",
308
- );
309
- }
310
- }
311
-
312
- private emitClosedOnce(): void {
313
- this.emit({ type: "closed" });
314
- }
315
- }
@@ -1,108 +0,0 @@
1
- /**
2
- * Cadet Cloud batch STT: transcription through the cloud's managed
3
- * speech endpoint (Cadet-held Deepgram key, billed to Cadet credits). No
4
- * provider API key exists on this machine — availability is the cloud
5
- * connection itself.
6
- */
7
-
8
- import {
9
- type CloudSpeechResult,
10
- managedSpeechAvailable,
11
- managedSpeechTranscribe,
12
- } from "../../cloud/cloud-speech.js";
13
- import {
14
- SttError,
15
- type SttErrorCategory,
16
- type SttTranscribeResult,
17
- } from "../../stt/types.js";
18
-
19
- type CloudSpeechFailure = Extract<CloudSpeechResult<never>, { ok: false }>;
20
-
21
- /**
22
- * Whether cloud speech can be used at all: a cloud connection with a
23
- * platform assistant ID. The credential-store lookup used for API-key
24
- * providers does not apply to `cadet`.
25
- */
26
- export async function cadetCloudSpeechAvailable(): Promise<boolean> {
27
- return managedSpeechAvailable();
28
- }
29
-
30
- /**
31
- * Map a cloud-speech failure onto the normalized STT error categories.
32
- *
33
- * `insufficient_balance` gets a user-actionable message — the fix is topping
34
- * up Cadet credits, not editing provider config.
35
- *
36
- * Cloud speech has no provider API key on this machine, so the BYOK "check
37
- * your API key" rewrite never applies. A failure is flagged `userFacing` —
38
- * surfaced verbatim by `describeSttFailure` — when it carries actionable
39
- * remediation: a cloud reconnect (`unavailable`), a credit top-up
40
- * (`insufficient_balance`), or the cloud's own `detail` message. A failure
41
- * that fell back to a bare HTTP status (no `detail`) is left non-user-facing so
42
- * `describeSttFailure` emits friendly category copy instead of leaking the raw
43
- * status — except a bare auth status (401/403), which would be rewritten to the
44
- * wrong BYOK "check your API key" guidance, so it gets its own sanitized
45
- * cloud-connection message.
46
- */
47
- export function sttErrorFromCloudSpeech(failure: CloudSpeechFailure): SttError {
48
- const hasDetail = failure.detail !== undefined;
49
- const userFacing =
50
- failure.kind === "unavailable" ||
51
- failure.code === "insufficient_balance" ||
52
- hasDetail;
53
- const managed = (category: SttErrorCategory, message: string): SttError =>
54
- new SttError(category, message, { userFacing });
55
-
56
- if (failure.kind === "unavailable") {
57
- return managed("auth", failure.message);
58
- }
59
- if (failure.code === "insufficient_balance") {
60
- return managed(
61
- "provider-error",
62
- "Cadet credits are exhausted — add funds to your Cadet account to continue using managed transcription.",
63
- );
64
- }
65
- switch (failure.status) {
66
- case 401:
67
- case 403:
68
- // A bare auth failure must stay user-facing so describeSttFailure skips
69
- // its BYOK "check your API key" rewrite — managed STT has no local key.
70
- // `managed()` would be non-user-facing here (see `userFacing` above), so
71
- // construct the sanitized cloud-connection message directly.
72
- return hasDetail
73
- ? managed("auth", failure.message)
74
- : new SttError(
75
- "auth",
76
- "Cadet couldn't authenticate with the cloud while transcribing — reconnect your Cadet account, then retry.",
77
- { userFacing: true },
78
- );
79
- case 429:
80
- return managed("rate-limit", failure.message);
81
- case 413:
82
- return managed("invalid-audio", failure.message);
83
- default:
84
- return managed("provider-error", failure.message);
85
- }
86
- }
87
-
88
- /**
89
- * Transcribe a complete audio buffer via the cloud. Throws {@link SttError}
90
- * on failure (already normalized — `normalizeSttError` passes it through).
91
- */
92
- export async function cadetCloudTranscribe(
93
- audio: Buffer,
94
- mimeType: string,
95
- signal?: AbortSignal,
96
- language?: string,
97
- ): Promise<SttTranscribeResult> {
98
- const result = await managedSpeechTranscribe({
99
- audio,
100
- mimeType,
101
- signal,
102
- language,
103
- });
104
- if (!result.ok) {
105
- throw sttErrorFromCloudSpeech(result);
106
- }
107
- return { text: result.value.text };
108
- }
@@ -1,176 +0,0 @@
1
- /**
2
- * Shared plumbing for dialing the GATEWAY's cloud speech relay
3
- * (`/v1/speech/{stt,tts}/stream`): connection resolution, and mapping the
4
- * relay's error contract onto SttError categories.
5
- *
6
- * The daemon never dials tunnel itself — tunnel contact is gateway-only so
7
- * deterministic policy stays in a process the agent cannot rewrite. The
8
- * gateway route authenticates the daemon with a self-minted service JWT
9
- * (both processes share the HMAC signing key), attaches the Cadet
10
- * assistant API key on its own upstream leg to tunnel, and pipes Deepgram
11
- * wire frames verbatim. The gateway mirrors tunnel's error contract:
12
- * rejections are JSON `{code, detail}` bodies, and mid-session failures
13
- * are one `tunnel_error` text frame followed by an abnormal close.
14
- */
15
-
16
- import { getGatewayInternalBaseUrl } from "../../config/env.js";
17
- import { CURRENT_POLICY_EPOCH } from "../../runtime/auth/policy.js";
18
- import { mintToken } from "../../runtime/auth/token-service.js";
19
- import type { SttErrorCategory } from "../../stt/types.js";
20
- import { getLogger } from "../../util/logger.js";
21
-
22
- const log = getLogger("cadet-speech-relay");
23
-
24
- /**
25
- * The principal the gateway's speech-relay route accepts. Must match
26
- * DAEMON_SERVICE_SUB in gateway/src/http/routes/speech-relay-websocket.ts
27
- * — the packages don't share auth constants.
28
- */
29
- const DAEMON_SERVICE_SUB = "svc:daemon:self";
30
-
31
- /**
32
- * Per-dial token TTL. Deliberately short: the gateway validates at
33
- * upgrade time only, and every (re-)dial mints fresh — a session that
34
- * outlives its token (tunnel caps at 30 minutes) is fine, but a re-dial
35
- * must never reuse one.
36
- */
37
- const RELAY_TOKEN_TTL_SECONDS = 300;
38
-
39
- export interface SpeechRelayConnection {
40
- /** ws(s) origin of the gateway for the relay WebSocket dial. */
41
- wsBaseUrl: string;
42
- /** http(s) origin of the gateway for diagnostic (non-upgrade) probes. */
43
- httpBaseUrl: string;
44
- /**
45
- * Mint a fresh daemon service token for one dial. Called per
46
- * connection attempt so session-cap re-dials never present an expired
47
- * token.
48
- */
49
- mintServiceToken: () => string;
50
- }
51
-
52
- /**
53
- * Resolve everything needed to dial the gateway's speech relay, or null
54
- * when the daemon cannot mint a service token (signing key unavailable) —
55
- * the availability signal for the streaming resolver.
56
- */
57
- export async function resolveSpeechRelayConnection(): Promise<SpeechRelayConnection | null> {
58
- const mintServiceToken = () =>
59
- mintToken({
60
- aud: "cadet-gateway",
61
- sub: DAEMON_SERVICE_SUB,
62
- // Least-privilege (ATL-1033): this token rides in the dial URL, so it
63
- // must open nothing but the speech relay.
64
- scope_profile: "speech_relay_v1",
65
- policy_epoch: CURRENT_POLICY_EPOCH,
66
- ttlSeconds: RELAY_TOKEN_TTL_SECONDS,
67
- });
68
- try {
69
- // Fail here, not mid-dial, when the signing key is unavailable.
70
- mintServiceToken();
71
- } catch (err) {
72
- log.warn(
73
- { error: err instanceof Error ? err.message : String(err) },
74
- "Cloud speech relay unavailable: cannot mint a gateway service token",
75
- );
76
- return null;
77
- }
78
- const httpBaseUrl = getGatewayInternalBaseUrl().replace(/\/+$/, "");
79
- return {
80
- httpBaseUrl,
81
- wsBaseUrl: httpBaseUrl.replace(/^http/, "ws"),
82
- mintServiceToken,
83
- };
84
- }
85
-
86
- /** The relay's error contract: HTTP reject bodies and tunnel_error frames. */
87
- export interface TunnelErrorInfo {
88
- code: string;
89
- detail?: string;
90
- }
91
-
92
- /**
93
- * Map a relay error code onto an SttError category and a user-actionable
94
- * message. Codes are tunnel's contract plus the gateway route's own
95
- * (`missing_platform_connection`, `invalid_token`) — unknown codes degrade
96
- * to a provider error carrying the code for diagnosability.
97
- */
98
- export function mapTunnelError(error: TunnelErrorInfo): {
99
- category: SttErrorCategory;
100
- message: string;
101
- } {
102
- switch (error.code) {
103
- case "invalid_key":
104
- return {
105
- category: "auth",
106
- message:
107
- "The Cadet speech relay rejected this assistant's API key — the assistant's platform environment may not match the relay it is dialing (set TUNNEL_BASE_URL on the gateway to the matching environment, e.g. https://tunnel-staging.mycadet.ai for staging).",
108
- };
109
- case "missing_platform_connection":
110
- return {
111
- category: "auth",
112
- message:
113
- "Cloud speech needs a working Cadet Cloud connection — reconnect with 'assistant cloud connect'.",
114
- };
115
- case "invalid_token":
116
- return {
117
- category: "auth",
118
- message:
119
- "The assistant's cloud-speech session was rejected by its gateway — the services may be out of sync; restart the assistant.",
120
- };
121
- case "insufficient_balance":
122
- return {
123
- category: "provider-error",
124
- message:
125
- "Cloud speech is paused: your Cadet organization is out of credits. Top up credits to continue.",
126
- };
127
- case "provider_unreachable":
128
- case "upstream_error":
129
- return {
130
- category: "provider-error",
131
- message: `Cloud speech relay could not reach the speech provider (${error.code}).`,
132
- };
133
- default:
134
- return {
135
- category: "provider-error",
136
- message: `Cloud speech relay error: ${error.code}${
137
- error.detail ? ` — ${error.detail}` : ""
138
- }`,
139
- };
140
- }
141
- }
142
-
143
- /**
144
- * Diagnose a failed WebSocket dial by replaying the request as a plain
145
- * HTTP GET. The gateway route runs its whole gate on non-upgrade requests
146
- * (token, stored credential, then tunnel's own gate via an upstream probe),
147
- * so a rejected upgrade reproduces as a JSON `{code, detail}` body; a
148
- * request that passes everything returns a 426 whose body is ignored here
149
- * (null → the original failure was transient/transport-level).
150
- *
151
- * The WebSocket API exposes no HTTP response details on a failed upgrade,
152
- * so this probe is the only way to distinguish "bad connection state"
153
- * from "gateway down".
154
- */
155
- export async function probeTunnelRejection(
156
- httpUrl: string,
157
- ): Promise<TunnelErrorInfo | null> {
158
- try {
159
- const res = await fetch(httpUrl, {
160
- signal: AbortSignal.timeout(5_000),
161
- });
162
- if (res.ok || res.status === 426) {
163
- return null;
164
- }
165
- const body = (await res.json()) as { code?: unknown; detail?: unknown };
166
- if (typeof body.code !== "string") {
167
- return null;
168
- }
169
- return {
170
- code: body.code,
171
- ...(typeof body.detail === "string" ? { detail: body.detail } : {}),
172
- };
173
- } catch {
174
- return null;
175
- }
176
- }