@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
package/corpus/README.md
CHANGED
|
@@ -198,6 +198,10 @@ interface PendingNativeUpload {
|
|
|
198
198
|
|
|
199
199
|
type PendingDesktopUpload = PendingNativeUpload | PendingBrowserRecordingUpload;
|
|
200
200
|
|
|
201
|
+
type NativeUploadProgress = {
|
|
202
|
+
message?: string;
|
|
203
|
+
};
|
|
204
|
+
|
|
201
205
|
type PopoverView =
|
|
202
206
|
| "recorder"
|
|
203
207
|
| "memory"
|
|
@@ -1137,6 +1141,26 @@ export function App({
|
|
|
1137
1141
|
const [retryingUploadStatus, setRetryingUploadStatus] = useState<
|
|
1138
1142
|
string | null
|
|
1139
1143
|
>(null);
|
|
1144
|
+
const retryUploadAbortRef = useRef<AbortController | null>(null);
|
|
1145
|
+
const retryingUploadKindRef = useRef<PendingDesktopUpload["kind"] | null>(
|
|
1146
|
+
null,
|
|
1147
|
+
);
|
|
1148
|
+
useEffect(() => {
|
|
1149
|
+
if (!retryingUploadId) return;
|
|
1150
|
+
let disposed = false;
|
|
1151
|
+
let unlisten: (() => void) | null = null;
|
|
1152
|
+
listen<NativeUploadProgress>("clips:native-upload-progress", (event) => {
|
|
1153
|
+
const message = event.payload?.message?.trim();
|
|
1154
|
+
if (message) setRetryingUploadStatus(message);
|
|
1155
|
+
}).then((cleanup) => {
|
|
1156
|
+
if (disposed) cleanup();
|
|
1157
|
+
else unlisten = cleanup;
|
|
1158
|
+
});
|
|
1159
|
+
return () => {
|
|
1160
|
+
disposed = true;
|
|
1161
|
+
unlisten?.();
|
|
1162
|
+
};
|
|
1163
|
+
}, [retryingUploadId]);
|
|
1140
1164
|
const [exportingUploadId, setExportingUploadId] = useState<string | null>(
|
|
1141
1165
|
null,
|
|
1142
1166
|
);
|
|
@@ -3107,6 +3131,9 @@ export function App({
|
|
|
3107
3131
|
const targetServerUrl = serverUrlForPendingUpload(upload, serverUrl);
|
|
3108
3132
|
setRecError(null);
|
|
3109
3133
|
setRetryingUploadId(upload.recordingId);
|
|
3134
|
+
const abortController = new AbortController();
|
|
3135
|
+
retryUploadAbortRef.current = abortController;
|
|
3136
|
+
retryingUploadKindRef.current = upload.kind;
|
|
3110
3137
|
try {
|
|
3111
3138
|
const authToken = loadDesktopAuthToken(targetServerUrl);
|
|
3112
3139
|
if (upload.kind === "native") {
|
|
@@ -3132,13 +3159,16 @@ export function App({
|
|
|
3132
3159
|
recordingId: upload.recordingId,
|
|
3133
3160
|
serverUrl: targetServerUrl,
|
|
3134
3161
|
authToken,
|
|
3162
|
+
signal: abortController.signal,
|
|
3135
3163
|
onRecoveryDecision: ({ action, progress }) => {
|
|
3136
3164
|
setRetryingUploadStatus(
|
|
3137
3165
|
action === "resume"
|
|
3138
3166
|
? `Resuming · ${Math.round(progress * 100)}% already uploaded`
|
|
3139
|
-
: action === "
|
|
3140
|
-
? "
|
|
3141
|
-
:
|
|
3167
|
+
: action === "wait"
|
|
3168
|
+
? "Waiting for prior retry"
|
|
3169
|
+
: action === "restart"
|
|
3170
|
+
? "Restarting upload"
|
|
3171
|
+
: "Finishing upload",
|
|
3142
3172
|
);
|
|
3143
3173
|
},
|
|
3144
3174
|
});
|
|
@@ -3152,6 +3182,14 @@ export function App({
|
|
|
3152
3182
|
emit("clips:popover-visible", false).catch(() => {});
|
|
3153
3183
|
} catch (err) {
|
|
3154
3184
|
const message = err instanceof Error ? err.message : String(err);
|
|
3185
|
+
if (
|
|
3186
|
+
abortController.signal.aborted ||
|
|
3187
|
+
(err instanceof DOMException && err.name === "AbortError") ||
|
|
3188
|
+
message === "native recording upload retry cancelled"
|
|
3189
|
+
) {
|
|
3190
|
+
await loadPendingUploads();
|
|
3191
|
+
return;
|
|
3192
|
+
}
|
|
3155
3193
|
console.error("[clips-tray] retry saved upload failed:", err);
|
|
3156
3194
|
setRecError(
|
|
3157
3195
|
isStorageSetupFailureMessage(message)
|
|
@@ -3160,11 +3198,28 @@ export function App({
|
|
|
3160
3198
|
);
|
|
3161
3199
|
await loadPendingUploads();
|
|
3162
3200
|
} finally {
|
|
3201
|
+
if (retryUploadAbortRef.current === abortController) {
|
|
3202
|
+
retryUploadAbortRef.current = null;
|
|
3203
|
+
retryingUploadKindRef.current = null;
|
|
3204
|
+
}
|
|
3163
3205
|
setRetryingUploadId(null);
|
|
3164
3206
|
setRetryingUploadStatus(null);
|
|
3165
3207
|
}
|
|
3166
3208
|
}
|
|
3167
3209
|
|
|
3210
|
+
function cancelPendingUploadRetry(upload: PendingDesktopUpload) {
|
|
3211
|
+
if (retryingUploadId !== upload.recordingId) return;
|
|
3212
|
+
retryUploadAbortRef.current?.abort();
|
|
3213
|
+
if (retryingUploadKindRef.current === "native") {
|
|
3214
|
+
invoke("native_fullscreen_recording_cancel_retry", {
|
|
3215
|
+
recordingId: upload.recordingId,
|
|
3216
|
+
}).catch((err) => {
|
|
3217
|
+
console.error("[clips-tray] cancel saved upload retry failed:", err);
|
|
3218
|
+
});
|
|
3219
|
+
}
|
|
3220
|
+
setRetryingUploadStatus("Cancelling retry");
|
|
3221
|
+
}
|
|
3222
|
+
|
|
3168
3223
|
async function exportPendingUpload(upload: PendingDesktopUpload) {
|
|
3169
3224
|
if (retryingUploadId || exportingUploadId || dismissingUploadId) return;
|
|
3170
3225
|
setRecError(null);
|
|
@@ -3873,6 +3928,7 @@ export function App({
|
|
|
3873
3928
|
dismissingUploadId={dismissingUploadId}
|
|
3874
3929
|
onExport={exportPendingUpload}
|
|
3875
3930
|
onRetry={retryPendingUpload}
|
|
3931
|
+
onCancelRetry={cancelPendingUploadRetry}
|
|
3876
3932
|
onDismiss={dismissPendingUpload}
|
|
3877
3933
|
onOpenFolder={openPendingUploadFolder}
|
|
3878
3934
|
onConnectStorage={(upload) => openVideoStorageSetup(upload.serverUrl)}
|
|
@@ -4637,6 +4693,7 @@ function PendingUploadBanner({
|
|
|
4637
4693
|
dismissingUploadId,
|
|
4638
4694
|
onExport,
|
|
4639
4695
|
onRetry,
|
|
4696
|
+
onCancelRetry,
|
|
4640
4697
|
onDismiss,
|
|
4641
4698
|
onOpenFolder,
|
|
4642
4699
|
onConnectStorage,
|
|
@@ -4648,6 +4705,7 @@ function PendingUploadBanner({
|
|
|
4648
4705
|
dismissingUploadId: string | null;
|
|
4649
4706
|
onExport: (upload: PendingDesktopUpload) => void;
|
|
4650
4707
|
onRetry: (upload: PendingDesktopUpload) => void;
|
|
4708
|
+
onCancelRetry: (upload: PendingDesktopUpload) => void;
|
|
4651
4709
|
onDismiss: (upload: PendingDesktopUpload) => void;
|
|
4652
4710
|
onOpenFolder: (upload: PendingDesktopUpload) => void;
|
|
4653
4711
|
onConnectStorage: (upload: PendingDesktopUpload) => void;
|
|
@@ -4656,6 +4714,8 @@ function PendingUploadBanner({
|
|
|
4656
4714
|
if (!latest) return null;
|
|
4657
4715
|
|
|
4658
4716
|
const retrying = retryingUploadId === latest.recordingId;
|
|
4717
|
+
const retryCancelling =
|
|
4718
|
+
retrying && retryingUploadStatus === "Cancelling retry";
|
|
4659
4719
|
const storageSetupFailure = isStorageSetupFailureMessage(latest.lastError);
|
|
4660
4720
|
|
|
4661
4721
|
const canOpenFolder = latest.kind === "native" && !!latest.folderPath;
|
|
@@ -4762,12 +4822,22 @@ function PendingUploadBanner({
|
|
|
4762
4822
|
<button
|
|
4763
4823
|
type="button"
|
|
4764
4824
|
className={`pending-upload-retry${retrying ? " pending-upload-retry-spinning" : ""}`}
|
|
4765
|
-
disabled={actionsDisabled}
|
|
4766
|
-
onClick={() =>
|
|
4825
|
+
disabled={retrying ? retryCancelling : actionsDisabled}
|
|
4826
|
+
onClick={() =>
|
|
4827
|
+
retrying ? onCancelRetry(latest) : onRetry(latest)
|
|
4828
|
+
}
|
|
4767
4829
|
aria-busy={retrying}
|
|
4768
4830
|
>
|
|
4769
|
-
|
|
4770
|
-
|
|
4831
|
+
{retrying ? (
|
|
4832
|
+
<IconX size={14} stroke={2} />
|
|
4833
|
+
) : (
|
|
4834
|
+
<IconRefresh size={14} stroke={2} />
|
|
4835
|
+
)}
|
|
4836
|
+
{retryCancelling
|
|
4837
|
+
? "Cancelling retry"
|
|
4838
|
+
: retrying
|
|
4839
|
+
? "Cancel retry"
|
|
4840
|
+
: "Retry"}
|
|
4771
4841
|
</button>
|
|
4772
4842
|
</>
|
|
4773
4843
|
)}
|
|
@@ -104,6 +104,7 @@ import {
|
|
|
104
104
|
planStreamingRecovery,
|
|
105
105
|
retryAttemptIdAfterRestartSignal,
|
|
106
106
|
retryAttemptIdAfterResumeResponse,
|
|
107
|
+
retryConflictDelay,
|
|
107
108
|
type UploadResumeResponse,
|
|
108
109
|
} from "./upload-recovery";
|
|
109
110
|
import {
|
|
@@ -1061,6 +1062,7 @@ async function postBackupChunk(
|
|
|
1061
1062
|
url: string,
|
|
1062
1063
|
blob: Blob,
|
|
1063
1064
|
authToken?: string,
|
|
1065
|
+
signal?: AbortSignal,
|
|
1064
1066
|
): Promise<FinalizeReceipt | null> {
|
|
1065
1067
|
const res = await fetch(url, {
|
|
1066
1068
|
method: "POST",
|
|
@@ -1070,6 +1072,7 @@ async function postBackupChunk(
|
|
|
1070
1072
|
),
|
|
1071
1073
|
credentials: "include",
|
|
1072
1074
|
body: blob,
|
|
1075
|
+
signal,
|
|
1073
1076
|
});
|
|
1074
1077
|
const body = await res.text().catch(() => "");
|
|
1075
1078
|
if (!res.ok) {
|
|
@@ -1103,6 +1106,7 @@ async function resetBrowserRecordingBackupUpload(
|
|
|
1103
1106
|
authToken?: string,
|
|
1104
1107
|
attemptId?: string,
|
|
1105
1108
|
uploadGenerationId?: string,
|
|
1109
|
+
signal?: AbortSignal,
|
|
1106
1110
|
): Promise<{ uploadMode: UploadMode; uploadGenerationId?: string }> {
|
|
1107
1111
|
const res = await fetch(
|
|
1108
1112
|
`${meta.serverUrl.replace(/\/+$/, "")}/api/uploads/${meta.recordingId}/reset-chunks`,
|
|
@@ -1120,6 +1124,7 @@ async function resetBrowserRecordingBackupUpload(
|
|
|
1120
1124
|
...(attemptId ? { attemptId } : {}),
|
|
1121
1125
|
...(uploadGenerationId ? { uploadGenerationId } : {}),
|
|
1122
1126
|
}),
|
|
1127
|
+
signal,
|
|
1123
1128
|
},
|
|
1124
1129
|
);
|
|
1125
1130
|
if (!res.ok) {
|
|
@@ -1147,32 +1152,60 @@ async function getBrowserRecordingUploadResume(
|
|
|
1147
1152
|
meta: BrowserRecordingBackupMeta,
|
|
1148
1153
|
attemptId: string,
|
|
1149
1154
|
authToken?: string,
|
|
1155
|
+
onWaiting?: (delayMs: number) => void,
|
|
1156
|
+
signal?: AbortSignal,
|
|
1150
1157
|
): Promise<UploadResumeResponse> {
|
|
1151
1158
|
const resumeUrl = new URL(
|
|
1152
1159
|
`${meta.serverUrl.replace(/\/+$/, "")}/api/uploads/${meta.recordingId}/resume`,
|
|
1153
1160
|
);
|
|
1154
1161
|
resumeUrl.searchParams.set("attemptId", attemptId);
|
|
1155
|
-
const
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1162
|
+
const deadline = Date.now() + 5 * 60_000;
|
|
1163
|
+
for (;;) {
|
|
1164
|
+
const res = await fetch(resumeUrl, {
|
|
1165
|
+
method: "GET",
|
|
1166
|
+
headers: buildRetryHeaders("application/json", authToken),
|
|
1167
|
+
credentials: "include",
|
|
1168
|
+
signal,
|
|
1169
|
+
});
|
|
1170
|
+
let body: string;
|
|
1171
|
+
try {
|
|
1172
|
+
body = await res.text();
|
|
1173
|
+
} catch {
|
|
1174
|
+
throw new Error("Upload resume check response could not be read");
|
|
1175
|
+
}
|
|
1176
|
+
let parsed: UploadResumeResponse;
|
|
1177
|
+
try {
|
|
1178
|
+
parsed = JSON.parse(body) as UploadResumeResponse;
|
|
1179
|
+
} catch {
|
|
1180
|
+
throw new Error("Upload resume check returned an unreadable response");
|
|
1181
|
+
}
|
|
1182
|
+
if (!res.ok) {
|
|
1183
|
+
const delayMs = retryConflictDelay(parsed);
|
|
1184
|
+
if (delayMs !== null && Date.now() + delayMs <= deadline) {
|
|
1185
|
+
onWaiting?.(delayMs);
|
|
1186
|
+
await abortableWait(delayMs, signal);
|
|
1187
|
+
continue;
|
|
1188
|
+
}
|
|
1189
|
+
if (!parsed.resumable && parsed.reason === "retry_already_active") {
|
|
1190
|
+
throw new Error(
|
|
1191
|
+
"Another upload retry is still active. Wait a moment and try again.",
|
|
1192
|
+
);
|
|
1193
|
+
}
|
|
1194
|
+
if (
|
|
1195
|
+
!parsed.resumable &&
|
|
1196
|
+
parsed.reason === "retry_claim_liveness_unavailable"
|
|
1197
|
+
) {
|
|
1198
|
+
throw new Error(
|
|
1199
|
+
"Clips could not verify whether another retry is active. Your local clip is safe; try again.",
|
|
1200
|
+
);
|
|
1201
|
+
}
|
|
1202
|
+
throw new Error(`Upload resume check failed (${res.status})`);
|
|
1203
|
+
}
|
|
1204
|
+
if (parsed.resumable && parsed.attemptId !== attemptId) {
|
|
1205
|
+
throw new Error("Upload resume check returned a mismatched retry token");
|
|
1206
|
+
}
|
|
1207
|
+
return parsed;
|
|
1174
1208
|
}
|
|
1175
|
-
return parsed;
|
|
1176
1209
|
}
|
|
1177
1210
|
|
|
1178
1211
|
async function replayBrowserBackupToResumableSession(
|
|
@@ -1185,6 +1218,7 @@ async function replayBrowserBackupToResumableSession(
|
|
|
1185
1218
|
bytesReceived: 0,
|
|
1186
1219
|
nextChunkIndex: 0,
|
|
1187
1220
|
},
|
|
1221
|
+
signal?: AbortSignal,
|
|
1188
1222
|
): Promise<FinalizeReceipt | null> {
|
|
1189
1223
|
// The backup is stored in raw MediaRecorder blobs, which have arbitrary
|
|
1190
1224
|
// boundaries. A resumable provider needs every non-final request aligned,
|
|
@@ -1217,6 +1251,7 @@ async function replayBrowserBackupToResumableSession(
|
|
|
1217
1251
|
}),
|
|
1218
1252
|
body,
|
|
1219
1253
|
authToken,
|
|
1254
|
+
signal,
|
|
1220
1255
|
);
|
|
1221
1256
|
}
|
|
1222
1257
|
|
|
@@ -1243,6 +1278,7 @@ async function replayBrowserBackupToResumableSession(
|
|
|
1243
1278
|
}),
|
|
1244
1279
|
finalBody,
|
|
1245
1280
|
authToken,
|
|
1281
|
+
signal,
|
|
1246
1282
|
);
|
|
1247
1283
|
}
|
|
1248
1284
|
|
|
@@ -1250,8 +1286,9 @@ export async function retryBrowserRecordingBackup(input: {
|
|
|
1250
1286
|
recordingId: string;
|
|
1251
1287
|
serverUrl?: string;
|
|
1252
1288
|
authToken?: string;
|
|
1289
|
+
signal?: AbortSignal;
|
|
1253
1290
|
onRecoveryDecision?: (decision: {
|
|
1254
|
-
action: "resume" | "restart" | "reconcile";
|
|
1291
|
+
action: "wait" | "resume" | "restart" | "reconcile";
|
|
1255
1292
|
progress: number;
|
|
1256
1293
|
}) => void;
|
|
1257
1294
|
}): Promise<{ recordingId: string; viewUrl: string }> {
|
|
@@ -1284,6 +1321,8 @@ export async function retryBrowserRecordingBackup(input: {
|
|
|
1284
1321
|
meta,
|
|
1285
1322
|
activeAttemptId,
|
|
1286
1323
|
input.authToken,
|
|
1324
|
+
() => input.onRecoveryDecision?.({ action: "wait", progress: 0 }),
|
|
1325
|
+
input.signal,
|
|
1287
1326
|
);
|
|
1288
1327
|
const recoveryPlan = planStreamingRecovery({
|
|
1289
1328
|
response: resumeResponse,
|
|
@@ -1294,7 +1333,7 @@ export async function retryBrowserRecordingBackup(input: {
|
|
|
1294
1333
|
activeAttemptId,
|
|
1295
1334
|
resumeResponse,
|
|
1296
1335
|
);
|
|
1297
|
-
activeUploadGenerationId =
|
|
1336
|
+
activeUploadGenerationId = activeAttemptId
|
|
1298
1337
|
? resumeResponse.uploadGenerationId
|
|
1299
1338
|
: undefined;
|
|
1300
1339
|
if (recoveryPlan.action === "reconcile") {
|
|
@@ -1343,6 +1382,7 @@ export async function retryBrowserRecordingBackup(input: {
|
|
|
1343
1382
|
input.authToken,
|
|
1344
1383
|
activeAttemptId,
|
|
1345
1384
|
activeUploadGenerationId,
|
|
1385
|
+
input.signal,
|
|
1346
1386
|
);
|
|
1347
1387
|
uploadMode = reset.uploadMode;
|
|
1348
1388
|
activeUploadGenerationId = reset.uploadGenerationId;
|
|
@@ -1358,6 +1398,7 @@ export async function retryBrowserRecordingBackup(input: {
|
|
|
1358
1398
|
activeAttemptId,
|
|
1359
1399
|
activeUploadGenerationId,
|
|
1360
1400
|
resumeFrom,
|
|
1401
|
+
input.signal,
|
|
1361
1402
|
);
|
|
1362
1403
|
} catch (err) {
|
|
1363
1404
|
if (err instanceof UploadRestartRequiredError) {
|
|
@@ -1365,9 +1406,6 @@ export async function retryBrowserRecordingBackup(input: {
|
|
|
1365
1406
|
activeAttemptId,
|
|
1366
1407
|
err.recoveryEnabled,
|
|
1367
1408
|
);
|
|
1368
|
-
if (err.recoveryEnabled === false) {
|
|
1369
|
-
activeUploadGenerationId = undefined;
|
|
1370
|
-
}
|
|
1371
1409
|
input.onRecoveryDecision?.({ action: "restart", progress: 0 });
|
|
1372
1410
|
console.info("[clips-recorder] restarting expired upload session", {
|
|
1373
1411
|
recordingId: meta.recordingId,
|
|
@@ -1378,6 +1416,7 @@ export async function retryBrowserRecordingBackup(input: {
|
|
|
1378
1416
|
input.authToken,
|
|
1379
1417
|
activeAttemptId,
|
|
1380
1418
|
activeUploadGenerationId,
|
|
1419
|
+
input.signal,
|
|
1381
1420
|
);
|
|
1382
1421
|
uploadMode = reset.uploadMode;
|
|
1383
1422
|
activeUploadGenerationId = reset.uploadGenerationId;
|
|
@@ -1388,6 +1427,8 @@ export async function retryBrowserRecordingBackup(input: {
|
|
|
1388
1427
|
input.authToken,
|
|
1389
1428
|
activeAttemptId,
|
|
1390
1429
|
activeUploadGenerationId,
|
|
1430
|
+
undefined,
|
|
1431
|
+
input.signal,
|
|
1391
1432
|
);
|
|
1392
1433
|
}
|
|
1393
1434
|
} else if (
|
|
@@ -1439,6 +1480,7 @@ export async function retryBrowserRecordingBackup(input: {
|
|
|
1439
1480
|
}),
|
|
1440
1481
|
chunk.blob,
|
|
1441
1482
|
input.authToken,
|
|
1483
|
+
input.signal,
|
|
1442
1484
|
);
|
|
1443
1485
|
}
|
|
1444
1486
|
|
|
@@ -1466,6 +1508,7 @@ export async function retryBrowserRecordingBackup(input: {
|
|
|
1466
1508
|
finalChunkUrl,
|
|
1467
1509
|
new Blob([], { type: meta.mimeType }),
|
|
1468
1510
|
input.authToken,
|
|
1511
|
+
input.signal,
|
|
1469
1512
|
);
|
|
1470
1513
|
const receiptStatus = verifyFinalizeReceipt(receipt, meta);
|
|
1471
1514
|
if (receiptStatus === "processing") {
|
|
@@ -1498,6 +1541,12 @@ export async function retryBrowserRecordingBackup(input: {
|
|
|
1498
1541
|
await deleteBrowserRecordingBackup(meta.recordingId);
|
|
1499
1542
|
return { recordingId: meta.recordingId, viewUrl: `/r/${meta.recordingId}` };
|
|
1500
1543
|
} catch (err) {
|
|
1544
|
+
if (
|
|
1545
|
+
input.signal?.aborted ||
|
|
1546
|
+
(err instanceof DOMException && err.name === "AbortError")
|
|
1547
|
+
) {
|
|
1548
|
+
throw err;
|
|
1549
|
+
}
|
|
1501
1550
|
const message = err instanceof Error ? err.message : String(err);
|
|
1502
1551
|
if (
|
|
1503
1552
|
await recoverAcceptedRecordingAfterFinalizeError({
|
|
@@ -1638,6 +1687,23 @@ function wait(ms: number): Promise<void> {
|
|
|
1638
1687
|
return new Promise((resolve) => window.setTimeout(resolve, ms));
|
|
1639
1688
|
}
|
|
1640
1689
|
|
|
1690
|
+
function abortableWait(ms: number, signal?: AbortSignal): Promise<void> {
|
|
1691
|
+
if (!signal) return wait(ms);
|
|
1692
|
+
if (signal.aborted)
|
|
1693
|
+
return Promise.reject(new DOMException("Aborted", "AbortError"));
|
|
1694
|
+
return new Promise((resolve, reject) => {
|
|
1695
|
+
const timer = window.setTimeout(() => {
|
|
1696
|
+
signal.removeEventListener("abort", onAbort);
|
|
1697
|
+
resolve();
|
|
1698
|
+
}, ms);
|
|
1699
|
+
const onAbort = () => {
|
|
1700
|
+
window.clearTimeout(timer);
|
|
1701
|
+
reject(new DOMException("Aborted", "AbortError"));
|
|
1702
|
+
};
|
|
1703
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
1704
|
+
});
|
|
1705
|
+
}
|
|
1706
|
+
|
|
1641
1707
|
interface NativeFullscreenUploadResult {
|
|
1642
1708
|
recordingId: string;
|
|
1643
1709
|
durationMs: number;
|
|
@@ -17,8 +17,25 @@ export type UploadResumeResponse =
|
|
|
17
17
|
videoUrl?: string | null;
|
|
18
18
|
reason?: string;
|
|
19
19
|
attemptId?: string;
|
|
20
|
+
uploadGenerationId?: string;
|
|
21
|
+
retryAfterMs?: number;
|
|
20
22
|
};
|
|
21
23
|
|
|
24
|
+
export function retryConflictDelay(
|
|
25
|
+
response: UploadResumeResponse,
|
|
26
|
+
): number | null {
|
|
27
|
+
if (
|
|
28
|
+
response.resumable ||
|
|
29
|
+
response.recoveryEnabled !== true ||
|
|
30
|
+
response.reason !== "retry_already_active" ||
|
|
31
|
+
!Number.isSafeInteger(response.retryAfterMs) ||
|
|
32
|
+
(response.retryAfterMs ?? 0) <= 0
|
|
33
|
+
) {
|
|
34
|
+
return null;
|
|
35
|
+
}
|
|
36
|
+
return Math.min(Math.max(response.retryAfterMs!, 250), 30_000);
|
|
37
|
+
}
|
|
38
|
+
|
|
22
39
|
export type StreamingRecoveryPlan =
|
|
23
40
|
| {
|
|
24
41
|
action: "resume";
|
|
@@ -38,15 +55,16 @@ export interface StreamingReplayRequest {
|
|
|
38
55
|
|
|
39
56
|
export function retryAttemptIdAfterRestartSignal(
|
|
40
57
|
attemptId: string | undefined,
|
|
41
|
-
|
|
58
|
+
_recoveryEnabled: unknown,
|
|
42
59
|
): string | undefined {
|
|
43
|
-
return
|
|
60
|
+
return attemptId;
|
|
44
61
|
}
|
|
45
62
|
|
|
46
63
|
export function retryAttemptIdAfterResumeResponse(
|
|
47
64
|
attemptId: string | undefined,
|
|
48
65
|
response: UploadResumeResponse,
|
|
49
66
|
): string | undefined {
|
|
67
|
+
if (response.recoveryEnabled === false) return response.attemptId;
|
|
50
68
|
return response.resumable && response.attemptId === attemptId
|
|
51
69
|
? attemptId
|
|
52
70
|
: undefined;
|
|
@@ -67,7 +67,13 @@ reqwest = { version = "0.12", default-features = false, features = [
|
|
|
67
67
|
"rustls-tls",
|
|
68
68
|
"json",
|
|
69
69
|
] }
|
|
70
|
-
tokio = { version = "1", features = [
|
|
70
|
+
tokio = { version = "1", features = [
|
|
71
|
+
"rt",
|
|
72
|
+
"rt-multi-thread",
|
|
73
|
+
"time",
|
|
74
|
+
"sync",
|
|
75
|
+
"macros",
|
|
76
|
+
] }
|
|
71
77
|
chrono = { version = "0.4", features = ["serde"] }
|
|
72
78
|
# Verify the integrity of the Whisper model we download from HuggingFace.
|
|
73
79
|
sha2 = "0.10"
|
|
@@ -161,6 +161,7 @@ pub fn run() {
|
|
|
161
161
|
native_screen::native_fullscreen_pending_uploads,
|
|
162
162
|
native_screen::native_fullscreen_recover_orphaned_uploads,
|
|
163
163
|
native_screen::native_fullscreen_recording_retry_upload,
|
|
164
|
+
native_screen::native_fullscreen_recording_cancel_retry,
|
|
164
165
|
native_screen::native_fullscreen_recording_mark_upload_error,
|
|
165
166
|
native_screen::native_fullscreen_recording_clear_upload,
|
|
166
167
|
native_screen::native_fullscreen_recording_dismiss_upload,
|