Repository navigation
fix(autosave): say which open project needs a decision instead of reporting an error - #9861
Conversation
…orting an error A save that leaves a project with a sync conflict or schema refusal now shows "Needs attention", and the tooltip and project switcher name the project and the reason. Other leftover pending work retries instead of sticking; every chip status is translated, and session-save failures are logged.
lstein
left a comment
There was a problem hiding this comment.
Reviewed at 86cf47f. The UI side is sound. lint:tsc, the full unit suite (10,177) and the new browser tests pass. Two things need fixing before merge.
Blocking
1. A partly failed delete now starts an endless 30-second autosave loop.
The new rule in persistenceRuntime.ts:260 retries whenever hasPendingChanges && !awaitsDecision. scheduleRetry has no attempt cap, and retryAttempt resets only on a successful save. If a pendingProjectIds entry can never clear, the runtime now re-saves every 30s forever; on main it stopped in a stuck state.
One reachable sequence:
- Create a project while the backend returns 503. The push is
unsynced, so the project is pending. - Delete it. The server delete succeeds (a 404 is accepted too).
- Removing the local draft fails, and
durableSyncedPersistence.ts:2034or:2041throws "…Retry deletion". The id stays indeletedProjectIds. The project stays open, becausedeleteLibraryProjectthrows before the close. - Every later
saveWorkbenchgoes throughpushProjectPlan(durableSyncedPersistence.ts:~1389). It returnssupersededfor that project without removing it frompendingProjectIds.pushStagedProject:1177does remove it.
From then on, each save returns hasPendingChanges: true, awaitsDecision: false, error: null, shouldRetry: false. A throwaway test against the real service got that result on three consecutive saves. Each cycle stages, saves the session and logs a persistence.autosave-pending warning. The chip reads "Unsaved changes" permanently.
Fix: pendingProjectIds.delete(plan.project.id) in that deletedProjectIds early return.
2. awaitsDecision is not tested where it is computed.
Replacing awaitsDecision: conflicts.size > 0 || schemaRefusals.size > 0 with awaitsDecision: false in durableSyncedPersistence.ts leaves all 10,177 unit tests passing. With that change, a real conflict retries forever instead of showing "Needs attention". The only coverage is persistenceRuntime.test.ts, and it supplies the flag by hand. Please add a durableSyncedPersistence.test.ts case that drives a real revision conflict, and one for a schema refusal, and checks awaitsDecision and hasPendingChanges on the save result.
The runtime branch itself is well covered: removing either new clause fails a test.
Non-blocking
- One project's decision still blocks retries for the others.
awaitsDecisionis global (conflicts.size > 0 || schemaRefusals.size > 0). Suppose P is conflicted and Q's push is acknowledged while a newer Q draft is staged, sosettleAckre-adds Q as pending (:1100–1103). The result then hasawaitsDecision: true, so nothing retries. Q's newer draft stays local until the next edit, and the chip names only P. The PR lists this as a limitation, but it is the same bug this PR fixes, and a per-project check would close it: retry when some pending id is in neitherconflictsnorschemaRefusals. - "Needs attention" can name no project.
deleteProjectOnServerclearsconflictsbut notschemaRefusals(:2045–2050). The library broker closes the tab with a bareprojects.close, andreleaseProjectSyncruns only afterdeleteLibraryProject's network awaits. Deleting a schema-refused project can therefore let the scheduled autosave land first and setattention. Nothing re-saves after the release, so the chip says "Needs attention" with an empty tooltip and no switcher marker until the next edit. It is rare, andmainhas the same problem as a stuck error.schemaRefusals.delete(resolvedProjectId)next to theconflicts.deletefixes it. - With finding 1, the new retry-path warning is logged every 30s; capping or deduping it would keep the log useful.
Checked, no issue
- Selectors:
useProjectSyncAttentionand the tooltip's names-by-id selector return new objects, but both stores default to shallow equality, so there is no render loop. - Workflow toolbar: a retrying save now shows
pending, noterror.attentionfalls through to the active project's own sync info, which is correct. The pending error text is not rendered there. - Clearing after a decision: use-server (bumps
documentRevision), discard (closes) and save-as-new (retargets) all change persisted state, so a save runs and the chip clears. - Pending ids that cannot clear (none found): hydrate or adopt without open, copy/retarget targets, close with a conflict (refused by the flush), account change (new service,
clearWorkbenchclears maps), and offline/5xx (alreadyshouldRetryonmain). - Runtime: a new edit resets
retryAttemptand replaces the timer. A stale retry is dropped, a closed-session save is held, and the exit/hide paths are unchanged.
…after a partly failed delete A save that supersedes a deleted project's pending push now clears it, so a delete whose local cleanup failed no longer keeps autosave retrying every 30 s; deleting a project also forgets its schema refusal. The save result reports retryable pending work separately from decisions, so one project's conflict no longer blocks another project's push, and attention is shown once nothing retryable is left. The autosave retry warning is logged once per retry cycle instead of on every attempt.
…n status Editor routes grow about 44 KB over the 2026-10-09 capture (this change plus earlier main growth), which reached the 1% hard ceiling; request counts and source owners are unchanged.
# Conflicts: # invokeai/frontend/webv2/performance/architecture-baseline.json # invokeai/frontend/webv2/performance/browser-baseline.json
Wait for passive effect cleanup after the lazy dialog appears to avoid a timing-dependent browser test failure.
lstein
left a comment
There was a problem hiding this comment.
Re-reviewed at e3e683d. 51c6d8d resolves both blockers and all three non-blockers from the last round.
- Endless retry after a partly failed delete: fixed. The
deletedProjectIdsearly return inpushProjectPlannow clearspendingProjectIds.markProjectDeletedhas no production callers, sodeleteProjectOnServeris the only place the fence is set. Its rollback and everypushProjectPlancaller run throughenqueue, so a failed delete can't race a save into dropping a live project's pending push. awaitsDecisioncoverage: fixed. The newdurableSyncedPersistence.test.tscases drive the real service. I removed each fix line one at a time:awaitsDecision: false, the superseded pending delete, theschemaRefusals.deleteon delete, both halves of thehasRetryablePendingChangespredicate, the runtime branch, the log dedupe, and its reset on success. All nine mutations fail a test.- Per-project retry:
hasRetryablePendingChangeslets one project's decision stop blocking another project's push. - Schema refusal cleared on delete, and the retry warning is logged once per cycle.
The perf-baseline commit is moot now: the baselines match main. The palette test change replaces a one-shot assertion with a poll for the same condition.
lint:tsc is clean, the full unit suite passes (10,642), and the autosave, switcher and palette browser tests pass.
Nit (non-blocking): while another project is still retrying and a conflict exists, the chip reads "Unsaved changes". The tooltip shows only the retrying line, because the attention list renders only when autosave.status === 'attention'. The list appears once the retry settles, and the switcher marks the conflicted project throughout, so this only stays visible while pushes keep failing. Rendering the attention list whenever useProjectSyncAttention() names an open project, whatever the status, would close it.
The autosave tooltip now names conflicted or schema-refused open projects whatever the save status. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Summary
When an open project had a sync conflict (edited in another tab or browser, or deleted elsewhere) or a schema refusal, the status bar read Autosave: error. Its tooltip said only "Autosave requires your attention.", and nothing was logged to the server or the console. The banner that lets you resolve it only appears above the active project, so a conflict in another open project could not be found.
Now:
attentionstatus. A save that finishes while an open project awaits a decision setsattentioninstead oferror. The chip reads Autosave: Needs attention in warning tone. The tooltip lists each affected open project and why: changed elsewhere, deleted on the server, or needs a newer version of Invoke. It keeps the last-saved time, because the other projects did save.Related Issues / Discussions
Reported by a user who saw "Autosave: error" and "Autosave requires attention" with no server or console output and no way to clear it.
QA Instructions
Checks, all run from
invokeai/frontend/webv2:pnpm check:releasepassed on the final logic. It covers lint, architecture, unit (10177), browser (2900), fixtures, performance contracts/build/architecture/browser, project-file journeys, and accessibility.toLocaleString(). For that change, lint,tsc, and the widget browser tests were rerun and passed.New or updated tests:
persistenceRuntime.test.ts: a decision produces attention, not pending. Pending work without a decision retries and then succeeds.workbenchStore.test.ts: retry →pendingwithlastSavedAtkept; attention →attentionwithlastSavedAt; no error notification.AutosaveStatusWidgetView.browser.test.tsx: lists only open projects with a reason; schema reason; retry wording; failure message.ProjectSwitcher.browser.test.tsx: row marker and trigger accessible name.Manual repro, run against the mock backend in Chromium:
PUT /api/v1/projects/fixture-project-001.Review
The review found four issues, now resolved:
attentionwas originally triggered by any leftover pending change, including a draft rebased during an in-flight push, which needs no decision. The save result now carriesawaitsDecision(conflicts or schema refusals). Other pending work retries, and a regression test covers it.lastSavedAt. It is kept now.{{name}} — {{reason}}key now.Remaining limitations:
invalid-server-documentrefusal is described as "needs a newer version of Invoke". The existing banner and Project widget use the same wording.main.Checklist
What's Newcopy (if doing a release after this PR)Need help on this PR? Tag
@codesmith-botwith what you need.