ADFA-5253 | Persist model URI instead of copying multi-GB models - #84
ADFA-5253 | Persist model URI instead of copying multi-GB models#84jatezzz wants to merge 2 commits into
Conversation
…hable ADFA-5253: read the model through a held descriptor instead of copying it, and persist the picker's read grant. The settings pane derives its model and engine status from a live readability check, so a deleted file no longer reads as ready.
There was a problem hiding this comment.
Claude Code Review
This repository is configured for manual code reviews. Comment @claude review for a one-time review, or @claude review always to subscribe this PR to a review on every future push.
Tip: disable this comment in your organization's Code Review settings.
itsaky-adfa
left a comment
There was a problem hiding this comment.
Reviewed at 426e5bd. One confirmed defect on a path a user will hit, so requesting changes; the rest are minor. The refactor itself is careful, and the seam design (NativeModelSource / ModelResidencyEngine / ModelSourceWatcher) is what makes the residency rules testable at all - that part is a clear improvement.
Verified locally: ./gradlew testDebugUnitTest in ai-agent-local - 137 tests, 0 failures. git fetch origin first, so the diff is against current origin/main (68abdd6).
Blocking: the isAvailable() memo (LocalLlmBackend.kt:243) makes the first message after a restored model file fail with Backend 'local' is not available. Traced through ai-core's LlmInferenceServiceImpl.generateStreaming, which returns that string verbatim on a false.
Two things only a device can settle, both called out inline:
- A streaming DocumentsProvider hands back a pipe, which the three
openStream()calls consume before llama.cpp reads it. Worth picking a.gguffrom Drive to see what happens. - Re-opening
/proc/self/fd/Nis a fresh path-basedopen(), checked against path permissions rather than the SAF grant that produced the fd. The attached video covers the happy case; an SD-card and a FUSE-volume load would close it.du -sh .../files/llm-modelsproves no copy was made, not that the load works everywhere.
Checked and cleared: descriptor lifecycle in ensureModelLoaded (the adopted/finally pairing and the unload-before-close ordering are right, and both header readers .use their streams); double-close() on modelWatch (unreachable - stopWatching nulls it); the close()-vs-onModelSourceGone race on cleanupScope; and no remaining callers anywhere in the repo of the removed or privatised API (engineState, savedModelPath, modelLoadingState, getSavedModelName, fallbackDisplayName, isGguf(String), EngineState.Uninitialized).
Two candidate findings I dropped after checking:
- "The model is permanently unavailable once the memo is set" - false.
LlmInferenceServiceImpl.getAvailableBackends()does not filter onisAvailable(), solocalstays selectable and the warm-up clears the memo. Only the one-shot stalefalsesurvives, which is the blocking finding above. - "
diagnose()losingFileMissingdropped its test" - false.givenMissingFile_whenDiagnosed_thenFileMissingwas ported togivenAFilesystemPath_whenDiagnoseUnopenable_thenFileMissing, anddiagnosenow only runs after a successful open, so the narrowing is deliberate.
No prior review comments on this PR, so there was nothing to re-check from an earlier round. This repo has no written approve/request-changes rule, so the reviewer default applied: any confirmed IMPORTANT blocks. Its CLAUDE.md does require device-level verification over a green build, which is what the two items above are asking for.
Docs are in step with the code - ai-agent-local.html, assets/docs/index.html and the plugin.permissions comment all describe read-in-place rather than the old copy.
Drop the stale isAvailable() memo, refuse a non-seekable descriptor as SourceNotSeekable, key the pane's unavailable marker off engine status, coalesce watch notifications, and cover openDocument + the grant lifecycle.
Description
Implemented direct-access behavior for LLM models selected via the Storage Access Framework (SAF). The application now retains persistent read permissions for the chosen URI and queries the
.gguffile in-place. This prevents copying multi-gigabyte models into the app's internal sandbox, drastically saving device storage and speeding up the setup process. Additionally, a cleanup routine was added to delete legacy model copies, and the plugin's HTML documentation was updated to reflect this direct-access behavior.Details
LocalLlmBackendto hand the native loader a file descriptor path (/proc/self/fd/N) for direct reading.ModelSourceWatcherandNativeModelSourceinterfaces to safely monitor, open, and evict models if the underlying source file is deleted or unmounted.LocalLlmSettingsViewModelandLocalLlmSettingsFragmentto properly display and handle "Unavailable" model states when a URI becomes unreachable.GgufModelInspectorandModelLoadDiagnosticsto operate onInputStreamfactories rather than static file paths.document_4956368585724266828.mp4
Ticket
ADFA-5253
Observation
To facilitate
mmapoperations in the native code without copying the file,ContentNativeModelSourceresolves thecontent://URI to a file descriptor and passes the/proc/self/fd/path to thellama.cppbackend. A legacy cache cleanup (deleteLegacyModelCache) is executed in the background upon initialization to reclaim space from older app versions.