@agent-native/core 0.173.1 → 0.173.2-nightly-20260825163448

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (33) hide show
  1. package/corpus/README.md +1 -1
  2. package/corpus/templates/clips/desktop/src/app.tsx +77 -7
  3. package/corpus/templates/clips/desktop/src/lib/recorder.ts +91 -25
  4. package/corpus/templates/clips/desktop/src/lib/upload-recovery.ts +20 -2
  5. package/corpus/templates/clips/desktop/src-tauri/Cargo.toml +7 -1
  6. package/corpus/templates/clips/desktop/src-tauri/src/lib.rs +1 -0
  7. package/corpus/templates/clips/desktop/src-tauri/src/native_screen.rs +372 -70
  8. package/corpus/templates/clips/server/lib/resumable-session.ts +15 -0
  9. package/corpus/templates/clips/server/routes/api/uploads/[recordingId]/chunk.post.ts +245 -118
  10. package/corpus/templates/clips/server/routes/api/uploads/[recordingId]/reset-chunks.post.ts +21 -11
  11. package/corpus/templates/clips/server/routes/api/uploads/[recordingId]/resume.get.ts +132 -25
  12. package/corpus/templates/content/actions/_database-property-input.ts +211 -0
  13. package/corpus/templates/content/actions/_database-row-mutation.ts +151 -26
  14. package/corpus/templates/content/actions/add-database-item.ts +15 -5
  15. package/corpus/templates/content/actions/update-database-item.ts +15 -7
  16. package/corpus/templates/content/actions/upsert-database-item-by-key.ts +15 -5
  17. package/corpus/templates/content/package.json +1 -0
  18. package/corpus/templates/content/parity/README.md +11 -2
  19. package/corpus/templates/content/parity/eval-scenarios.ts +52 -0
  20. package/corpus/templates/content/parity/run-database-create-property-preservation.ts +140 -0
  21. package/corpus/templates/content/parity/scenario-to-eval.ts +390 -0
  22. package/dist/agent/run-store.js +0 -1
  23. package/dist/client/agent-engine-key.js +26 -4
  24. package/dist/client/settings/ProviderSetupForm.js +1 -1
  25. package/dist/collab/struct-routes.d.ts +1 -1
  26. package/dist/eval/agent-runner.js +23 -0
  27. package/dist/eval/types.d.ts +11 -0
  28. package/dist/observability/routes.d.ts +1 -1
  29. package/dist/resources/handlers.d.ts +1 -1
  30. package/dist/server/agent-engine-api-key-route.d.ts +7 -0
  31. package/dist/server/agent-engine-api-key-route.js +38 -0
  32. package/dist/server/realtime-token.d.ts +1 -1
  33. package/package.json +1 -1
@@ -17,7 +17,6 @@ import {
17
17
  readAppState,
18
18
  writeAppState,
19
19
  } from "@agent-native/core/application-state";
20
- import { isFeatureFlagEnabled } from "@agent-native/core/feature-flags";
21
20
  import { runWithRequestContext } from "@agent-native/core/server";
22
21
  import { track } from "@agent-native/core/tracking";
23
22
  import { normalizeChunkUploadNumber } from "@shared/recording-core.js";
@@ -35,7 +34,6 @@ import {
35
34
  } from "h3";
36
35
 
37
36
  import finalizeRecording from "../../../../../actions/finalize-recording.js";
38
- import { UPLOAD_RETRY_RESUME_FLAG } from "../../../../../shared/feature-flags.js";
39
37
  import { getDb, schema } from "../../../../db/index.js";
40
38
  import { debugLog } from "../../../../lib/debug.js";
41
39
  import {
@@ -47,15 +45,16 @@ import {
47
45
  ownerEmailMatches,
48
46
  } from "../../../../lib/recordings.js";
49
47
  import {
50
- deleteResumableSession,
48
+ compareAndSetResumableSession,
51
49
  getResumableSession,
52
- setResumableSession,
53
50
  type StoredResumableSession,
54
51
  } from "../../../../lib/resumable-session.js";
55
- import { abortResumableUploadSession } from "../../../../lib/resumable-upload-cleanup.js";
56
52
  import { resolveResumableUploadProvider } from "../../../../lib/resumable-upload-provider.js";
57
53
  import { isStreamingUploadDisabled } from "../../../../lib/streaming-upload-mode.js";
58
- import { renewUploadLease } from "../../../../lib/upload-lease.js";
54
+ import {
55
+ renewUploadLease,
56
+ type UploadLeaseResult,
57
+ } from "../../../../lib/upload-lease.js";
59
58
  import {
60
59
  allowsSqlRecordingChunkScratch,
61
60
  shouldRejectVideoUploadWithoutStorage,
@@ -68,6 +67,42 @@ const RECORDING_TOO_LARGE_REASON = `Recording exceeds the ${Math.round(MAX_RECOR
68
67
  // are base64 encoded by the gateway and effectively cap out around 4.5 MB.
69
68
  // Keep our own cap lower so dev/local failures match production.
70
69
  const MAX_CHUNK_BYTES = 4 * 1024 * 1024;
70
+ const RETRY_OWNERSHIP_HEARTBEAT_MS = 10 * 1000;
71
+
72
+ async function relayWithRetryOwnershipHeartbeat<T>(
73
+ recordingId: string,
74
+ attemptId: string | null,
75
+ generationId: string | null,
76
+ relay: () => Promise<T>,
77
+ ): Promise<{ result: T; ownershipFailure: UploadLeaseResult | Error | null }> {
78
+ if (attemptId === null)
79
+ return { result: await relay(), ownershipFailure: null };
80
+ let ownershipFailure: UploadLeaseResult | Error | null = null;
81
+ let pending: Promise<void> | null = null;
82
+ const heartbeat = () => {
83
+ if (pending || ownershipFailure) return;
84
+ pending = renewUploadLease(recordingId, { attemptId, generationId })
85
+ .then((lease) => {
86
+ if (!lease.held) ownershipFailure = lease;
87
+ })
88
+ .catch((error) => {
89
+ ownershipFailure =
90
+ error instanceof Error ? error : new Error(String(error));
91
+ })
92
+ .finally(() => {
93
+ pending = null;
94
+ });
95
+ };
96
+ const timer = setInterval(heartbeat, RETRY_OWNERSHIP_HEARTBEAT_MS);
97
+ let result: T;
98
+ try {
99
+ result = await relay();
100
+ } finally {
101
+ clearInterval(timer);
102
+ await pending;
103
+ }
104
+ return { result: result!, ownershipFailure };
105
+ }
71
106
 
72
107
  const ALLOWED_RECORDING_MIME_TYPES = new Set([
73
108
  "video/webm",
@@ -239,23 +274,6 @@ export default defineEventHandler(async (event: H3Event) => {
239
274
  }
240
275
  debugLog("[chunk] resolved owner:", ownerEmail);
241
276
 
242
- if (
243
- attemptId !== null &&
244
- !(await isFeatureFlagEnabled(UPLOAD_RETRY_RESUME_FLAG, {
245
- userEmail: ownerEmail,
246
- userKey: ownerEmail,
247
- orgId,
248
- }))
249
- ) {
250
- setResponseStatus(event, 409);
251
- return {
252
- ok: false,
253
- error: "Resumable upload retry is disabled.",
254
- restartRequired: true,
255
- recoveryEnabled: false,
256
- };
257
- }
258
-
259
277
  return runWithRequestContext({ userEmail: ownerEmail, orgId }, async () => {
260
278
  const db = getDb();
261
279
 
@@ -855,21 +873,55 @@ async function handleResumableChunk(
855
873
  `[resumable-chunk-${recordingId}] resumable session exists - bytesUploaded=${session.bytesUploaded} index=${index} isFinal=${isFinal}`,
856
874
  );
857
875
 
858
- const cleanupFailedFinalSession = async () => {
859
- const cleaned = await abortResumableUploadSession(session, {
860
- provider: uploadProvider,
861
- label: `resumable-final-${recordingId}`,
862
- });
863
- if (cleaned) {
864
- await deleteResumableSession(recordingId, uploadGenerationId).catch(
865
- (error) =>
866
- console.warn(
867
- `[resumable-chunk-${recordingId}] failed to retire aborted session:`,
868
- error,
869
- ),
870
- );
876
+ const settleAcceptedProviderEffect = async (
877
+ next: StoredResumableSession,
878
+ ): Promise<"settled" | "superseded" | "contradictory"> => {
879
+ if (
880
+ await compareAndSetResumableSession(
881
+ recordingId,
882
+ session,
883
+ next,
884
+ uploadGenerationId,
885
+ )
886
+ ) {
887
+ session = next;
888
+ return "settled";
889
+ }
890
+
891
+ const current = await getResumableSession(recordingId, uploadGenerationId);
892
+ if (!current || current.sessionId !== session.sessionId) {
893
+ return "superseded";
871
894
  }
872
- return !cleaned;
895
+ const metaSettled = Object.entries(next.meta).every(
896
+ ([key, value]) =>
897
+ JSON.stringify(current.meta[key]) === JSON.stringify(value),
898
+ );
899
+ if (
900
+ current.bytesUploaded >= next.bytesUploaded &&
901
+ (current.lastCommittedIndex ?? -1) >= (next.lastCommittedIndex ?? -1) &&
902
+ (!next.providerClosed || current.providerClosed === true) &&
903
+ metaSettled
904
+ ) {
905
+ session = current;
906
+ return "settled";
907
+ }
908
+ return "contradictory";
909
+ };
910
+
911
+ const settlementFailure = (outcome: "superseded" | "contradictory") => {
912
+ setResponseStatus(event, 409);
913
+ return outcome === "superseded"
914
+ ? {
915
+ ok: false,
916
+ error:
917
+ "Upload retry ownership was lost while the provider was responding.",
918
+ staleAttempt: true,
919
+ }
920
+ : {
921
+ ok: false,
922
+ error: "Accepted provider state could not be reconciled safely.",
923
+ restartRequired: true,
924
+ };
873
925
  };
874
926
 
875
927
  const raw = await readRawBody(event, false);
@@ -902,40 +954,95 @@ async function handleResumableChunk(
902
954
  // 0-byte sentinel from the recorder after stop(). All data chunks have
903
955
  // already been PUT to the provider; send Content-Range: bytes */<total>
904
956
  // to close the session before handing off to finalize-recording.
905
- let closeRes;
906
- try {
907
- closeRes = await uploadProvider.resumable.relayChunk(
908
- { sessionId: session.sessionId, meta: session.meta },
909
- `bytes */${session.bytesUploaded}`,
910
- new Uint8Array(0),
911
- );
912
- } catch (error) {
913
- const cleanupFailed = await cleanupFailedFinalSession();
914
- const detail = error instanceof Error ? error.message : String(error);
915
- console.error(
916
- `[resumable-chunk-${recordingId}] session close threw:`,
917
- error,
918
- );
919
- setResponseStatus(event, 502);
920
- return {
921
- ok: false,
922
- error: `Resumable session close failed: ${detail}`,
923
- ...(cleanupFailed ? { cleanupFailed: true } : {}),
924
- };
925
- }
926
- if (!closeRes.ok || closeRes.status === 308) {
927
- console.error(
928
- `[resumable-chunk-${recordingId}] session close failed (${closeRes.status})`,
929
- );
930
- const cleanupFailed = await cleanupFailedFinalSession();
931
- setResponseStatus(event, 502);
932
- return {
933
- ok: false,
934
- error: `Resumable session close failed (${closeRes.status})`,
935
- ...(cleanupFailed ? { cleanupFailed: true } : {}),
936
- };
937
- }
938
- if (closeRes.updatedMeta) {
957
+ if (session.providerClosed) {
958
+ // A prior close response was accepted but its caller lost ownership.
959
+ // The durable marker makes replay a no-op before idempotent finalization.
960
+ } else {
961
+ let closeRes;
962
+ try {
963
+ const relayed = await relayWithRetryOwnershipHeartbeat(
964
+ recordingId,
965
+ attemptId,
966
+ uploadGenerationId,
967
+ () =>
968
+ uploadProvider.resumable!.relayChunk(
969
+ { sessionId: session.sessionId, meta: session.meta },
970
+ `bytes */${session.bytesUploaded}`,
971
+ new Uint8Array(0),
972
+ ),
973
+ );
974
+ closeRes = relayed.result;
975
+ if (closeRes.ok && closeRes.status !== 308) {
976
+ const settlement = await settleAcceptedProviderEffect({
977
+ ...session,
978
+ ...(closeRes.updatedMeta
979
+ ? { meta: { ...session.meta, ...closeRes.updatedMeta } }
980
+ : {}),
981
+ providerClosed: true,
982
+ });
983
+ if (settlement !== "settled") return settlementFailure(settlement);
984
+ }
985
+ if (relayed.ownershipFailure) {
986
+ setResponseStatus(event, 409);
987
+ return {
988
+ ok: false,
989
+ error:
990
+ "Upload retry ownership was lost while the provider was responding.",
991
+ staleAttempt: true,
992
+ };
993
+ }
994
+ } catch (error) {
995
+ const failedCloseLease = await renewUploadLease(recordingId, {
996
+ attemptId,
997
+ generationId: uploadGenerationId,
998
+ });
999
+ if (!failedCloseLease.held) {
1000
+ setResponseStatus(event, 409);
1001
+ return {
1002
+ ok: false,
1003
+ error:
1004
+ "Upload retry ownership was lost while the provider was responding.",
1005
+ staleAttempt: true,
1006
+ };
1007
+ }
1008
+ const detail = error instanceof Error ? error.message : String(error);
1009
+ console.error(
1010
+ `[resumable-chunk-${recordingId}] session close threw:`,
1011
+ error,
1012
+ );
1013
+ setResponseStatus(event, 409);
1014
+ return {
1015
+ ok: false,
1016
+ error: `Resumable session close outcome is unknown: ${detail}`,
1017
+ restartRequired: true,
1018
+ };
1019
+ }
1020
+ if (!closeRes.ok || closeRes.status === 308) {
1021
+ const failedCloseLease = await renewUploadLease(recordingId, {
1022
+ attemptId,
1023
+ generationId: uploadGenerationId,
1024
+ });
1025
+ if (!failedCloseLease.held) {
1026
+ setResponseStatus(event, 409);
1027
+ return {
1028
+ ok: false,
1029
+ error:
1030
+ "Upload retry ownership was lost while the provider was responding.",
1031
+ staleAttempt: true,
1032
+ };
1033
+ }
1034
+ console.error(
1035
+ `[resumable-chunk-${recordingId}] session close failed (${closeRes.status})`,
1036
+ );
1037
+ const restartRequired =
1038
+ closeRes.status === 404 || closeRes.status === 410;
1039
+ setResponseStatus(event, restartRequired ? 409 : 502);
1040
+ return {
1041
+ ok: false,
1042
+ error: `Resumable session close failed (${closeRes.status})`,
1043
+ ...(restartRequired ? { restartRequired: true } : {}),
1044
+ };
1045
+ }
939
1046
  const postCloseLease = await renewUploadLease(recordingId, {
940
1047
  attemptId,
941
1048
  generationId: uploadGenerationId,
@@ -947,16 +1054,9 @@ async function handleResumableChunk(
947
1054
  error:
948
1055
  postCloseLease.failureReason ??
949
1056
  "Recording upload has already failed.",
1057
+ staleAttempt: true,
950
1058
  };
951
1059
  }
952
- await setResumableSession(
953
- recordingId,
954
- {
955
- ...session,
956
- meta: { ...session.meta, ...closeRes.updatedMeta },
957
- },
958
- uploadGenerationId,
959
- );
960
1060
  }
961
1061
  } else {
962
1062
  // Idempotent replay guard: a client retry (after a lost response) can
@@ -1002,28 +1102,65 @@ async function handleResumableChunk(
1002
1102
  const putT0 = Date.now();
1003
1103
  let putResult;
1004
1104
  try {
1005
- putResult = await uploadProvider.resumable.relayChunk(
1006
- { sessionId: session.sessionId, meta: session.meta },
1007
- contentRange,
1008
- bytes,
1009
- { mimeType: mimeType.split(";")[0].trim() },
1105
+ const relayed = await relayWithRetryOwnershipHeartbeat(
1106
+ recordingId,
1107
+ attemptId,
1108
+ uploadGenerationId,
1109
+ () =>
1110
+ uploadProvider.resumable!.relayChunk(
1111
+ { sessionId: session.sessionId, meta: session.meta },
1112
+ contentRange,
1113
+ bytes,
1114
+ { mimeType: mimeType.split(";")[0].trim() },
1115
+ ),
1010
1116
  );
1117
+ putResult = relayed.result;
1118
+ if (isFinal ? putResult.ok && putResult.status !== 308 : putResult.ok) {
1119
+ const settlement = await settleAcceptedProviderEffect({
1120
+ ...session,
1121
+ ...(putResult.updatedMeta
1122
+ ? { meta: { ...session.meta, ...putResult.updatedMeta } }
1123
+ : {}),
1124
+ bytesUploaded: start + bytes.byteLength,
1125
+ lastCommittedIndex: index,
1126
+ });
1127
+ if (settlement !== "settled") return settlementFailure(settlement);
1128
+ finalizedSourceSizeBytes = start + bytes.byteLength;
1129
+ }
1130
+ if (relayed.ownershipFailure) {
1131
+ setResponseStatus(event, 409);
1132
+ return {
1133
+ ok: false,
1134
+ error:
1135
+ "Upload retry ownership was lost while the provider was responding.",
1136
+ staleAttempt: true,
1137
+ };
1138
+ }
1011
1139
  } catch (error) {
1012
- if (isFinal) {
1013
- const cleanupFailed = await cleanupFailedFinalSession();
1014
- const detail = error instanceof Error ? error.message : String(error);
1015
- console.error(
1016
- `[resumable-chunk-${recordingId}] final chunk upload threw:`,
1017
- error,
1018
- );
1019
- setResponseStatus(event, 502);
1140
+ const failedUploadLease = await renewUploadLease(recordingId, {
1141
+ attemptId,
1142
+ generationId: uploadGenerationId,
1143
+ });
1144
+ if (!failedUploadLease.held) {
1145
+ setResponseStatus(event, 409);
1020
1146
  return {
1021
1147
  ok: false,
1022
- error: `Final chunk upload failed: ${detail}`,
1023
- ...(cleanupFailed ? { cleanupFailed: true } : {}),
1148
+ error:
1149
+ "Upload retry ownership was lost while the provider was responding.",
1150
+ staleAttempt: true,
1024
1151
  };
1025
1152
  }
1026
- throw error;
1153
+ const detail = error instanceof Error ? error.message : String(error);
1154
+ console.error(
1155
+ `[resumable-chunk-${recordingId}] provider response was ambiguous:`,
1156
+ error,
1157
+ );
1158
+ setResponseStatus(event, 409);
1159
+ return {
1160
+ ok: false,
1161
+ error: `Chunk upload outcome is unknown: ${detail}`,
1162
+ restartRequired: true,
1163
+ };
1027
1164
  }
1028
1165
  console.log(
1029
1166
  `[resumable-chunk-${recordingId}] PUT ${Date.now() - putT0}ms status=${putResult.status} range="${contentRange}"`,
@@ -1033,22 +1170,26 @@ async function handleResumableChunk(
1033
1170
  ? putResult.ok && putResult.status !== 308
1034
1171
  : putResult.ok;
1035
1172
  if (!resultOk) {
1173
+ const failedUploadLease = await renewUploadLease(recordingId, {
1174
+ attemptId,
1175
+ generationId: uploadGenerationId,
1176
+ });
1177
+ if (!failedUploadLease.held) {
1178
+ setResponseStatus(event, 409);
1179
+ return {
1180
+ ok: false,
1181
+ error:
1182
+ "Upload retry ownership was lost while the provider was responding.",
1183
+ staleAttempt: true,
1184
+ };
1185
+ }
1036
1186
  const restartRequired =
1037
1187
  putResult.status === 404 || putResult.status === 410;
1038
- const cleanupFailed = isFinal
1039
- ? await cleanupFailedFinalSession()
1040
- : false;
1041
- if (restartRequired && !cleanupFailed) {
1042
- await deleteResumableSession(recordingId, uploadGenerationId).catch(
1043
- () => {},
1044
- );
1045
- }
1046
1188
  setResponseStatus(event, restartRequired ? 409 : 502);
1047
1189
  return {
1048
1190
  ok: false,
1049
1191
  error: `Chunk upload failed (${putResult.status})`,
1050
1192
  ...(restartRequired ? { restartRequired: true } : {}),
1051
- ...(cleanupFailed ? { cleanupFailed: true } : {}),
1052
1193
  };
1053
1194
  }
1054
1195
 
@@ -1065,20 +1206,6 @@ async function handleResumableChunk(
1065
1206
  "Recording upload has already failed.",
1066
1207
  };
1067
1208
  }
1068
- await setResumableSession(
1069
- recordingId,
1070
- {
1071
- ...session,
1072
- ...(putResult.updatedMeta
1073
- ? { meta: { ...session.meta, ...putResult.updatedMeta } }
1074
- : {}),
1075
- bytesUploaded: start + bytes.byteLength,
1076
- lastCommittedIndex: index,
1077
- },
1078
- uploadGenerationId,
1079
- );
1080
- finalizedSourceSizeBytes = start + bytes.byteLength;
1081
-
1082
1209
  if (!isFinal) {
1083
1210
  return { ok: true, finalized: false, index, bytes: bytes.byteLength };
1084
1211
  }
@@ -238,7 +238,10 @@ export default defineEventHandler(async (event: H3Event) => {
238
238
  : null;
239
239
  const existingAttemptId = existing.uploadAttemptId ?? null;
240
240
  const existingGenerationId = existing.uploadGenerationId ?? null;
241
- if (recoveryEnabled && existingAttemptId !== requestedAttemptId) {
241
+ if (
242
+ existingAttemptId !== null &&
243
+ existingAttemptId !== requestedAttemptId
244
+ ) {
242
245
  setResponseStatus(event, 409);
243
246
  return {
244
247
  error: "A newer upload retry is already active.",
@@ -251,7 +254,10 @@ export default defineEventHandler(async (event: H3Event) => {
251
254
  body.uploadGenerationId.length <= 128
252
255
  ? body.uploadGenerationId
253
256
  : null;
254
- if (recoveryEnabled && existingGenerationId !== requestedGenerationId) {
257
+ if (
258
+ existingGenerationId !== null &&
259
+ existingGenerationId !== requestedGenerationId
260
+ ) {
255
261
  setResponseStatus(event, 409);
256
262
  return {
257
263
  error: "A newer upload generation is already active.",
@@ -281,10 +287,12 @@ export default defineEventHandler(async (event: H3Event) => {
281
287
  // fence. Retry claims always opt in; legacy reset callers keep the null
282
288
  // generation wire contract until they are upgraded.
283
289
  const useGenerationFence =
284
- recoveryEnabled &&
285
- (requestedAttemptId !== null ||
286
- requestedGenerationId !== null ||
287
- body?.useGenerationFence === true);
290
+ existingAttemptId !== null ||
291
+ existingGenerationId !== null ||
292
+ (recoveryEnabled &&
293
+ (requestedAttemptId !== null ||
294
+ requestedGenerationId !== null ||
295
+ body?.useGenerationFence === true));
288
296
  const nextGenerationId = useGenerationFence ? randomUUID() : null;
289
297
  const uploadStateKey = `recording-upload-${recordingId}`;
290
298
  const uploadStateSnapshot = await readAppState(uploadStateKey);
@@ -314,7 +322,9 @@ export default defineEventHandler(async (event: H3Event) => {
314
322
  failureReason: null,
315
323
  uploadProgress: 0,
316
324
  uploadGenerationId: nextGenerationId,
317
- ...(!recoveryEnabled ? { uploadAttemptId: null } : {}),
325
+ ...(!recoveryEnabled && existingAttemptId === null
326
+ ? { uploadAttemptId: null }
327
+ : {}),
318
328
  uploadLeaseExpiresAt: uploadLeaseExpiry(),
319
329
  updatedAt: now,
320
330
  })
@@ -503,8 +513,9 @@ export default defineEventHandler(async (event: H3Event) => {
503
513
  }
504
514
  }
505
515
 
516
+ const preservedAttemptId = existingAttemptId;
506
517
  const resetLease = await renewUploadLease(recordingId, {
507
- attemptId: recoveryEnabled ? existingAttemptId : null,
518
+ attemptId: preservedAttemptId,
508
519
  generationId: nextGenerationId,
509
520
  });
510
521
  if (!resetLease.held) {
@@ -528,7 +539,7 @@ export default defineEventHandler(async (event: H3Event) => {
528
539
  progress: 0,
529
540
  chunksReceived: 0,
530
541
  bytesReceived: 0,
531
- uploadAttemptId: recoveryEnabled ? existingAttemptId : null,
542
+ uploadAttemptId: preservedAttemptId,
532
543
  uploadGenerationId: nextGenerationId,
533
544
  maxBytes: MAX_RECORDING_UPLOAD_BYTES,
534
545
  updatedAt: now,
@@ -550,8 +561,7 @@ export default defineEventHandler(async (event: H3Event) => {
550
561
  );
551
562
  if (
552
563
  current?.status !== "uploading" ||
553
- (current.uploadAttemptId ?? null) !==
554
- (recoveryEnabled ? existingAttemptId : null) ||
564
+ (current.uploadAttemptId ?? null) !== preservedAttemptId ||
555
565
  (current.uploadGenerationId ?? null) !== nextGenerationId
556
566
  ) {
557
567
  setResponseStatus(event, 409);