Native SAF export/backup/restore (Android OOM fix) + export/import fixes, #330, #116#367
Native SAF export/backup/restore (Android OOM fix) + export/import fixes, #330, #116#367Pento95 wants to merge 4 commits into
Conversation
Reduce Android memory pressure that led to OutOfMemoryError crashes (RustWebViewClient.shouldInterceptRequest allocates each IPC/SQL payload as one contiguous Java-heap block). - Add EmbeddedImageMeta (EmbeddedImage without the heavy base64 imageData) and metadata-only DB reads (getEmbeddedImageMetaForStory, getEmbeddedImageIdsForStory, countStories). Hot paths that only need metadata (message send, retry backup, gallery list) no longer pull the base64 of every story image through the bridge in one buffer. - Retry backup now stores embedded-image ids instead of full images; the ids are all the undo path (set-difference delete) ever needed. - Gallery lazily loads each image's pixels via IntersectionObserver, so a story with many images never loads them all at once. - Only buffer AI response bodies for the debug panel when debug logging is active, instead of cloning and reading every streamed response. - Batch entity deletes on retry/undo (DELETE ... WHERE id IN (...), chunked under SQLite's 999-parameter limit) instead of one IPC round-trip per id. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013VbdCmGtp3WtyqAgirVMpi
Definitive fix for the Android OutOfMemoryError crashes: the heavy byte payloads never cross the Tauri IPC bridge nor land in the WebView JS heap. Only paths and ids travel over IPC; the bytes are streamed file-to-file (or DB-to-file) inside native code. New Rust commands (src-tauri/src/backup.rs): - backup_database: streams the VACUUMed DB straight into the ZIP on disk. - restore_database: extracts aventura.db from the archive directly onto the live DB file (native safety copy + WAL/SHM cleanup); ignores legacy stories/*.avt entries in older backups. - export_images_zip: one ordered streaming query, decoding/writing a single PNG at a time (peak memory = one image). - export_single_image: decodes one image natively to a PNG file. Frontend: - backupService/imageExport now invoke the native commands; the backup is DB-only (settings already live inside the DB, so settings.json was dropped). - Remove the chunked-IPC helper (fileStream.ts) and the now-unused jszip dependency; drop the outdated .avt mention in the backup settings copy. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013VbdCmGtp3WtyqAgirVMpi
Route large exports, backups and restores straight to the user-chosen SAF destination natively, fixing the Android OutOfMemory crashes caused by streaming hundreds of MB through the JS/IPC bridge. Also: show Pollinations image models (AventurasTeam#330), adjustable chapter summary length (AventurasTeam#116), a generation info popover, and assorted memory/generation improvements.
|
Caution Review failedAn error occurred during the review process. Please try again later. 📝 WalkthroughWalkthroughThe pull request adds native Rust backup, restore, and image export commands; switches frontend export flows to resolved save targets; reduces embedded-image transfer through metadata and IDs; adds configurable summary detail, paid Pollinations model filtering, generation metadata display, standardized errors, and stricter Android build setup. ChangesNative backup and export
Configurable generation and model selection
Build tooling
Estimated code review effort: 5 (Critical) | ~120 minutes Possibly related PRs
Sequence Diagram(s)sequenceDiagram
participant User
participant ExperimentalSettings
participant BackupService
participant Tauri
participant NativeBackup
User->>ExperimentalSettings: Start backup
ExperimentalSettings->>BackupService: createFullBackup()
BackupService->>Tauri: invoke backup_database
Tauri->>NativeBackup: stream database and metadata into ZIP
NativeBackup-->>BackupService: saved destination path
BackupService-->>ExperimentalSettings: return saved path
ExperimentalSettings-->>User: show backup completion
Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Code Review
This pull request moves heavy payload operations (database backup/restore, image exports, and story exports) entirely to the native Rust side to prevent Android OutOfMemoryError (OOM) crashes. It also introduces lazy loading of image data in the gallery view, batched database deletions, a 'Summary Detail' configuration option for chapter summarization, and a utility to extract human-readable error messages. Feedback on these changes includes suggestions to write the restored database to a temporary file first and atomically rename it to prevent corruption, clean up old temporary restore files in the cache directory on Android, and add optional chaining with fallback values in formatStoryTime to avoid potential runtime crashes.
Important
The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.
There was a problem hiding this comment.
Actionable comments posted: 7
🧹 Nitpick comments (3)
src/lib/types/index.ts (1)
100-109: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winKeep one source of truth for the memory configuration types.
src/lib/services/ai/index.tsalso declaresSummaryDetailandMemoryConfig. Import and re-export these definitions from$lib/typesthere to prevent the parallel contracts from diverging.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/lib/types/index.ts` around lines 100 - 109, Update the AI service module’s SummaryDetail and MemoryConfig declarations to import these types from $lib/types and re-export them, removing the duplicate local definitions so src/lib/types/index.ts remains the single source of truth.src/lib/components/settings/ExperimentalSettings.svelte (1)
148-150: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueImport
openstatically.Since
@tauri-apps/plugin-dialogis already statically imported at the top of the file (forask), you can also importopenthere to avoid the redundant dynamic import.♻️ Proposed refactor
Update the top-level import to include
open:import { ask, open } from '`@tauri-apps/plugin-dialog`'And then simplify this function:
- // Pick the backup file. On Android this returns a SAF content:// URI; on desktop a real path. - const { open } = await import('`@tauri-apps/plugin-dialog`') - const selected = await open({ + // Pick the backup file. On Android this returns a SAF content:// URI; on desktop a real path. + const selected = await open({🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/lib/components/settings/ExperimentalSettings.svelte` around lines 148 - 150, Update the top-level `@tauri-apps/plugin-dialog` import in ExperimentalSettings.svelte to include open alongside ask, then remove the redundant dynamic import inside the backup-file selection flow and use the statically imported open directly.src/lib/services/export.ts (1)
156-170: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueClean up duplicated comments.
There is a duplicated comment block explaining the native
.avtbuild process.♻️ Proposed fix to remove duplication
if (!target) return false - // Build the .avt natively: Rust injects each image's base64 from SQLite and writes the JSON - // straight to the real destination path — no image bytes cross the JS/IPC bridge. // Build the .avt natively: Rust injects each image's base64 from SQLite and writes the JSON // straight to the chosen destination (a SAF content:// URI on Android) — no image bytes cross // the JS/IPC bridge. await invoke<string>('export_story_avt', {🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/lib/services/export.ts` around lines 156 - 170, In the export flow around resolveSaveTarget and export_story_avt, remove the duplicated native .avt build comment and retain a single accurate comment describing Rust’s image injection and destination-path behavior.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@src-tauri/src/backup.rs`:
- Around line 345-381: Ensure every restore attempt removes the staged
`.tmp-restore-*` file created by `import_saf_to_temp`, including when restore
fails. Add cleanup in the restore command’s finally-equivalent/error paths,
preserving the existing restore result and handling cleanup as best-effort.
- Around line 114-138: The restore flow around the archive extraction must stage
the database in a temporary file instead of truncating the live target via
File::create(&target). Ensure extraction completes and the staged database is
readable/validated, handle safety-copy failure as an actual restore error, close
active database handles before replacement, then atomically rename the validated
temporary database over target and clean up temporary and WAL/SHM files on
failure or success.
In `@src/lib/components/memory/MemoryView.svelte`:
- Around line 158-161: Update the resummarize argument construction in
MemoryView to safely access story.memoryConfig.summaryDetail with optional
chaining and an appropriate fallback when memoryConfig is null, while preserving
the existing value when configuration is present.
In `@src/lib/components/story/GalleryTab.svelte`:
- Around line 71-85: Update lazyImage and ensureImageData so failed image-data
loads remain retryable instead of leaving a permanent loading state. Make
ensureImageData expose whether the cache fill succeeded, and only call
observer.unobserve(node) after success; preserve observation or provide an
explicit retry state on failure. Apply the same handling to the related
image-loading paths around the referenced sections.
- Around line 27-32: Bound imageDataCache by clearing it whenever the active
story changes and evicting payloads for images that are no longer near the
visible or prefetched range. Update the related loading state as needed so stale
requests do not repopulate evicted entries, preserving lazy loading for
currently relevant images. Anchor the changes to imageDataCache,
loadingImageIds, and the story-change logic around the referenced gallery flow.
In `@src/lib/services/backupService.ts`:
- Around line 147-156: Update the restore flow around database.close() and
invoke('restore_database', { zipPath }) to handle native restore failures. Catch
errors from restore_database and recover to a usable state by reopening the
existing database connection when possible, otherwise exit or relaunch through
the established application recovery path; preserve successful restore behavior.
- Around line 59-85: The backup flow around database.vacuumInto must not fall
back to the live aventura.db file when snapshot creation fails. Remove the
livePath fallback and make the operation fail with the existing no-snapshot
error when VACUUM INTO fails or produces no snapshot; retain cleanupTemp
handling for successful temporary snapshots.
---
Nitpick comments:
In `@src/lib/components/settings/ExperimentalSettings.svelte`:
- Around line 148-150: Update the top-level `@tauri-apps/plugin-dialog` import in
ExperimentalSettings.svelte to include open alongside ask, then remove the
redundant dynamic import inside the backup-file selection flow and use the
statically imported open directly.
In `@src/lib/services/export.ts`:
- Around line 156-170: In the export flow around resolveSaveTarget and
export_story_avt, remove the duplicated native .avt build comment and retain a
single accurate comment describing Rust’s image injection and destination-path
behavior.
In `@src/lib/types/index.ts`:
- Around line 100-109: Update the AI service module’s SummaryDetail and
MemoryConfig declarations to import these types from $lib/types and re-export
them, removing the duplicate local definitions so src/lib/types/index.ts remains
the single source of truth.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro Plus
Run ID: 516596c7-b003-43ec-8061-6a3257e9377f
⛔ Files ignored due to path filters (2)
package-lock.jsonis excluded by!**/package-lock.jsonsrc-tauri/Cargo.lockis excluded by!**/*.lock
📒 Files selected for processing (46)
compileApk.shpackage.jsonsrc-tauri/Cargo.tomlsrc-tauri/src/backup.rssrc-tauri/src/lib.rssrc-tauri/tauri.conf.jsonsrc/lib/components/layout/Header.sveltesrc/lib/components/lorebook/LorebookExportModal.sveltesrc/lib/components/memory/MemorySettings.sveltesrc/lib/components/memory/MemoryView.sveltesrc/lib/components/settings/ExperimentalSettings.sveltesrc/lib/components/settings/ProfileForm.sveltesrc/lib/components/settings/tabs/api-connection.sveltesrc/lib/components/settings/tabs/images.sveltesrc/lib/components/story/ActionInput.sveltesrc/lib/components/story/GalleryTab.sveltesrc/lib/components/story/StoryEntry.sveltesrc/lib/components/vault/VaultExportModal.sveltesrc/lib/components/vault/prompts/PromptPackList.sveltesrc/lib/services/ai/generation/MemoryService.tssrc/lib/services/ai/image/providers/fetchAdapter.tssrc/lib/services/ai/image/providers/pollinations.tssrc/lib/services/ai/image/providers/registry.tssrc/lib/services/ai/image/providers/types.tssrc/lib/services/ai/index.tssrc/lib/services/ai/sdk/providers/fetch.tssrc/lib/services/ai/sdk/providers/modelFetcher.tssrc/lib/services/backupService.tssrc/lib/services/database.tssrc/lib/services/export.tssrc/lib/services/export/ExportCoordinationService.tssrc/lib/services/exportTarget.tssrc/lib/services/generation/ChapterService.tssrc/lib/services/generation/GenerationPipeline.tssrc/lib/services/generation/RetryService.tssrc/lib/services/generation/phases/PreGenerationPhase.tssrc/lib/services/imageExport.tssrc/lib/services/lorebookImportExport/export/vault.tssrc/lib/services/lorebookImportExport/export/write.tssrc/lib/services/packs/import-export.tssrc/lib/services/prompts/templates/memory.tssrc/lib/services/templates/variables.tssrc/lib/stores/story.svelte.tssrc/lib/stores/ui.svelte.tssrc/lib/types/index.tssrc/lib/utils/error.ts
💤 Files with no reviewable changes (2)
- package.json
- src/lib/services/generation/RetryService.ts
|
Thanks both. I went through every finding and verified each one against the code rather than taking it at face value. 11 applied, 2 rejected as false positives. Details below. Applied
Both correct, and this was the only genuinely serious one. Fixed by staging the extracted DB next to the target, validating it, and only then swapping. One deviation from both suggestions: the safety copy is now a rename, not a Also extracted the logic into
Real, and the doc comment claiming "the temp is best-effort cleaned up by the restore" was simply false — nothing removed them. These are full copies of the user's backup (~363 MB in my testing). I went with @gemini-code-assist's approach over CodeRabbit's here: cleanup happens on the way in, at the top of
Valid, and I confirmed the premise empirically before acting: the header bytes of my
Valid, and thematically the important one on an OOM PR.
Valid.
Applied, though as defensive hardening rather than a demonstrated bug:
Rejected
False positive. get memoryConfig(): MemoryConfig {
return this.currentStory?.memoryConfig || DEFAULT_MEMORY_CONFIG
}It never returns null. The reasoning came from "Keep one source of truth for the memory configuration types" (CodeRabbit nitpick) False positive. NotesCodeRabbit's Both bots reviewed 6dba668, which predates the removal of the Pollinations "Include paid models" toggle — CodeRabbit's release notes still advertise it. The API filters by tier server-side from the key, so the toggle was a client-side duplicate that was inert wherever the key was actually sent. The list now reflects what the key can really generate.
|
|
Hi Pento95, thanks for the detailed breakdown of the fixes. It's great to see the thorough verification and the performance-conscious approach to the database restoration and image handling. Regarding your notes:
Your analysis of the "potential null pointer dereference" and the "source of truth" comments as false positives is correct based on the code structure you've described. This PR looks very well-considered and addresses the critical OOM issues effectively. Nice work. |
Addresses the Gemini/CodeRabbit review of AventurasTeam#367, plus bugs found while verifying their findings. Restore no longer destroys the database on failure. File::create truncated the live aventura.db before extraction succeeded, so a CRC error or a full disk left it in pieces with no rollback. The DB is now staged and validated first, then swapped in by renaming the old file aside (atomic, and free in disk space, which matters for a 270 MB DB on Android). Import of a large .avt no longer fails. The Library's Import button read the file with file.text(), which cannot exceed V8's max string length (~512 MiB); a real 546 MB export failed with a misleading "not a valid JSON file". It now goes through the native picker into a streaming Rust reader: structure stays in TypeScript, only the payloads move. Peak memory is one image regardless of file size (measured: 16 kB vs 108 MB for a naive parse of a 100 MB file). Also fixed: - Backups no longer fall back to the live DB file when VACUUM INTO fails. The DB runs in WAL mode, so that could ship a backup missing recent commits. - Gallery images are now an LRU with a byte budget, cleared on story change. Lazy loading bounded the peak at open, not the total. - Checkpoint snapshots kept stale chapter ids: checkpoints are imported before chapters, so remapping found nothing and silently kept the export's ids. - Every export was stamped v1.7.0 while the importer checked for v1.8.0, so re-importing our own file always warned that the background image was lost. - Android file pickers dropped their filters: the dialog plugin maps extensions to MIME types and silently discards unknown ones, including 'avt' and the '*' wildcard. Filters are now omitted there, where they only ever lied. - Staged SAF restore temps (full copies of the backup) were never deleted. - Removed the Pollinations "Include paid models" toggle: the API already filters by tier from the key, so it was a client-side duplicate that was inert wherever the key was actually sent. Adds vitest and the first frontend tests (33), covering the import remapping this change refactors, plus 14 Rust tests. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
Caution Review failedAn error occurred during the review process. Please try again later. Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
1 similar comment
|
Caution Review failedAn error occurred during the review process. Please try again later. Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
Apologies up front 🙏
This is a big, multi-topic PR — a "god-PR", and I'm sorry for that. The changes grew out of one investigation (Android OOM on export/backup) and pulled in several adjacent fixes I had queued locally. I know a bundle like this is harder to review than a focused branch, so I've documented each area separately below and I'm happy to split it into smaller PRs if you'd prefer — just say the word.
Overview
The core of this work fixes Android OutOfMemory crashes during export, backup, restore and import, and makes those flows fast. It also folds in two issue fixes and a few UX/quality improvements.
1. Android OOM: native, streamed export / backup / restore
Problem. Exports and backups routed hundreds of MB (base64 images, the full SQLite DB) through the JS/IPC bridge as strings. On Android's constrained WebView heap this reliably OOM-crashed.
Fix. All heavy file I/O now happens natively in Rust and streams file-to-file — nothing large ever lives in the JS heap or crosses the bridge:
backup_database,export_story_avt,export_single_image,export_images_zipwrite directly to the chosen destination.content://URI from the save dialog, opened natively via the fs plugin's content-URI file descriptor (ContentResolver). Writes stream in ~8 KB blocks, so peak memory is a single buffer regardless of file size.content://backup is streamed into a temp file in the app's internal cache dir, then restored natively..avtexporter fills each image's base64 from SQLite on the native side, so image bytes never touch the WebView heap.Perf. Backup DB compression dropped from Deflate level 6 → level 1 (the DB is mostly already-compressed base64 PNGs, so level 1 recovers the base64 bloat at a fraction of the CPU), and the
.avtexporter now pulls all images in one streamed query instead of one query per image.2. Native
.avtimport — the OOM's other halfThe known limitation this PR originally shipped with ("importing an extremely large
.avtstill parses JSON in the WebView") turned out to be a hard failure, not just memory pressure, and it is now fixed.Problem. The Library's Import button read the picked file with an
<input type="file">andfile.text(). That has to materialise the whole.avtas a JS string, and V8 caps a string at2^29 - 24bytes (~512 MiB). A real 546 MB export exceeded it by 1.8% and failed with a misleading "not a valid JSON file" — the file was intact; the read never delivered it. No amount of RAM helps: the ceiling is structural.Fix. Import now goes through the native picker into a streaming Rust reader, mirroring the exporter's split — JS owns the structure, Rust owns the bytes. Id remapping, insertion order and foreign keys stay in TypeScript where they are tested; only the payloads move.
Two passes, both streaming:
avt_read_lightreturns the JSON with everyimageDataremoved (serde'sIgnoredAnyskips each base64 without allocating it). JS parses that and runs the ordinary import.avt_import_imagesre-reads the file and streams each payload straight into SQLite, using the small old-image-id → new-entry-id table JS produced.Re-reading is deliberate: a staging table would need a migration and would leak orphan rows if an import died half way. Peak memory is one image regardless of file size — measured at 16 kB vs 108 MB for a naive parse of a 100 MB file (
cargo test --lib avt_import -- --ignored --nocapture).The structure and the images cannot share a transaction (the native side writes over its own connection), so a failed image pass deletes the half-imported story rather than leaving one with its pictures silently missing.
Verified on-device: the 546 MB
.avtthat previously failed now imports in ~1 minute.3. Export/import robustness
errMessage()helper: Tauri command rejections arrive as plain strings, so the olde instanceof Error ? … : 'Unknown error'discarded the real (often precise) native message. Now surfaced everywhere export/import can fail..avtgather step used to run outside the try/catch).4. Closes #330 — Pollinations image models missing
The provider filtered out models flagged
paid_only, hiding valid image models from the list. Filter removed so all image-capable models appear.The "Include paid models" toggle is gone with it: the API already filters by tier server-side from the API key, so the toggle was a client-side duplicate that was inert wherever the key was actually sent (images) and misleading where it wasn't (text). The text model fetch now sends the key too, so the list reflects what the key can really generate.
5. Closes #116 — Adjustable chapter summary length
Chapter summary length is now configurable rather than fixed.
6. Generation info popover
Narration entries now have an info popover showing the model / profile / effort / timestamp used to generate them.
Review fixes
From the Gemini and CodeRabbit reviews, plus bugs found while verifying their findings. Full reasoning in this comment.
Restore could destroy the database.
File::createtruncated the liveaventura.dbbefore extraction succeeded, so a CRC error, a truncated archive or a full disk left it in pieces — and the safety copy's failure was onlyeprintln!'d, with nothing ever restoring from it. The DB is now staged and validated first, then swapped in by renaming the old file aside: atomic, and free in disk space, which matters when the DB is ~270 MB on a phone. Covered by 6 Rust tests, checked to fail against the old logic.Backups could silently omit recent data. When
VACUUM INTOfailed, the code fell back to archiving the liveaventura.db. The DB runs in WAL mode (verified: header bytes02 02), so recent commits can live inaventura.db-wal— the fallback could ship a stale backup, the worst failure mode for a backup, since it only surfaces at restore time. A failed snapshot is now a hard error.Gallery could re-create the OOM this PR fixes.
imageDataCachewas cleared on refresh but not on story change, and had no eviction: lazy loading bounds the peak at open, not the total. It is now an LRU with a byte budget, cleared on story change. Failed loads render a Retry instead of a permanent spinner.Staged SAF restore temps were never deleted despite the doc comment claiming otherwise — full copies of the backup, hundreds of MB. They are now swept on the way in: a cleanup after the restore would never run, because the app calls
exit(0)on success.Bugs found while reviewing the reviews
.avtfile format; it is unrelated to the app version, which the code never said. It does now.)MimeTypeMap.getMimeTypeFromExtension, which returns null for anything Android does not know — including our ownavtand the*wildcard — and discards it silently.['avt', 'json', '*']became "JSON only", grey-ing out every.avt; the "All Files" escape hatch never worked. Confirmed on-device for restore too:backup.zip (1), the exact duplicate-name case the fallback was written for, was unselectable. Filters are now omitted on Android, where they could only lie.oldToNewId.set(image.id, …)wrote image ids into the same map used for entry lookups two lines below — harmless today, corrupting on a collision.Not applied (false positives)
memoryConfig" —story.memoryConfigis a store getter that coalesces toDEFAULT_MEMORY_CONFIG; it never returns null.services/ai/index.tsimports those types from$lib/typesrather than declaring them; there is one declaration of each.Refactor
importFromContentwas a single ~490-line function holding the most delicate logic in the project (id remapping, COWoverridesId, topological branch insertion). It is nowservices/import/, split intovalidate/idMaps/structure/images, with the image payloads behind a strategy — inline for sync, native for a picked file. Both entry points share one implementation of the structure.Adds vitest and the first frontend tests. 33 tests, written against the import's public entry point before the refactor so it could be judged by "these still pass", and checked by mutating the source to confirm they fail on a regression (one of them didn't, and was fixed). Plus 14 Rust tests, three writing to a real SQLite database.
Testing
npm run check,npm run lint,cargo fmt,cargo clippyclean. 33 frontend tests, 14 Rust tests..avt(~486 MB, valid JSON confirmed byte-for-byte), backup (~363 MB, well compressed), restore (app restarts cleanly), a gallery of 800+ images, Pollinations model fetch, and the 546 MB.avtimport that previously failed.Known limitations
SyncModalreceives a story as a string over the network, so there is no file to hand to Rust and a synced story over ~512 MiB would fail the same way. That is a change to the sync transport, not the import.Housekeeping
src-tauri/gen/android/app/tauri.propertiesis now untracked. Tauri's own generated.gitignorealready listed it, but a file committed before a rule exists stays tracked regardless — so it sat atversionCode=7003while the app shipped 0.7.6, andversionCodeis what Android compares to allow an update. Verified thattauri android buildrecreates it fromtauri.conf.json, which is what CI runs.Thanks for reading through all of it — and again, sorry for the size. Happy to break it up or adjust anything.