@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,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
- }