fix(images): harden staged artifact recovery - #44
Conversation
There was a problem hiding this comment.
Important
The degradation logic is correct for its stated intent, but the fail-closed state has no recovery path and its block is app-wide with a message that promises a fix no restart can deliver.
Reviewed changes
- Degrades staging failures instead of aborting startup —
initialize()records anunavailableReasonfor corrupt or unsafe files rather than throwing, so a baddisplay-image-artifacts.jsonno longer crashes the app. - Fails closed on every mutator —
stage/remove/deleteChat/pending/recovernow callrequireAvailable(); startup recovery is guarded byavailability()and wrapped in a warn-leveltry/catch. hasPendingshort-circuits totruewhen unavailable — keeps sends/copies/exports blocked without reading the file.- Stops cloning staged payloads for usage projection —
usageByChatreads cached records directly instead ofstructuredClone-ing the whole store viapending().
⚠️ Degraded state has no recovery path
When the staging file is unreadable or has an unsupported shape, the app now stays up but blocks send/copy/export/delete for every chat, and nothing in the UI lets a user clear or repair display-image-artifacts.json. "Retains staged payloads for repair" describes the on-disk data, not an actual repair mechanism — a restart cannot clear a corrupt file, so a user who hits this is stranded until they locate and delete the file by hand.
Technical details
# No recovery path for the degraded staging state
## Affected sites
- main/services/display-image-artifact-store.ts:332 — `hasPending` returns `true` for all chats when unavailable
- main/handlers/chats.ts:247/332/357/466/572 — send/copy/export/delete all blocked with "Restart Aiden to recover…"
## Required outcome
- A user with a corrupt/unsafe staging file must have some way to recover, or the block messages must not promise a restart that cannot succeed.
## Open questions for the human
- Is a manual delete of `display-image-artifacts.json` the intended recovery path? If so, should the app surface it (e.g. a "Reset image staging" action), or at least log the exact path?
- Is the app-wide fail-closed scope (blocking chats that never had a staged image) the accepted tradeoff, or should the block be narrower?DeepSeek Pro | 𝕏
There was a problem hiding this comment.
ℹ️ No critical issues — the prior review's findings are addressed cleanly. One minor residual on the degraded-state messaging below.
Reviewed changes
- Quarantines invalid staging instead of failing closed app-wide —
initialize()renames a corrupt/unsafedisplay-image-artifacts.jsonaside to<file>.invalid-<stamp>-<uuid>and opens a clean store, so the unreadable-file case no longer blocks every chat. - Rewords block messages from "Restart Aiden" to "Delete this chat to discard it" — across
chats.ts,llm-client.ts, andchat-pane.tsx, matching the now-truthful per-chat recovery path. - Logs the preserved path —
main/index.tswarns with the exact.invalid-*path and reports degraded availability. - Adds quarantine coverage — corrupt and unsupported-shape tests assert availability, non-blocking
hasPending, and preserved bytes.
DeepSeek Pro | 𝕏
There was a problem hiding this comment.
✅ No new issues found.
Reviewed changes
- Added a store-wide
imageArtifactRecoveryUnavailableflag to the chat read contract —chats:getreportsavailability()and short-circuitshasPendingtofalsewhen staging is unavailable, keeping per-chat pending distinct from store-wide unavailability. - Branched copy/export/send block messages on
availability()— the unavailable state now directs the user to the developer log (${availability.reason} Open Aiden's developer log to locate the staging file that needs repair.) instead of prescribing "Delete this chat to discard it", whichdeleteChatrefuses in that state. - Gated the display-image generation path and the renderer composer/copy on the unavailable flag —
prepareGenerationthrows the repair message, andChatPanedisables the composer and copy path with the same truthful copy. - Reworded the quarantine-failure reason to name the exact source and preserved paths —
Aiden could not move ${source} to ${preserved}instead of the vague "could not preserve it".
The prior ℹ️ thread (quarantine-failure message prescribing an impossible deletion) is now resolved by this distinction, and the renderer/main block messages agree across all four mutation paths.
DeepSeek Pro | 𝕏

Summary
Addresses the remaining actionable review findings from #36.
Verification
npx tsx --test main/services/display-image-artifact-store.test.ts— 12 passednpm run type-checknpm run lintgit diff --check