2026.7.13
A big one: your KarvyLoop is no longer tied to one machine or one desk. Your
devices become one mesh, your phone reaches home from anywhere, your memory is
yours to lock and rewrite, and a decision card is now something you can question
before you decide.
Added
- Your devices are one KarvyLoop (same-owner device mesh). Register your machines
into a roster (karvyloop devices), each advertising a capability fingerprint, and
what you learn on one flows to the others over one shared, causally-ordered log (HLC,
not vector clocks): crystallized skills and memory beliefs sync between devices
(outbox on the write choke + idempotent replay — never resurrects a belief you
invalidated), and a decentralized task board (lease/claim + reclaim-on-death) means
losing a device loses the machine a job runs on, never the job. A My Devices panel
surfaces the roster with capability chips, an online light, and informed-consent removal
(it warns before you delete a device that provides a capability no other device has). - Reach your home console from anywhere (away-from-home). Your phone on cellular, a
café network — a phone and a home machine both behind NATs meet at your own public
relay (karvyloop relay-serve; blind-forwarding, sees only ciphertext). Pair a device
once, from the/mphone page, and it gets its own key — end-to-end encrypted (X25519 +
ChaCha20-Poly1305), the browser tunneling the app home over the relay. Pairing is a
one-time code, never a raw token in a URL or QR; management (issue/list/revoke) is
local-only — a stolen phone can use the surface but can never mint access or revoke
another device. The relay is bring-your-own-server: its address is config
(relay:in config.yaml), never hardcoded, with a one-command setup script and a
self-host guide. - A phone page that does one thing well (
/m). Open it on your phone: the decisions
waiting on you, three thumb buttons, and a chat strip to start work — a fluid layout
that reflows from phone to foldable to tablet, no coined jargon on the first screen. - Ask about a decision before you decide. A decision card now has an opt-in "💬 Ask
about this" — question Karvy about this card ("why now?", "what if I decline?") and it
answers grounded only in that card's own evidence, staying neutral (it won't argue you
toward accept). The two-button fast path is untouched; asking never touches the decision. - Your memory is yours to lock and rewrite. In the knowledge library you can now pin
a belief (never auto-archived) and edit it as a ledger-style supersede — the new
version becomes active, the old one moves to the history layer with an auditable "edited
by you" trail. Nothing is silently rewritten.
Changed
- The README leads with the story, not the category. First screen is now the 40th-run
value curve and three contrarian bets, not "another agent runtime." - Honest egress + honest away-from-home labels. Domain-level network egress is positioned
as an opt-in enhancement — the default binary fail-closed floor is the honest, universal
guarantee; nothing bypassable is ever labeled "enforced."
Fixed
- LAN boot no longer hangs on a busy instance. Decision cards load lazily — an instance
with dozens of backed-up cards opens fast instead of firing every card's LLM at once. - The keyword gate no longer locks out orchestration. Karvy can decompose and route a
no-keyword task instead of falling through to a literal reply. - Preference crystallization has a deterministic floor. "You keep deciding X the same
way" is counted from the evidence, not gated solely on an LLM flag that can wobble.
Added
- Bring your own AI runtime into the driver's seat (M1). You can now attach an external
headless-CLI AI runtime as a channel citizen, @-dispatch a task to it, and get its real reply
back — while it stays an opaque external executor whose output is always untrusted data that
never auto-enters your memory. Federate capability, not trust, not memory: the external runtime's
token spend is metered under its own independentext:<name>ledger source (never mixed into your
main gateway buckets), its stdout/stderr are treated as possibly-carrying-secrets and scrubbed
before touching any log/Trace, and credentials are never placed in the subprocess environment (a
hard first line of defense, not a redaction afterthought). The global assistant orchestrates this
(attach / dispatch / list) through capability-gated tools; failures are fail-loud (nonzero exit,
timeout, empty-success, or an approval request all surface instead of hanging). Adding another
runtime is a recipe file, not new code. - Manage your external runtimes from the console — and connect one on demand. A new
External Runtimes panel (🔌 in the left nav) lists every connected external citizen with a
distinct, off-colored external badge (so an opaque, untrusted external executor is never
mistaken for a native role), a live online/offline/unreachable status light you can refresh per
citizen, a remove (detach) button, and a direct-chat button — external citizens get their own
l0 conversation line, addressed as(domain, external, citizen_id)so they never blend into the
native role space. New management endpoints (GET /api/external/citizens,
GET /api/external/liveness,POST /api/external/detach) live in their ownroutes_external.py
and go through the same local-first origin gate as one-click install (writes only from
loopback/LAN). On-demand onboarding: Diagnose and the panel deterministically detect whether a
compatible headless CLI runtime is installed and, if not, walk you through connecting one from
its own official source — the same degradation-guidance pattern as the[asr]/[ocr]unlocks
and the MCP registry links. The hard boundary is spelled out in the UI and the README: an external
runtime is third-party software you bring yourself — KarvyLoop does not bundle, host, download,
orgit cloneit. We ship the bridge, adapters, and management UI; you bring the runtime.
Fixed
- The one-click update now tells you what happened instead of leaving you guessing. A failed
upgrade (e.g.git pullcouldn't reach GitHub) used to silently restart the old version and
re-show the same ambiguous "a newer version is available" banner — the user was stranded on "did
it update or not?" (the exact 怎么样了? status-anxiety anti-pattern this product exists to
kill). Three fixes: (1) the upgrade outcome survives a normal reopen — the result window went
from 10 minutes to 24 hours, so closing the tab and coming back still shows why it didn't take;
(2) any failed upgrade now surfaces a fail-loud red banner with the reason and a Retry
button — previously only an auto-rollback did, a plaingit pullfailure showed nothing; the
banner is suppressed once you're actually on the target version (no stale false alarms); and
(3) the current running version is now always visible in the top bar next to the logo, so
"what am I on?" is answerable at a glance without reverse-engineering the banner.
Added
- 🧾 Receipt Reader (票据员) — the fourth resident: receipt/invoice recognition → one checked
structured line. Hand it a receipt or invoice — pasted text or a photo — and it returns a clean
structured record (merchant, date, currency, total, tax id, itemised amounts) with the items
actually summed and checked against the stated total, the usual OCR mess (O↔0, l↔1, misplaced
decimals) calibrated from context, and any unsure field leftnullrather than guessed.
Recognition and structuring only: it suggests a category as a hint from your human-owned
company category sheet (the growth soul, like the scribe's glossary), never rules on
reimbursability and never files anything. Image input rides the samefile_extractpath as
audio: photos are OCR'd on-device via the optional[ocr]extra (PaddleOCR), and without it
(and no vision model) it asks for the pasted text instead of faking it — no bespoke pipeline, no
special UI, it's just a resident you drop a receipt on. Real-model verified including the hard
case: given a receipt whose items don't add up, it flaggedsum_mismatchwith both numbers and
refused to fudge the figures to balance. - 📚 study-buddy and 🎙️ meeting-notes promoted from skills to full residents. Both now ship as
complete 7-file paradigm images (identity / soul / user / commitment / verify / memory +
composition), read-only, each carrying a growth soul — study-buddy's concept map & weak
spots, the scribe's team glossary — the reason each is a role you can watch learn, not a static
skill. The empty house now offers four tenants (file-butler, study-buddy, meeting-notes,
expense). - Internal agents use our own paradigm (知行合一). A new
AgentSpecdeclares an internal
agent's engineering core — identity, principles, contract, verify, tools — with the persona
layer deliberately omitted for stateless internals;convergeis the first adopter. This
dogfoods the paradigm on our own code, for developers reading the source; it is not surfaced
in the user UI (that would clutter and create a see-but-can't-use asymmetry). - Knowledge chat「追问」(converge flow A). On the sediment card you can now settle the rest
while keeping the questioned point open for discussion, instead of an all-or-nothing accept. - Onboarding intake: 4 questions whose answers immediately change behavior. At the start of
the first-10-minutes journey (after the key is configured, before the first demo chip), the
journey bar asks 4 quick questions — conclusion-first vs process-first output, ask-me vs
draft-first when unsure, direct vs measured tone, file-by-type vs file-by-time. Each answer is
seeded into the existing decision-preference mechanism (an explicit, confirmed
decision_prefBelief withorigin=user_explicit+intake_q/intake_optprovenance, persisted
tobeliefs.json) — no new storage, and the seeds flow into pre-alignment and the violation
gate the moment they land. Skipping plants nothing and costs nothing; replaying the journey
re-opens the intake, and re-answering replaces the old seed for that question. Old
instances never see it (it lives inside the journey's existing fresh-stage gate). Copy
discipline: the receipt says your standards are written down and kept at hand, pre-aligned —
it never claims "I understand you" (locked by test). - File Butler's first lesson: referral → read-only scan → preview card → your call → real
execution. Accepting the resident-referral card now hands you a first-task chip: let the
butler scan your Desktop & Downloads (read-only, whitelist-enforced via the fs-grants ledger,
sensitive-path floor immune) and draft a tidy-up plan. The scan/plan/dedup chain is
deterministic, zero LLM: type or time buckets (the grouping mode is decided by your intake
answer — the first place an intake seed visibly changes behavior), duplicates verified by
content hash (reported only, never deleted), space hogs reported only. The plan arrives as an
H2A preview card with the spotlight treatment — nothing moves until you approve, and "just
looking" (reject) is an explicitly legitimate choice. On accept it executes exactly the plan:
moves only, never a delete, never an overwrite (conflicts are skipped and reported), every move
journaled inbutler_moves.jsonso the whole job is reversible. Empty folders get an honest
"nothing to tidy" instead of an invented plan. Verified end-to-end in a real browser
(Playwright: referral ACCEPT → chip → plan card → ACCEPT → files actually moved,
out-of-whitelist canary untouched) and on the real-key rig (R3). - A demo instance that's already lived a week — a readable daily timeline + a declining
participation curve. A read-only bundled instance ("Lin", a freelance writer) whose skills,
growth curve, decision preferences and role experience are the real output of a real
seven-workday run (compressed in wall-clock, not faked) — so a first-time visitor can watch
what "more like you with every use" looks like at day 7 instead of starting from zero. The panel
now leads with the person (Lin's persona is the headline; the honest "fictional demo" notice
is a footnote, not the star) and its centerpiece is a declining effort curve: hands-on turns
fall from 5 to 2 a day, corrections from 2 to 0, and the decision mode shifts from cold
deliberation to a pre-aligned glance — the from-heads-down-doing-to-a-glance arc rendered as a
mini bar chart plus a six-cell before→after strip. Below it, seven day-by-day cards show what
Lin did / talked about / produced / deposited each day (draft snippets open inline; H2A cards
show the ACCEPT/REJECT and her reason). The earned-silence gate is shown honestly as 9/35, 26 to
go — never auto-pilot (H2A never breaks). The old unreadable belief-fragment dump is gone;
beliefs/knowledge/skills are collapsed detail sections you can expand on demand, not the first
screen. The panel also enlarges/fullscreens (⤢ three-state, reusing the desktop body-class
seam). Browsable via a 👀 entry (GET-only API, 405 on any write; the new timeline/effort-curve
fields are read-only, zero new accounting); guaranteed zero-pollution of your own instance (a
tree-hash test asserts the package files never mutate on browse); every growth number is derived
read-only from the bundled Trace by the same productionbuild_curves, not hand-typed; a
disclosure footnote marks it as a demo throughout. Verified in a real browser (Playwright:
persona headline dwarfs the disclosure, 7 daily cards, the curve's first bar taller than its
last, beliefs stay folded until opened, ⤢ three-state, zero console errors). Ships zh first
(Lin); the English translation is a planned next step.
LLM tagging fragments the vocabulary over time — one memory gets tagged "夜间模式", a synonymous
one "深色主题", and tag-overlap matching goes blind between them. Tag assignment is now
reuse-first: the existing tag vocabulary is pre-filtered (content token overlap + frequency
top-up, zero LLM) into a top-K candidate list that rides along in the tagging prompt with the
instruction reuse an existing tag when one fits; creating a new one requires a reason. New tags
are determined deterministically (never by the model's say-so) and each one is an explicit
tag_createdTrace event — the vocabulary can grow, but never silently sprawl. The comparison
happens at the discrete symbol level: no vectors, inspectable, hand-editable.- Daily synonym convergence. A slow-lane tick (
tag_merge_tick; vocabulary-fingerprint
watermark + judged-pair cooldown; zero LLM when nothing changed) finds synonym candidates by
tag-name overlap and second-order co-occurrence (true synonyms almost never co-occur on one
note — the model picks one phrasing per note — but they orbit the same neighbor tags), has one
cheap LLM call judge them, and merges automatically into an alias table. Tags are derived
data, not user data, so auto-merge is safe: no belief's stored tags are rewritten, the old tag
survives as an alias that still matches, and the audit trail is the alias file (via/ts) plus a
tag_mergedTrace event. Recall seeds, graph-edge keys and supersede candidates all read the
alias-expanded view, so converged phrasings become mutually visible — fragment scenario
measured: paraphrase recall@8 0.67 → 1.00 at N=1k/5k with latency unchanged; the typing hot
path stays zero-LLM, zero-IO. - Ingest-time reconciliation shipped (was Planned). The single supersede LLM call at write
time now also judgesduplicateandextends, adding no calls: a high-confidence duplicate
(LLM verdict plus deterministic lexical/tag corroboration — the model's word alone never
touches the store) is auto-merged by invalidating the losing copy (invalidate-don't-delete,
reason on record, provenance authority still wins); anextends(same topic, new information)
raises the existing merge-knowledge decision card with the model's proposed merged text —
adding information stays your call.
- Daily synonym convergence. A slow-lane tick (
[asr]extra — meeting recordings become minutes, locally. Audio files (mp3/wav/m4a) now
ride the same attachment pipeline as PDFs: upload → transcribed on-device → the text flows into
the existing channels (files-panel preview,read_filefor roles, the meeting-notes skill).
Selection was a real bake-off (verdict in the internal docs): faster-whisper (MIT,
CTranslate2) wins on pip-only install (bundled PyAV — no system ffmpeg), CPU int8 viability,
one model for zh+en, and active maintenance; SenseVoice (stronger Chinese, custom model license,
manual model download) is recorded as the challenger. Honesty is wired in: the speech model is
not in the wheel (first use downloads it — defaultsmall≈480 MB,KARVYLOOP_ASR_MODELto
choose); fake-extension/corrupt audio is refused empty with a clear error (never garbage in the
context); a failed model download reports "model load failed", not "your file is broken"; and
without the extra, audio returns a clear install hint — the meeting-notes skill's contract now
says "audio via optional[asr]" instead of over-promising ASR. Unlock panel got the 🎙️ row
(status + the honest model-download note), i18n en+zh.- Residents fed more method, not more answers. The File Butler's method library grew the
parts a real butler is judged on: the full duplicate decision order (size → hash →
"same name, different hash = versions, not duplicates"), a hot/cold archiving default
(~180 days untouched →Archives/<year>/, finance files exempt by rule not by timer),
per-file-type conventions (screenshots by month, installers as 30-day deletion candidates,
camera filenames kept), and an explicit "never silent" operations list — all in the skill +
seeded into the resident's memory as candidates to confirm, not facts about the owner.
meeting-notes gained per-meeting-type templates (weekly sync leads with last week's items,
reviews lead with the verdict, 1-on-1s record outcomes not personal discussion, brainstorms
keep ideas ungraded), a SMART bar for action items, a decided-by/basis/options decision-record
block, and glossary entries shown as clearly-marked fictional examples — shapes to imitate,
never content to copy in. - 📚 study-buddy system skill (pure asset, third alongside data-analyst/meeting-notes; the
resident role comes later): retrieval-first studying grounded in the two techniques rated
high-utility by the evidence (practice testing + distributed practice, Dunlosky et al. 2013) —
quiz from the learner's own material, an SM-2-lineage review ladder (1→3→7→14→30 days),
Feynman teach-back, Cornell cues, Bloom's ladder for question depth. Answers are graded
against the material ("your notes don't settle this — check the source" is a valid grade),
and the growth story is a human-owned study ledger template (intervals stretching + old
mistakes not resurfacing = the progress report; no invented mastery percentages). - Residents: the empty house gets its first tenant. A fresh install used to greet you with an
empty role library. Now, on the first visit with zero roles, Karvy raises a referral decision
card introducing the first resident — the 📁 File Butler — and nothing happens until you
say so: ACCEPT actually creates the role (its identity, temperament, verification gates and the
seeded collaboration contract are all plain files you can open and edit — a working example of
how to constrain an agent); REJECT means it never asks again; DEFER just keeps the card. No
preset subsystem was built: residents are read-only in-package mirrors
(karvyloop/system_residents/), and moving in is a normalRoleRegistryinstantiation — your
instance is never overwritten by upgrades.- 📁 File Butler ships with a method, not canned answers: a
file-butlersystem skill
(PARA / GTD inbox-zero / Johnny.Decimal / ISO naming / size-then-hash dedupe, sources named)
plus a human-ownedfiling-preferencestemplate that grows into your filing rules. Safety
lives in the deterministic layer, not in prose: folder access is a hard whitelist
(Desktop / Downloads / Documents) recorded in the fs-grants ledger (visible, revocable),
deletion always requires explicit confirmation with a backup first, and sensitive paths are
immune to any grant. Deliberately absent: vector indexes, OCR, self-owned cron. - 🎙️ meeting-notes system skill (pure asset, alongside data-analyst): transcript → minutes
with the three-bucket method (decisions / action items as who-what-by-when / open questions),
and a glossary gate — unknown terms are checked against a human-owned team glossary
template and flagged "needs confirmation", never expanded by guessing. Honest input contract:
it consumes text; audio only via the optional[asr]extra (see above). Growth stays real: the
glossary file getting longer is the metric — no invented percentages.
- 📁 File Butler ships with a method, not canned answers: a
- "Unlock more capabilities" panel — degraded features now guide instead of hiding. Graceful
degradation had a blind spot: if you never configure MCP, push channels or attachment parsing,
nothing ever tells you they exist ("if you don't guide users, they genuinely don't know the
config is there"). A new deterministic endpoint (GET /api/capability/unlocks, zero LLM, never
echoes a secret) reports each optional capability's live status — ready / not set up / needs
install — and the console renders it as a checklist: one line of value, the exact command or
config.yamlsnippet (copyable), a one-click jump to the MCP presets / paste-URL screen, and
where to find MCP servers (the official registry at registry.modelcontextprotocol.io plus
community directories PulseMCP and Glama — links verified live). Voice input is probed
browser-side, so the silent "no 🎤 in this browser" case now explains itself. Entrances where
you'd actually look: a Skill Library card, the Diagnose panel (the health card says what's
broken; this says what's dormant), the end of the first-10-minutes journey, and right where a
PDF preview fails for the missing[files]extra. i18n en+zh; pattern: setup-checklist /
connectors-directory, one clear action per row. - Webhook inbound approval (v2). The webhook channel can now carry your decision back:
setchannels.webhook.reply_urlto a polled reply source (e.g. a private ntfy topic's
/json?poll=1endpoint — the console polls it outbound, still no listening port) and reply
ACCEPT <code>/REJECT <code>/DEFER <code>from your phone. Codes reuse the email
channel's HMAC single-use, time-limited mechanism (same secret, same mint/verify) and land on
the same decide path as the console and email; strict-parse only — anything else in the reply
source is data, never instructions; high-risk cards remain notify-only (no code is ever minted
for them, and the poller rejects them again as a second fence); a persisted watermark plus a
processed-id ring prevents re-consumption across restarts. Unsetreply_url= v1
outbound-only behavior, unchanged. - Observability, grown on the Trace (external suggestion, converged). No parallel event
stream, no second ledger — three additions that live on what's already there:run_idthreads one run together. Each drive (and each daily/background entry) opens a
per-run scope (contextvar, same pattern as the deontic gate); every Trace entry and every
token-ledger row written on that chain is stamped with the same short id. Old records without
the field read back unchanged; existing DBs migrate in place.karvyloop replay --run <run_id>. Filter the replay to a single run across tasks, with a
stderr summary line (entries / duration / tokens — computed read-only from existing data).- Real causes surface, not misdiagnoses. "Infra dead" (model/network unreachable) is now a
whitelist — network/timeout/auth/rate-limit/5xx only. Code defects (TypeError,
AttributeError, bad-request 4xx…) fail loud with the original exception chain, and the true
cause (exception class + traceback) is recorded into the Trace; budget/context-ceiling gates
report as budget stops instead of "network down". This fixes the week's real pain: a swallowed
TypeErrorwas reported as "model unreachable" and sent debugging in the wrong direction.
Changed
- Desktop view redesigned for a calm, single-focus layout. The desktop used to open with every
sticky note auto-spread across the right half — busy, no hierarchy, visually crowded. It now
opens empty and single-focus: a large centered clock anchors the top with a lightweight
"waiting on you" list beneath it (minimal one-line entries for pending decisions and tasks —
visible but not shouting, never full cards); a compact centered chat sits between the clock
and the dock — a single-window chat with no conversation sidebar, like a small chat app. A⤢
button in the chat titlebar cycles three states: compact → expanded (full chat with the
conversation list and history) → full (fills the whole console viewport) → back. The four
sticky notes (decisions / intel / ideas / who's-busy) default to collapsed and parked in a
tidy strip (title only, click to open); the board is folded into a single dock icon (📋) with
a badge that lights up when there's a new decision or new data — click it to fan out all four
full tabs, click again to tuck them away. The dock, the day/night wallpaper (auto/day/night/off),
and the "no fake theater" soul layer (zero engine timers, real-event-driven only) are all
preserved; the app's default view is still the conversation (the desktop is a place you switch
into). Saved layouts you'd rearranged are still honored; an incompatible old layout falls back to
the new default without breaking. Verified in a real browser (Playwright) with screenshots of the
empty / expanded / full states, and the pending list, board badge, and three chat states are
locked by smoke + browser tests.
Fixed
- Knowledge chat: real back-and-forth, no self-contradiction, links actually fetch. Chat now
renders as separated turns (question vs answer) instead of one wall of text; the same module
no longer offers to sediment X while another part says "nothing to sediment" — one referent, one
source of truth, enforced structurally rather than by prompt; and librarian fetches of a pasted
link now send real browser headers and allow the fake-ip proxy range (198.18.0.0/15) so
paste-a-link works behind Clash/Surge/V2Ray fake-ip mode (an earlier SSRF hardening had blocked
it). Starting a new knowledge session no longer loses the previous one. - Management modals now scroll when content runs past one screen. Atom and role edit modals
clipped anything below the fold with no scrollbar —.modal-bodywas missingflex: 1+
min-height: 0, so the flex child wouldn't shrink and the overflow never engaged. - Capability-grant card now covers the delegated execution path, the decision-card popup no
longer overlaps the role-edit modal, and the paradigm form is discoverable. - Image receipts wired into
file_extractwithout crashing when[ocr]is absent — a missing
OCR extra degrades honestly (missing_dependency), and a fake/corrupt image is refused
(bad_file) rather than spilling bytes into context. - Desktop view: five real layout bugs from the calm single-focus screenshots. All caught
visually (the previous Playwright tests asserted existence/z-index but never saw occlusion) and
now locked with rect-level visual assertions + screenshots:
(1) The big clock is no longer squeezed. The weekly-memento tile used to sit dead-centre on
top of the clock and the compact chat crept up under it — the clock is the desktop anchor and got
clipped top and bottom. The memento moved to the top-left corner and the chat's default top is
measured to land clearly below the clock, so the anchor keeps its own clear vertical space.
(2) The compact chat shows the conversation, not a squished card over a blank void. The chat
body is a200px + 1frgrid; hiding the conversation list withdisplay:noneleft the 200px
column reserved, sochat-logwas crushed to ~200px with a huge blank area beside it. Compact
mode now collapses the body to a single column so the message flow fills the window, and an empty
chat-logshows a proper "Say something to Karvy" placeholder instead of blank.
(3) Pop-up windows no longer hide under the dock. The Lin demo panel (and any window) could
extend its bottom beneath the floating dock. Windowmax-heightnow reserves the dock band, and
clamp/positioning keep every window's bottom above the dock.
(4) Windows drag by their title bar again. The injected minimise/expand/close buttons were
spread across the whole management title bar byspace-between, so grabbing the bar usually
landed on a button and refused to drag. The buttons now group tight on the right, leaving a large
clean drag handle; dragged positions still persist tokarvyloop_desk.v1. (Mobile keeps no drag.)
(5) Collapsed side notes stack cleanly. The docked-note lane stepped by a stale 40px constant
while the real collapsed cards are ~65px tall, so each card overlapped the next and text bled
together. The lane step is now measured from the real collapsed height, giving clean vertical
spacing. Locked by browser tests (clock ∩ memento/chat empty, demo bottom ≤ dock top, title-bar
drag moves left/top, collapsed cards pairwise non-overlapping) and archived day/night screenshots. - The global Karvy (bottom-right capybara) can no longer be covered by a panel. On the desktop
it used to sit below windows and panels (z-index 210), so an open management panel or the board
could hide it. It now stays pinned on top (z-index 9550 — above the dock and every window/note),
so the one place you always talk to Karvy is always reachable. It still sits below its own
"carrying a card" walk-on and speech bubble (those are its theater). Regression-locked by a
browser test that opens a panel over the mascot's corner and asserts the mascot is still the
top-most element at that point. - Onboarding guidance is now impossible to miss. First-10-minutes feedback: the guidance
bubbles were easy to overlook. Guidance now uses the standard spotlight treatment — a black
semi-transparent mask (0.7) covers everything else, the target is cut out with a pulsing accent
ring, and the guide popover got a high-contrast restyle (larger title, accent CTA button). The
journey's action moments (first demo-task chip, run-it-again chip, and the method-reuse receipt
on a real recall hit) get the same spotlight — one mask per moment, never re-popped by polling.
The mask only exists while guidance is active: Esc or clicking the mask dismisses it, the
spotlit button stays clickable through the cutout, andprefers-reduced-motiondisables the
animations. Locked with a real-browser regression test across both views (chat + desk).