Skip to content

Code-review fixes across both platforms; release 0.13.0 - #8

Merged
lukoplt merged 1 commit into
mainfrom
fix/code-review-0.13.0
Jul 24, 2026
Merged

Code-review fixes across both platforms; release 0.13.0#8
lukoplt merged 1 commit into
mainfrom
fix/code-review-0.13.0

Conversation

@lukoplt

@lukoplt lukoplt commented Jul 24, 2026

Copy link
Copy Markdown
Owner

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. NotebookStore keeps one SqliteConnection for its lifetime — 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. I confirmed the failure mode before fixing: 128 unsynchronized operations produced ArgumentOutOfRangeException from corrupted internal state, then a NullReferenceException on Close() — 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's DatabaseQueue already 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 sets html: false, so I found no exfiltration path) but the containment the code claimed was not enforced.

Also fixed

  • Assistant messages were tagged with the engine's construction-time model instead of the per-call override.
  • Windows discarded provider-qualified chat models, and the "Regenerate with …" menu did not exist — its localization strings had been sitting unused in both locales. Added; the prefix is validated against the providers table before being treated as composite, since Ollama tags contain colons themselves.
  • Retry-After on a 429 was ignored in favor of the app's own much shorter backoff.
  • Backups checkpointed a write-ahead log that is never enabled, then copied the live database file. Now uses SQLite's online-backup API — the same mechanism macOS reaches through GRDB.
  • A retried answer rendered appended to the failed attempt's partial text.
  • PasswordVault.Delete swallowed every exception, making a stuck credential look deleted and then breaking the following Save on the duplicate.

Note on the backup fix

VACUUM INTO was 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 XCTAssertThrowsError accepted a "file not found" error. Tightened to assert the specific error and plant a real file outside the root; it then reported read ..: did not throw an error, demonstrating the escape concretely.

The App layer is unverified locallynet10.0-windows with a Windows-only XAML compiler, so none of ProviderRouter, ChatViewModel, ChatPage, or WindowsPasswordVaultSecretStore compiles on macOS. Windows CI on this PR is its first real check. Hand-audited instead: all SendAsync callers use named arguments, x:Bind-inside-a-Flyout is already used in the same file, and MenuFlyoutSubItem.Items has no ItemsSource, hence the code-behind population.

Tag v0.13.0 to follow once checks are green.

🤖 Generated with Claude Code

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>
@lukoplt
lukoplt merged commit 8a78d31 into main Jul 24, 2026
3 checks passed
@lukoplt
lukoplt deleted the fix/code-review-0.13.0 branch July 24, 2026 20:26
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant