@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.
- package/corpus/README.md +1 -1
- package/corpus/templates/clips/desktop/src/app.tsx +77 -7
- package/corpus/templates/clips/desktop/src/lib/recorder.ts +91 -25
- package/corpus/templates/clips/desktop/src/lib/upload-recovery.ts +20 -2
- package/corpus/templates/clips/desktop/src-tauri/Cargo.toml +7 -1
- package/corpus/templates/clips/desktop/src-tauri/src/lib.rs +1 -0
- package/corpus/templates/clips/desktop/src-tauri/src/native_screen.rs +372 -70
- package/corpus/templates/clips/server/lib/resumable-session.ts +15 -0
- package/corpus/templates/clips/server/routes/api/uploads/[recordingId]/chunk.post.ts +245 -118
- package/corpus/templates/clips/server/routes/api/uploads/[recordingId]/reset-chunks.post.ts +21 -11
- package/corpus/templates/clips/server/routes/api/uploads/[recordingId]/resume.get.ts +132 -25
- package/corpus/templates/content/actions/_database-property-input.ts +211 -0
- package/corpus/templates/content/actions/_database-row-mutation.ts +151 -26
- package/corpus/templates/content/actions/add-database-item.ts +15 -5
- package/corpus/templates/content/actions/update-database-item.ts +15 -7
- package/corpus/templates/content/actions/upsert-database-item-by-key.ts +15 -5
- package/corpus/templates/content/package.json +1 -0
- package/corpus/templates/content/parity/README.md +11 -2
- package/corpus/templates/content/parity/eval-scenarios.ts +52 -0
- package/corpus/templates/content/parity/run-database-create-property-preservation.ts +140 -0
- package/corpus/templates/content/parity/scenario-to-eval.ts +390 -0
- package/dist/agent/run-store.js +0 -1
- package/dist/client/agent-engine-key.js +26 -4
- package/dist/client/settings/ProviderSetupForm.js +1 -1
- package/dist/collab/struct-routes.d.ts +1 -1
- package/dist/eval/agent-runner.js +23 -0
- package/dist/eval/types.d.ts +11 -0
- package/dist/observability/routes.d.ts +1 -1
- package/dist/resources/handlers.d.ts +1 -1
- package/dist/server/agent-engine-api-key-route.d.ts +7 -0
- package/dist/server/agent-engine-api-key-route.js +38 -0
- package/dist/server/realtime-token.d.ts +1 -1
- 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
|
-
|
|
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 {
|
|
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
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
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
|
-
|
|
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
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
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
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
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
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
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:
|
|
1023
|
-
|
|
1148
|
+
error:
|
|
1149
|
+
"Upload retry ownership was lost while the provider was responding.",
|
|
1150
|
+
staleAttempt: true,
|
|
1024
1151
|
};
|
|
1025
1152
|
}
|
|
1026
|
-
|
|
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 (
|
|
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 (
|
|
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
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
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
|
|
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:
|
|
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:
|
|
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);
|