Releases: lumizone/local-waifu
Local Waifu 0.1.21
Small polish + safety release shipped the same day as v0.1.20. Adds the
standard macOS menu bar that was missing, gives you a manual escape
hatch when auto-update breaks, and lets you dismiss any notification
with a click.
Added
- Native macOS app menu. The menu bar now has App / Edit / View /
Window / Help submenus like every other Mac app. "Check for
Updates…" lives in both the app submenu (under About) and the Help
submenu so it's wherever your muscle memory expects. Standard
shortcuts (Cmd+C, Cmd+V, Cmd+W, Cmd+Q, etc.) now work from the menu.
Polish + English labels follow the existing language pref. - Manual update fallback. When the auto-updater can't reach GitHub
Releases (network down, repo briefly unavailable, signature mismatch),
Settings → Advanced now shows a "Download latest manually from
GitHub →" link so you're never stuck on an old version with a cryptic
error. The same link surfaces as an action on update-failed toasts. - Close button on every notification. Toasts in the top-right now
show an X on hover, so you can dismiss any notification without
waiting for it to time out.
Fixed
- License deactivate device no longer POSTs blank credentials to
Dodo on a second call. Previously, after one successful deactivate,
pressing the button again would surface an "invalid_key" error from
Dodo instead of a clean no-op. - OAuth token storage (currently inert in the shipped UI, but live
in the codebase) is now a single atomic JSON blob instead of 7
sequential writes. A crash mid-sequence can no longer leave a
partially-saved bundle that dropsaccount_idon the next read. - Secret store recovery is fail-closed. If your
secrets.json
ever gets corrupt AND the app can't move it aside, the app now
refuses to overwrite it rather than silently destroying anything
recoverable. You'll see a clear error message instead. - License key masking is UTF-8 safe. A defensive fix; current Dodo
keys are all ASCII, but the underlying slice is no longer panic-prone
if that ever changes.
Mostly a payments + UX cleanup release. License processing moves from
Dodo test to Dodo live (real cards now). The paywall got loading-state
polish so the Buy buttons no longer look dead during the first second
of cold start.
Changed
- License processing is now live. New purchases hit the real Dodo
Payments live environment, $25 lifetime / $35 renewal, license keys
arrive by email from Dodo. Existing license keys you redeemed during
the test/beta window keep working on this build, no re-activation
required. - Paywall buttons show a loading state. "Buy lifetime" and "Buy
renewal" now render a small spinner while the app fetches the
checkout URLs from the backend, instead of looking disabled. Fixes
the "is this broken?" first-click moment. - Lifetime price copy simplified to "$25". Previous "$25 launch ·
$35 standard" framing implied a discount code that no longer applies
on live checkout. The actual charge has always been $25, and that's
what the button now says. - Refund + revocation copy is honest about timing. Terms,
withdrawal form, and privacy policy no longer promise an automatic
"Locked within ~12 hours" after a refund. The current build doesn't
poll Dodo on a schedule, so the app will pick up a revoked license
when you next re-redeem or manage devices in Settings → License.
Marketing site
- Polar → Dodo across all legal pages. The Polar Software Inc.
Merchant of Record references in /terms, /privacy, /thank-you,
/withdrawal-form, and /contact have been updated to Dodo Payments,
Inc. The post-purchase thank-you page now links to the Dodo customer
portal instead of polar.sh. GDPR/CRD-aligned.
Fixed
- License deactivation error path now scrubs response bodies. The
/licenses/deactivatecall previously surfaced a raw 400-character
slice of any future error format Dodo returns. Now routes through
the same sanitiser as validate/activate (stripssk-/Bearer/
AIza/x-goog-api-keypatterns), so a future Dodo error shape
that echoes user data or tokens can't leak via this path. - Cancellation guards on license-tab and paywall fetches. Closes a
hard-rule-66 violation where a user switching tabs mid-fetch could
briefly see stale state from the prior async resolve. - dev.sh sanity check now matches the active provider. Was
validating Polar env vars only; now picks Dodo or Polar based on
LW_LICENSE_PROVIDERand warns on missing values for the active set.
Local Waifu 0.1.20
Mostly a payments + UX cleanup release. License processing moves from
Dodo test to Dodo live (real cards now). The paywall got loading-state
polish so the Buy buttons no longer look dead during the first second
of cold start.
Changed
- License processing is now live. New purchases hit the real Dodo
Payments live environment, $25 lifetime / $35 renewal, license keys
arrive by email from Dodo. Existing license keys you redeemed during
the test/beta window keep working on this build, no re-activation
required. - Paywall buttons show a loading state. "Buy lifetime" and "Buy
renewal" now render a small spinner while the app fetches the
checkout URLs from the backend, instead of looking disabled. Fixes
the "is this broken?" first-click moment. - Lifetime price copy simplified to "$25". Previous "$25 launch ·
$35 standard" framing implied a discount code that no longer applies
on live checkout. The actual charge has always been $25, and that's
what the button now says. - Refund + revocation copy is honest about timing. Terms,
withdrawal form, and privacy policy no longer promise an automatic
"Locked within ~12 hours" after a refund. The current build doesn't
poll Dodo on a schedule, so the app will pick up a revoked license
when you next re-redeem or manage devices in Settings → License.
Marketing site
- Polar → Dodo across all legal pages. The Polar Software Inc.
Merchant of Record references in /terms, /privacy, /thank-you,
/withdrawal-form, and /contact have been updated to Dodo Payments,
Inc. The post-purchase thank-you page now links to the Dodo customer
portal instead of polar.sh. GDPR/CRD-aligned.
Fixed
- License deactivation error path now scrubs response bodies. The
/licenses/deactivatecall previously surfaced a raw 400-character
slice of any future error format Dodo returns. Now routes through
the same sanitiser as validate/activate (stripssk-/Bearer/
AIza/x-goog-api-keypatterns), so a future Dodo error shape
that echoes user data or tokens can't leak via this path. - Cancellation guards on license-tab and paywall fetches. Closes a
hard-rule-66 violation where a user switching tabs mid-fetch could
briefly see stale state from the prior async resolve. - dev.sh sanity check now matches the active provider. Was
validating Polar env vars only; now picks Dodo or Polar based on
LW_LICENSE_PROVIDERand warns on missing values for the active set.
Local Waifu 0.1.19
The prior release was about how the character learns about YOU. This
one is about her interior life — modes she can be in, things she
likes, things she's afraid of, who she is when she's with you. Three
layers landed together.
New (named behaviour modes)
- Mode catalog. 13 named modes the character can be in for a
turn:neutral,playful,naughty,tender,comforting,
excited,sleepy,focused,melancholic,grumpy,
protective,curious,quiet. Each ships with a one-line
behavioural cue so the LLM has a discrete posture to organise the
reply around (instead of trying to translate 6 analog mood
numbers into tone). - Mode selector. Each turn picks a mode from mood + user-message
signals (sad-keyword lexicon, threat keywords, new-topic openers)- stage + time-of-day. Strong overrides (distress → comforting,
threat → protective) bypass mood entirely.naughtyis gated by
relationship stage (≥ Close) AND the absence of an NSFW
hard-boundary.
- stage + time-of-day. Strong overrides (distress → comforting,
- Sticky modes. Modes hold for at least 3 turns once chosen so
the character doesn't whiplash between postures when mood values
jiggle by 0.05. Strong overrides break stickiness instantly. - LLM-driven shifts. New
[[MODE shift <name> | <reason>]]
marker lets the LLM consciously override the heuristic when it
senses something the lexicon missed. - Mode telemetry. Last 50 transitions persisted per character
inapp_meta, ready for future Growth Dashboard surfacing.
New (character self-content blocks)
Four new Letta-style working-memory blocks every character now
ships with, alongside the existing persona / user / session
/ scratch:
likes— things SHE enjoys, discovered through your
conversations.dislikes— things that bother her.quirks— verbal tics and mannerisms she's noticed in
herself.fears— soft worries she carries, even with you.
All four start mostly empty and grow organically — from in-chat
[[BLOCK append <label>]] markers the LLM emits AND from the new
weekly self-reflection pass.
New (weekly self-reflection sub-agent)
character/self_reflection.rs— runs once per 7 days per
character from the daily background pass. Reads the last 50
chat turns plus the current self-content blocks, asks the LLM
to reflect in FIRST person ("I notice I enjoy…"), and appends
up to 3 entries to each block. Also produces:- Mode preference weights —
mode → weightmap blended into
the existing weights (smoothing 0.7 × existing + 0.3 × new) so
"she has learned that playful works with you" emerges over
months without a single week's outlier flipping it. - Insight of the week — 1-2 sentence honest self-observation
stored as aself_insightmemory, ready for the Growth
Dashboard.
- Mode preference weights —
- License-gated like the other background loops.
Removed / cleaned up
- The pre-v0.1.19 mood-only prompt line ("Current mood: warm
(happiness=…)") is now LABELLED as "Underlying mood" because
the discrete mode + cue sits above it. The mood numbers still
feed the model — they just no longer carry the whole tonal
responsibility alone.
Fixed (caught by new gemma4:e4b integration test)
- System-prompt block dump no longer leaks into the chat.
Reasoning models (gemma4:e4b especially) sometimes echoed the
rendered working-memory block dump —--- persona (188/500 — Your self-image) --- … --- end blocks ---— verbatim into the
visible reply, exposing internal state. The pipeline now strips
any block-dump-shaped region from the assistant text right
before persisting and surfacing it, so even when the model
mimics the system-prompt format, only her actual words reach
you. (character::blocks::strip_leaked_block_dump, wired in
chat_pipeline.rsafter marker parsing; 5 unit tests covering
with/without terminator, markdown horizontal rules, unknown
labels, and clean text.) - Polish distress lexicon now catches reversed word order.
The distress cue list only had"fatalnie się czuję"but
Polish lets you reorder freely —"Czuję się fatalnie."was
slipping through and Comforting mode wasn't firing. Added 12
new cues: both word orders forczuję się fatalnie/
fatalnie się czuję/źle się czuję/kiepsko się czuję,
pluskiepsko mi,ciężko mi,rozpadam się,
przejebany dzień/tydzień, and ASCII-fold variants for users
who skip Polish diacritics. Threat-mode (boję się) still
takes precedence when both signals fire, by design. - Multi-turn self-learning integration test.
Addedtests/v019_self_learning_demo.rs— drives a 6-turn
Polish conversation against gemma4:e4b, parses every marker
the character emits ([[MEM]]/[[BLOCK]]/[[THREAD]]/
[[MODE shift]]/[[MILESTONE]]/<thought>), folds the
resulting state back into the next prompt, and asserts the
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.
Local Waifu 0.1.16
Internal-quality release driven by a 4-agent backend audit. No
new user-facing features — just hardening every production-risk
finding the audit surfaced.
Fixed (BLOCKER class)
- Migration runner crash window. The DB migration tracker row
used to be inserted after the migration body's COMMIT. A crash
in the millisecond between those two writes would leave the
schema modified but the migration unrecorded; the next launch
would re-attempt the sameALTER TABLE ... ADD COLUMN, fail
(SQLite has noIF NOT EXISTSfor ADD COLUMN), and brick the
database. The body + bookkeeping now commit atomically. - Fire-and-forget tasks.
commands/feedback.rswas using raw
tokio::spawninstead oftauri::async_runtime::spawnfor the
background consolidation pass — broke the codebase convention.
Swapped. (Telegram dispatcher keepstokio::spawnbecause its
is_finished()API powers status reporting — panic visibility
is already covered by the global panic hook.)
Fixed (production hardening)
- Marker parser robustness.
[[MEM ...]],[[THREAD ...]],
[[BLOCK ...]]parsers used to terminate on the first]]
encountered — if the model mirrored user text containing]]
(markdown footnotes, nested quotes), the marker would fire on
the wrong delimiter and either drop a real call or swallow
user-visible content. Now any[[inside the body is treated
as nested content and the marker is preserved verbatim. - Lorebook write validation.
lorebook_upserthad no length
caps; a user could push a 50 MB entry into SQLite. Now bounded
at 200 chars (title), 4,000 chars (content), 32 keywords ×
80 chars each. Update path also now correctly returns the
originalcreated_atinstead ofnow, and rejects updates
against an ID that doesn't belong to the active character. - Anticipations table runaway. The pruning DELETE used
LIMIT = MAX_UNCONSUMED - parsed.len()which could go negative
if those constants ever drifted. Clamped to >= 0. - DB lock starvation.
anticipations::build_contextused to
hold the SQLite mutex across two prepared-statement iterations.
Concurrent chat turns now don't block on the background
reflection pass. - Ollama non-stream timeout. The shared client default 120 s
was killing background drift detection / KG extraction /
consolidation calls on Power-tier 27B models (real responses
take 2-5 min). Per-request override raised to 10 min on the
non-streaming path. - License gate for background loops. Daily consolidation +
sleep-time anticipations now checklicense::gate::current_status
and skip the LLM call entirely for Locked / TrialExpired /
NotStarted users. They can't chat with the app anyway — no
reason to burn their local CPU/GPU on reflection passes for a
product they haven't paid for.
Audit deferrals
- Retention sweep on memories / chat_messages / entities /
open_threads / relationship_milestones. All grow unboundedly.
Realistic ceiling for a daily-use install: ~100 MB after a year.
Acceptable for v0.1.x; will land as a configurable retention
policy in v0.2.
Local Waifu 0.1.15
Added
- Lorebook (per-character). Settings → Lorebook. Pre-author
backstory snippets with trigger keywords; each one only injects
into her prompt when the keywords appear in your message. Means
you can pour thousands of words of canon into her without paying
the token cost every turn. Empty-keywords mode (always active)
available for the rare entries that must always be in context.
SillyTavern-inspired keyword-triggered injection. - Sleep-time anticipations. Once every ~6 h of idle, a small
background pass asks a fast model "looking at the recent chat
and her open threads, what would she naturally bring up next
time?" Stores 1-3 short anticipations per pass. When the
spontaneous-callback trigger fires after long silence, she picks
the freshest anticipation first ("hey, was thinking about your
interview nerves") and falls back to the bare open-thread topic
only if nothing's queued. Letta-inspired.
Local Waifu 0.1.14
Added
- Time-aware. Every reply now knows what day and part of day it
is (Monday morning, Friday evening, late night Sunday). She'll
open with something appropriate to the wall clock instead of the
same neutral greeting at 3 AM and 3 PM. Pulled from the system
clock — matches what you actually see. - She reads before she replies. A short reading pause kicks in
between the moment you hit Enter and the moment her bubble
appears. Length-tied: "ok" feels instant, a paragraph buys her a
beat to "take it in". AIRI-inspired typing-simulation; capped at
1.5 s so long pastes still feel responsive. - Inner thoughts (💭). She can now keep a private thought
separate from her spoken reply — "you sound stressed today"
while saying "tell me more". Tap the 💭 badge under her message
to peek; the user only sees thoughts they choose to. AIRI-style
<thought>blocks parsed server-side. - Open threads. She tracks what you're mid-conversation about
("the job interview Friday", "the row with your mum") so she
can circle back instead of treating every turn as a cold start.
Resolves them when wrapped up. Letta-inspired. - Working memory blocks. Four labelled scratchpads she
maintains across turns:persona(her own mood),user(running
notes),session(top-of-mind now),scratch(notepad). She
can compact them when full. Letta-style multi-block memory. - Spontaneous callbacks on idle. Instead of a generic "you
there?" after long silence, she now pings about a specific open
thread: "hey, how did the interview go?". Falls back to the
generic nudge when there's no open thread.
Local Waifu 0.1.13
Added
- She knows the difference between what you said and what she
guessed. Memories now split into two kinds: deductive (direct
quotes from things you explicitly told her) and inductive
(patterns she noticed over multiple turns but were never said).
The digest in her system prompt labels inductive entries as
(impression)so she hedges them ("I get the sense that…")
instead of asserting them as fact ("you told me…"). Inspired by
Honcho's deductive/inductive observation split. - Memory provenance. Every memory she creates now records which
chat message produced it (source_message_idscolumn). Future
work can wire citations into her replies — "Last Tuesday you
mentioned X" — instead of asserting context in a vacuum. - Smarter knowledge extraction. Instead of running the KG
extraction LLM every 5 turns regardless of what you said, she
now checks if your latest message is semantically novel vs what
she already remembers. If you're repeating a topic she's heard,
she skips the extraction call (saves Ollama). If you bring up
something new, she extracts immediately. Force-fallback every 20
turns so slow patterns still get logged. Net result: fewer
background LLM calls + faster reactions when the topic IS new.
Inspired by
- plastic-labs/honcho —
their peer-representation work + deductive/inductive split + dream
cycle. AGPL-3.0; we ported only patterns, no code. Honcho remains
the right pick if you want managed cross-session user modeling.
Local Waifu 0.1.12
Added
- Persona notes per character. Settings → Characters now has a
free-text "Persona notes" editor for the active character. Type
quirks, tone hints, mannerisms ("she's grumpy in the morning",
"she calls me by my name often") and they slot into the system
prompt above the slider-derived personality every reply. 2,000-char
cap; leave empty to use defaults from onboarding. /compressslash command. Power-user escape hatch for when the
memory digest fills up — type/compressin the chat input and
the app programmatically trims low-importance memories that haven't
been surfaced recently. Shows stats in a toast ("Trimmed 12 entries
→ 8 kept, freed 340 chars"). No LLM call; instant.- Auto-fallback to cloud BYOK when Ollama is unreachable. If you
picked a local model but Ollama has crashed / isn't running, and
you've configured a cloud BYOK key (Anthropic / OpenAI / DeepSeek),
the next chat turn quietly retries via that cloud provider with a
cheap default model. You stay in conversation instead of staring
at an "offline" error. Offline Mode in Network settings still
disables the cloud path entirely — your privacy choice wins.
Local Waifu 0.1.11
Added
- She remembers you better. Every chat turn now starts with a
bounded plain-text snapshot of what she knows about you (USER
digest, ~500 tokens) and the relationship so far (MEMORY digest,
~800 tokens), alongside the existing semantic recall. The snapshot
includes a usage % header — so when memory fills up she
proactively consolidates instead of forgetting things at random. - She can explicitly write to memory. When you reveal something
durable (preferences, important people, hard limits, big news),
she emits an inline marker that the app picks up and stores. You
never see the marker — it's stripped before display — but she'll
remember next session. Same mechanism for correcting outdated
facts (replace) or asking her to forget (remove). - Capacity-aware consolidation. When either store crosses 80%
capacity, the background consolidation pass kicks in automatically
so the next turn has room.
Inspired by
- Hermes Agent's
MEMORY.md / USER.md pattern + memory tool grammar. Their plain-
text bounded approach + substring matcher are ported here; the
underlying storage stays our existing encrypted soul files +
SQLite memories table (embeddings preserved, plain text is
additive).