Pending import retry survives an app restart - #82
Conversation
The remaining piece of the #79 dismissal blocker: 'Later' promised a recovery path, but SyncFlow is in-memory — a restart returned to a plain synced menu with the local rows stranded. Engine: the marker entry records pending_retry alongside the outcome (a run that ends with errors sets it; a clean retry clears it), and LocalImportStatus surfaces it — restart-durable via the same atomic marker publish. UI: a one-shot LocalImportStatus probe per attached synced runtime (Ready + Idle flow only, re-armed across runtime replacement) restores the postponed ImportFailed state when the engine reports a recorded pending retry, so the account menu regains its 'Finish sync setup' entry after any restart. Sessions that never had a failure — including 'Start fresh' users, whose marker has no pending flag — see nothing. Regression test at the bootstrap level, per review: real runtimes end to end — seed local work, boot the synced profile from a saved session, fail the import via a planted journal obstruction, stop the runtime, bootstrap a fresh one, and assert LocalImportStatus still reports the pending retry (then that a clean retry on the restarted runtime clears it). The engine failure-injection test also asserts the marker flag both ways. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
wingleeio
left a comment
There was a problem hiding this comment.
Blocking review
The normal partial-item failure path is covered well, but the restart-durable guarantee does not yet hold for several real failure paths:
-
Not every error summary records
pendingRetry.LocalImporter::runcan return through?before reachingrecord_import(open_source, registry reads, etc.); the RPC wrapper then synthesizes an error summary, but no pending marker is written. Also,pending_retryis computed beforerecord_import, so a marker-persistence failure is appended to the summary only after the attempted entry was built withpendingRetry: false—and in practice no new marker was published at all. Thus the existingmarker_persistence_failure_is_an_import_errorcase still loses its retry path after restart, directly contradicting “a run whose summary carries errors sets it.” Please persist pending intent before the fallible import starts and clear it only after a clean terminal outcome, while retaining a discoverable manual recovery route for cases where the marker itself cannot be written. Add coverage for a top-level importer error and the existing marker-write obstruction, asserting the restarted status/UI rather than only the summary. -
A clean no-source retry never clears a prior pending flag. The
open_source() == Nonebranch emits a clean summary and returns beforerecord_import. If a failed import recordedpendingRetry: trueand the local profile or registry is subsequently removed, Retry reports success, but every later boot restoresImportFailedforever. Clear the pending outcome on this clean no-op path and add a regression test. -
The boot probe permanently suppresses recovery after one failed or mistimed status call.
import_status_checkedis set before the RPC, and any RPC/status error is collapsed intopending = falsewithout re-arming. A pending marker paired with an unreadable local store makesLocalImportStatusfail while scanning availability, so the account-menu recovery entry remains hidden for the entire runtime. The same happens if a true result arrives while the flow is briefly non-Idle: it is discarded, but the checked flag stays set. Please return/recover the marker state independently from availability scanning and retry or re-arm failed/deferred probes.
Validation on 01cd61ebccdd6f9ea4a4efb93f1d93136e6779a7:
cargo test -p zeron-engine --test local_import: 8 passed.cargo test -p zeron-ui --lib shell::tests::: 31 passed.cargo fmt --all -- --check: passed.git diff --check: passed.- Prospective merge with current
origin/main: conflict-free. - No malicious, backdoor-like, dependency, or dangerous execution changes found.
This is a blocking, non-approving review.
…nt probe
Three fixes for the durability holes in the previous patch:
1. Intent is ARMED on disk before any fallible import work and cleared
only by a clean terminal outcome. A ?-exit (unreadable source,
registry errors), a per-item failure, or a failed final marker write
all leave the armed flag for the next boot; the arming write itself
failing is the one unpersistable case, covered by the new manual
recovery route below.
2. The no-source path is a clean terminal outcome: it clears a stale
armed flag (deleted local profile no longer haunts every boot) and
reports a clear failure if even that write fails.
3. The boot probe can no longer permanently swallow recovery:
- import_probe_outcome (pure, tested): failed RPCs and pending
results that land while the flow is busy DEFER — the guard re-arms
for a later state change (bounded attempts stop hot loops);
- LocalImportStatus reports marker state unconditionally; the
availability scan is best-effort (zeros on error), so an
unreadable local store cannot hide a recorded pending retry;
- the account menu gains a standing 'Import local work' entry
(synced + idle + importable rows, driven by the probe's
availability counts) — the discoverable recovery route that works
even when the marker itself cannot be written.
Tests: top-level importer error arms pending across a real restart;
clean no-source retry clears a stale flag; status answers with pending
despite an unreadable source; the marker-obstruction case asserts the
restarted status; probe-outcome and manual-row visibility tables.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
All three addressed in 1. Arm first, clear only on clean outcomes. 2. No-source retry clears the flag. The 3. The probe can no longer swallow recovery.
Validation on |
wingleeio
left a comment
There was a problem hiding this comment.
Follow-up blocking review
The arm-first/clean-clear importer changes address the normal and top-level failure paths well. Three recovery/UI gaps remain:
-
The claimed markerless recovery route is hidden in the marker-write failure case.
marker_persistence_failure_is_an_import_errorimports both chats successfully (imported == 2) and then fails only while publishing the marker. After restart,pendingRetryis necessarily false and target deduplication reportsavailableChats == 0/availableSpaces == 0, because the rows already landed.show_manual_import_rowrequires availability greater than zero, so Import local work is not shown. The user therefore still cannot retry the marker publication, and imported transcript attachments lose their read-root grant after restart. The updated test never asserts availability or the menu predicate; its comment even notes the rows are already present. Please base markerless recovery on the missing grant/source state rather than only unimported rows, and add the exact restart-to-visible-menu assertion. -
Deferdoes not actually schedule another probe.maybe_restore_pending_importonly runs fromon_state_changed. When the RPC fails, or a pending result arrives while the flow is temporarily busy, the callback leavesimport_status_checkedunset but neither schedules a bounded retry nor invokes the probe when the flow returns to Idle. If the app state is otherwise stable, there is no later callback and recovery remains hidden indefinitely; the four-attempt limit is never exercised. Please add an actual backoff/retry task or explicitly trigger probing on the relevant Shell flow transitions, with an async lifecycle test rather than only testing the pure enum mapping. -
Successful manual imports leave stale availability cached. A settled probe sets
import_status_checked = trueand cacheslocal_import_available, butspawn_local_importand a clean summary never invalidate or refresh either field. After using Import local work successfully, returning to Idle continues to show that row forever even though the engine now reports zero available items. Clear/re-probe the cache after terminal import outcomes and cover the menu disappearing after success.
Validation on 34277c8ef24ed25e630cd60a4ad5ecef6aef6937:
cargo test -p zeron-engine --test local_import: 11 passed.cargo test -p zeron-ui --lib shell::tests::: 33 passed.cargo fmt --all -- --check: passed.git diff --check: passed.- Prospective merge with current
origin/main: conflict-free. - No malicious, backdoor-like, dependency, or dangerous execution changes found.
This remains a blocking, non-approving review.
Follow-up to #79, addressing the remaining blocker from the final review: the pending-import retry path disappeared on app restart, so "Later" promised a recovery route the next launch couldn't deliver.
What changed
Persisted intent. The
local-import.jsonmarker entry now recordspendingRetryalongside the outcome: a run whose summary carries errors sets it, a clean retry clears it. It rides the same atomic publish (temp file + fsync + rename) added in #79, so it's crash-safe, andLocalImportStatussurfaces it.Restored on boot. The shell runs a one-shot
LocalImportStatusprobe per attached synced runtime (only fromReady+Idleflow, re-armed across runtime replacement). If the engine reports a recorded pending retry, the shell restores the postponedImportFailedstate — the account menu regains its "Finish sync setup" entry and reopening shows the retry dialog, exactly what the pre-restart "Later" promised. Accounts with no recorded failure — including "Start fresh" users, whose marker never gains the flag — see nothing, so there is no boot-time nag for people who declined the import by choice.Testing
Per the review's ask, the regression test is at the bootstrap level, not a reducer over one in-memory
Shell—pending_import_retry_survives_an_app_restartruns real runtimes end to end:EngineHandle::bootstrap),LocalImportStatus.pendingRetryis stilltrueon the restarted runtime and that the restored state's menu action is the reopen entry,The engine failure-injection test from #79 additionally asserts the marker flag both ways at the importer level.
Validation: full workspace suite 848 passed / 0 failed; clippy and
rustfmt --checkclean on touched files.🤖 Generated with Claude Code