Local Waifu 0.1.17
Second internal-quality release in 24 hours. A 5-agent bug hunt
swept the chat hot path, security surfaces, lifecycle / sidecars,
frontend, and external integrations. Every fixable finding landed.
Fixed (privacy / security)
- Offline-mode bypass. When
offlineMode=truewas set and the
local Ollama crashed mid-conversation, the cloud-fallback path
would silently forward your message to BYOK Anthropic / OpenAI /
DeepSeek anyway — directly contradicting your privacy opt-out.
The offline check is now re-validated before any fallback. - Soul-file tmp 0600. The encrypted tmp file the soul saver
writes (before atomic-rename) now has 0600 perms from creation
rather than briefly landing at the umask default of 0644. - Error-body sanitisation gaps. Three cloud / BYOK error paths
(Anthropic mid-stream SSE error, Dodo deactivate failures, the
byok_testdetail field) were passing raw HTTP body text
through to the UI. Each now runs through the same token-masking
sanitiser the other cloud error paths use.
Fixed (chat correctness)
- Surprisal gate firing every turn. The "is this user message
novel enough to trigger KG extraction?" check compared a
weighted-by-importance-and-recency similarity against a 0.78
threshold designed for raw cosine. Result: aged memories always
scored under threshold, and KG extraction fired on every chat
turn for users with any history. Now compares raw cosine — only
fires when the user is bringing up something genuinely new. - Orphan user messages. The chat pipeline writes the user turn
to the DB before the LLM call so it survives crashes mid-stream.
Downside: an actual crash (or?-propagated error) between the
user write and the assistant write left a dangling user row
with no reply, rendering as a lonely user bubble forever. A
startup sweep now deletes any user row older than 1 hour that
has nocharacterrow following it — preserves recent legit
messages, cleans up genuine orphans. - Empty model routing.
route("openai:")(and the other
cloud prefixes) used to send an emptymodelfield to the
provider and get a 400. Empty model after a prefix now falls
back to the Ollama path so the standard error surface kicks in.
Fixed (lifecycle / data integrity)
- Backup integrity.
backup_app_datanow runs
PRAGMA wal_checkpoint(TRUNCATE)before the file copy — the
previous version copied the live WAL alongside the main DB
with no synchronisation, so a write committing mid-copy could
produce a backup that refused to open. - Cmd+Q sidecar shutdown. Cmd+Q on the macOS menu bar used
to bypass both theWindowEvent::Destroyedhandler (intercepted
byhideToTray) and the tray Quit handler — leaving Ollama and
the TTS sidecar as zombie child processes. A new
RunEvent::Exithandler now catches every exit path and
shuts down sidecars with a 5 s deadline. post_update_finalizewarning loop. A crash between the
version-marker write and the soul-file check used to hide any
warning forever. The check now runs before the marker, and the
marker is only set when the check passes — or after 2 retries
with a separate counter, so a genuinely missing soul doesn't
loop the warning forever either.
Fixed (resource exhaustion)
- Swift script timeouts. Calendar, Reminders, and OCR all
shelled out to/usr/bin/swiftwith no deadline. A hung Swift
process (TCC dialog blocked, missing dylib, signal-stopped)
would pin a Tokio blocking-pool thread until the OS reaped the
process — and enough hung calls could starve every other
blocking caller. All three paths now route through a shared
watchdog with a 15 s deadline (kills the child + clear error). - Image download cap. The fal.ai and OpenAI image-URL
download paths called.bytes().awaitwith no content-length
check, so a misbehaving CDN streaming gigabytes would OOM the
app. Both now cap at 20 MB.
Fixed (frontend stream race)
- Stop button leaving markers visible. Hitting Stop mid-stream
after the assistant bubble appeared but beforedonearrived
used to leave the partial tokens (with any[[MEM ...]]
bookkeeping markers) frozen in the bubble. The bubble now still
receives the cleaned text ondoneeven when stopped — so the
final state matches what the server actually committed. - Character switch race. Switching characters while a stream
was in flight relied onbumpStreamGenerationto drop later
tokens, but adoneevent arriving in the microtask window
beforeclearMessagescould still write into the old
character's array. Now usesstopStreamingMessagesfor an
atomic bump-and-finalise.
Audit deferrals
- Soul-file double-PBKDF2.
Cocoon::new(key)runs its own
100k-iteration PBKDF2 on top of our already-PBKDF2'd master key,
for 200k iterations total per soul read/write. Switching to
MiniCocoon::from_keywould skip cocoon's KDF but the wire
format is incompatible — would brick every existing user's
soul files. The performance cost (~50 ms per character switch,
never on the chat hot path) is acceptable; a format-version
byte for future migration is tracked as v0.2 work.