@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.
- package/node_modules/@mycadet/ces-client/node_modules/@mycadet/service-contracts/src/__tests__/ingress.test.ts +2 -2
- package/node_modules/@mycadet/ces-client/node_modules/@mycadet/service-contracts/src/ingress.ts +2 -2
- package/node_modules/@mycadet/gateway-client/node_modules/@mycadet/service-contracts/src/__tests__/ingress.test.ts +2 -2
- package/node_modules/@mycadet/gateway-client/node_modules/@mycadet/service-contracts/src/ingress.ts +2 -2
- package/node_modules/@mycadet/gateway-client/src/gateway-ipc-contracts.ts +0 -20
- package/node_modules/@mycadet/service-contracts/src/__tests__/ingress.test.ts +2 -2
- package/node_modules/@mycadet/service-contracts/src/ingress.ts +2 -2
- package/openapi.yaml +2 -708
- package/package.json +2 -3
- package/scripts/sync-web-search-catalog.ts +1 -2
- package/src/__tests__/cloud-profile-guard.test.ts +2 -2
- package/src/__tests__/config-schema.test.ts +6 -38
- package/src/__tests__/conversation-queue.test.ts +45 -6
- package/src/__tests__/db-readiness-http-gate.test.ts +0 -6
- package/src/__tests__/persist-onboarding-artifacts.test.ts +0 -1
- package/src/__tests__/persona-resolver.test.ts +0 -51
- package/src/__tests__/provider-cloud-proxy-integration.test.ts +2 -110
- package/src/__tests__/run-due-schedules.test.ts +0 -4
- package/src/__tests__/schedule-store.test.ts +0 -12
- package/src/__tests__/stt-stream-session.test.ts +4 -4
- package/src/__tests__/telephony-tts-guard.test.ts +2 -50
- package/src/__tests__/voice-config-update.test.ts +12 -167
- package/src/__tests__/web-search-catalog-parity.test.ts +0 -2
- package/src/__tests__/workspace-migration-130-speech-mode-to-provider.test.ts +9 -7
- package/src/__tests__/workspace-migration-132-web-search-mode-to-provider.test.ts +6 -6
- package/src/calls/__tests__/voice-session-bridge.test.ts +1 -4
- package/src/calls/resolve-call-tts-provider.ts +6 -25
- package/src/calls/telephony-credential-preflight.ts +0 -10
- package/src/calls/telephony-tts-capability.ts +1 -17
- package/src/cli/commands/tts.ts +1 -2
- package/src/cli/index.help.ts +0 -2
- package/src/cli/program.ts +0 -2
- package/src/config/bundled-skills/settings/tools/voice-config-update.ts +4 -42
- package/src/config/schemas/__tests__/stt.test.ts +10 -14
- package/src/config/schemas/services.ts +5 -6
- package/src/config/schemas/stt.ts +1 -3
- package/src/config/schemas/tts.ts +1 -30
- package/src/daemon/daemon-readiness.ts +4 -29
- package/src/daemon/lifecycle.ts +0 -31
- package/src/daemon/providers-setup.ts +0 -8
- package/src/heartbeat/__tests__/heartbeat-service.test.ts +0 -1
- package/src/heartbeat/heartbeat-service.ts +1 -66
- package/src/ipc/__tests__/db-readiness-ipc-gate.test.ts +1 -9
- package/src/live-voice/__tests__/live-voice-credential-preflight.test.ts +0 -44
- package/src/live-voice/__tests__/live-voice-flux-turn-end.test.ts +4 -6
- package/src/live-voice/__tests__/live-voice-stt.test.ts +1 -1
- package/src/live-voice/live-voice-credential-preflight.ts +9 -33
- package/src/live-voice/live-voice-session.ts +10 -6
- package/src/live-voice/live-voice-tts.ts +1 -8
- package/src/plugins/defaults/memory/worker.ts +3 -3
- package/src/prompts/persona-resolver.ts +0 -35
- package/src/providers/__tests__/search-provider-catalog.test.ts +5 -8
- package/src/providers/__tests__/voice-error-copy.test.ts +9 -10
- package/src/providers/search-provider-catalog.ts +7 -39
- package/src/providers/speech-to-text/__tests__/provider-catalog.test.ts +3 -47
- package/src/providers/speech-to-text/__tests__/resolve.test.ts +6 -372
- package/src/providers/speech-to-text/provider-catalog.ts +2 -73
- package/src/providers/speech-to-text/resolve.ts +3 -120
- package/src/runtime/http-router-types.ts +1 -3
- package/src/runtime/http-server.ts +1 -1
- package/src/runtime/routes/__tests__/tts-routes.test.ts +2 -6
- package/src/runtime/routes/inbound-stages/transcribe-audio.test.ts +7 -7
- package/src/runtime/routes/index.ts +0 -4
- package/src/runtime/routes/live-voice-routes.test.ts +4 -11
- package/src/runtime/routes/live-voice-routes.ts +8 -11
- package/src/runtime/routes/secret-routes.ts +0 -6
- package/src/runtime/routes/tts-routes.ts +0 -38
- package/src/schedule/__tests__/plugin-schedule-reconciler.test.ts +1 -7
- package/src/schedule/__tests__/worker-mcp-readiness.test.ts +0 -4
- package/src/schedule/schedule-store.ts +0 -7
- package/src/schedule/scheduler.ts +0 -5
- package/src/schedule/worker.ts +1 -1
- package/src/stt/__tests__/daemon-batch-transcriber.test.ts +2 -10
- package/src/stt/daemon-batch-transcriber.ts +4 -38
- package/src/stt/types.ts +1 -9
- package/src/tools/network/__tests__/web-search.test.ts +3 -250
- package/src/tools/network/web-search.ts +4 -165
- package/src/tts/__tests__/provider-resolution.test.ts +0 -5
- package/src/tts/provider-catalog.ts +0 -4
- package/src/tts/types.ts +0 -1
- package/src/__tests__/cloud-speech-defaults.test.ts +0 -330
- package/src/__tests__/migration-cross-version-compatibility.test.ts +0 -1708
- package/src/__tests__/migration-export-http.test.ts +0 -557
- package/src/__tests__/migration-export-streaming.test.ts +0 -350
- package/src/__tests__/migration-export-to-gcs.test.ts +0 -706
- package/src/__tests__/migration-import-commit-http.test.ts +0 -1176
- package/src/__tests__/migration-import-from-gcs.test.ts +0 -534
- package/src/__tests__/migration-import-from-path.test.ts +0 -324
- package/src/__tests__/migration-import-from-url.test.ts +0 -588
- package/src/__tests__/migration-import-preflight-http.test.ts +0 -739
- package/src/__tests__/migration-jobs-status.test.ts +0 -139
- package/src/__tests__/migration-parity-persistence.test.ts +0 -1891
- package/src/__tests__/migration-transport.test.ts +0 -1089
- package/src/__tests__/migration-validate-http.test.ts +0 -634
- package/src/__tests__/migration-wizard.test.ts +0 -1395
- package/src/__tests__/rebind-secrets-screen.test.ts +0 -876
- package/src/__tests__/sanitize-config-for-transfer.test.ts +0 -259
- package/src/__tests__/transfer-progress-screen.test.ts +0 -1217
- package/src/__tests__/validation-results-screen.test.ts +0 -1130
- package/src/__tests__/vbundle-pax-and-symlink.test.ts +0 -147
- package/src/background-wake/background-wake-routes.test.ts +0 -906
- package/src/background-wake/cloud-client.test.ts +0 -328
- package/src/background-wake/cloud-client.ts +0 -196
- package/src/background-wake/next-wake.test.ts +0 -335
- package/src/background-wake/next-wake.ts +0 -188
- package/src/background-wake/publisher.ts +0 -93
- package/src/background-wake/wake-intent-hooks.test.ts +0 -234
- package/src/cli/commands/__tests__/backup.test.ts +0 -363
- package/src/cli/commands/backup.help.ts +0 -180
- package/src/cli/commands/backup.ts +0 -398
- package/src/cloud/cloud-speech.test.ts +0 -335
- package/src/cloud/cloud-speech.ts +0 -300
- package/src/config/cloud-speech-defaults.ts +0 -300
- package/src/config/sanitize-for-transfer.ts +0 -111
- package/src/live-voice/__tests__/live-voice-cloud-speech-fallback.test.ts +0 -250
- package/src/providers/speech-to-text/__tests__/cadet-cloud-realtime.test.ts +0 -457
- package/src/providers/speech-to-text/__tests__/cadet-cloud.test.ts +0 -271
- package/src/providers/speech-to-text/__tests__/cadet-speech-relay-connection.test.ts +0 -126
- package/src/providers/speech-to-text/cadet-cloud-flux-realtime.ts +0 -309
- package/src/providers/speech-to-text/cadet-cloud-realtime.ts +0 -315
- package/src/providers/speech-to-text/cadet-cloud.ts +0 -108
- package/src/providers/speech-to-text/cadet-speech-relay-connection.ts +0 -176
- package/src/runtime/migrations/__tests__/gcs-signed-url.test.ts +0 -254
- package/src/runtime/migrations/__tests__/job-registry.test.ts +0 -346
- package/src/runtime/migrations/__tests__/origin-mode.test.ts +0 -62
- package/src/runtime/migrations/__tests__/rebind-secrets-credentials.test.ts +0 -184
- package/src/runtime/migrations/__tests__/staged-import-path.test.ts +0 -110
- package/src/runtime/migrations/__tests__/v1-test-helpers.ts +0 -112
- package/src/runtime/migrations/__tests__/vbundle-builder-credentials.test.ts +0 -285
- package/src/runtime/migrations/__tests__/vbundle-builder-fd-leak.test.ts +0 -126
- package/src/runtime/migrations/__tests__/vbundle-builder-v1-shape.test.ts +0 -253
- package/src/runtime/migrations/__tests__/vbundle-import-credentials.test.ts +0 -208
- package/src/runtime/migrations/__tests__/vbundle-import-parity.test.ts +0 -485
- package/src/runtime/migrations/__tests__/vbundle-import-policy.test.ts +0 -349
- package/src/runtime/migrations/__tests__/vbundle-import-version-compat.test.ts +0 -196
- package/src/runtime/migrations/__tests__/vbundle-legacy-user-md.test.ts +0 -551
- package/src/runtime/migrations/__tests__/vbundle-metadata-merge-integration.test.ts +0 -428
- package/src/runtime/migrations/__tests__/vbundle-metadata-merge.test.ts +0 -220
- package/src/runtime/migrations/__tests__/vbundle-streaming-importer.test.ts +0 -1823
- package/src/runtime/migrations/__tests__/vbundle-streaming-validator.test.ts +0 -598
- package/src/runtime/migrations/__tests__/vbundle-symlink-importer.test.ts +0 -463
- package/src/runtime/migrations/__tests__/vbundle-symlink-streaming-importer.test.ts +0 -538
- package/src/runtime/migrations/__tests__/vbundle-symlink-streaming.test.ts +0 -515
- package/src/runtime/migrations/__tests__/vbundle-symlink-tar.test.ts +0 -453
- package/src/runtime/migrations/__tests__/vbundle-symlink-walker.test.ts +0 -319
- package/src/runtime/migrations/__tests__/vbundle-tar-stream.test.ts +0 -240
- package/src/runtime/migrations/__tests__/vbundle-validator-v1-schema.test.ts +0 -473
- package/src/runtime/migrations/gcs-signed-url.ts +0 -190
- package/src/runtime/migrations/job-registry.ts +0 -297
- package/src/runtime/migrations/migration-transport.ts +0 -612
- package/src/runtime/migrations/migration-wizard.ts +0 -747
- package/src/runtime/migrations/origin-mode.ts +0 -42
- package/src/runtime/migrations/rebind-secrets-screen.ts +0 -412
- package/src/runtime/migrations/staged-import-path.ts +0 -116
- package/src/runtime/migrations/transfer-progress-screen.ts +0 -323
- package/src/runtime/migrations/validation-results-screen.ts +0 -469
- package/src/runtime/migrations/vbundle-builder.ts +0 -1382
- package/src/runtime/migrations/vbundle-import-analyzer.ts +0 -405
- package/src/runtime/migrations/vbundle-import-policy.ts +0 -241
- package/src/runtime/migrations/vbundle-importer.ts +0 -826
- package/src/runtime/migrations/vbundle-metadata-merge.ts +0 -113
- package/src/runtime/migrations/vbundle-streaming-importer.ts +0 -2844
- package/src/runtime/migrations/vbundle-streaming-validator.ts +0 -426
- package/src/runtime/migrations/vbundle-tar-stream.ts +0 -244
- package/src/runtime/migrations/vbundle-validator.ts +0 -724
- package/src/runtime/routes/__tests__/migration-cadet-metadata-reconcile.test.ts +0 -227
- package/src/runtime/routes/__tests__/migration-export-secrets-redacted.test.ts +0 -54
- package/src/runtime/routes/__tests__/migration-import-allowed-hosts-env.test.ts +0 -70
- package/src/runtime/routes/__tests__/migration-import-credential-filter.test.ts +0 -257
- package/src/runtime/routes/background-wake-routes.ts +0 -387
- package/src/runtime/routes/migration-routes.ts +0 -2584
- package/src/tools/network/__tests__/cloud-search-proxy.test.ts +0 -282
- package/src/tools/network/cloud-search-proxy.ts +0 -211
- package/src/tts/__tests__/cadet-provider.test.ts +0 -253
- package/src/tts/providers/cadet-provider.ts +0 -244
- 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
|
-
}
|