@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
|
@@ -211,9 +211,11 @@ struct NativeUploadResumeResponse {
|
|
|
211
211
|
next_chunk_index: Option<u64>,
|
|
212
212
|
attempt_id: Option<String>,
|
|
213
213
|
upload_generation_id: Option<String>,
|
|
214
|
+
reason: Option<String>,
|
|
215
|
+
retry_after_ms: Option<u64>,
|
|
214
216
|
}
|
|
215
217
|
|
|
216
|
-
#[derive(Debug, Deserialize)]
|
|
218
|
+
#[derive(Debug, Deserialize, PartialEq, Eq)]
|
|
217
219
|
#[serde(rename_all = "camelCase")]
|
|
218
220
|
struct NativeUploadResetResponse {
|
|
219
221
|
upload_mode: Option<String>,
|
|
@@ -226,6 +228,16 @@ impl NativeUploadResetResponse {
|
|
|
226
228
|
}
|
|
227
229
|
}
|
|
228
230
|
|
|
231
|
+
fn accept_native_retry_reset(
|
|
232
|
+
reset: NativeUploadResetResponse,
|
|
233
|
+
cancelled: bool,
|
|
234
|
+
) -> Result<NativeUploadResetResponse, String> {
|
|
235
|
+
if cancelled {
|
|
236
|
+
return Err(NATIVE_UPLOAD_RETRY_CANCELLED.to_string());
|
|
237
|
+
}
|
|
238
|
+
Ok(reset)
|
|
239
|
+
}
|
|
240
|
+
|
|
229
241
|
impl NativeUploadMode {
|
|
230
242
|
pub(crate) fn from_option(value: Option<String>) -> Self {
|
|
231
243
|
match value.as_deref() {
|
|
@@ -956,6 +968,8 @@ fn clear_recording_active(app: &AppHandle) {
|
|
|
956
968
|
static LAST_NATIVE_UPLOAD_FINISHED: OnceLock<Mutex<Option<NativeUploadFinishedPayload>>> =
|
|
957
969
|
OnceLock::new();
|
|
958
970
|
static CLAIMED_NATIVE_UPLOAD_OPEN: OnceLock<Mutex<Option<String>>> = OnceLock::new();
|
|
971
|
+
static CANCELLED_NATIVE_UPLOAD_RETRIES: OnceLock<Mutex<BTreeSet<String>>> = OnceLock::new();
|
|
972
|
+
const NATIVE_UPLOAD_RETRY_CANCELLED: &str = "native recording upload retry cancelled";
|
|
959
973
|
|
|
960
974
|
fn last_native_upload_finished() -> &'static Mutex<Option<NativeUploadFinishedPayload>> {
|
|
961
975
|
LAST_NATIVE_UPLOAD_FINISHED.get_or_init(|| Mutex::new(None))
|
|
@@ -965,6 +979,36 @@ fn claimed_native_upload_open() -> &'static Mutex<Option<String>> {
|
|
|
965
979
|
CLAIMED_NATIVE_UPLOAD_OPEN.get_or_init(|| Mutex::new(None))
|
|
966
980
|
}
|
|
967
981
|
|
|
982
|
+
fn cancelled_native_upload_retries() -> &'static Mutex<BTreeSet<String>> {
|
|
983
|
+
CANCELLED_NATIVE_UPLOAD_RETRIES.get_or_init(|| Mutex::new(BTreeSet::new()))
|
|
984
|
+
}
|
|
985
|
+
|
|
986
|
+
fn native_upload_retry_cancelled(recording_id: &str) -> bool {
|
|
987
|
+
cancelled_native_upload_retries()
|
|
988
|
+
.lock()
|
|
989
|
+
.map(|cancelled| cancelled.contains(recording_id))
|
|
990
|
+
.unwrap_or(true)
|
|
991
|
+
}
|
|
992
|
+
|
|
993
|
+
fn clear_native_upload_retry_cancelled(recording_id: &str) {
|
|
994
|
+
if let Ok(mut cancelled) = cancelled_native_upload_retries().lock() {
|
|
995
|
+
cancelled.remove(recording_id);
|
|
996
|
+
}
|
|
997
|
+
}
|
|
998
|
+
|
|
999
|
+
fn take_native_upload_retry_cancelled(recording_id: &str) -> bool {
|
|
1000
|
+
cancelled_native_upload_retries()
|
|
1001
|
+
.lock()
|
|
1002
|
+
.map(|mut cancelled| cancelled.remove(recording_id))
|
|
1003
|
+
.unwrap_or(true)
|
|
1004
|
+
}
|
|
1005
|
+
|
|
1006
|
+
async fn wait_for_native_upload_retry_cancel(recording_id: &str) {
|
|
1007
|
+
while !native_upload_retry_cancelled(recording_id) {
|
|
1008
|
+
tokio::time::sleep(Duration::from_millis(100)).await;
|
|
1009
|
+
}
|
|
1010
|
+
}
|
|
1011
|
+
|
|
968
1012
|
fn reset_native_upload_completion_state() {
|
|
969
1013
|
if let Ok(mut last) = last_native_upload_finished().lock() {
|
|
970
1014
|
*last = None;
|
|
@@ -2130,6 +2174,7 @@ pub async fn native_fullscreen_recording_stop_and_upload(
|
|
|
2130
2174
|
|
|
2131
2175
|
match result {
|
|
2132
2176
|
Ok(result) => {
|
|
2177
|
+
clear_native_upload_retry_cancelled(&recording_id);
|
|
2133
2178
|
if !result.verification_pending {
|
|
2134
2179
|
clear_saved_recording_after_success(&app, &saved);
|
|
2135
2180
|
}
|
|
@@ -3636,6 +3681,10 @@ pub async fn native_fullscreen_recording_retry_upload(
|
|
|
3636
3681
|
auth_token: Option<String>,
|
|
3637
3682
|
cookie: Option<String>,
|
|
3638
3683
|
) -> Result<NativeFullscreenUploadResult, String> {
|
|
3684
|
+
if take_native_upload_retry_cancelled(&recording_id) {
|
|
3685
|
+
emit_native_upload_progress(&app, "paused", "Retry cancelled", None, None);
|
|
3686
|
+
return Err(NATIVE_UPLOAD_RETRY_CANCELLED.to_string());
|
|
3687
|
+
}
|
|
3639
3688
|
let mut saved = read_saved_recording_metadata(&app, &recording_id)?;
|
|
3640
3689
|
saved.server_url = server_url.trim_end_matches('/').to_string();
|
|
3641
3690
|
saved.last_attempt_at = Some(now_iso());
|
|
@@ -3658,6 +3707,7 @@ pub async fn native_fullscreen_recording_retry_upload(
|
|
|
3658
3707
|
// second click cannot steal an upload session already owned by this
|
|
3659
3708
|
// local recording.
|
|
3660
3709
|
let retry_plan = match get_native_retry_upload_plan(
|
|
3710
|
+
&app,
|
|
3661
3711
|
&saved.server_url,
|
|
3662
3712
|
&saved.recording_id,
|
|
3663
3713
|
prepared.bytes,
|
|
@@ -3670,16 +3720,18 @@ pub async fn native_fullscreen_recording_retry_upload(
|
|
|
3670
3720
|
{
|
|
3671
3721
|
Ok(plan) => plan,
|
|
3672
3722
|
Err(err) => {
|
|
3673
|
-
|
|
3674
|
-
|
|
3675
|
-
|
|
3676
|
-
|
|
3677
|
-
|
|
3678
|
-
|
|
3679
|
-
|
|
3680
|
-
|
|
3681
|
-
|
|
3682
|
-
|
|
3723
|
+
if err != NATIVE_UPLOAD_RETRY_CANCELLED {
|
|
3724
|
+
interrupt_native_retry_upload(
|
|
3725
|
+
&saved.server_url,
|
|
3726
|
+
&saved.recording_id,
|
|
3727
|
+
&err,
|
|
3728
|
+
Some(&claimed_attempt_id),
|
|
3729
|
+
None,
|
|
3730
|
+
&auth_token,
|
|
3731
|
+
&cookie,
|
|
3732
|
+
)
|
|
3733
|
+
.await;
|
|
3734
|
+
}
|
|
3683
3735
|
cleanup_prepared_saved_recording_files(&prepared, retry_combined_path);
|
|
3684
3736
|
return Err(err);
|
|
3685
3737
|
}
|
|
@@ -3753,19 +3805,25 @@ pub async fn native_fullscreen_recording_retry_upload(
|
|
|
3753
3805
|
{
|
|
3754
3806
|
Ok(reset) => reset,
|
|
3755
3807
|
Err(err) => {
|
|
3756
|
-
|
|
3757
|
-
|
|
3758
|
-
|
|
3759
|
-
|
|
3760
|
-
|
|
3761
|
-
|
|
3762
|
-
|
|
3763
|
-
|
|
3764
|
-
|
|
3765
|
-
|
|
3808
|
+
if err != NATIVE_UPLOAD_RETRY_CANCELLED {
|
|
3809
|
+
interrupt_native_retry_upload(
|
|
3810
|
+
&saved.server_url,
|
|
3811
|
+
&saved.recording_id,
|
|
3812
|
+
&err,
|
|
3813
|
+
active_attempt_id.as_deref(),
|
|
3814
|
+
active_upload_generation_id.as_deref(),
|
|
3815
|
+
&auth_token,
|
|
3816
|
+
&cookie,
|
|
3817
|
+
)
|
|
3818
|
+
.await;
|
|
3819
|
+
}
|
|
3766
3820
|
return Err(err);
|
|
3767
3821
|
}
|
|
3768
3822
|
};
|
|
3823
|
+
let reset = accept_native_retry_reset(
|
|
3824
|
+
reset,
|
|
3825
|
+
native_upload_retry_cancelled(&saved.recording_id),
|
|
3826
|
+
)?;
|
|
3769
3827
|
(reset.mode(), None, reset.upload_generation_id)
|
|
3770
3828
|
}
|
|
3771
3829
|
NativeRetryUploadPlan::Reconcile => unreachable!("handled above"),
|
|
@@ -3814,6 +3872,10 @@ pub async fn native_fullscreen_recording_retry_upload(
|
|
|
3814
3872
|
{
|
|
3815
3873
|
Ok(reset) => {
|
|
3816
3874
|
interruption_upload_generation_id = reset.upload_generation_id.clone();
|
|
3875
|
+
let reset = accept_native_retry_reset(
|
|
3876
|
+
reset,
|
|
3877
|
+
native_upload_retry_cancelled(&saved.recording_id),
|
|
3878
|
+
)?;
|
|
3817
3879
|
upload_prepared_recording_file(
|
|
3818
3880
|
&app,
|
|
3819
3881
|
&prepared,
|
|
@@ -3839,16 +3901,18 @@ pub async fn native_fullscreen_recording_retry_upload(
|
|
|
3839
3901
|
upload_result
|
|
3840
3902
|
};
|
|
3841
3903
|
if let Err(err) = &upload_result {
|
|
3842
|
-
|
|
3843
|
-
|
|
3844
|
-
|
|
3845
|
-
|
|
3846
|
-
|
|
3847
|
-
|
|
3848
|
-
|
|
3849
|
-
|
|
3850
|
-
|
|
3851
|
-
|
|
3904
|
+
if err != NATIVE_UPLOAD_RETRY_CANCELLED {
|
|
3905
|
+
interrupt_native_retry_upload(
|
|
3906
|
+
&saved.server_url,
|
|
3907
|
+
&saved.recording_id,
|
|
3908
|
+
err,
|
|
3909
|
+
replay_attempt_id.as_deref(),
|
|
3910
|
+
interruption_upload_generation_id.as_deref(),
|
|
3911
|
+
&auth_token,
|
|
3912
|
+
&cookie,
|
|
3913
|
+
)
|
|
3914
|
+
.await;
|
|
3915
|
+
}
|
|
3852
3916
|
}
|
|
3853
3917
|
cleanup_prepared_saved_recording_files(&prepared, retry_combined_path);
|
|
3854
3918
|
upload_result
|
|
@@ -3863,6 +3927,11 @@ pub async fn native_fullscreen_recording_retry_upload(
|
|
|
3863
3927
|
Ok(result)
|
|
3864
3928
|
}
|
|
3865
3929
|
Err(err) => {
|
|
3930
|
+
clear_native_upload_retry_cancelled(&recording_id);
|
|
3931
|
+
if err == NATIVE_UPLOAD_RETRY_CANCELLED {
|
|
3932
|
+
emit_native_upload_progress(&app, "paused", "Retry cancelled", None, None);
|
|
3933
|
+
return Err(err);
|
|
3934
|
+
}
|
|
3866
3935
|
if is_moov_corrupt_error(&err) {
|
|
3867
3936
|
saved.corrupt = true;
|
|
3868
3937
|
}
|
|
@@ -3878,6 +3947,15 @@ pub async fn native_fullscreen_recording_retry_upload(
|
|
|
3878
3947
|
}
|
|
3879
3948
|
}
|
|
3880
3949
|
|
|
3950
|
+
#[tauri::command]
|
|
3951
|
+
pub fn native_fullscreen_recording_cancel_retry(recording_id: String) -> Result<(), String> {
|
|
3952
|
+
cancelled_native_upload_retries()
|
|
3953
|
+
.lock()
|
|
3954
|
+
.map_err(|_| "native upload retry cancellation state is unavailable".to_string())?
|
|
3955
|
+
.insert(recording_id);
|
|
3956
|
+
Ok(())
|
|
3957
|
+
}
|
|
3958
|
+
|
|
3881
3959
|
#[tauri::command]
|
|
3882
3960
|
pub async fn native_fullscreen_recording_mark_upload_error(
|
|
3883
3961
|
app: AppHandle,
|
|
@@ -5554,7 +5632,8 @@ async fn upload_prepared_recording_file(
|
|
|
5554
5632
|
let mut buffer = vec![0_u8; UPLOAD_CHUNK_BYTES];
|
|
5555
5633
|
file.read_exact(&mut buffer)
|
|
5556
5634
|
.map_err(|e| format!("native recording read failed: {e}"))?;
|
|
5557
|
-
|
|
5635
|
+
tokio::select! {
|
|
5636
|
+
result = send_upload_post_with_attempt(
|
|
5558
5637
|
&client,
|
|
5559
5638
|
&server_url,
|
|
5560
5639
|
&recording_id,
|
|
@@ -5575,8 +5654,11 @@ async fn upload_prepared_recording_file(
|
|
|
5575
5654
|
upload_attempt_id,
|
|
5576
5655
|
upload_generation_id,
|
|
5577
5656
|
buffer,
|
|
5578
|
-
|
|
5579
|
-
|
|
5657
|
+
) => result,
|
|
5658
|
+
_ = wait_for_native_upload_retry_cancel(&recording_id) => {
|
|
5659
|
+
Err(NATIVE_UPLOAD_RETRY_CANCELLED.to_string())
|
|
5660
|
+
}
|
|
5661
|
+
}?;
|
|
5580
5662
|
emit_native_upload_progress(
|
|
5581
5663
|
app,
|
|
5582
5664
|
"uploading",
|
|
@@ -5599,7 +5681,8 @@ async fn upload_prepared_recording_file(
|
|
|
5599
5681
|
None,
|
|
5600
5682
|
Some(streaming_full_chunks as f32 / total_posts as f32),
|
|
5601
5683
|
);
|
|
5602
|
-
verification_pending =
|
|
5684
|
+
verification_pending = tokio::select! {
|
|
5685
|
+
result = send_upload_post_with_attempt(
|
|
5603
5686
|
&client,
|
|
5604
5687
|
&server_url,
|
|
5605
5688
|
&recording_id,
|
|
@@ -5620,8 +5703,11 @@ async fn upload_prepared_recording_file(
|
|
|
5620
5703
|
upload_attempt_id,
|
|
5621
5704
|
upload_generation_id,
|
|
5622
5705
|
final_body,
|
|
5623
|
-
|
|
5624
|
-
|
|
5706
|
+
) => result,
|
|
5707
|
+
_ = wait_for_native_upload_retry_cancel(&recording_id) => {
|
|
5708
|
+
Err(NATIVE_UPLOAD_RETRY_CANCELLED.to_string())
|
|
5709
|
+
}
|
|
5710
|
+
}?;
|
|
5625
5711
|
} else {
|
|
5626
5712
|
for index in 0..total_chunks {
|
|
5627
5713
|
let mut buffer = vec![0_u8; UPLOAD_CHUNK_BYTES];
|
|
@@ -5632,7 +5718,8 @@ async fn upload_prepared_recording_file(
|
|
|
5632
5718
|
return Err("Native recording ended before all chunks were read.".into());
|
|
5633
5719
|
}
|
|
5634
5720
|
buffer.truncate(read);
|
|
5635
|
-
|
|
5721
|
+
tokio::select! {
|
|
5722
|
+
result = send_upload_post_with_attempt(
|
|
5636
5723
|
&client,
|
|
5637
5724
|
&server_url,
|
|
5638
5725
|
&recording_id,
|
|
@@ -5653,8 +5740,11 @@ async fn upload_prepared_recording_file(
|
|
|
5653
5740
|
upload_attempt_id,
|
|
5654
5741
|
upload_generation_id,
|
|
5655
5742
|
buffer,
|
|
5656
|
-
|
|
5657
|
-
|
|
5743
|
+
) => result,
|
|
5744
|
+
_ = wait_for_native_upload_retry_cancel(&recording_id) => {
|
|
5745
|
+
Err(NATIVE_UPLOAD_RETRY_CANCELLED.to_string())
|
|
5746
|
+
}
|
|
5747
|
+
}?;
|
|
5658
5748
|
emit_native_upload_progress(
|
|
5659
5749
|
app,
|
|
5660
5750
|
"uploading",
|
|
@@ -5671,7 +5761,8 @@ async fn upload_prepared_recording_file(
|
|
|
5671
5761
|
None,
|
|
5672
5762
|
Some(total_chunks as f32 / total_posts as f32),
|
|
5673
5763
|
);
|
|
5674
|
-
verification_pending =
|
|
5764
|
+
verification_pending = tokio::select! {
|
|
5765
|
+
result = send_upload_post_with_attempt(
|
|
5675
5766
|
&client,
|
|
5676
5767
|
&server_url,
|
|
5677
5768
|
&recording_id,
|
|
@@ -5692,8 +5783,11 @@ async fn upload_prepared_recording_file(
|
|
|
5692
5783
|
upload_attempt_id,
|
|
5693
5784
|
upload_generation_id,
|
|
5694
5785
|
Vec::new(),
|
|
5695
|
-
|
|
5696
|
-
|
|
5786
|
+
) => result,
|
|
5787
|
+
_ = wait_for_native_upload_retry_cancel(&recording_id) => {
|
|
5788
|
+
Err(NATIVE_UPLOAD_RETRY_CANCELLED.to_string())
|
|
5789
|
+
}
|
|
5790
|
+
}?;
|
|
5697
5791
|
}
|
|
5698
5792
|
|
|
5699
5793
|
emit_native_upload_progress(app, "opening", "Uploading clip", None, Some(1.0));
|
|
@@ -5708,6 +5802,7 @@ async fn upload_prepared_recording_file(
|
|
|
5708
5802
|
}
|
|
5709
5803
|
|
|
5710
5804
|
async fn get_native_retry_upload_plan(
|
|
5805
|
+
app: &AppHandle,
|
|
5711
5806
|
server_url: &str,
|
|
5712
5807
|
recording_id: &str,
|
|
5713
5808
|
local_bytes: u64,
|
|
@@ -5735,30 +5830,104 @@ async fn get_native_retry_upload_plan(
|
|
|
5735
5830
|
if !cookie.trim().is_empty() {
|
|
5736
5831
|
request = request.header("Cookie", cookie.trim());
|
|
5737
5832
|
}
|
|
5738
|
-
let
|
|
5739
|
-
|
|
5740
|
-
|
|
5741
|
-
|
|
5742
|
-
|
|
5743
|
-
|
|
5744
|
-
|
|
5745
|
-
|
|
5746
|
-
|
|
5747
|
-
|
|
5833
|
+
let deadline = tokio::time::Instant::now() + Duration::from_secs(5 * 60);
|
|
5834
|
+
loop {
|
|
5835
|
+
if native_upload_retry_cancelled(recording_id) {
|
|
5836
|
+
return Err(NATIVE_UPLOAD_RETRY_CANCELLED.to_string());
|
|
5837
|
+
}
|
|
5838
|
+
let response = tokio::select! {
|
|
5839
|
+
response = request
|
|
5840
|
+
.try_clone()
|
|
5841
|
+
.ok_or_else(|| "native recording resume request could not be retried".to_string())?
|
|
5842
|
+
.send() => response.map_err(|e| format!("native recording resume check failed: {e}"))?,
|
|
5843
|
+
_ = wait_for_native_upload_retry_cancel(recording_id) => {
|
|
5844
|
+
return Err(NATIVE_UPLOAD_RETRY_CANCELLED.to_string());
|
|
5845
|
+
}
|
|
5846
|
+
};
|
|
5847
|
+
let status = response.status();
|
|
5848
|
+
let body = response.text().await.unwrap_or_default();
|
|
5849
|
+
let parsed = serde_json::from_str::<NativeUploadResumeResponse>(&body);
|
|
5850
|
+
if !status.is_success() {
|
|
5851
|
+
if let Ok(conflict) = &parsed {
|
|
5852
|
+
if let Some(delay) = native_retry_conflict_delay(conflict) {
|
|
5853
|
+
if tokio::time::Instant::now() + delay <= deadline {
|
|
5854
|
+
emit_native_upload_progress(
|
|
5855
|
+
app,
|
|
5856
|
+
"uploading",
|
|
5857
|
+
"Waiting for prior retry",
|
|
5858
|
+
None,
|
|
5859
|
+
None,
|
|
5860
|
+
);
|
|
5861
|
+
tokio::select! {
|
|
5862
|
+
_ = tokio::time::sleep(delay) => {}
|
|
5863
|
+
_ = wait_for_native_upload_retry_cancel(recording_id) => {
|
|
5864
|
+
return Err(NATIVE_UPLOAD_RETRY_CANCELLED.to_string());
|
|
5865
|
+
}
|
|
5866
|
+
}
|
|
5867
|
+
continue;
|
|
5868
|
+
}
|
|
5869
|
+
return Err(
|
|
5870
|
+
"Another upload retry is still active. Wait a moment and try again"
|
|
5871
|
+
.to_string(),
|
|
5872
|
+
);
|
|
5873
|
+
}
|
|
5874
|
+
if conflict.reason.as_deref() == Some("retry_claim_liveness_unavailable") {
|
|
5875
|
+
return Err(
|
|
5876
|
+
"Clips could not verify whether another retry is active".to_string()
|
|
5877
|
+
);
|
|
5878
|
+
}
|
|
5879
|
+
}
|
|
5880
|
+
return Err(format!("native recording resume check failed ({status})"));
|
|
5881
|
+
}
|
|
5882
|
+
let response = parsed.map_err(|_| {
|
|
5883
|
+
"native recording resume check returned an unreadable response".to_string()
|
|
5884
|
+
})?;
|
|
5885
|
+
if response.resumable && response.attempt_id.as_deref() != Some(claimed_attempt_id) {
|
|
5886
|
+
return Err(
|
|
5887
|
+
"native recording resume check did not acknowledge its attempt claim".to_string(),
|
|
5888
|
+
);
|
|
5889
|
+
}
|
|
5890
|
+
let recovery_enabled = response.recovery_enabled;
|
|
5891
|
+
let rollback_attempt_id = response.attempt_id.clone();
|
|
5892
|
+
let rollback_generation_id = response.upload_generation_id.clone();
|
|
5893
|
+
return Ok(preserve_native_retry_fence_during_rollback(
|
|
5894
|
+
plan_native_retry_upload(response, local_bytes, exact_local_stream),
|
|
5895
|
+
recovery_enabled,
|
|
5896
|
+
rollback_attempt_id,
|
|
5897
|
+
rollback_generation_id,
|
|
5748
5898
|
));
|
|
5749
5899
|
}
|
|
5750
|
-
|
|
5751
|
-
|
|
5752
|
-
|
|
5753
|
-
|
|
5754
|
-
|
|
5755
|
-
|
|
5900
|
+
}
|
|
5901
|
+
|
|
5902
|
+
fn preserve_native_retry_fence_during_rollback(
|
|
5903
|
+
mut plan: NativeRetryUploadPlan,
|
|
5904
|
+
recovery_enabled: bool,
|
|
5905
|
+
acknowledged_attempt_id: Option<String>,
|
|
5906
|
+
upload_generation_id: Option<String>,
|
|
5907
|
+
) -> NativeRetryUploadPlan {
|
|
5908
|
+
if !recovery_enabled {
|
|
5909
|
+
if let NativeRetryUploadPlan::Restart {
|
|
5910
|
+
attempt_id,
|
|
5911
|
+
upload_generation_id: planned_generation_id,
|
|
5912
|
+
} = &mut plan
|
|
5913
|
+
{
|
|
5914
|
+
*attempt_id = acknowledged_attempt_id;
|
|
5915
|
+
*planned_generation_id = upload_generation_id;
|
|
5916
|
+
}
|
|
5756
5917
|
}
|
|
5757
|
-
|
|
5758
|
-
|
|
5759
|
-
|
|
5760
|
-
|
|
5761
|
-
|
|
5918
|
+
plan
|
|
5919
|
+
}
|
|
5920
|
+
|
|
5921
|
+
fn native_retry_conflict_delay(response: &NativeUploadResumeResponse) -> Option<Duration> {
|
|
5922
|
+
if response.resumable
|
|
5923
|
+
|| !response.recovery_enabled
|
|
5924
|
+
|| response.reason.as_deref() != Some("retry_already_active")
|
|
5925
|
+
{
|
|
5926
|
+
return None;
|
|
5927
|
+
}
|
|
5928
|
+
response
|
|
5929
|
+
.retry_after_ms
|
|
5930
|
+
.map(|delay| Duration::from_millis(delay.clamp(250, 30_000)))
|
|
5762
5931
|
}
|
|
5763
5932
|
|
|
5764
5933
|
#[derive(Deserialize)]
|
|
@@ -5944,14 +6113,55 @@ fn native_retry_interruption_payload(
|
|
|
5944
6113
|
#[cfg(test)]
|
|
5945
6114
|
mod native_retry_upload_plan_tests {
|
|
5946
6115
|
use super::{
|
|
5947
|
-
|
|
5948
|
-
|
|
5949
|
-
|
|
5950
|
-
|
|
5951
|
-
|
|
5952
|
-
|
|
6116
|
+
accept_native_retry_reset, is_native_upload_restart_required,
|
|
6117
|
+
is_native_upload_unfenced_restart_required, native_fullscreen_recording_cancel_retry,
|
|
6118
|
+
native_replay_attempt_id, native_retry_attempt_id, native_retry_conflict_delay,
|
|
6119
|
+
native_retry_interruption_payload, native_upload_retry_cancelled, plan_native_retry_upload,
|
|
6120
|
+
preserve_native_retry_fence_during_rollback, saved_native_retry_attempt_id,
|
|
6121
|
+
take_native_upload_retry_cancelled, upload_url, NativeFullscreenUploadResult,
|
|
6122
|
+
NativeRetryUploadPlan, NativeUploadResetResponse, NativeUploadResumeResponse,
|
|
6123
|
+
NATIVE_UPLOAD_RESTART_REQUIRED, NATIVE_UPLOAD_RETRY_CANCELLED,
|
|
6124
|
+
NATIVE_UPLOAD_UNFENCED_RESTART_REQUIRED, UPLOAD_CHUNK_BYTES,
|
|
5953
6125
|
};
|
|
5954
6126
|
|
|
6127
|
+
#[test]
|
|
6128
|
+
fn consumes_a_cancellation_that_arrives_before_retry_startup() {
|
|
6129
|
+
let recording_id = "pre-start-cancel-recording".to_string();
|
|
6130
|
+
assert!(!take_native_upload_retry_cancelled(&recording_id));
|
|
6131
|
+
|
|
6132
|
+
native_fullscreen_recording_cancel_retry(recording_id.clone())
|
|
6133
|
+
.expect("record pre-start cancellation");
|
|
6134
|
+
assert!(native_upload_retry_cancelled(&recording_id));
|
|
6135
|
+
assert!(take_native_upload_retry_cancelled(&recording_id));
|
|
6136
|
+
assert!(!native_upload_retry_cancelled(&recording_id));
|
|
6137
|
+
}
|
|
6138
|
+
|
|
6139
|
+
#[test]
|
|
6140
|
+
fn cancellation_after_a_committed_reset_preserves_the_authoritative_response() {
|
|
6141
|
+
let reset = NativeUploadResetResponse {
|
|
6142
|
+
upload_mode: Some("streaming".to_string()),
|
|
6143
|
+
upload_generation_id: Some("generation-after-reset".to_string()),
|
|
6144
|
+
};
|
|
6145
|
+
|
|
6146
|
+
assert_eq!(
|
|
6147
|
+
accept_native_retry_reset(reset, true),
|
|
6148
|
+
Err(NATIVE_UPLOAD_RETRY_CANCELLED.to_string())
|
|
6149
|
+
);
|
|
6150
|
+
assert_eq!(
|
|
6151
|
+
accept_native_retry_reset(
|
|
6152
|
+
NativeUploadResetResponse {
|
|
6153
|
+
upload_mode: Some("streaming".to_string()),
|
|
6154
|
+
upload_generation_id: Some("generation-after-reset".to_string()),
|
|
6155
|
+
},
|
|
6156
|
+
false,
|
|
6157
|
+
)
|
|
6158
|
+
.expect("retry may re-enter the committed reset fence")
|
|
6159
|
+
.upload_generation_id
|
|
6160
|
+
.as_deref(),
|
|
6161
|
+
Some("generation-after-reset")
|
|
6162
|
+
);
|
|
6163
|
+
}
|
|
6164
|
+
|
|
5955
6165
|
fn response(bytes_received: u64, next_chunk_index: u64) -> NativeUploadResumeResponse {
|
|
5956
6166
|
NativeUploadResumeResponse {
|
|
5957
6167
|
resumable: true,
|
|
@@ -5962,6 +6172,8 @@ mod native_retry_upload_plan_tests {
|
|
|
5962
6172
|
next_chunk_index: Some(next_chunk_index),
|
|
5963
6173
|
attempt_id: Some("attempt-1".to_string()),
|
|
5964
6174
|
upload_generation_id: Some("generation-1".to_string()),
|
|
6175
|
+
reason: None,
|
|
6176
|
+
retry_after_ms: None,
|
|
5965
6177
|
}
|
|
5966
6178
|
}
|
|
5967
6179
|
|
|
@@ -6013,6 +6225,8 @@ mod native_retry_upload_plan_tests {
|
|
|
6013
6225
|
next_chunk_index: None,
|
|
6014
6226
|
attempt_id: Some("ignored-attempt".to_string()),
|
|
6015
6227
|
upload_generation_id: Some("ignored-generation".to_string()),
|
|
6228
|
+
reason: Some("feature_disabled".to_string()),
|
|
6229
|
+
retry_after_ms: None,
|
|
6016
6230
|
},
|
|
6017
6231
|
UPLOAD_CHUNK_BYTES as u64,
|
|
6018
6232
|
true,
|
|
@@ -6027,6 +6241,66 @@ mod native_retry_upload_plan_tests {
|
|
|
6027
6241
|
));
|
|
6028
6242
|
}
|
|
6029
6243
|
|
|
6244
|
+
#[test]
|
|
6245
|
+
fn preserves_an_existing_fence_when_resumable_retry_is_disabled() {
|
|
6246
|
+
let plan = preserve_native_retry_fence_during_rollback(
|
|
6247
|
+
NativeRetryUploadPlan::Restart {
|
|
6248
|
+
attempt_id: None,
|
|
6249
|
+
upload_generation_id: None,
|
|
6250
|
+
},
|
|
6251
|
+
false,
|
|
6252
|
+
Some("attempt-1".to_string()),
|
|
6253
|
+
Some("generation-1".to_string()),
|
|
6254
|
+
);
|
|
6255
|
+
assert!(matches!(
|
|
6256
|
+
plan,
|
|
6257
|
+
NativeRetryUploadPlan::Restart {
|
|
6258
|
+
attempt_id: Some(attempt_id),
|
|
6259
|
+
upload_generation_id: Some(generation_id),
|
|
6260
|
+
} if attempt_id == "attempt-1" && generation_id == "generation-1"
|
|
6261
|
+
));
|
|
6262
|
+
}
|
|
6263
|
+
|
|
6264
|
+
#[test]
|
|
6265
|
+
fn keeps_an_unacknowledged_legacy_restart_unfenced_when_resumable_retry_is_disabled() {
|
|
6266
|
+
let plan = preserve_native_retry_fence_during_rollback(
|
|
6267
|
+
NativeRetryUploadPlan::Restart {
|
|
6268
|
+
attempt_id: None,
|
|
6269
|
+
upload_generation_id: None,
|
|
6270
|
+
},
|
|
6271
|
+
false,
|
|
6272
|
+
None,
|
|
6273
|
+
None,
|
|
6274
|
+
);
|
|
6275
|
+
assert!(matches!(
|
|
6276
|
+
plan,
|
|
6277
|
+
NativeRetryUploadPlan::Restart {
|
|
6278
|
+
attempt_id: None,
|
|
6279
|
+
upload_generation_id: None,
|
|
6280
|
+
}
|
|
6281
|
+
));
|
|
6282
|
+
}
|
|
6283
|
+
|
|
6284
|
+
#[test]
|
|
6285
|
+
fn preserves_an_acknowledged_legacy_attempt_without_a_generation() {
|
|
6286
|
+
let plan = preserve_native_retry_fence_during_rollback(
|
|
6287
|
+
NativeRetryUploadPlan::Restart {
|
|
6288
|
+
attempt_id: None,
|
|
6289
|
+
upload_generation_id: None,
|
|
6290
|
+
},
|
|
6291
|
+
false,
|
|
6292
|
+
Some("attempt-1".to_string()),
|
|
6293
|
+
None,
|
|
6294
|
+
);
|
|
6295
|
+
assert!(matches!(
|
|
6296
|
+
plan,
|
|
6297
|
+
NativeRetryUploadPlan::Restart {
|
|
6298
|
+
attempt_id: Some(attempt_id),
|
|
6299
|
+
upload_generation_id: None,
|
|
6300
|
+
} if attempt_id == "attempt-1"
|
|
6301
|
+
));
|
|
6302
|
+
}
|
|
6303
|
+
|
|
6030
6304
|
#[test]
|
|
6031
6305
|
fn reconciles_terminal_resume_without_an_attempt_echo() {
|
|
6032
6306
|
let terminal = plan_native_retry_upload(
|
|
@@ -6039,6 +6313,8 @@ mod native_retry_upload_plan_tests {
|
|
|
6039
6313
|
next_chunk_index: None,
|
|
6040
6314
|
attempt_id: None,
|
|
6041
6315
|
upload_generation_id: None,
|
|
6316
|
+
reason: None,
|
|
6317
|
+
retry_after_ms: None,
|
|
6042
6318
|
},
|
|
6043
6319
|
UPLOAD_CHUNK_BYTES as u64,
|
|
6044
6320
|
true,
|
|
@@ -6064,6 +6340,8 @@ mod native_retry_upload_plan_tests {
|
|
|
6064
6340
|
next_chunk_index: Some(0),
|
|
6065
6341
|
attempt_id: Some(claimed_attempt_id.clone()),
|
|
6066
6342
|
upload_generation_id: Some("generation-1".to_string()),
|
|
6343
|
+
reason: None,
|
|
6344
|
+
retry_after_ms: None,
|
|
6067
6345
|
},
|
|
6068
6346
|
UPLOAD_CHUNK_BYTES as u64,
|
|
6069
6347
|
true,
|
|
@@ -6108,6 +6386,30 @@ mod native_retry_upload_plan_tests {
|
|
|
6108
6386
|
assert_eq!(saved_attempt_id.as_deref(), Some(first.as_str()));
|
|
6109
6387
|
}
|
|
6110
6388
|
|
|
6389
|
+
#[test]
|
|
6390
|
+
fn waits_only_for_a_typed_bounded_retry_conflict() {
|
|
6391
|
+
let conflict = NativeUploadResumeResponse {
|
|
6392
|
+
resumable: false,
|
|
6393
|
+
recovery_enabled: true,
|
|
6394
|
+
status: Some("uploading".to_string()),
|
|
6395
|
+
upload_mode: None,
|
|
6396
|
+
bytes_received: None,
|
|
6397
|
+
next_chunk_index: None,
|
|
6398
|
+
attempt_id: None,
|
|
6399
|
+
upload_generation_id: None,
|
|
6400
|
+
reason: Some("retry_already_active".to_string()),
|
|
6401
|
+
retry_after_ms: Some(60_000),
|
|
6402
|
+
};
|
|
6403
|
+
assert_eq!(
|
|
6404
|
+
native_retry_conflict_delay(&conflict),
|
|
6405
|
+
Some(std::time::Duration::from_secs(30))
|
|
6406
|
+
);
|
|
6407
|
+
|
|
6408
|
+
let mut untyped = conflict;
|
|
6409
|
+
untyped.retry_after_ms = None;
|
|
6410
|
+
assert_eq!(native_retry_conflict_delay(&untyped), None);
|
|
6411
|
+
}
|
|
6412
|
+
|
|
6111
6413
|
#[test]
|
|
6112
6414
|
fn reports_retry_interruptions_with_or_without_a_fencing_claim() {
|
|
6113
6415
|
let unfenced = native_retry_interruption_payload("upload failed", None, None);
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import {
|
|
2
|
+
compareAndSetAppState,
|
|
2
3
|
deleteAppState,
|
|
3
4
|
readAppState,
|
|
4
5
|
writeAppState,
|
|
@@ -10,6 +11,20 @@ export interface StoredResumableSession {
|
|
|
10
11
|
meta: Record<string, unknown>;
|
|
11
12
|
bytesUploaded: number;
|
|
12
13
|
lastCommittedIndex?: number;
|
|
14
|
+
providerClosed?: boolean;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export async function compareAndSetResumableSession(
|
|
18
|
+
recordingId: string,
|
|
19
|
+
expected: StoredResumableSession,
|
|
20
|
+
next: StoredResumableSession,
|
|
21
|
+
generationId?: string | null,
|
|
22
|
+
): Promise<boolean> {
|
|
23
|
+
return compareAndSetAppState(
|
|
24
|
+
key(recordingId, generationId),
|
|
25
|
+
expected as unknown as Record<string, unknown>,
|
|
26
|
+
next as unknown as Record<string, unknown>,
|
|
27
|
+
);
|
|
13
28
|
}
|
|
14
29
|
|
|
15
30
|
const key = (recordingId: string, generationId?: string | null) =>
|