Code-review fixes across both platforms; release 0.13.0 - #8
Merged
Conversation
Serialize Windows database access. NotebookStore keeps one SqliteConnection for its lifetime, which is not thread-safe, but it was used concurrently by EmbeddingWorker's drain, FolderWatchService, Retriever's async continuations, and every Task.Run in the view models. Unsynchronized use corrupts the connection's internal state, not just the query in flight. All 73 public store methods now hold a re-entrant gate, as do Retriever and AttachmentStore where they use Connection directly. macOS was never affected — GRDB's DatabaseQueue already serializes. Tag assistant messages with the model that produced them. ChatEngine streamed with `model ?? ChatModel` but persisted ChatModel, so a per-call override was recorded wrong. Resolved once as activeModel. Honor a provider-qualified chat model on Windows, and add the "Regenerate with ..." menu it feeds. The router discarded the composite key, and the menu did not exist — its localization strings had been sitting unused. The prefix is validated against the providers table before it is treated as composite, since Ollama tags contain colons themselves. Honor Retry-After on a 429 instead of retrying after the app's own much shorter backoff. ProviderRateLimitException now carries the hint; ThrowForStatus parses both the delta-seconds and HTTP-date forms. Contain attachment paths on both platforms. The filename was already reduced to a single path component, but the note-folder segment beside it was not — and it arrives as the host of the editor's attachment:// URL, where .NET parses `attachment://../db.sqlite` into Host == "..". A crafted link in a note reached one directory above the attachments root. Fails closed. Back up with SQLite's online-backup API. The old code checkpointed a write-ahead log that is never enabled, then copied the database file, which can capture a partial write, and could not back up the in-memory store at all. VACUUM INTO was the first attempt and is worse: it silently produces no file when the source is in-memory. Clear the streaming draft on retry so a retried answer does not render appended to the failed attempt's partial text. Report Credential Manager removal failures instead of swallowing every exception, which made a stuck credential look deleted and then broke the following Save on the duplicate. Regression tests for each fix, verified to fail when the fix is reverted. 273 C# Core tests and 357 Swift tests pass. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes the seven findings from the repo code review, plus one minor issue, and bumps to 0.13.0.
Highest impact
Windows database access was unsynchronized.
NotebookStorekeeps oneSqliteConnectionfor its lifetime — not thread-safe — but it was used concurrently byEmbeddingWorker's drain,FolderWatchService,Retriever's async continuations, and everyTask.Runin the view models. I confirmed the failure mode before fixing: 128 unsynchronized operations producedArgumentOutOfRangeExceptionfrom corrupted internal state, then aNullReferenceExceptiononClose()— the connection object itself gets wrecked, not just the query in flight. This is the shape of the intermittent Windows crashes that CI never reproduces. macOS was never affected; GRDB'sDatabaseQueuealready serializes.Attachment paths could escape their folder, both platforms. The filename was already reduced to one path component, with a comment naming path traversal as the threat — but the note-folder segment beside it was not, and it arrives as the host of the editor's
attachment://URL. Verified:new Uri("attachment://../db.sqlite").Host == "..", resolving one directory above the attachments root, where the database and settings live. Impact is limited (the editor setshtml: false, so I found no exfiltration path) but the containment the code claimed was not enforced.Also fixed
Retry-Afteron a 429 was ignored in favor of the app's own much shorter backoff.PasswordVault.Deleteswallowed every exception, making a stuck credential look deleted and then breaking the followingSaveon the duplicate.Note on the backup fix
VACUUM INTOwas the obvious smaller change and I wrote it first. The tests caught that it silently produces no file at all when the source database is in-memory — no exception, nothing. That would have shipped a backup button that reports success and writes nothing.Testing
273 C# Core tests (was 256) and 357 Swift tests (was 353) pass. Each new test was verified to fail with its fix reverted — that caught a defect in the tests themselves: the Swift traversal test initially passed with the fix removed, because
XCTAssertThrowsErroraccepted a "file not found" error. Tightened to assert the specific error and plant a real file outside the root; it then reportedread ..: did not throw an error, demonstrating the escape concretely.The App layer is unverified locally —
net10.0-windowswith a Windows-only XAML compiler, so none ofProviderRouter,ChatViewModel,ChatPage, orWindowsPasswordVaultSecretStorecompiles on macOS. Windows CI on this PR is its first real check. Hand-audited instead: allSendAsynccallers use named arguments,x:Bind-inside-a-Flyoutis already used in the same file, andMenuFlyoutSubItem.Itemshas noItemsSource, hence the code-behind population.Tag
v0.13.0to follow once checks are green.🤖 Generated with Claude Code