@opengeni/api-router 0.21.14 → 0.22.2

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.
@@ -13,14 +13,15 @@ import {
13
13
  OPENGENI_API_CONTRACT_REVISION,
14
14
  OPENGENI_CORRELATION_HEADER,
15
15
  resolveWorkspaceMemoryEnabled,
16
- VOICE_INPUT_ACCEPTED_MIME_TYPES as VOICE_INPUT_ACCEPTED_MIME_TYPES2
16
+ VOICE_INPUT_ACCEPTED_MIME_TYPES as VOICE_INPUT_ACCEPTED_MIME_TYPES2,
17
+ TRANSCRIPTION_RECORDING_PROVIDER_SEGMENT_SECONDS as TRANSCRIPTION_RECORDING_PROVIDER_SEGMENT_SECONDS2
17
18
  } from "@opengeni/contracts";
18
19
  import {
19
20
  createDocumentServices,
20
21
  getDocument as getDocument2,
21
22
  indexDocumentNow
22
23
  } from "@opengeni/documents";
23
- import { dbSql, getWorkspace as getWorkspace5, rlsContextForWorkspace } from "@opengeni/db";
24
+ import { dbSql, getWorkspace as getWorkspace6, rlsContextForWorkspace } from "@opengeni/db";
24
25
  import { createObservability } from "@opengeni/observability";
25
26
  import { createObjectStorage } from "@opengeni/storage";
26
27
  import { WebStandardStreamableHTTPServerTransport as WebStandardStreamableHTTPServerTransport3 } from "@modelcontextprotocol/sdk/server/webStandardStreamableHttp.js";
@@ -49,7 +50,7 @@ var ApiHttpError = class extends HTTPException {
49
50
  import {
50
51
  CodexCompactionV2ProviderLockedError,
51
52
  hasPermission as hasPermission15,
52
- requireAccessGrant as requireAccessGrant24,
53
+ requireAccessGrant as requireAccessGrant25,
53
54
  requirePermission,
54
55
  requireSessionAuthorization as requireSessionAuthorization3,
55
56
  SessionAuthorizationDeniedError as SessionAuthorizationDeniedError2,
@@ -7625,7 +7626,7 @@ function registerFleetTools(server, deps, grant, sessionId, json) {
7625
7626
  server.registerTool(
7626
7627
  "sandboxes_list",
7627
7628
  {
7628
- description: "List the sandboxes this session can run on: its own session sandbox plus enrolled selfhosted machines. `liveness` is conservative: online requires observed provider existence and verified workspace readiness. Provider, lease, route, archive, restore, workspace, lease epoch, and route epoch are also reported separately. Use an entry `id` as an attach/swap/run_on target.",
7629
+ description: "List the sandboxes this session can run on: its own session sandbox plus enrolled selfhosted machines. `liveness` is conservative: online requires observed provider existence and verified workspace readiness. An idle session-home sandbox may report offline/cold/draining and still wake or restore on the next ordinary sandbox operation; never infer that shell/files are unavailable from list liveness alone\u2014only a typed operation/attach failure proves that. Provider, lease, route, archive, restore, workspace, lease epoch, and route epoch are also reported separately. Use an entry `id` as an attach/swap/run_on target.",
7629
7630
  inputSchema: {}
7630
7631
  },
7631
7632
  async () => json(await listFleet(services, await fleetContext()))
@@ -23946,17 +23947,871 @@ function registerInsightsRoutes(app, deps) {
23946
23947
 
23947
23948
  // src/routes/transcriptions.ts
23948
23949
  import {
23949
- resolveWorkspaceVoiceInputEnabled
23950
+ resolveWorkspaceVoiceInputEnabled as resolveWorkspaceVoiceInputEnabled2
23950
23951
  } from "@opengeni/contracts";
23951
- import { requireAccessGrant as requireAccessGrant22, TranscriptionServiceError } from "@opengeni/core";
23952
+ import { requireAccessGrant as requireAccessGrant23, TranscriptionServiceError as TranscriptionServiceError2 } from "@opengeni/core";
23953
+ import { getWorkspace as getWorkspace4 } from "@opengeni/db";
23954
+
23955
+ // src/routes/transcription-recordings.ts
23956
+ import { createHash as createHash11 } from "crypto";
23957
+ import {
23958
+ CreateTranscriptionRecordingRequest,
23959
+ FinalizeTranscriptionRecordingRequest,
23960
+ resolveWorkspaceVoiceInputEnabled,
23961
+ TRANSCRIPTION_RECORDING_PROVIDER_SEGMENT_SECONDS,
23962
+ TRANSCRIPTION_RECORDING_RECOVERY_RETRY_AFTER_MILLISECONDS
23963
+ } from "@opengeni/contracts";
23964
+ import {
23965
+ isAcceptedMimeType,
23966
+ normalizeMimeType,
23967
+ requireAccessGrant as requireAccessGrant22,
23968
+ TRANSCRIPTION_PROVIDER_REQUEST_TIMEOUT_MILLISECONDS,
23969
+ TranscriptionServiceError
23970
+ } from "@opengeni/core";
23971
+ import {
23972
+ claimNextTranscriptionRecordingSegment,
23973
+ claimTranscriptionRecordingAssembly,
23974
+ completeTranscriptionRecordingAssembly,
23975
+ completeTranscriptionRecordingChunk,
23976
+ completeTranscriptionRecordingSegment,
23977
+ completeTranscriptionRecordingSegmentPreparation,
23978
+ createTranscriptionRecording,
23979
+ discardTranscriptionRecording,
23980
+ failTranscriptionRecordingAssembly,
23981
+ failTranscriptionRecordingSegment,
23982
+ getTranscriptionRecording,
23983
+ listTranscriptionRecordings,
23984
+ listTranscriptionRecordingChunks,
23985
+ markTranscriptionRecordingObjectCleaned,
23986
+ markTranscriptionRecordingObjectsCleaned,
23987
+ reserveTranscriptionRecordingChunk,
23988
+ reserveTranscriptionRecordingSegment,
23989
+ startTranscriptionRecordingSegmentProviderCall,
23990
+ transcriptionRecordingObjectKeys,
23991
+ TranscriptionRecordingConflictError,
23992
+ TranscriptionRecordingNotFoundError,
23993
+ TranscriptionRecordingStateError
23994
+ } from "@opengeni/db";
23952
23995
  import { getWorkspace as getWorkspace3 } from "@opengeni/db";
23996
+
23997
+ // src/transcription/segmenter.ts
23998
+ import { spawn } from "child_process";
23999
+ import { createWriteStream } from "fs";
24000
+ import { mkdtemp, readFile as readFile2, readdir, rm } from "fs/promises";
24001
+ import { tmpdir } from "os";
24002
+ import { join } from "path";
24003
+ import { once } from "events";
24004
+ var STDERR_MAX_BYTES = 64 * 1024;
24005
+ var TranscriptionSegmenterError = class extends Error {
24006
+ constructor(message, code, retryable) {
24007
+ super(message);
24008
+ this.code = code;
24009
+ this.retryable = retryable;
24010
+ }
24011
+ name = "TranscriptionSegmenterError";
24012
+ };
24013
+ function createFfmpegTranscriptionSegmenter(input) {
24014
+ let availability = null;
24015
+ return {
24016
+ available() {
24017
+ availability ??= commandSucceeds(input.ffmpegPath, ["-version"]);
24018
+ return availability;
24019
+ },
24020
+ async *segment(request) {
24021
+ if (request.signal?.aborted) {
24022
+ throw new TranscriptionSegmenterError(
24023
+ "Audio segmentation was cancelled",
24024
+ "cancelled",
24025
+ true
24026
+ );
24027
+ }
24028
+ if (!Number.isSafeInteger(request.providerSegmentSeconds) || request.providerSegmentSeconds <= 0 || !Number.isSafeInteger(request.totalDurationMilliseconds) || request.totalDurationMilliseconds <= 0) {
24029
+ throw new TranscriptionSegmenterError(
24030
+ "Audio segmentation bounds are invalid",
24031
+ "invalid_audio",
24032
+ false
24033
+ );
24034
+ }
24035
+ if (Math.ceil(request.totalDurationMilliseconds / (request.providerSegmentSeconds * 1e3)) > 1e3) {
24036
+ throw new TranscriptionSegmenterError(
24037
+ "Audio exceeds the bounded segment projection",
24038
+ "too_large",
24039
+ false
24040
+ );
24041
+ }
24042
+ const directory = await mkdtemp(join(tmpdir(), "opengeni-transcription-"));
24043
+ const inputPath = join(
24044
+ directory,
24045
+ `recording.${extensionForMimeType(request.sourceMimeType)}`
24046
+ );
24047
+ const outputPattern = join(directory, "segment-%06d.wav");
24048
+ try {
24049
+ await writeChunks(inputPath, request.chunks, request.signal);
24050
+ const result = await runCommand(
24051
+ input.ffmpegPath,
24052
+ [
24053
+ "-nostdin",
24054
+ "-hide_banner",
24055
+ "-loglevel",
24056
+ "error",
24057
+ "-y",
24058
+ "-i",
24059
+ inputPath,
24060
+ "-map",
24061
+ "0:a:0",
24062
+ "-vn",
24063
+ "-ac",
24064
+ "1",
24065
+ "-ar",
24066
+ "16000",
24067
+ "-c:a",
24068
+ "pcm_s16le",
24069
+ "-f",
24070
+ "segment",
24071
+ "-segment_time",
24072
+ String(request.providerSegmentSeconds),
24073
+ "-reset_timestamps",
24074
+ "1",
24075
+ outputPattern
24076
+ ],
24077
+ request.signal
24078
+ );
24079
+ if (result.cancelled) {
24080
+ throw new TranscriptionSegmenterError(
24081
+ "Audio segmentation was cancelled",
24082
+ "cancelled",
24083
+ true
24084
+ );
24085
+ }
24086
+ if (result.spawnError) {
24087
+ throw new TranscriptionSegmenterError(
24088
+ "Audio segmentation is unavailable",
24089
+ "unavailable",
24090
+ true
24091
+ );
24092
+ }
24093
+ if (result.exitCode !== 0) {
24094
+ throw new TranscriptionSegmenterError(
24095
+ result.stderr || "Audio could not be decoded",
24096
+ "invalid_audio",
24097
+ false
24098
+ );
24099
+ }
24100
+ const files = (await readdir(directory)).filter((file) => /^segment-[0-9]{6}\.wav$/.test(file)).sort();
24101
+ if (files.length === 0 || files.length > 1e3) {
24102
+ throw new TranscriptionSegmenterError(
24103
+ "Audio did not produce a bounded segment set",
24104
+ "invalid_audio",
24105
+ false
24106
+ );
24107
+ }
24108
+ const segmentMilliseconds = request.providerSegmentSeconds * 1e3;
24109
+ for (let segmentNumber = 0; segmentNumber < files.length; segmentNumber += 1) {
24110
+ if (request.signal?.aborted) {
24111
+ throw new TranscriptionSegmenterError(
24112
+ "Audio segmentation was cancelled",
24113
+ "cancelled",
24114
+ true
24115
+ );
24116
+ }
24117
+ const bytes = new Uint8Array(await readFile2(join(directory, files[segmentNumber])));
24118
+ if (bytes.byteLength === 0) {
24119
+ throw new TranscriptionSegmenterError(
24120
+ "Audio produced an empty segment",
24121
+ "invalid_audio",
24122
+ false
24123
+ );
24124
+ }
24125
+ const startMilliseconds = segmentNumber * segmentMilliseconds;
24126
+ const remaining = request.totalDurationMilliseconds - startMilliseconds;
24127
+ if (remaining <= 0) {
24128
+ throw new TranscriptionSegmenterError(
24129
+ "Audio segment count exceeds the declared duration",
24130
+ "invalid_audio",
24131
+ false
24132
+ );
24133
+ }
24134
+ yield {
24135
+ segmentNumber,
24136
+ startMilliseconds,
24137
+ durationMilliseconds: Math.min(segmentMilliseconds, remaining),
24138
+ mimeType: "audio/wav",
24139
+ bytes
24140
+ };
24141
+ }
24142
+ } catch (error) {
24143
+ if (error instanceof TranscriptionSegmenterError) throw error;
24144
+ throw new TranscriptionSegmenterError(
24145
+ error instanceof Error ? error.message : "Audio segmentation failed",
24146
+ request.signal?.aborted ? "cancelled" : "unknown",
24147
+ true
24148
+ );
24149
+ } finally {
24150
+ await rm(directory, { recursive: true, force: true }).catch(() => void 0);
24151
+ }
24152
+ }
24153
+ };
24154
+ }
24155
+ async function writeChunks(path, chunks, signal) {
24156
+ const stream = createWriteStream(path, { flags: "wx" });
24157
+ try {
24158
+ for await (const chunk of chunks) {
24159
+ if (signal?.aborted) {
24160
+ throw new TranscriptionSegmenterError(
24161
+ "Audio segmentation was cancelled",
24162
+ "cancelled",
24163
+ true
24164
+ );
24165
+ }
24166
+ if (!stream.write(chunk)) await once(stream, "drain");
24167
+ }
24168
+ stream.end();
24169
+ await once(stream, "close");
24170
+ } catch (error) {
24171
+ stream.destroy();
24172
+ throw error;
24173
+ }
24174
+ }
24175
+ function extensionForMimeType(mimeType) {
24176
+ switch (mimeType.trim().toLowerCase().split(";", 1)[0]) {
24177
+ case "audio/mp4":
24178
+ case "audio/m4a":
24179
+ return "mp4";
24180
+ case "audio/ogg":
24181
+ return "ogg";
24182
+ case "audio/mpeg":
24183
+ case "audio/mp3":
24184
+ return "mp3";
24185
+ case "audio/wav":
24186
+ case "audio/x-wav":
24187
+ return "wav";
24188
+ case "audio/webm":
24189
+ default:
24190
+ return "webm";
24191
+ }
24192
+ }
24193
+ async function commandSucceeds(command, args) {
24194
+ const result = await runCommand(command, args);
24195
+ return !result.spawnError && result.exitCode === 0;
24196
+ }
24197
+ async function runCommand(command, args, signal) {
24198
+ return await new Promise((resolve) => {
24199
+ const child = spawn(command, args, { stdio: ["ignore", "ignore", "pipe"] });
24200
+ let stderr = Buffer.alloc(0);
24201
+ let spawnError = false;
24202
+ let settled = false;
24203
+ const onAbort = () => child.kill("SIGKILL");
24204
+ signal?.addEventListener("abort", onAbort, { once: true });
24205
+ if (signal?.aborted) onAbort();
24206
+ child.stderr.on("data", (chunk) => {
24207
+ if (stderr.byteLength >= STDERR_MAX_BYTES) return;
24208
+ const remaining = STDERR_MAX_BYTES - stderr.byteLength;
24209
+ stderr = Buffer.concat([stderr, Buffer.from(chunk).subarray(0, remaining)]);
24210
+ });
24211
+ child.once("error", () => {
24212
+ spawnError = true;
24213
+ });
24214
+ child.once("close", (exitCode) => {
24215
+ if (settled) return;
24216
+ settled = true;
24217
+ signal?.removeEventListener("abort", onAbort);
24218
+ resolve({
24219
+ exitCode,
24220
+ stderr: stderr.toString("utf8").trim(),
24221
+ spawnError,
24222
+ cancelled: signal?.aborted ?? false
24223
+ });
24224
+ });
24225
+ });
24226
+ }
24227
+
24228
+ // src/routes/transcription-recordings.ts
24229
+ var CHUNK_SHA256_HEADER = "x-opengeni-chunk-sha256";
24230
+ var CHUNK_START_HEADER = "x-opengeni-chunk-start-milliseconds";
24231
+ var CHUNK_DURATION_HEADER = "x-opengeni-chunk-duration-milliseconds";
24232
+ var PROCESSING_LEASE_MILLISECONDS = 15 * 60 * 1e3;
24233
+ var RecordingProcessingError = class extends Error {
24234
+ constructor(message, code, retryable) {
24235
+ super(message);
24236
+ this.code = code;
24237
+ this.retryable = retryable;
24238
+ }
24239
+ name = "RecordingProcessingError";
24240
+ };
24241
+ function registerResumableTranscriptionRoutes(app, deps) {
24242
+ app.get("/v1/workspaces/:workspaceId/transcription-recordings", async (c) => {
24243
+ try {
24244
+ const authority = await requireRecordingAuthority(c, deps, false);
24245
+ return c.json({
24246
+ recordings: await listTranscriptionRecordings(deps.db, {
24247
+ workspaceId: authority.workspaceId,
24248
+ subjectId: authority.subjectId
24249
+ })
24250
+ });
24251
+ } catch (error) {
24252
+ return routeError(c, error);
24253
+ }
24254
+ });
24255
+ app.post("/v1/workspaces/:workspaceId/transcription-recordings", async (c) => {
24256
+ try {
24257
+ const authority = await requireRecordingAuthority(c, deps, true);
24258
+ if (!await resumableAvailable(deps, authority.workspaceId)) {
24259
+ return c.json({ code: "unavailable" }, 503);
24260
+ }
24261
+ const parsed = CreateTranscriptionRecordingRequest.safeParse(await jsonBody(c));
24262
+ if (!parsed.success) return c.json({ code: "invalid_request" }, 400);
24263
+ const mimeType = normalizeMimeType(parsed.data.mimeType);
24264
+ if (!isAcceptedMimeType(mimeType, deps.transcription.limits().acceptedMimeTypes)) {
24265
+ return c.json({ code: "not_supported" }, 415);
24266
+ }
24267
+ const recording = await createTranscriptionRecording(deps.db, {
24268
+ ...authority,
24269
+ recordingId: parsed.data.recordingId,
24270
+ mimeType,
24271
+ expiresAt: new Date(Date.now() + deps.settings.voiceInputResumableRetentionSeconds * 1e3)
24272
+ });
24273
+ return c.json(recording, 201);
24274
+ } catch (error) {
24275
+ return routeError(c, error);
24276
+ }
24277
+ });
24278
+ app.get("/v1/workspaces/:workspaceId/transcription-recordings/:recordingId", async (c) => {
24279
+ try {
24280
+ const authority = await requireRecordingAuthority(c, deps, false);
24281
+ const response = await getTranscriptionRecording(deps.db, {
24282
+ ...authority,
24283
+ recordingId: uuidParam(c, "recordingId")
24284
+ });
24285
+ return c.json(withRecoveryRetryHint(await cleanupTerminalObjects(deps, authority, response)));
24286
+ } catch (error) {
24287
+ return routeError(c, error);
24288
+ }
24289
+ });
24290
+ app.put(
24291
+ "/v1/workspaces/:workspaceId/transcription-recordings/:recordingId/chunks/:chunkNumber",
24292
+ async (c) => {
24293
+ try {
24294
+ const authority = await requireRecordingAuthority(c, deps, true);
24295
+ if (!deps.objectStorage || !deps.settings.voiceInputResumableEnabled) {
24296
+ return c.json({ code: "unavailable" }, 503);
24297
+ }
24298
+ const chunkNumber = nonnegativeInteger(c.req.param("chunkNumber"));
24299
+ const startMilliseconds = headerInteger(c, CHUNK_START_HEADER, true);
24300
+ const durationMilliseconds = headerInteger(c, CHUNK_DURATION_HEADER, true);
24301
+ const declaredSha256 = c.req.header(CHUNK_SHA256_HEADER)?.trim().toLowerCase() ?? "";
24302
+ if (!/^[0-9a-f]{64}$/.test(declaredSha256)) {
24303
+ return c.json({ code: "invalid_request" }, 400);
24304
+ }
24305
+ const body2 = await readBoundedBody(
24306
+ c.req.raw,
24307
+ deps.settings.voiceInputResumableMaxChunkSizeBytes
24308
+ );
24309
+ const sha256 = sha256Hex2(body2);
24310
+ if (sha256 !== declaredSha256) {
24311
+ return c.json({ code: "conflict" }, 409);
24312
+ }
24313
+ const existing = await getTranscriptionRecording(deps.db, {
24314
+ ...authority,
24315
+ recordingId: uuidParam(c, "recordingId")
24316
+ });
24317
+ if (normalizeMimeType(c.req.header("content-type") ?? "") !== normalizeMimeType(existing.recording.mimeType)) {
24318
+ return c.json({ code: "not_supported" }, 415);
24319
+ }
24320
+ const reservation = await reserveTranscriptionRecordingChunk(deps.db, {
24321
+ ...authority,
24322
+ recordingId: existing.recording.id,
24323
+ chunkNumber,
24324
+ byteLength: body2.byteLength,
24325
+ sha256,
24326
+ startMilliseconds,
24327
+ durationMilliseconds,
24328
+ maxTotalBytes: deps.settings.voiceInputResumableMaxSizeBytes,
24329
+ maxDurationMilliseconds: deps.settings.voiceInputResumableMaxDurationSeconds * 1e3
24330
+ });
24331
+ if (!reservation.deduplicated) {
24332
+ try {
24333
+ await deps.objectStorage.putObject({
24334
+ key: reservation.chunk.objectKey,
24335
+ contentType: existing.recording.mimeType,
24336
+ body: body2,
24337
+ sha256
24338
+ });
24339
+ } catch {
24340
+ throw new RecordingProcessingError("Chunk upload failed", "network", true);
24341
+ }
24342
+ }
24343
+ const completed = await completeTranscriptionRecordingChunk(deps.db, {
24344
+ workspaceId: authority.workspaceId,
24345
+ subjectId: authority.subjectId,
24346
+ recordingId: existing.recording.id,
24347
+ chunkNumber
24348
+ });
24349
+ const response = {
24350
+ recording: completed.recording.recording,
24351
+ chunk: {
24352
+ chunkNumber: completed.chunk.chunkNumber,
24353
+ byteLength: completed.chunk.byteLength,
24354
+ sha256: completed.chunk.sha256,
24355
+ startMilliseconds: completed.chunk.startMilliseconds,
24356
+ durationMilliseconds: completed.chunk.durationMilliseconds,
24357
+ deduplicated: reservation.deduplicated || completed.deduplicated
24358
+ }
24359
+ };
24360
+ return c.json(response);
24361
+ } catch (error) {
24362
+ return routeError(c, error);
24363
+ }
24364
+ }
24365
+ );
24366
+ app.post(
24367
+ "/v1/workspaces/:workspaceId/transcription-recordings/:recordingId/finalize",
24368
+ async (c) => {
24369
+ const owner = correlationId(c);
24370
+ let authority = null;
24371
+ let generation = 0;
24372
+ try {
24373
+ authority = await requireRecordingAuthority(c, deps, true);
24374
+ if (!await resumableAvailable(deps, authority.workspaceId)) {
24375
+ return c.json({ code: "unavailable" }, 503);
24376
+ }
24377
+ const parsed = FinalizeTranscriptionRecordingRequest.safeParse(await jsonBody(c));
24378
+ if (!parsed.success) return c.json({ code: "invalid_request" }, 400);
24379
+ const recordingId = uuidParam(c, "recordingId");
24380
+ const claim = await claimTranscriptionRecordingAssembly(deps.db, {
24381
+ workspaceId: authority.workspaceId,
24382
+ subjectId: authority.subjectId,
24383
+ recordingId,
24384
+ owner,
24385
+ ...parsed.data,
24386
+ staleBefore: new Date(Date.now() - PROCESSING_LEASE_MILLISECONDS)
24387
+ });
24388
+ generation = claim.generation;
24389
+ if (!claim.claimed) {
24390
+ const response = withRecoveryRetryHint(claim.recording);
24391
+ return c.json(response, claim.recording.recording.state === "segmenting" ? 202 : 200);
24392
+ }
24393
+ for (const key of claim.staleObjectKeys) {
24394
+ try {
24395
+ await deps.objectStorage.deleteObject(key);
24396
+ await markTranscriptionRecordingObjectCleaned(deps.db, {
24397
+ workspaceId: authority.workspaceId,
24398
+ subjectId: authority.subjectId,
24399
+ recordingId,
24400
+ objectKey: key
24401
+ });
24402
+ } catch {
24403
+ }
24404
+ }
24405
+ const chunks = await listTranscriptionRecordingChunks(deps.db, {
24406
+ workspaceId: authority.workspaceId,
24407
+ subjectId: authority.subjectId,
24408
+ recordingId
24409
+ });
24410
+ const providerMaxSegmentSeconds = deps.transcription.limits().maxDurationSeconds;
24411
+ const minimumBoundedSegmentSeconds = Math.ceil(
24412
+ claim.recording.recording.totalDurationMilliseconds / 1e3 / 1e3
24413
+ );
24414
+ if (providerMaxSegmentSeconds < minimumBoundedSegmentSeconds) {
24415
+ throw new RecordingProcessingError(
24416
+ "Recording cannot fit the bounded provider segment projection",
24417
+ "too_large",
24418
+ false
24419
+ );
24420
+ }
24421
+ const providerSegmentSeconds = Math.min(
24422
+ TRANSCRIPTION_RECORDING_PROVIDER_SEGMENT_SECONDS,
24423
+ providerMaxSegmentSeconds
24424
+ );
24425
+ for await (const segment of deps.transcriptionSegmenter.segment({
24426
+ sourceMimeType: claim.recording.recording.mimeType,
24427
+ totalDurationMilliseconds: claim.recording.recording.totalDurationMilliseconds,
24428
+ providerSegmentSeconds,
24429
+ chunks: verifiedChunkBytes(deps, chunks, c.req.raw.signal),
24430
+ signal: c.req.raw.signal
24431
+ })) {
24432
+ const sha256 = sha256Hex2(segment.bytes);
24433
+ const reservation = await reserveTranscriptionRecordingSegment(deps.db, {
24434
+ ...authority,
24435
+ recordingId,
24436
+ owner,
24437
+ generation,
24438
+ segmentNumber: segment.segmentNumber,
24439
+ byteLength: segment.bytes.byteLength,
24440
+ sha256,
24441
+ startMilliseconds: segment.startMilliseconds,
24442
+ durationMilliseconds: segment.durationMilliseconds
24443
+ });
24444
+ try {
24445
+ await deps.objectStorage.putObject({
24446
+ key: reservation.objectKey,
24447
+ contentType: segment.mimeType,
24448
+ body: segment.bytes,
24449
+ sha256
24450
+ });
24451
+ } catch {
24452
+ throw new RecordingProcessingError("Segment upload failed", "network", true);
24453
+ }
24454
+ await completeTranscriptionRecordingSegmentPreparation(deps.db, {
24455
+ workspaceId: authority.workspaceId,
24456
+ subjectId: authority.subjectId,
24457
+ recordingId,
24458
+ owner,
24459
+ generation,
24460
+ segmentNumber: segment.segmentNumber
24461
+ });
24462
+ }
24463
+ return c.json(
24464
+ await completeTranscriptionRecordingAssembly(deps.db, {
24465
+ workspaceId: authority.workspaceId,
24466
+ subjectId: authority.subjectId,
24467
+ recordingId,
24468
+ owner,
24469
+ generation
24470
+ })
24471
+ );
24472
+ } catch (error) {
24473
+ if (authority && generation > 0) {
24474
+ const failure = processingFailure(error);
24475
+ const persisted = await failTranscriptionRecordingAssembly(deps.db, {
24476
+ workspaceId: authority.workspaceId,
24477
+ subjectId: authority.subjectId,
24478
+ recordingId: uuidParam(c, "recordingId"),
24479
+ owner,
24480
+ generation,
24481
+ errorCode: failure.code,
24482
+ retryable: failure.retryable
24483
+ }).catch(() => null);
24484
+ if (persisted) return c.json(persisted);
24485
+ }
24486
+ return routeError(c, error);
24487
+ }
24488
+ }
24489
+ );
24490
+ app.post(
24491
+ "/v1/workspaces/:workspaceId/transcription-recordings/:recordingId/process-next",
24492
+ async (c) => {
24493
+ let authority = null;
24494
+ let attemptId = null;
24495
+ let segmentNumber = null;
24496
+ try {
24497
+ authority = await requireRecordingAuthority(c, deps, true);
24498
+ const service = deps.transcription;
24499
+ if (!deps.objectStorage || !service || !await service.available({ workspaceId: authority.workspaceId })) {
24500
+ return c.json({ code: "unavailable" }, 503);
24501
+ }
24502
+ const selectedProvider = service.selectProvider ? await service.selectProvider({ workspaceId: authority.workspaceId }) : "host";
24503
+ if (!selectedProvider) return c.json({ code: "unavailable" }, 503);
24504
+ attemptId = correlationId(c);
24505
+ const claim = await claimNextTranscriptionRecordingSegment(deps.db, {
24506
+ workspaceId: authority.workspaceId,
24507
+ subjectId: authority.subjectId,
24508
+ recordingId: uuidParam(c, "recordingId"),
24509
+ attemptId,
24510
+ providerId: selectedProvider,
24511
+ staleBefore: new Date(Date.now() - PROCESSING_LEASE_MILLISECONDS),
24512
+ providerDeadlineAt: new Date(Date.now() + PROCESSING_LEASE_MILLISECONDS)
24513
+ });
24514
+ if (!claim.claimed || !claim.segment) {
24515
+ const cleaned = await cleanupTerminalObjects(deps, authority, claim.recording);
24516
+ return c.json(
24517
+ withRecoveryRetryHint(cleaned),
24518
+ cleaned.recording.state === "transcribing" ? 202 : 200
24519
+ );
24520
+ }
24521
+ segmentNumber = claim.segment.segmentNumber;
24522
+ const stored = await deps.objectStorage?.getObjectBytes(claim.segment.objectKey);
24523
+ if (!stored) {
24524
+ throw new RecordingProcessingError("Provider segment is missing", "invalid_audio", false);
24525
+ }
24526
+ if (stored.bytes.byteLength !== claim.segment.byteLength || sha256Hex2(stored.bytes) !== claim.segment.sha256) {
24527
+ throw new RecordingProcessingError(
24528
+ "Provider segment failed integrity verification",
24529
+ "invalid_audio",
24530
+ false
24531
+ );
24532
+ }
24533
+ const providerStartedAt = /* @__PURE__ */ new Date();
24534
+ const providerDeadlineAt = new Date(
24535
+ providerStartedAt.getTime() + TRANSCRIPTION_PROVIDER_REQUEST_TIMEOUT_MILLISECONDS
24536
+ );
24537
+ await startTranscriptionRecordingSegmentProviderCall(deps.db, {
24538
+ workspaceId: authority.workspaceId,
24539
+ subjectId: authority.subjectId,
24540
+ recordingId: uuidParam(c, "recordingId"),
24541
+ segmentNumber,
24542
+ attemptId,
24543
+ providerStartedAt,
24544
+ providerDeadlineAt
24545
+ });
24546
+ const result = await service.transcribe({
24547
+ workspaceId: authority.workspaceId,
24548
+ accountId: authority.accountId,
24549
+ audio: stored.bytes,
24550
+ mimeType: "audio/wav",
24551
+ durationSeconds: claim.segment.durationMilliseconds / 1e3,
24552
+ requestId: attemptId,
24553
+ providerDeadlineAt,
24554
+ ...claim.segment.providerId && claim.segment.providerId !== "host" ? { providerId: claim.segment.providerId } : {}
24555
+ });
24556
+ const completed = await completeTranscriptionRecordingSegment(deps.db, {
24557
+ workspaceId: authority.workspaceId,
24558
+ subjectId: authority.subjectId,
24559
+ recordingId: uuidParam(c, "recordingId"),
24560
+ segmentNumber,
24561
+ attemptId,
24562
+ text: result.text,
24563
+ languages: result.languages,
24564
+ providerId: claim.segment.providerId ?? result.providerId
24565
+ });
24566
+ return c.json(await cleanupTerminalObjects(deps, authority, completed));
24567
+ } catch (error) {
24568
+ if (authority && attemptId && segmentNumber !== null) {
24569
+ const failure = processingFailure(error);
24570
+ const persisted = await failTranscriptionRecordingSegment(deps.db, {
24571
+ workspaceId: authority.workspaceId,
24572
+ subjectId: authority.subjectId,
24573
+ recordingId: uuidParam(c, "recordingId"),
24574
+ segmentNumber,
24575
+ attemptId,
24576
+ errorCode: failure.code,
24577
+ retryable: failure.retryable
24578
+ }).catch(() => null);
24579
+ if (persisted) return c.json(persisted);
24580
+ }
24581
+ return routeError(c, error);
24582
+ }
24583
+ }
24584
+ );
24585
+ app.delete("/v1/workspaces/:workspaceId/transcription-recordings/:recordingId", async (c) => {
24586
+ try {
24587
+ const authority = await requireRecordingAuthority(c, deps, false);
24588
+ const discarded = await discardTranscriptionRecording(deps.db, {
24589
+ workspaceId: authority.workspaceId,
24590
+ subjectId: authority.subjectId,
24591
+ recordingId: uuidParam(c, "recordingId")
24592
+ });
24593
+ return c.json(await cleanupTerminalObjects(deps, authority, discarded));
24594
+ } catch (error) {
24595
+ return routeError(c, error);
24596
+ }
24597
+ });
24598
+ }
24599
+ function withRecoveryRetryHint(response) {
24600
+ if (response.recording.state === "segmenting" || response.recording.state === "transcribing") {
24601
+ return {
24602
+ ...response,
24603
+ retryAfterMilliseconds: TRANSCRIPTION_RECORDING_RECOVERY_RETRY_AFTER_MILLISECONDS
24604
+ };
24605
+ }
24606
+ return response;
24607
+ }
24608
+ async function requireRecordingAuthority(c, deps, requirePolicy) {
24609
+ const workspaceId = c.req.param("workspaceId");
24610
+ if (!workspaceId) {
24611
+ throw new TranscriptionRecordingNotFoundError("Workspace not found");
24612
+ }
24613
+ const grant = await requireAccessGrant22(c, deps, workspaceId, "sessions:create");
24614
+ if (requirePolicy) {
24615
+ const workspace = await getWorkspace3(deps.db, workspaceId);
24616
+ if (!workspace) throw new TranscriptionRecordingNotFoundError("Workspace not found");
24617
+ if (resolveWorkspaceVoiceInputEnabled(workspace.settings) === false) {
24618
+ throw new RecordingProcessingError("Voice input is disabled", "policy_blocked", false);
24619
+ }
24620
+ }
24621
+ return {
24622
+ accountId: grant.accountId,
24623
+ workspaceId,
24624
+ subjectId: grant.subjectId
24625
+ };
24626
+ }
24627
+ async function resumableAvailable(deps, workspaceId) {
24628
+ return Boolean(
24629
+ deps.settings.voiceInputResumableEnabled && deps.objectStorage && deps.transcription && deps.transcriptionSegmenter && await deps.transcription.available({ workspaceId }) && await deps.transcriptionSegmenter.available()
24630
+ );
24631
+ }
24632
+ async function cleanupTerminalObjects(deps, authority, response) {
24633
+ if (!deps.objectStorage || response.recording.objectsCleaned || response.recording.state !== "complete" && response.recording.state !== "discarded" && !(response.recording.state === "failed" && !response.recording.retryable)) {
24634
+ return response;
24635
+ }
24636
+ try {
24637
+ const keys = await transcriptionRecordingObjectKeys(deps.db, {
24638
+ workspaceId: authority.workspaceId,
24639
+ subjectId: authority.subjectId,
24640
+ recordingId: response.recording.id
24641
+ });
24642
+ let current = response;
24643
+ for (const key of keys) {
24644
+ await deps.objectStorage.deleteObject(key);
24645
+ current = await markTranscriptionRecordingObjectCleaned(deps.db, {
24646
+ workspaceId: authority.workspaceId,
24647
+ subjectId: authority.subjectId,
24648
+ recordingId: response.recording.id,
24649
+ objectKey: key
24650
+ });
24651
+ }
24652
+ if (current.recording.objectsCleaned) return current;
24653
+ return await markTranscriptionRecordingObjectsCleaned(deps.db, {
24654
+ workspaceId: authority.workspaceId,
24655
+ subjectId: authority.subjectId,
24656
+ recordingId: response.recording.id
24657
+ });
24658
+ } catch {
24659
+ return response;
24660
+ }
24661
+ }
24662
+ async function* verifiedChunkBytes(deps, chunks, signal) {
24663
+ for (const chunk of chunks) {
24664
+ if (signal.aborted) {
24665
+ throw new RecordingProcessingError("Audio assembly was cancelled", "cancelled", true);
24666
+ }
24667
+ let stored;
24668
+ try {
24669
+ stored = await deps.objectStorage.getObjectBytes(chunk.objectKey);
24670
+ } catch {
24671
+ throw new RecordingProcessingError("Chunk download failed", "network", true);
24672
+ }
24673
+ if (!stored) {
24674
+ throw new RecordingProcessingError("Chunk is missing", "invalid_audio", false);
24675
+ }
24676
+ if (stored.bytes.byteLength !== chunk.byteLength || sha256Hex2(stored.bytes) !== chunk.sha256) {
24677
+ throw new RecordingProcessingError(
24678
+ "Chunk integrity verification failed",
24679
+ "invalid_audio",
24680
+ false
24681
+ );
24682
+ }
24683
+ yield stored.bytes;
24684
+ }
24685
+ }
24686
+ function processingFailure(error) {
24687
+ if (error instanceof RecordingProcessingError) {
24688
+ return { code: error.code, retryable: error.retryable };
24689
+ }
24690
+ if (error instanceof TranscriptionSegmenterError) {
24691
+ return { code: error.code, retryable: error.retryable };
24692
+ }
24693
+ if (error instanceof TranscriptionServiceError) {
24694
+ return {
24695
+ code: error.code,
24696
+ retryable: error.retryable || error.code === "cancelled" || error.code === "network" || error.code === "timeout" || error.code === "unavailable" || error.code === "provider"
24697
+ };
24698
+ }
24699
+ if (error instanceof DOMException && error.name === "AbortError") {
24700
+ return { code: "cancelled", retryable: true };
24701
+ }
24702
+ return { code: "unknown", retryable: true };
24703
+ }
24704
+ function routeError(c, error) {
24705
+ if (error instanceof TranscriptionRecordingNotFoundError) {
24706
+ return c.json({ code: "not_found" }, 404);
24707
+ }
24708
+ if (error instanceof TranscriptionRecordingConflictError) {
24709
+ return c.json({ code: "conflict" }, 409);
24710
+ }
24711
+ if (error instanceof TranscriptionRecordingStateError) {
24712
+ return c.json({ code: "invalid_state" }, 409);
24713
+ }
24714
+ if (error instanceof RecordingProcessingError) {
24715
+ const status = error.code === "policy_blocked" ? 403 : error.code === "not_supported" ? 415 : error.code === "too_large" ? 413 : error.code === "invalid_audio" ? 400 : error.code === "unavailable" ? 503 : 502;
24716
+ return c.json({ code: error.code }, status);
24717
+ }
24718
+ return c.json({ code: "unknown" }, 500);
24719
+ }
24720
+ async function jsonBody(c) {
24721
+ try {
24722
+ return await c.req.json();
24723
+ } catch {
24724
+ return null;
24725
+ }
24726
+ }
24727
+ function correlationId(c) {
24728
+ const value = c.req.header("x-opengeni-correlation-id")?.trim();
24729
+ return value && /^[A-Za-z0-9._:-]{1,128}$/.test(value) ? valueAsUuid(value) : crypto.randomUUID();
24730
+ }
24731
+ function valueAsUuid(value) {
24732
+ if (/^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test(value)) {
24733
+ return value;
24734
+ }
24735
+ const hex = createHash11("sha256").update(value).digest("hex").slice(0, 32);
24736
+ return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-4${hex.slice(13, 16)}-a${hex.slice(17, 20)}-${hex.slice(20, 32)}`;
24737
+ }
24738
+ function nonnegativeInteger(value) {
24739
+ if (!/^(0|[1-9][0-9]*)$/.test(value)) {
24740
+ throw new RecordingProcessingError("Invalid integer", "invalid_audio", false);
24741
+ }
24742
+ const parsed = Number(value);
24743
+ if (!Number.isSafeInteger(parsed)) {
24744
+ throw new RecordingProcessingError("Invalid integer", "invalid_audio", false);
24745
+ }
24746
+ return parsed;
24747
+ }
24748
+ function uuidParam(c, name) {
24749
+ const value = c.req.param(name);
24750
+ if (!value || !/^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test(value)) {
24751
+ throw new TranscriptionRecordingNotFoundError("Recording not found");
24752
+ }
24753
+ return value;
24754
+ }
24755
+ function headerInteger(c, name, allowZero) {
24756
+ const raw = c.req.header(name) ?? "";
24757
+ const value = nonnegativeInteger(raw);
24758
+ if (!allowZero && value === 0) {
24759
+ throw new RecordingProcessingError("Invalid integer", "invalid_audio", false);
24760
+ }
24761
+ return value;
24762
+ }
24763
+ async function readBoundedBody(request, maxBytes) {
24764
+ const contentLength = Number(request.headers.get("content-length"));
24765
+ if (Number.isFinite(contentLength) && contentLength > maxBytes) {
24766
+ throw new RecordingProcessingError("Chunk is too large", "too_large", false);
24767
+ }
24768
+ if (!request.body) {
24769
+ throw new RecordingProcessingError("Chunk is required", "invalid_audio", false);
24770
+ }
24771
+ const reader = request.body.getReader();
24772
+ const chunks = [];
24773
+ let total = 0;
24774
+ try {
24775
+ for (; ; ) {
24776
+ if (request.signal.aborted) {
24777
+ throw new RecordingProcessingError("Chunk upload was cancelled", "cancelled", true);
24778
+ }
24779
+ const next = await reader.read();
24780
+ if (next.done) break;
24781
+ total += next.value.byteLength;
24782
+ if (total > maxBytes) {
24783
+ await reader.cancel();
24784
+ throw new RecordingProcessingError("Chunk is too large", "too_large", false);
24785
+ }
24786
+ chunks.push(next.value);
24787
+ }
24788
+ } finally {
24789
+ reader.releaseLock();
24790
+ }
24791
+ if (total === 0) {
24792
+ throw new RecordingProcessingError("Chunk is required", "invalid_audio", false);
24793
+ }
24794
+ const body2 = new Uint8Array(total);
24795
+ let offset = 0;
24796
+ for (const chunk of chunks) {
24797
+ body2.set(chunk, offset);
24798
+ offset += chunk.byteLength;
24799
+ }
24800
+ return body2;
24801
+ }
24802
+ function sha256Hex2(bytes) {
24803
+ return createHash11("sha256").update(bytes).digest("hex");
24804
+ }
24805
+
24806
+ // src/routes/transcriptions.ts
23953
24807
  function registerTranscriptionRoutes(app, deps) {
24808
+ registerResumableTranscriptionRoutes(app, deps);
23954
24809
  app.post("/v1/workspaces/:workspaceId/transcriptions", async (c) => {
23955
24810
  const workspaceId = c.req.param("workspaceId");
23956
- const grant = await requireAccessGrant22(c, deps, workspaceId, "sessions:create");
23957
- const workspace = await getWorkspace3(deps.db, workspaceId);
24811
+ const grant = await requireAccessGrant23(c, deps, workspaceId, "sessions:create");
24812
+ const workspace = await getWorkspace4(deps.db, workspaceId);
23958
24813
  if (!workspace) return c.json({ code: "not_found" }, 404);
23959
- if (resolveWorkspaceVoiceInputEnabled(workspace.settings) === false) {
24814
+ if (resolveWorkspaceVoiceInputEnabled2(workspace.settings) === false) {
23960
24815
  return c.json({ code: "policy_blocked" }, 403);
23961
24816
  }
23962
24817
  const service = deps.transcription;
@@ -23980,7 +24835,7 @@ function registerTranscriptionRoutes(app, deps) {
23980
24835
  };
23981
24836
  return c.json(response);
23982
24837
  } catch (error) {
23983
- if (error instanceof TranscriptionServiceError) {
24838
+ if (error instanceof TranscriptionServiceError2) {
23984
24839
  return c.json({ code: error.code }, error.status);
23985
24840
  }
23986
24841
  if (isAbort(error)) return c.json({ code: "cancelled" }, 499);
@@ -23991,7 +24846,7 @@ function registerTranscriptionRoutes(app, deps) {
23991
24846
  async function audioRequest(request, maxSizeBytes) {
23992
24847
  const contentLength = Number(request.headers.get("content-length"));
23993
24848
  if (Number.isFinite(contentLength) && contentLength > maxSizeBytes + 64 * 1024) {
23994
- throw new TranscriptionServiceError({
24849
+ throw new TranscriptionServiceError2({
23995
24850
  code: "too_large",
23996
24851
  message: "Audio is too large."
23997
24852
  });
@@ -24001,13 +24856,13 @@ async function audioRequest(request, maxSizeBytes) {
24001
24856
  const form = await request.formData();
24002
24857
  const audio = form.get("audio");
24003
24858
  if (!(audio instanceof File)) {
24004
- throw new TranscriptionServiceError({
24859
+ throw new TranscriptionServiceError2({
24005
24860
  code: "invalid_audio",
24006
24861
  message: "Audio file is required."
24007
24862
  });
24008
24863
  }
24009
24864
  if (audio.size > maxSizeBytes) {
24010
- throw new TranscriptionServiceError({
24865
+ throw new TranscriptionServiceError2({
24011
24866
  code: "too_large",
24012
24867
  message: "Audio is too large."
24013
24868
  });
@@ -24021,7 +24876,7 @@ async function audioRequest(request, maxSizeBytes) {
24021
24876
  }
24022
24877
  const mimeType = contentType.split(";", 1)[0]?.trim() ?? "";
24023
24878
  if (!mimeType.toLowerCase().startsWith("audio/")) {
24024
- throw new TranscriptionServiceError({
24879
+ throw new TranscriptionServiceError2({
24025
24880
  code: "not_supported",
24026
24881
  message: "Unsupported audio format."
24027
24882
  });
@@ -24035,7 +24890,7 @@ async function audioRequest(request, maxSizeBytes) {
24035
24890
  }
24036
24891
  async function readBounded(body2, maxSizeBytes, signal) {
24037
24892
  if (!body2)
24038
- throw new TranscriptionServiceError({
24893
+ throw new TranscriptionServiceError2({
24039
24894
  code: "invalid_audio",
24040
24895
  message: "Audio is required."
24041
24896
  });
@@ -24050,7 +24905,7 @@ async function readBounded(body2, maxSizeBytes, signal) {
24050
24905
  size += next.value.byteLength;
24051
24906
  if (size > maxSizeBytes) {
24052
24907
  await reader.cancel();
24053
- throw new TranscriptionServiceError({
24908
+ throw new TranscriptionServiceError2({
24054
24909
  code: "too_large",
24055
24910
  message: "Audio is too large."
24056
24911
  });
@@ -24072,7 +24927,7 @@ function durationValue(value) {
24072
24927
  if (value == null || value.trim() === "") return void 0;
24073
24928
  const duration = Number(value);
24074
24929
  if (!Number.isFinite(duration)) {
24075
- throw new TranscriptionServiceError({
24930
+ throw new TranscriptionServiceError2({
24076
24931
  code: "invalid_audio",
24077
24932
  message: "Invalid audio duration."
24078
24933
  });
@@ -24091,19 +24946,21 @@ import { resolveVoiceInputProviderRegistry } from "@opengeni/config";
24091
24946
  import { VOICE_INPUT_ACCEPTED_MIME_TYPES } from "@opengeni/contracts";
24092
24947
  import {
24093
24948
  filenameForMimeType,
24094
- isAcceptedMimeType,
24095
- normalizeMimeType,
24096
- TranscriptionServiceError as TranscriptionServiceError4
24949
+ isAcceptedMimeType as isAcceptedMimeType2,
24950
+ normalizeMimeType as normalizeMimeType2,
24951
+ TRANSCRIPTION_PROVIDER_REQUEST_TIMEOUT_MILLISECONDS as TRANSCRIPTION_PROVIDER_REQUEST_TIMEOUT_MILLISECONDS2,
24952
+ TranscriptionServiceError as TranscriptionServiceError5
24097
24953
  } from "@opengeni/core";
24098
24954
 
24099
24955
  // src/transcription/providers/openai.ts
24100
- import { TranscriptionServiceError as TranscriptionServiceError2 } from "@opengeni/core";
24956
+ import { TranscriptionServiceError as TranscriptionServiceError3 } from "@opengeni/core";
24101
24957
  function createOpenAiTranscriptionProvider(input) {
24102
24958
  const fetchImpl = input.fetch ?? fetch;
24103
24959
  return {
24104
24960
  id: "openai",
24961
+ supportsServerDeadline: true,
24105
24962
  available: () => true,
24106
- async transcribe({ audio, mimeType, filename, signal }) {
24963
+ async transcribe({ audio, mimeType, filename, requestId, signal }) {
24107
24964
  const form = new FormData();
24108
24965
  form.append("file", audioBlob(audio, mimeType), filename);
24109
24966
  form.append("model", input.model);
@@ -24111,7 +24968,11 @@ function createOpenAiTranscriptionProvider(input) {
24111
24968
  try {
24112
24969
  response = await fetchImpl(`${input.baseUrl}/audio/transcriptions`, {
24113
24970
  method: "POST",
24114
- headers: { Authorization: `Bearer ${input.apiKey}` },
24971
+ headers: {
24972
+ Authorization: `Bearer ${input.apiKey}`,
24973
+ // Observability only; the upstream API is not treated as idempotent.
24974
+ "x-opengeni-request-id": requestId
24975
+ },
24115
24976
  body: form,
24116
24977
  ...signal ? { signal } : {}
24117
24978
  });
@@ -24121,7 +24982,7 @@ function createOpenAiTranscriptionProvider(input) {
24121
24982
  if (!response.ok) throw responseError(response.status);
24122
24983
  const body2 = await response.json().catch(() => null);
24123
24984
  if (!body2 || typeof body2.text !== "string") {
24124
- throw new TranscriptionServiceError2({
24985
+ throw new TranscriptionServiceError3({
24125
24986
  code: "provider",
24126
24987
  message: "Invalid transcription response."
24127
24988
  });
@@ -24138,31 +24999,31 @@ function audioBlob(audio, mimeType) {
24138
24999
  }
24139
25000
  function responseError(status) {
24140
25001
  if (status === 401 || status === 403) {
24141
- return new TranscriptionServiceError2({
25002
+ return new TranscriptionServiceError3({
24142
25003
  code: "unavailable",
24143
25004
  message: "Transcription is unavailable."
24144
25005
  });
24145
25006
  }
24146
25007
  if (status === 413) {
24147
- return new TranscriptionServiceError2({
25008
+ return new TranscriptionServiceError3({
24148
25009
  code: "too_large",
24149
25010
  message: "Audio is too large."
24150
25011
  });
24151
25012
  }
24152
25013
  if (status === 400 || status === 422) {
24153
- return new TranscriptionServiceError2({
25014
+ return new TranscriptionServiceError3({
24154
25015
  code: "invalid_audio",
24155
25016
  message: "Audio could not be transcribed."
24156
25017
  });
24157
25018
  }
24158
25019
  if (status === 408 || status === 504) {
24159
- return new TranscriptionServiceError2({
25020
+ return new TranscriptionServiceError3({
24160
25021
  code: "timeout",
24161
25022
  message: "Transcription timed out.",
24162
25023
  retryable: true
24163
25024
  });
24164
25025
  }
24165
- return new TranscriptionServiceError2({
25026
+ return new TranscriptionServiceError3({
24166
25027
  code: status >= 500 ? "unavailable" : "provider",
24167
25028
  message: "Transcription provider failed.",
24168
25029
  retryable: status >= 500
@@ -24170,12 +25031,12 @@ function responseError(status) {
24170
25031
  }
24171
25032
  function fetchError(error) {
24172
25033
  if (error instanceof DOMException && error.name === "AbortError") {
24173
- return new TranscriptionServiceError2({
25034
+ return new TranscriptionServiceError3({
24174
25035
  code: "cancelled",
24175
25036
  message: "Transcription was cancelled."
24176
25037
  });
24177
25038
  }
24178
- return new TranscriptionServiceError2({
25039
+ return new TranscriptionServiceError3({
24179
25040
  code: "network",
24180
25041
  message: "Transcription provider is unreachable.",
24181
25042
  retryable: true
@@ -24188,11 +25049,12 @@ function createAzureOpenAiTranscriptionProvider(input) {
24188
25049
  const url = `${input.endpoint}/openai/deployments/${encodeURIComponent(input.deployment)}/audio/transcriptions?api-version=${encodeURIComponent(input.apiVersion)}`;
24189
25050
  return {
24190
25051
  id: "azure-openai",
25052
+ supportsServerDeadline: true,
24191
25053
  available: () => Boolean(input.apiKey || input.adToken),
24192
- async transcribe({ audio, mimeType, filename, signal }) {
25054
+ async transcribe({ audio, mimeType, filename, requestId, signal }) {
24193
25055
  const form = new FormData();
24194
25056
  form.append("file", new Blob([Uint8Array.from(audio).buffer], { type: mimeType }), filename);
24195
- const headers = input.apiKey ? { "api-key": input.apiKey } : { Authorization: `Bearer ${input.adToken}` };
25057
+ const headers = input.apiKey ? { "api-key": input.apiKey, "x-opengeni-request-id": requestId } : { Authorization: `Bearer ${input.adToken}`, "x-opengeni-request-id": requestId };
24196
25058
  let response;
24197
25059
  try {
24198
25060
  response = await fetchImpl(url, {
@@ -24220,7 +25082,7 @@ function createAzureOpenAiTranscriptionProvider(input) {
24220
25082
  // src/transcription/providers/codex-subscription.ts
24221
25083
  import { CODEX_CLIENT_VERSION as CODEX_CLIENT_VERSION3, CODEX_ORIGINATOR } from "@opengeni/codex/constants";
24222
25084
  import {
24223
- TranscriptionServiceError as TranscriptionServiceError3
25085
+ TranscriptionServiceError as TranscriptionServiceError4
24224
25086
  } from "@opengeni/core";
24225
25087
  import { buildCodexTokenResolver as buildCodexTokenResolver3, listCodexAccountStatuses as listCodexAccountStatuses3 } from "@opengeni/db";
24226
25088
  var TRANSCRIBE_URL = "https://chatgpt.com/backend-api/transcribe";
@@ -24238,14 +25100,15 @@ function createCodexSubscriptionTranscriptionProvider(input) {
24238
25100
  });
24239
25101
  return {
24240
25102
  id: "codex-subscription",
25103
+ supportsServerDeadline: true,
24241
25104
  experimental: true,
24242
25105
  available: probe,
24243
- async transcribe({ audio, mimeType, filename, workspaceId, signal }) {
25106
+ async transcribe({ audio, mimeType, filename, workspaceId, requestId, signal }) {
24244
25107
  const account = (await listCodexAccountStatuses3(input.db, workspaceId)).find(
24245
25108
  (candidate) => candidate.isActive && candidate.status === "active"
24246
25109
  );
24247
25110
  if (!account) {
24248
- throw new TranscriptionServiceError3({
25111
+ throw new TranscriptionServiceError4({
24249
25112
  code: "unavailable",
24250
25113
  message: "Transcription is unavailable."
24251
25114
  });
@@ -24255,7 +25118,7 @@ function createCodexSubscriptionTranscriptionProvider(input) {
24255
25118
  try {
24256
25119
  token = await resolver.getToken();
24257
25120
  } catch {
24258
- throw new TranscriptionServiceError3({
25121
+ throw new TranscriptionServiceError4({
24259
25122
  code: "unavailable",
24260
25123
  message: "Transcription is unavailable."
24261
25124
  });
@@ -24274,7 +25137,9 @@ function createCodexSubscriptionTranscriptionProvider(input) {
24274
25137
  ...accountId ? { "ChatGPT-Account-ID": accountId } : {},
24275
25138
  originator: CODEX_ORIGINATOR,
24276
25139
  "User-Agent": `${CODEX_ORIGINATOR}/${CODEX_CLIENT_VERSION3}`,
24277
- version: CODEX_CLIENT_VERSION3
25140
+ version: CODEX_CLIENT_VERSION3,
25141
+ // Observability only; the upstream API is not treated as idempotent.
25142
+ "x-opengeni-request-id": requestId
24278
25143
  },
24279
25144
  body: form,
24280
25145
  ...signal ? { signal } : {}
@@ -24333,6 +25198,8 @@ function createTranscriptionService(input) {
24333
25198
  maxSizeBytes: input.settings.voiceInputMaxSizeBytes,
24334
25199
  acceptedMimeTypes: [...VOICE_INPUT_ACCEPTED_MIME_TYPES]
24335
25200
  };
25201
+ const providerRequestTimeoutMilliseconds = input.providerRequestTimeoutMilliseconds ?? TRANSCRIPTION_PROVIDER_REQUEST_TIMEOUT_MILLISECONDS2;
25202
+ const now = input.now ?? (() => /* @__PURE__ */ new Date());
24336
25203
  return {
24337
25204
  limits: () => limits,
24338
25205
  async available(context) {
@@ -24340,43 +25207,81 @@ function createTranscriptionService(input) {
24340
25207
  Boolean
24341
25208
  );
24342
25209
  },
25210
+ async selectProvider(context) {
25211
+ return (await firstAvailable(providers, context))?.id ?? null;
25212
+ },
24343
25213
  async transcribe(request) {
24344
- const mimeType = normalizeMimeType(request.mimeType);
24345
- if (!isAcceptedMimeType(mimeType, limits.acceptedMimeTypes)) {
24346
- throw new TranscriptionServiceError4({
25214
+ const mimeType = normalizeMimeType2(request.mimeType);
25215
+ if (!isAcceptedMimeType2(mimeType, limits.acceptedMimeTypes)) {
25216
+ throw new TranscriptionServiceError5({
24347
25217
  code: "not_supported",
24348
25218
  message: "Unsupported audio format."
24349
25219
  });
24350
25220
  }
24351
25221
  if (request.audio.byteLength > limits.maxSizeBytes) {
24352
- throw new TranscriptionServiceError4({
25222
+ throw new TranscriptionServiceError5({
24353
25223
  code: "too_large",
24354
25224
  message: "Audio is too large."
24355
25225
  });
24356
25226
  }
24357
25227
  if (request.durationSeconds !== void 0 && (!Number.isFinite(request.durationSeconds) || request.durationSeconds < 0 || request.durationSeconds > limits.maxDurationSeconds)) {
24358
- throw new TranscriptionServiceError4({
25228
+ throw new TranscriptionServiceError5({
24359
25229
  code: "invalid_audio",
24360
25230
  message: "Invalid audio duration."
24361
25231
  });
24362
25232
  }
24363
- const provider = await firstAvailable(providers, {
24364
- workspaceId: request.workspaceId
24365
- });
25233
+ const provider = request.providerId ? await exactAvailable(providers, request.providerId, { workspaceId: request.workspaceId }) : await firstAvailable(providers, { workspaceId: request.workspaceId });
24366
25234
  if (!provider) {
24367
- throw new TranscriptionServiceError4({
25235
+ throw new TranscriptionServiceError5({
24368
25236
  code: "unavailable",
24369
25237
  message: "Transcription is unavailable."
24370
25238
  });
24371
25239
  }
25240
+ if (provider.supportsServerDeadline !== true) {
25241
+ throw new TranscriptionServiceError5({
25242
+ code: "unavailable",
25243
+ message: "Transcription provider does not support bounded requests."
25244
+ });
25245
+ }
24372
25246
  const startedAt = performance.now();
24373
- const result = await provider.transcribe({
24374
- audio: request.audio,
24375
- mimeType,
24376
- filename: filenameForMimeType(mimeType),
24377
- workspaceId: request.workspaceId,
24378
- signal: request.signal
24379
- });
25247
+ const remainingMilliseconds = request.providerDeadlineAt ? remainingTranscriptionProviderRequestMilliseconds(request.providerDeadlineAt, now()) : providerRequestTimeoutMilliseconds;
25248
+ if (request.providerDeadlineAt && (!Number.isFinite(remainingMilliseconds) || remainingMilliseconds <= 0)) {
25249
+ throw new TranscriptionServiceError5({
25250
+ code: "timeout",
25251
+ message: "Transcription provider deadline expired.",
25252
+ retryable: true
25253
+ });
25254
+ }
25255
+ const deadline = createProviderRequestDeadline(request.signal, remainingMilliseconds);
25256
+ let result;
25257
+ try {
25258
+ result = await provider.transcribe({
25259
+ audio: request.audio,
25260
+ mimeType,
25261
+ filename: filenameForMimeType(mimeType),
25262
+ workspaceId: request.workspaceId,
25263
+ requestId: request.requestId,
25264
+ signal: deadline.signal
25265
+ });
25266
+ if (deadline.timedOut && !request.signal?.aborted) {
25267
+ throw new TranscriptionServiceError5({
25268
+ code: "timeout",
25269
+ message: "Transcription provider timed out.",
25270
+ retryable: true
25271
+ });
25272
+ }
25273
+ } catch (error) {
25274
+ if (deadline.timedOut && !request.signal?.aborted) {
25275
+ throw new TranscriptionServiceError5({
25276
+ code: "timeout",
25277
+ message: "Transcription provider timed out.",
25278
+ retryable: true
25279
+ });
25280
+ }
25281
+ throw error;
25282
+ } finally {
25283
+ deadline.dispose();
25284
+ }
24380
25285
  return {
24381
25286
  ...result,
24382
25287
  providerId: provider.id,
@@ -24386,15 +25291,50 @@ function createTranscriptionService(input) {
24386
25291
  }
24387
25292
  };
24388
25293
  }
25294
+ function remainingTranscriptionProviderRequestMilliseconds(providerDeadlineAt, now) {
25295
+ return providerDeadlineAt.getTime() - now.getTime();
25296
+ }
25297
+ function createProviderRequestDeadline(parentSignal, timeoutMilliseconds) {
25298
+ const controller = new AbortController();
25299
+ let timedOut = false;
25300
+ const timeout = setTimeout(
25301
+ () => {
25302
+ timedOut = true;
25303
+ controller.abort(new DOMException("Transcription provider timed out", "TimeoutError"));
25304
+ },
25305
+ Math.max(1, Math.ceil(timeoutMilliseconds))
25306
+ );
25307
+ const abortFromParent = () => {
25308
+ controller.abort(parentSignal?.reason);
25309
+ };
25310
+ if (parentSignal) {
25311
+ if (parentSignal.aborted) abortFromParent();
25312
+ else parentSignal.addEventListener("abort", abortFromParent, { once: true });
25313
+ }
25314
+ return {
25315
+ signal: controller.signal,
25316
+ get timedOut() {
25317
+ return timedOut;
25318
+ },
25319
+ dispose: () => {
25320
+ clearTimeout(timeout);
25321
+ parentSignal?.removeEventListener("abort", abortFromParent);
25322
+ }
25323
+ };
25324
+ }
24389
25325
  async function firstAvailable(providers, context) {
24390
25326
  for (const provider of providers) {
24391
25327
  if (await provider.available(context)) return provider;
24392
25328
  }
24393
25329
  return null;
24394
25330
  }
25331
+ async function exactAvailable(providers, providerId, context) {
25332
+ const provider = providers.find((candidate) => candidate.id === providerId);
25333
+ return provider && await provider.available(context) ? provider : null;
25334
+ }
24395
25335
 
24396
25336
  // src/integrations/slack-interactions.ts
24397
- import { createHash as createHash11, createHmac as createHmac2, timingSafeEqual as timingSafeEqual2 } from "crypto";
25337
+ import { createHash as createHash12, createHmac as createHmac2, timingSafeEqual as timingSafeEqual2 } from "crypto";
24398
25338
  import {
24399
25339
  DEFAULT_FIRST_PARTY_MCP_TOOLS as DEFAULT_FIRST_PARTY_MCP_TOOLS2,
24400
25340
  hasOpenGeniSlackReactionScope,
@@ -24420,7 +25360,7 @@ import {
24420
25360
  getSlackInteractionByClientEventId,
24421
25361
  getSlackInteractionByRoute,
24422
25362
  getSessionEventByClientEventId,
24423
- getWorkspace as getWorkspace4,
25363
+ getWorkspace as getWorkspace5,
24424
25364
  getWorkspaceGrant as getWorkspaceGrant6,
24425
25365
  listSessionEventPage as listSessionEventPage3,
24426
25366
  listSessionHumanInputRequests as listSessionHumanInputRequests2,
@@ -24438,7 +25378,7 @@ import {
24438
25378
  controlHumanSessionWorkstream as controlHumanSessionWorkstream3,
24439
25379
  createSessionForRequest as createSessionForRequest3,
24440
25380
  hasPermission as hasPermission14,
24441
- requireAccessGrant as requireAccessGrant23
25381
+ requireAccessGrant as requireAccessGrant24
24442
25382
  } from "@opengeni/core";
24443
25383
  import { publishDurableSessionEvents as publishDurableSessionEvents3 } from "@opengeni/events";
24444
25384
  import { HTTPException as HTTPException33 } from "hono/http-exception";
@@ -24542,7 +25482,7 @@ function slackReactionInboxEntry(payload, bot, settings) {
24542
25482
  if (!settings.enabled || !event || event.type !== "reaction_added" || item?.type !== "message" || !teamId || !eventId || !userId || userId === bot.botUserId || !reaction || reaction !== settings.emoji || !channelId || !workspaceSlackReactionChannelAllowed(settings, channelId) || !timestamp) {
24543
25483
  return null;
24544
25484
  }
24545
- const stableReactionIdentity = createHash11("sha256").update([teamId, userId, channelId, timestamp, reaction].join("\n")).digest("hex");
25485
+ const stableReactionIdentity = createHash12("sha256").update([teamId, userId, channelId, timestamp, reaction].join("\n")).digest("hex");
24546
25486
  return {
24547
25487
  providerEventId: eventId,
24548
25488
  providerMessageId: `reaction:${stableReactionIdentity}`,
@@ -24576,7 +25516,7 @@ function registerSlackInteractionRoutes(app, deps) {
24576
25516
  const event = record3(payload.event);
24577
25517
  if (event?.type === "reaction_added") {
24578
25518
  const [workspace, connection] = await Promise.all([
24579
- getWorkspace4(deps.db, installation.workspaceId),
25519
+ getWorkspace5(deps.db, installation.workspaceId),
24580
25520
  getConnectionMetadata4(deps.db, installation.workspaceId, installation.connectionId, null)
24581
25521
  ]);
24582
25522
  if (!workspace || !connection || !hasOpenGeniSlackReactionScope(connection.grantedScopes)) {
@@ -24675,7 +25615,7 @@ function registerSlackInteractionRoutes(app, deps) {
24675
25615
  });
24676
25616
  app.post("/v1/workspaces/:workspaceId/integrations/slack/user-links", async (c) => {
24677
25617
  const workspaceId = c.req.param("workspaceId");
24678
- const grant = await requireAccessGrant23(c, deps, workspaceId, "sessions:create");
25618
+ const grant = await requireAccessGrant24(c, deps, workspaceId, "sessions:create");
24679
25619
  const body2 = record3(await c.req.json().catch(() => null));
24680
25620
  const linkToken = boundedString(body2?.linkToken, 2048);
24681
25621
  const signingSecret = deps.settings.slackSigningSecret;
@@ -24708,7 +25648,7 @@ function registerSlackInteractionRoutes(app, deps) {
24708
25648
  "/v1/workspaces/:workspaceId/integrations/slack/user-links/:slackUserId",
24709
25649
  async (c) => {
24710
25650
  const workspaceId = c.req.param("workspaceId");
24711
- await requireAccessGrant23(c, deps, workspaceId, "connections:write");
25651
+ await requireAccessGrant24(c, deps, workspaceId, "connections:write");
24712
25652
  const connectionId = boundedString(c.req.query("connectionId"), 64);
24713
25653
  if (!connectionId) throw new HTTPException33(400, { message: "connectionId is required" });
24714
25654
  return c.json({
@@ -24723,7 +25663,7 @@ function registerSlackInteractionRoutes(app, deps) {
24723
25663
  );
24724
25664
  app.get("/v1/workspaces/:workspaceId/integrations/slack/reaction-channels", async (c) => {
24725
25665
  const workspaceId = c.req.param("workspaceId");
24726
- const grant = await requireAccessGrant23(c, deps, workspaceId, "workspace:admin");
25666
+ const grant = await requireAccessGrant24(c, deps, workspaceId, "workspace:admin");
24727
25667
  const connectionId = boundedString(c.req.query("connectionId"), 64);
24728
25668
  if (!connectionId) throw new HTTPException33(400, { message: "connectionId is required" });
24729
25669
  const cursor = boundedString(c.req.query("cursor"), 1024);
@@ -24959,7 +25899,7 @@ async function processSlackInboxEntry(deps, entry) {
24959
25899
  }
24960
25900
  async function processSlackReactionInboxEntry(deps, entry) {
24961
25901
  const [workspace, connection, link] = await Promise.all([
24962
- getWorkspace4(deps.db, entry.workspaceId),
25902
+ getWorkspace5(deps.db, entry.workspaceId),
24963
25903
  getConnectionMetadata4(deps.db, entry.workspaceId, entry.connectionId, null),
24964
25904
  getSlackBotUserLink(deps.db, entry.workspaceId, entry.connectionId, entry.slackUserId)
24965
25905
  ]);
@@ -25543,7 +26483,7 @@ function slackRouteKey(channelId, threadTs) {
25543
26483
  return `${channelId}:${threadTs}`;
25544
26484
  }
25545
26485
  function deterministicUuid(value) {
25546
- const bytes = createHash11("sha256").update(value).digest().subarray(0, 16);
26486
+ const bytes = createHash12("sha256").update(value).digest().subarray(0, 16);
25547
26487
  bytes[6] = bytes[6] & 15 | 80;
25548
26488
  bytes[8] = bytes[8] & 63 | 128;
25549
26489
  const hex = bytes.toString("hex");
@@ -25662,7 +26602,11 @@ import {
25662
26602
  import { workflowIdForSession as workflowIdForSession2 } from "@opengeni/core";
25663
26603
  var API_MAX_REQUEST_BODY_BYTES = 8 * 1024 * 1024;
25664
26604
  function apiRequestBodyLimitBytes(settings) {
25665
- return Math.max(API_MAX_REQUEST_BODY_BYTES, settings.voiceInputMaxSizeBytes + 64 * 1024);
26605
+ return Math.max(
26606
+ API_MAX_REQUEST_BODY_BYTES,
26607
+ settings.voiceInputMaxSizeBytes + 64 * 1024,
26608
+ (settings.voiceInputResumableMaxChunkSizeBytes ?? 0) + 64 * 1024
26609
+ );
25666
26610
  }
25667
26611
  var API_PUBLIC_ERROR_MESSAGE_MAX_BYTES = 512;
25668
26612
  function createApp(deps) {
@@ -25732,6 +26676,9 @@ function createAppComposition(deps) {
25732
26676
  db: deps.db,
25733
26677
  ...deps.codexFetch ? { codexFetch: deps.codexFetch } : {}
25734
26678
  }) : deps.transcription;
26679
+ const transcriptionSegmenter = deps.transcriptionSegmenter === void 0 ? createFfmpegTranscriptionSegmenter({
26680
+ ffmpegPath: deps.settings.voiceInputFfmpegPath
26681
+ }) : deps.transcriptionSegmenter;
25735
26682
  const routeDeps = {
25736
26683
  ...deps,
25737
26684
  observability,
@@ -25741,15 +26688,16 @@ function createAppComposition(deps) {
25741
26688
  documentIndexer,
25742
26689
  getDocumentServices,
25743
26690
  transcription,
26691
+ transcriptionSegmenter,
25744
26692
  ...sandboxClient ? { sandboxClient } : {},
25745
26693
  resumeBoxById
25746
26694
  };
25747
26695
  const app = new Hono();
25748
26696
  const correlationIds = /* @__PURE__ */ new WeakMap();
25749
26697
  app.use("*", async (c, next) => {
25750
- const correlationId = boundedCorrelationId(c.req.header(OPENGENI_CORRELATION_HEADER)) ?? crypto.randomUUID();
25751
- correlationIds.set(c.req.raw, correlationId);
25752
- c.header(OPENGENI_CORRELATION_HEADER, correlationId);
26698
+ const correlationId2 = boundedCorrelationId(c.req.header(OPENGENI_CORRELATION_HEADER)) ?? crypto.randomUUID();
26699
+ correlationIds.set(c.req.raw, correlationId2);
26700
+ c.header(OPENGENI_CORRELATION_HEADER, correlationId2);
25753
26701
  await next();
25754
26702
  });
25755
26703
  const corsHeaders = {
@@ -25799,7 +26747,7 @@ function createAppComposition(deps) {
25799
26747
  app.use("*", async (c, next) => {
25800
26748
  const url = new URL(c.req.url);
25801
26749
  const route = routeLabel(url.pathname);
25802
- const correlationId = correlationIds.get(c.req.raw) ?? crypto.randomUUID();
26750
+ const correlationId2 = correlationIds.get(c.req.raw) ?? crypto.randomUUID();
25803
26751
  const start = performance.now();
25804
26752
  const span = observability.startSpan(`HTTP ${c.req.method} ${route}`, {
25805
26753
  "http.request.method": c.req.method,
@@ -25829,7 +26777,7 @@ function createAppComposition(deps) {
25829
26777
  durationMs: Math.round(durationSeconds * 1e3),
25830
26778
  traceId: span.traceId,
25831
26779
  spanId: span.spanId,
25832
- correlationId
26780
+ correlationId: correlationId2
25833
26781
  });
25834
26782
  } catch (error) {
25835
26783
  const status = httpStatusForError(error);
@@ -25860,7 +26808,7 @@ function createAppComposition(deps) {
25860
26808
  durationMs: Math.round(durationSeconds * 1e3),
25861
26809
  traceId: span.traceId,
25862
26810
  spanId: span.spanId,
25863
- correlationId,
26811
+ correlationId: correlationId2,
25864
26812
  errorCode,
25865
26813
  errorClass: error instanceof Error ? error.name : "NonErrorThrown"
25866
26814
  });
@@ -25939,7 +26887,18 @@ function createAppComposition(deps) {
25939
26887
  available: await transcription?.available() ?? false,
25940
26888
  maxDurationSeconds: deps.settings.voiceInputMaxDurationSeconds,
25941
26889
  maxSizeBytes: deps.settings.voiceInputMaxSizeBytes,
25942
- acceptedMimeTypes: [...VOICE_INPUT_ACCEPTED_MIME_TYPES2]
26890
+ acceptedMimeTypes: [...VOICE_INPUT_ACCEPTED_MIME_TYPES2],
26891
+ ...deps.settings.voiceInputResumableEnabled && objectStorage && transcription && transcriptionSegmenter && await transcription.available() && await transcriptionSegmenter.available() ? {
26892
+ resumable: {
26893
+ maxDurationSeconds: deps.settings.voiceInputResumableMaxDurationSeconds,
26894
+ maxSizeBytes: deps.settings.voiceInputResumableMaxSizeBytes,
26895
+ maxChunkSizeBytes: deps.settings.voiceInputResumableMaxChunkSizeBytes,
26896
+ providerSegmentSeconds: Math.min(
26897
+ TRANSCRIPTION_RECORDING_PROVIDER_SEGMENT_SECONDS2,
26898
+ deps.settings.voiceInputMaxDurationSeconds
26899
+ )
26900
+ }
26901
+ } : {}
25943
26902
  },
25944
26903
  productAccessMode: deps.settings.productAccessMode,
25945
26904
  auth: clientAuthConfig(deps.settings),
@@ -26005,7 +26964,7 @@ function createAppComposition(deps) {
26005
26964
  throw error;
26006
26965
  }
26007
26966
  }
26008
- const workspace = await getWorkspace5(routeDeps.db, workspaceId);
26967
+ const workspace = await getWorkspace6(routeDeps.db, workspaceId);
26009
26968
  const workspaceMemoryEnabled = resolveWorkspaceMemoryEnabled(workspace?.settings);
26010
26969
  const transport = new WebStandardStreamableHTTPServerTransport3({
26011
26970
  enableJsonResponse: true
@@ -26096,7 +27055,7 @@ function appendVary(current, value) {
26096
27055
  return values.join(", ");
26097
27056
  }
26098
27057
  async function requireMcpAccessGrant(c, deps, workspaceId) {
26099
- const grant = await requireAccessGrant24(c, deps, workspaceId);
27058
+ const grant = await requireAccessGrant25(c, deps, workspaceId);
26100
27059
  if (hasPermission15(grant.permissions, "workspace:read")) {
26101
27060
  return grant;
26102
27061
  }
@@ -26695,4 +27654,4 @@ export {
26695
27654
  withDefaultEnabledCapabilityMcpTools,
26696
27655
  workflowIdForSession2 as workflowIdForSession
26697
27656
  };
26698
- //# sourceMappingURL=chunk-R5PDSH2A.js.map
27657
+ //# sourceMappingURL=chunk-HWXJW5C7.js.map