Skip to content

fix(autosave): say which open project needs a decision instead of reporting an error - #9861

Merged
joshistoast merged 11 commits into
mainfrom
fix/autosave-attention
Oct 18, 2026
Merged

joshistoast merged 11 commits into
mainfrom
fix/autosave-attention

Conversation

@joshistoast

@joshistoast joshistoast commented Oct 15, 2026 •

Copy link
Copy Markdown
Collaborator

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:

  • New attention status. A save that finishes while an open project awaits a decision sets attention instead of error. 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.
  • Project switcher marker. The switcher marks those projects Needs attention. The trigger shows a warning icon and switches to an accessible-name variant while any open project needs attention.
  • Leftover pending work retries. Pending work that needs no decision, such as a draft staged while its push was in flight, now takes the existing retry path and shows as Unsaved changes. Previously it sat in the same stuck error state. A transient retry is shown as pending, not as an error.
  • Translated labels. Every chip status is translated; the chip used to print the raw status word ("error", "saving", "idle"). The tooltip formats the last-saved time.
  • Workflow toolbar. The toolbar's project save state no longer shows an error for the active project because a different project has a conflict.
  • Session-save logging. Session-save failures are logged as warnings instead of being swallowed.
  • Docs. The Projects user guide ("Saving and syncing") explains the new status-bar and switcher signals.

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:release passed on the final logic. It covers lint, architecture, unit (10177), browser (2900), fixtures, performance contracts/build/architecture/browser, project-file journeys, and accessibility.
  • After it, the only change was formatting the tooltip's last-saved time with 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 → pending with lastSavedAt kept; attention → attention with lastSavedAt; 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:

  1. Open Fixture Project 001 and Fixture Project 002, then switch back to 001.
  2. Bump 001's revision on the server: PUT /api/v1/projects/fixture-project-001.
  3. Type in 001's prompt. The Project changed elsewhere banner appears and the chip reads Autosave: Needs attention.
  4. Switch to 002. The chip still reads Needs attention, and its tooltip lists "Fixture Project 001 — changed elsewhere".
  5. Open the switcher. Its trigger shows the warning icon, and the 001 row reads Needs attention.
  6. Switch to 001 and choose Save as new project. The chip returns to Autosave: Saved and the trigger warning clears.

Review

The review found four issues, now resolved:

  • attention was originally triggered by any leftover pending change, including a draft rebased during an in-flight push, which needs no decision. The save result now carries awaitsDecision (conflicts or schema refusals). Other pending work retries, and a regression test covers it.
  • The attention state dropped lastSavedAt. It is kept now.
  • Tooltip items were assembled from fragments. They use a single translatable {{name}} — {{reason}} key now.
  • The user guide did not describe the new signals. It does now.

Remaining limitations:

  • The chip updates when a save completes. The switcher reads the sync store live, so a conflict detected outside autosave (for example on a flush during rename or export) marks the switcher before the chip.
  • An invalid-server-document refusal is described as "needs a newer version of Invoke". The existing banner and Project widget use the same wording.
  • If one project has a conflict while another has pending work that needs no decision, the status shows attention, and the other project's work is pushed with the next edit. This is unchanged from main.

Checklist

  • The PR has a short but descriptive title, suitable for a changelog
  • Meaningful regression coverage added / updated where needed; obsolete tests/code removed
  • Persisted-state and API changes include required migrations / compatibility validation
  • Relevant performance/efficiency opportunities considered; material claims have evidence
  • Material review findings resolved and relevant checks rerun
  • Documentation added / updated (if applicable)
  • Updated What's New copy (if doing a release after this PR)

View with [code]smith Autofix with [code]smith
Need help on this PR? Tag @codesmith-bot with what you need.

…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.
@github-actions github-actions Bot added frontend PRs that change frontend files docs PRs that change docs labels Oct 15, 2026
@joshistoast
joshistoast requested a review from lstein October 15, 2026 03:52
@lstein lstein self-assigned this Oct 16, 2026
@lstein lstein added the 7.0.0 label Oct 16, 2026
@lstein lstein moved this to 7.0 Theme: Modular Design in Invoke - Community Roadmap Oct 16, 2026

@lstein lstein left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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:

  1. Create a project while the backend returns 503. The push is unsynced, so the project is pending.
  2. Delete it. The server delete succeeds (a 404 is accepted too).
  3. Removing the local draft fails, and durableSyncedPersistence.ts:2034 or :2041 throws "…Retry deletion". The id stays in deletedProjectIds. The project stays open, because deleteLibraryProject throws before the close.
  4. Every later saveWorkbench goes through pushProjectPlan (durableSyncedPersistence.ts:~1389). It returns superseded for that project without removing it from pendingProjectIds. pushStagedProject:1177 does 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. awaitsDecision is global (conflicts.size > 0 || schemaRefusals.size > 0). Suppose P is conflicted and Q's push is acknowledged while a newer Q draft is staged, so settleAck re-adds Q as pending (:1100–1103). The result then has awaitsDecision: 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 neither conflicts nor schemaRefusals.
  • "Needs attention" can name no project. deleteProjectOnServer clears conflicts but not schemaRefusals (:2045–2050). The library broker closes the tab with a bare projects.close, and releaseProjectSync runs only after deleteLibraryProject's network awaits. Deleting a schema-refused project can therefore let the scheduled autosave land first and set attention. 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, and main has the same problem as a stuck error. schemaRefusals.delete(resolvedProjectId) next to the conflicts.delete fixes 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: useProjectSyncAttention and 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, not error. attention falls 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, clearWorkbench clears maps), and offline/5xx (already shouldRetry on main).
  • Runtime: a new edit resets retryAttempt and 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.
@joshistoast
joshistoast requested a review from lstein October 17, 2026 22:05
@joshistoast
joshistoast enabled auto-merge (squash) October 17, 2026 22:07
# 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 lstein left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 deletedProjectIds early return in pushProjectPlan now clears pendingProjectIds. markProjectDeleted has no production callers, so deleteProjectOnServer is the only place the fence is set. Its rollback and every pushProjectPlan caller run through enqueue, so a failed delete can't race a save into dropping a live project's pending push.
  • awaitsDecision coverage: fixed. The new durableSyncedPersistence.test.ts cases drive the real service. I removed each fix line one at a time: awaitsDecision: false, the superseded pending delete, the schemaRefusals.delete on delete, both halves of the hasRetryablePendingChanges predicate, the runtime branch, the log dedupe, and its reset on success. All nine mutations fail a test.
  • Per-project retry: hasRetryablePendingChanges lets 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.

joshistoast and others added 2 commits October 18, 2026 14:42
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>
@joshistoast
joshistoast merged commit 3d69753 into main Oct 18, 2026
20 checks passed
@joshistoast
joshistoast deleted the fix/autosave-attention branch October 18, 2026 22:06
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

7.0.0 docs PRs that change docs frontend PRs that change frontend files

Projects

Status: 7.0 Theme: Modular Design

Development

Successfully merging this pull request may close these issues.

2 participants