Skip to content

implementing ROCM and metal support - #3

Open
someRandomDude-a wants to merge 42 commits into
someRandomDude-a:rewrite-2from
msmayankshetty99:main
Open

someRandomDude-a wants to merge 42 commits into
someRandomDude-a:rewrite-2from
msmayankshetty99:main

Conversation

@someRandomDude-a

Copy link
Copy Markdown
Owner

No description provided.

msmayankshetty99 and others added 30 commits October 4, 2026 23:35
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Bus listeners run on the publisher's thread and take SessionManager._voice_lock,
so WakeWord, ActionController, Initiative, ResourceEvents, EmotionProbe, the
speech queue and the Discord launcher could each deadlock against it.

- Add events/outbox.py: queue events/callbacks under a lock, flush after release,
  delivered in order by a single drainer.
- Move every publish/callback out of component locks; ResourceEvents drops
  superseded values so a stale snapshot is never published last.
- _playback_event ignores non voice/speech events before taking _voice_lock.
- EventBus logs listener exceptions instead of swallowing them.
- tests/test_lock_discipline.py: static scan for publish-under-lock plus
  regression tests for the reproduced WakeWord and ActionController deadlocks.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…t loop

- cancel() only cuts a turn still generating or playing: Stop, Sleep and shutdown
  no longer erase a finished reply; muted or failed TTS keeps the visible text.
- Failed, cancelled and shutdown-interrupted turns keep the user's message;
  errors are archived as errors, not interruptions.
- respond() releases the turn lock if superseding the previous reply fails, and
  validates voice settings before changing turn state.
- Unreadable chat history, tool approvals, todo list and probe files are kept
  as <name>.unreadable-<timestamp> (persistence/preserve.py) or never saved over.
  Corrupt approvals require approval for every tool; the panel shows the error.
- Probe: weights-only restore from probe.pt; weights not loaded are kept on disk.
- todo_list: stable IDs from a persisted counter, never reused.
- WS bootstrap snapshots, /api/chat, whiteboard capture and Discord attach/detach
  no longer take session locks on the event loop; close() unsubscribes first.
- Log listener/getter failures that were swallowed.

Fixes audit findings V001 V012 V015 V018 V067 V068 V114 V115 V160, plus the
issues confirmed by the adversarial review of this change.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The scientific_calculator tool passed model-controlled text to eval(); attribute
walking (().__class__.__base__.__subclasses__()) reached os.system, so prompt
injection could run shell commands. It now evaluates only numbers, arithmetic
operators, the listed constants and direct calls to the listed math functions,
with bounds on input length, nesting, integer size (checked before powers) and
complex results. Errors raise, so the registry reports them as tool errors.

Fixes audit finding V003, plus issues found by an adversarial attack review
(flat sums rejected as nesting, over-cautious power check, parser MemoryError
leaking an empty message, unclear errors).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…confirmations

Any local web page or process could drive the backend on 127.0.0.1:8765. Now
every HTTP request and WebSocket handshake needs this backend start's bearer
token and a loopback Host, and sockets need an app Origin (LocalAPIGuard).

- run_server.py binds 127.0.0.1:8765 before the model loads, then mints the
  token and a separate confirmation key. Dev writes both owner-only under
  persistent_memories; packaged Electron generates them, passes them to the
  backend it spawns, and sends the token only after the backend prints
  RIKO_BACKEND_LISTENING. Both are removed from the backend's environment.
- Electron main injects the header for 127.0.0.1:8765 only, so renderer code
  never sees the token; main's own calls use backendFetch.
- Security-sensitive settings (code, models, libraries, programs, file
  access, network endpoints, judged by key, by each leaf of a section saved
  at once, and by URL/absolute-path values), turning tool approval off, and
  Discord access changes return 428 with a single-use challenge. Electron main
  shows a native dialog (hidden characters escaped) and signs it with the
  confirmation key, which never goes over the wire.
- The Discord worker gets the token from its launcher, exits with its backend,
  talks only to 127.0.0.1 (localhost URLs are rewritten), and its socket now
  receives only Discord's own turns.
- API docs are served only with RIKO_DEV=1.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Port the llama.cpp branch's approach (talk to an external llama-server via
its base URL and /tokenize) onto main's shared LlamaContextProvider, so any
llama.cpp build (CUDA, ROCm/HIP, Metal, Vulkan, CPU) can serve Riko while it
keeps the reserved live slot, exact template/tokenizer counts, Responses
streaming, tools and cancellation.

- LlamaServerProvider waits on /health while the model loads, then requires
  parallel_slots slots each holding the largest live/initiative/reflection
  budget, and prints the matching llama-server command if not.
- One http.client connection per request; cancelling shuts the socket down,
  so a request stopped during prefill returns at once and llama-server stops
  it too. Connects time out after 5 s, and a lost server is checked again on
  the next request.
- runtime.base_url (default http://127.0.0.1:8080) and api_key are validated
  when the config loads, so Settings rejects a bad address before saving.
- Shared slot validation moves into validate_slots; native-only checks stay
  with InProcessLlamaProvider. The emotion probe remains native-only.
- Settings shows the server address, API key and slot fields for this
  provider with the launch command; README and stale messages point to it.

Verified against stock llama-server b11408 with a local Qwen3 4B GGUF on
Apple Silicon: warmup, exact counts, streaming timings, tool calls, and
cancellation mid-prefill and mid-stream (0.04 s).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…, clean shutdown

Audit wave 2, verified on an Apple Silicon Mac with the native Metal bridge.

- ASR (V013): one faster-whisper factory, audio/asr.py. auto/default resolve
  to what CTranslate2 can run here (CPU int8 on macOS); an unsupported pair
  in an existing config falls back once with a warning, and Settings names
  the value that cannot run when the pair is edited. Packaged first run
  writes cpu/int8.
- Memory (V007): the semantic index is a numpy float32 matrix instead of
  FAISS, whose bundled libomp killed the backend beside torch's on macOS
  (reproduced with one memory and warmup off). Retrieval is identical; the
  index file is no longer written, and faiss-cpu is gone.
- Torch devices (V090): runtime/torch_device.py (auto/cpu/cuda[:n]/mps),
  Julia maps mps to CPU, retries a failed GPU load on CPU, and no longer
  leaves torch capped at 4 threads; new memory.device setting.
- llama.cpp options (V176, V211): flash_attn auto/on/off (default auto; YAML
  booleans keep their meaning), n_gpu_layers -2 for all layers and --fit only
  for -1, row split refused with an actionable message, budgets above the
  GGUF training context rejected before loading. riko-native returns
  llama.cpp's own WARN/ERROR lines on failure and offload/flash-attention
  notes on success; the probe identity records what flash attention resolved.
- Shutdown (V162, V094): the active turn is cancelled before uvicorn drains
  (2 s), Discord stops in parallel with the session close, live native
  contexts are destroyed at exit or skipped with os._exit so ggml-metal's
  static destructor cannot abort, managed mode exits itself at 14 s, and
  Electron escalates SIGTERM to SIGKILL. Ctrl+C no longer prints a traceback.
- Environment (V133, V117, V122): install_reqs.sh is strict and picks torch
  wheels by platform, NVIDIA driver and compute capability (ROCm with
  rocminfo), preferring the repo .venv; pyproject gains a runtime extra kept
  equal to requirements-runtime.txt; MCP commands resolve on the child PATH
  and load failures reach the desktop; the packaged macOS backend gets the
  Homebrew PATH; only the popup global shortcut is bound by default.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- V022: the setup wizard leaves room for the reply (memory budget is
  n_ctx - max_output), llama_cpp configs without a memory key default the
  same way (a legacy preset key sets n_ctx only), and Settings checks the
  budget only when a budget key changes, so existing installs can save.
- V025: a second packaged launch stops evaluating main.cjs right after
  losing the single-instance lock, so it cannot spawn another backend.
- V027: Setup refuses a data folder equal to or inside the install folder,
  which updates and uninstall delete.
- V030: quitting with unsaved Settings asks first, before the backend is
  stopped; a reload asks instead of silently cancelling, and the renderer's
  guard follows real edits only.
- V031: the setup and training-data windows get an opaque dark background.
- V020: the tool worker, MCP children (PYTHONIOENCODING) and the frozen
  backend (PyInstaller UTF-8 mode) use UTF-8 stdio on Windows.
- V029: a pending approval takes the overlay's pointer only while hovered.
- V028: Linux emulates click-through forwarding by polling the cursor for
  ignoring windows not covered by another Riko window, and runs under
  XWayland (deb/AppImage launcher args keep --no-sandbox; direct Wayland
  launches relaunch once with --ozone-platform=x11).
- Whiteboard: a fresh board no longer throws on its first restore.
- release.yml installs frontend dependencies before the Python checks.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…s something

- V026: electron/package.json keeps only yaml (the one module main-process
  files require) as a runtime dependency; electron, electron-builder 26.0.12
  and the renderer libraries are exact devDependencies, so electron-builder
  accepts the package and release.yml no longer installs it ad hoc.
- V131: Python release dependencies are one hash-locked universal file for
  Windows, Linux and macOS (tools/release/lock.py via uv; CPU torch), plus a
  --no-deps lock for EfficientWord-Net; CI installs with --require-hashes.
  Verified here: the lock installs with pip and the full suite passes on it,
  and it resolves for x86_64 Linux and Windows.
- V032: PyInstaller comes from the lock (6.22.3), and numpy._core._exceptions
  is a hidden import, so the frozen backend survives numpy 2.4.
- V033: the CUDA backend builds with GGML_CUDA_NO_VMM, so riko-native no longer
  hard-links the NVIDIA driver, and the release check runs in a cleaned
  environment on GPU-less runners.
- V203: --release-check also proves the bundle loads its own libraries and no
  driver library, imports every lazily imported module and desktop_server,
  runs a calculator call through the real --tool-worker and dry-runs the
  Discord worker; without a library it checks the dev tree (and a test does).
- A tool worker that prints nothing now reports its exit code and logs stderr.
- opencv-python is dropped: nothing imports cv2.
- release.yml runs every step under bash, so a failing command fails its step
  on Windows too.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…, macOS DMG

- V023/V207: electron/native_backends.json names every bundled backend
  (cuda, vulkan, metal), the platforms that ship it, per-OS library names,
  shared-library patterns, rpaths and CMake switches. config.py
  (bundled:<backend>), release.cjs (hardware detection runs only the current
  OS's probes, none on macOS), first_setup.jsx and build.py read it, and
  naming is tested with explicit per-platform tables on every host.
- V034: build.py builds the manifest's backends for the host OS: Metal on
  macOS with @loader_path rpaths and .dylib collection (versioned links kept),
  CUDA and Vulkan on Windows and Linux. It also puts Code/ on PyInstaller's
  path: --collect-submodules process silently froze none of Riko's dynamically
  imported modules, so the frozen tool worker never worked on any OS.
- V057: an arm64 macOS 14+ DMG with hardened runtime and the three
  entitlements it needs (allow-jit, audio-input, disable-library-validation,
  each proven necessary), a microphone usage string, an icon rendered from
  assets/logo.svg, and ad-hoc signing unless Developer ID secrets are set
  (retried like electron-builder's own signing). The packaged app asks for the
  microphone before starting the backend and says once when access is off.
  release.yml gains a macos-15 leg and repeats the release check inside the
  signed app (build.py --check-app).
- Electron's runAsNode, NODE_OPTIONS and --inspect fuses are off, so other
  processes cannot relaunch Riko as Node to use its microphone permission.
- Checksums cover only shipped folders; macOS notes that signing changes them.

Built end to end on an M5 Pro: Metal bundle, frozen backend passing
--release-check on its own and inside the signed app, and a 547 MB DMG that
mounts and verifies.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…native smoke

- V035: .github/workflows/ci.yml runs the full Python suite on ubuntu-22.04,
  windows-2022 and macos-15 with the hash-locked release dependencies and a
  per-test timeout (pytest-timeout, now in the lock), plus an Electron job
  (npm ci, node --check, npm test, npm run build). A push and its pull request
  share one run. release.yml tests and builds the frontend before the long
  native build and has job and step timeouts.
- V200: the default suite passes on a fresh clone: test_server_logging tests
  configure_logging and run_server.main (bind-first, secrets, Ctrl+C, port in
  use) without importing desktop_server or binding a port; the private-config
  test skips when the file is absent; the permission-denied store test
  simulates a denied read instead of chmod(0), which Windows ignores.
- V199: .github/workflows/native-smoke.yml builds the patched bridge at the pin
  with Metal on macos-15 (build.py --smoke-bundle) and runs a pinned
  Apache-2.0 Qwen2.5-0.5B GGUF through InProcessLlamaProvider: warmup, GPU
  offload, bundle self-containment, slots, token counts, streaming, a
  background lane, cancellation, clean close, and emotion-probe samples on the
  live slot only (tests/test_native_smoke.py, marker `native`). It runs when
  native, inference, probe, configuration or release code changes, and weekly.
- A mocked test keeps the probe's FEATURE_VERSION equal to the native patch.

Verified here: 712 passed, 0 failed; npm test 175; the native tests pass
against the repo's Metal bridge and a freshly built smoke bundle; the suite
also passes in a Linux container as a non-root user.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Audit waves 1–3: deadlocks, data safety, API lockdown, Metal support, releases and CI
Prompts stay inside the context, their prefix stays cacheable, and stores
stop growing with the conversation.

- Memory captures (V002): each capture stores a bounded formation_context
  (the newest 12 history messages, strings clipped to 1000 characters,
  lists to 20 items) instead of the whole history, so the store no longer
  grows quadratically. Retrieval and index rebuilds no longer copy them.
- task_get (V016): returns the newest change summaries (history_limit,
  default 5, max 20) with a history_omitted count. TaskMCP server 1.1.
- Remote token estimates (V008): OpenAI-compatible providers estimate with
  a pre-tokenizer approximation calibrated against the Qwen3/3.5, Llama 3,
  Gemma 3, Mistral Nemo and DeepSeek V3 tokenizers, instead of UTF-8 bytes
  (3-4x over). High-entropy runs (base64, JWTs, API keys), all-caps text
  and random letters are charged per character so they never under-count.
- Strict chat templates (V061): OpenAI-compatible providers fold later
  system messages into the next input, as the native path does. Turn
  recall always folds into the input, so the system prompt stays
  byte-identical even when packing drops every past turn.
- Runtime observations (V056): the tool loop adds one only when the
  runtime state (not just its timestamp) changed, and keeps the ones it
  sent, so each tool-loop request extends the previous one (KV cache,
  Responses ids).
- Packing (V009): a per-ChatService anchor keeps the retained history
  start between turns, so a steady turn costs one count and the prefix
  stays fixed. Overflow trims history to three quarters of its room and
  re-expands below half; a cold start keeps a history that already fits.
- Oversized tool output (V058): as a last resort every current tool result
  is cut to the largest common size that fits, head and tail kept, with a
  marker, instead of failing the turn.

Tests: 750 passed (full suite), Electron 175, native Metal smoke with
Qwen2.5-0.5B. The adversarial review's four findings are fixed with
regression tests.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Spoken input is never lost or answered twice, remote turns stay isolated
from the room's microphone, Stop takes effect at once, and a multi-word
companion name no longer breaks startup.

- Voice-turn contract (V010, V069): voice_transcript returns what to do
  with each final transcript. 'fresh' starts a new turn (no spoken reply
  in flight, a stale anchor, or a reply already finished or stopped);
  'reply' redoes the reply the words cut, after merging them into its
  question or inserting them as speaking over; 'preserved' keeps
  tolerated speaking over. A continuation no longer cancels a reply and
  leaves Riko silent, a late follow-up is no longer merged into an
  answered question, and a redo that another turn overtook asks the
  words as a new turn instead of recording them twice. Speech that
  crosses the threshold after Stop cuts nothing.
- Remote turns (V011): speak=False turns (Discord) never engage the wake
  window or anchor local speech, so the room cannot cancel them.
- Waiting dispatch (V161): a finished utterance waits for the running
  turn (respond(wait=...)) instead of being refused and lost; input is
  recorded only once the turn lock is held; a turn's history base is
  measured as it takes over.
- Stop (V065, V109): reasoning deltas raise TurnCancelled; OpenAI-
  compatible streams are cancelled per request (cancellable_stream shuts
  the socket down, so a live Stop never touches initiative or
  reflection); tool calls wait in slices, and Stop kills that call's
  worker or sends the MCP server notifications/cancelled. /api/chat
  answers 409 while another turn runs, and the chat box keeps the text.
  /api/voice/interjection redoes a reply it cut.
- Wake name (V014): the wake phrase is voice.wake_word, else the first
  word of the companion name; an unusable one disables only wake
  detection. Settings validates the effective phrase and quotes it; the
  wizard writes a valid one; a SessionManager construction failure takes
  the setup-mode fallback; `animation: false` turns animation off.

Tests: 804 passed (full suite), Electron 177, the threaded suites 10/10
runs, native Metal smoke. The 12 new cancellation tests fail on the old
code. The adversarial review's confirmed finding (a stale redo recorded
the words twice) and four low findings are fixed with regression tests.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A network blip no longer costs a Discord reply or the bot's turn queue,
saves survive Windows sharing violations and power loss, and the memory
store reads past and future schemas instead of breaking every turn.

- Discord (V024): a failed streamed send or edit costs only that chunk
  and is retried on the next flush with the same per-chunk nonce, so
  Discord never posts it twice (a retry that gets back an earlier copy
  edits it to the current text). finish() is idempotent and always
  attempts the final text. process_queue guards every cleanup step, and a
  done-callback restarts the turn queue if it ever dies while not closing.
- Atomic writes (V080): persistence/atomic.py atomic_write writes a
  unique temporary file beside the target, syncs it (F_FULLFSYNC on
  macOS), retries os.replace briefly on PermissionError (antivirus,
  indexers), fsyncs the directory, and sweeps temporaries a killed process
  left. Chat history, the memory store and the JSON stores use it; a
  failed history save after a reply is logged and the next save catches
  up. Avatar geometry is written outside DesktopState's lock.
- Memory store schema (V084, V182): every field is type-checked; a wrong
  value is read losslessly or reset to its default, logged, and the file
  as it was is kept once, by copy only, before the next save (a move on a
  full disk could leave no store). Unknown keys are kept and written back.
  The store is written in the layout it was read in, so main and older
  branches still read it; {"version", "records"} and STORE_MIGRATIONS are
  ready for a real version bump, and a migrated store is kept once too.
  A newer version or structural damage fails closed. retrieve tolerates
  bad timestamps.
- Backups (*.unreadable-*) and write temporaries under
  persistent_memories/ are gitignored, so private memories and chat
  history cannot be committed by accident.

Tests: 841 passed (full suite), Electron 177, the async and store suites
10/10 runs, native Metal smoke. The user's real memory store loads with
no repairs and re-saves identically (checked on a copy). The adversarial
review's two confirmed findings (a duplicate chunk after an ambiguous
send failure; a full disk moving the only store aside) and its four low
findings are fixed with regression tests.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Audit wave 4: conversation and turn correctness
…nts build on

The app core had one import cycle through nine packages, and importing any
leaf module loaded about 38 of them. Neither is true any more, and two
ratchet tests keep it that way while later segments shrink the coupling.

- Lazy facade (V186): app_core/__init__.py resolves its 16 public names on
  first use (PEP 562), with the same objects as before. `import
  process.app_core` loads 1 app_core module instead of 38; the task store
  loads 3 instead of 39.
- kernel/: the leaf utilities that formed the cycle (workers, lifecycle,
  messages, streaming, output_filter, metrics, torch_device,
  background_budget, and cancellation with a Deferred base for
  TurnCancelled and BackgroundPreempted) import nothing else from app_core.
  read_gguf moved to inference/gguf.py. Every old path is a two-line
  re-export shim for one release, checked for identity.
- Cycle breakers: ReflectionDeferred subclasses kernel BackgroundPreempted;
  ToolRegistry takes an injected activity observer (the factory passes
  DesktopState) instead of importing it; run_server dispatches
  --tool-worker, --discord-worker and --release-check before uvicorn or the
  configuration load, and built-in tools load inside iter_tools(), so a
  tool call no longer imports uvicorn or pypdf.
- Ratchets: tests/test_package_boundaries.py pins the 43 cross-package
  import edges (load-time or lazy), forbids cycles even through
  function-level imports, keeps kernel free of outgoing edges, and rejects
  any route to the facade from a subpackage. tests/test_private_access.py
  pins the 58 cross-object private-attribute accesses, direct or through
  getattr/setattr/hasattr/delattr. Both fail on growth and on stale
  entries, so later segments shrink them in the same change.
- Release: the release check imports every kernel module and resolves
  every facade name; a probe proves neither importing run_server nor its
  worker dispatch loads uvicorn or the configuration.

Tests: 852 passed (full suite), Electron 177, run_server --release-check on
the dev tree, native Metal smoke, and a live managed-mode backend in an
isolated data root (chat over HTTP, then shutdown mid-reply in 0.6 s). The
adversarial review's findings, all on the ratchets' reach (getattr access,
own-name masking, facade imports via `process`, the worker-dispatch probe),
are fixed with self-tests.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…icit wiring

Other modules no longer reach into SessionManager's private fields, chat
history has a single writer, and collaborators are passed in instead of
patched on.

- Turns (V183, V194): kernel TurnBusy (a RuntimeError with the same
  message) and a TurnGate that owns the turn lock (begin with Wave 4's
  wait mode, try_begin for initiatives). /api/chat, /api/discord/chat and
  /ws/chat map TurnBusy to 409 by type, not by message text.
- Status (V183): one locked RuntimeStatus snapshot with named predicates
  (replying, local_reply, is_quiet, idle_for_background, model_state)
  replaces six hand-written busy checks that disagreed. Animation, wake
  feedback, voice anchors, initiatives, expression idling, the calibration
  guard and memory foregrounding all read it.
- Public surface: is_open, active_turn(), cancel_turn(turn_id),
  set_user_speaking(), transcribe() and history_snapshot(). AsrService owns
  the one Whisper model that voice input, warmup and Discord share. The
  Discord socket's event filter lives in the Discord router. The private-
  access ratchet shrinks by 27 entries.
- History (V299): ConversationHistory is the only writer of chat history
  and chat_history.json: snapshots are copies, every change is one
  rewrite under a leaf lock, saves are atomic, a failed save is retried by
  the next change, and unreadable files stay fail-closed. chat.history is
  a read-only copy.
- Injection (V184): ChatDeps (with a real close() owning the factory's
  ExitStack), a per-turn TurnContext instead of patched attributes,
  declared RuntimeConfig budget fields, DesktopServices injected into the
  desktop tools, EmotionWorker.playback_active(), and __slots__ on
  ChatService, DesktopState and RuntimeConfig so undeclared attributes
  fail at once.
- Found by the live smoke: a reply with no visible text keeps
  ChatService's '...' placeholder in history (only there, not in what
  voice anchors read), and template_messages joins consecutive user (or
  plain assistant) messages, because history keeps the user's message of
  a stopped, failed or unheard turn and Gemma/Mistral templates require
  alternating roles.
- From the review: a spoken initiative validates its voice and speech
  settings before committing its message, so a bad value no longer
  leaves a message nobody saw in history on every retry.

Tests: 881 passed (full suite), Electron 177, threaded suites 5/5 runs,
release check, native Metal smoke, and a live managed-mode backend in an
isolated data root (chat over HTTP, history saved through the single
writer, shutdown mid-reply in 0.6 s). The adversarial review confirmed no
medium or high findings; its low findings are fixed with tests.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Providers and tools now have one declared shape each, so callers stop
probing for members, an emotion-probe failure no longer takes the model
down, and an MCP server can no longer shadow a built-in tool or inherit
its approval.

- Providers (V185): an InferenceProvider protocol and a BaseProvider with
  defaults for every member except generate, ProviderCapabilities (slots,
  exact tokens, latent probe), role lanes, and optional ProbeHost,
  CaptureHook and GenerationObserver protocols. OpenAI, llama_server and
  the native provider all subclass it; expression_idle is a declared
  hook; a test forbids hasattr/getattr provider probing. The slot
  scheduler allows at most one initiative lease (a waiting one no longer
  holds reflections back), and cancel_live stops only a live lease.
- Emotion probe (V187): the model loads first and the probe starts in a
  separate try, so a stale library, missing torch or Julia, or an
  unreadable file leaves chat up, reports probe_error in
  /api/neural/status, and slows the unused capture to its sparsest. Probe
  parsing moved out of the generation loop into emotion/probe_hook.py
  observers (a failing observer never fails a request; inference no
  longer imports emotion). JuliaEmotionEngine has a public API; the probe
  identity key is pinned so trained probes still match. Example replay
  runs in EmotionProbe.replay on a 'probe_replay' role that gives slot 0
  back to any live request. Fingerprints are cached per file state.
- Tools (V193, V152, V118): one Tool protocol and one
  ToolRegistry.register(tool, *, source) for built-ins, desktop tools,
  TaskMCP, mcp.json servers and the session's tools. A clash between
  Riko's own tools raises; an external tool that clashes or has an
  invalid name becomes <server>__<name>, keeping its own name for
  tools/call. Isolation is an explicit attribute; tool-specific choice
  rules live in the tools; initiative offers only read_only tools.
  registry.py is split into tool, schema, mcp, isolation and policy
  modules. The model-visible tool set is unchanged.
- Approvals: rules are keyed by (source, name). Legacy {name: bool} files
  load unchanged. Saves write version 2 plus a top-level {name: bool}
  mirror that asks first wherever any registered source would ask, so a
  build from before sources fails closed on a downgrade.

Tests: 917 passed (full suite), Electron 177, threaded suites 5/5 runs,
release check, native Metal smoke (3, including a probe that cannot start
leaving the real model answering), and a live managed-mode backend in an
isolated data root. Review fixes: a gesture cancel with an empty name was
rejected by choice repair; a failed probe kept capturing densely; the
mirror ignored default-governed server tools.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Voice, speech and GPT-SoVITS values are checked once when the config
loads instead of at every turn or frame, and every setting is declared
once, by the feature that owns it.

- Typed sections (V188): VoiceConfig, SpeechConfig and SovitsConfig,
  checked in load_config through shared kernel/validation helpers, with
  the audio geometry constants beside them. Session, voice input, wake
  word, speech queue and TTS read the typed fields, so a bad value fails
  at load naming the key instead of failing every reply. Unknown keys in
  any section are reported (log and Settings warnings), never a startup
  failure. The KV pool is sized only for llama_cpp, so an OpenAI-
  compatible provider with llama-only keys no longer crashes load_config.
  Settings explains that an enrolled wake word's own threshold overrides
  voice.wake_threshold.
- Schema registry (V189, Python side): kernel/schema.py (Setting,
  Section, Rule, Registry) where each feature declares its own sections;
  configuration imports only kernel and persistence (six package edges
  removed). load_config runs every registered check; SettingsStore's
  fields and values are generated from the registry (the hand-kept
  ENUMS/RANGES/HELP/LABELS tables are gone); /api/settings adds
  visible_when, the cross-field rules, restart scopes and restart_pending;
  save_settings calls registered live-apply hooks. Adding a setting is
  one Setting entry. The Electron Settings UI is unchanged and keeps
  working; it adopts the new keys in segment 6.
- From the review: a section load_config cannot read no longer kills the
  backend before the setup fallback (load_config(recover='setup') loads
  it with its defaults and the lifespan starts in setup mode, Settings'
  snapshot opens it, validation stays strict); voice Settings ranges are
  exactly what the loader accepts, so a working value never blocks Save;
  a fractional stored value keeps a field non-integer; a null section can
  be edited; Avatar model stays above Animation engine.

Tests: 972 passed (full suite), Electron 177, release check, native
Metal smoke, a live managed-mode backend, and the user's real
character_config.yaml (checked on a copy: same values, clean validation,
Settings payload unchanged apart from additive keys).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The backend's file locations, desktop state and HTTP surface each have one
owner: stores get their paths from DataPaths, DesktopState is a facade over
components, and create_app builds the server from routers with no work at
import. No data moves; every route, payload and event is unchanged.

- Data paths (V106): configuration/paths.py builds a frozen DataPaths once
  in load_config (every store, honouring today's per-file overrides with ~
  and keeping data an earlier release wrote elsewhere where it is), and
  kernel/code_paths.py finds the checkout or frozen bundle. Every store,
  the Discord launcher and worker, the task MCP server (now defaulting to
  the backend's store), the tool worker and todo_list (no longer
  cwd-relative) use them; run_server exports an absolute RIKO_DATA_DIR.
  Tests pin every path to today's in the dev and packaged layouts.
- Desktop state (V191): WhiteboardModel, SurfaceGeometry, EffectsModel,
  playback flags and presence, and ActivityLog, each with its own lock and
  publishing through a shared DesktopEvents hub outside it, behind a thin
  facade whose state.snapshot is unchanged. Public methods replace the
  reaches into its lock and _emit (private-access ratchet -5).
- App factory (V095): create_app(config, ...) with no import-time work, a
  Backend on app.state instead of module globals, 15 domain routers plus
  the Discord router under app_core/http/, run_server building the app
  before binding and minting secrets after, ResourceEvents computing on a
  worker so publishers never run route code, and desktop tools registered
  by the factory (the tools -> desktop edge is gone). desktop_server.py
  stays a thin compatibility shim for one release.
- From the review: a Discord worker started by hand keeps using the .env
  and channel preferences it used before when the config's folder has
  none; a whiteboard note with a lone surrogate no longer stops every
  later save (ASCII escapes, failures reported in the snapshot); an event
  ordering test no longer depends on thread timing.

Tests: 998 passed (full suite), Electron 177, threaded and socket suites
5/5 runs, release check, native Metal smoke, a live managed-mode backend,
and a real Electron run against an isolated backend (0 refused requests,
no app errors). Real data paths checked read-only: every existing store
resolves to its current file.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…riven Settings

Electron gets its backend address from one place, its main process is
small modules with behavioural tests instead of source regexes, and the
Settings page, first run and launch reads follow the backend's schema.

- One backend origin (V192): electron/backend_origin.cjs defines the
  default (http://127.0.0.1:8765, RIKO_BACKEND_URL in dev); the packaged
  app runs its backend on a free port (RIKO_PORT, announced as
  'RIKO_BACKEND_LISTENING port=<n>'), so a dev backend on 8765 is never
  mistaken for it. Every window gets the origin through preload
  (rikoConfig.backend); the token goes only to that origin; api.mjs
  request() is the one renderer fetch wrapper and survives non-JSON
  errors. The Discord worker follows RIKO_PORT. A guard test fails on any
  other copy of the address.
- Main process (V195, V208): main.cjs is wiring for backend_supervisor,
  window_manager, window_gesture, window_layout (one compact-size rule),
  window_platform, overlay_input and an IPC router whose route table
  checks the sender for every channel (the displays channel had no
  check). Behavioural tests with fakes replace the source regexes, and
  StreamChat's events go through a tested reducer.
- Settings (V189, Electron half): the page shows fields by the backend's
  visible_when, checks the backend's cross-field rules (a shared contract
  file keeps JS and Python agreeing), and reports which restart each
  saved change awaits. The packaged first run asks 'riko-backend
  --setup-config' for its configuration instead of authoring one, and
  launch-time reads of desktop.debug, shortcuts and the name follow
  YAML 1.1 like the backend.
- From the review: the main-process test fixtures used the host platform,
  so on the Linux CI runner a pointer-forwarder timer kept the test
  process alive and a preload test threw; the fixtures now default to an
  explicit platform and the forwarder's poll never keeps a process alive.
  A field the draft's provider does not use no longer blocks Save, in
  Settings or in --validate-config. A logging test no longer depends on
  running after others.

Tests: 1032 passed (full Python suite), Electron 251 (and the suite run
as Linux no longer hangs), npm run build, node --check on every .cjs,
release check, native Metal smoke, a live managed-mode backend, and a
real Electron run against an isolated backend (0 refused requests, no
origin or URL errors). The packaged first run's --setup-config path is
tested with a fake backend only.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Found by PR #3's CI.

- Windows upper-cases environment names, so the launcher's
  Discord_backend_url arrived as DISCORD_BACKEND_URL and DiscordAccess,
  which matched exact names, let the .env's address win: a managed
  Discord worker on Windows would ignore its parent backend's port.
  BotSettings.overrides matches the environment to the keys from_env
  reads without case, for the settings and their cache key. The test
  now also sets the upper-case form.
- The avatar-assets test relied on the filesystem's timestamp
  resolution for "the source is newer"; it now sets the source's
  modification time explicitly.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Audit wave 5: modularity, seams and contracts
…bundle

The bridge finds its GPU backends wherever it is installed, x86 bundles
run on CPUs without AVX2, staging stores each library once, and AMD GPUs
on Linux get an opt-in HIP bundle that setup offers only where it can run.

- Backend loading (V318): riko-native loads ggml backends from its own
  folder before llama_backend_init (dladdr / GetModuleHandleExW; on
  Windows it pre-loads each ggml-*.dll with the DLL-directory search so
  cudart and cuBLAS resolve beside it) and notes when no GPU device
  registered. The patch adds an INSTALL_RPATH and stops a static Linux
  configure that would fail to link without position-independent code.
- CPU variants (V281): cuda and vulkan build GGML_BACKEND_DL with
  GGML_CPU_ALL_VARIANTS, decided per backend in the manifest; Metal stays
  linked. The release check proves a CPU module loads from the bundle.
- Staging (V130): build.py refuses stale outputs, moves PyInstaller's
  output instead of copying it, keeps versioned library links once, and
  checks every bundle for its GPU and CPU modules.
- ROCm/HIP: an optional linux-only 'hip' manifest entry (GGML_HIP, twelve
  GPU_TARGETS covering CDNA and RDNA2-4, the user's ROCm 7 linked through
  /opt/rocm/lib, never bundled). A default release builds what it always
  did; release.yml's hip input adds a hip job in rocm/dev-ubuntu-22.04
  7.2.1 (pinned by digest), and rocm-compile.yml runs the same recipe by
  hand only (twelve targets take hours; nothing cancels it). Setup offers
  HIP only when every rocminfo GPU agent is a built target and ROCm 7's
  libamdhip64.so.7 is installed; Vulkan stays the default for AMD.
- From the review: detection had accepted any gfx agent (and amd-smi), so
  setup would have preselected HIP on GPUs the bundle has no code for,
  aborting the backend on every launch; and a failed or timed-out hip job
  could block or silently strip the packages. A requested hip that did
  not build now stops only the Linux packages.

Tests: 1053 passed (full suite), Electron 254, release check, native
Metal smoke against the user's bridge and against a fresh Metal release
bundle built from the new bridge (with its strict release check). The
HIP bundle is proven only by the CI compile job; no AMD GPU was used.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Each tool's description is part of every prompt, and about half of a
typical 5.8k-token prompt was tool definitions, which a 27B model reads at
about 200 tokens/s before it can reply. tools.disabled lists tool names
the registry never registers, so they are never offered, never run and
cost no prompt tokens. It applies to every path (built-ins, desktop
tools, TaskMCP, mcp.json servers and the session's own tools); a server's
tool matches by its own name or the <server>__<name> it would run as.
Settings validates it as a list of tool names.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
three.js skips a mesh whose bounds leave the view, but a skinned mesh's
bounds are its rest pose, so once the avatar is swung, dragged or
animated, whole parts could blink out for frames (reported with the
VRoid "brazilian miku" model when moving her). The avatar is always on
screen, so its meshes are never culled, as three-vrm's examples do.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
msmayankshetty99 and others added 12 commits October 12, 2026 16:50
…ever faces away

While an avatar was held, restore() copied the root's saved quaternion
back, which makes three.js re-derive the Euler angles. A VRM 0.x root is
turned by PI, so the angles came back as (PI, -yaw, PI), and the
renderer's per-frame rotation.y write then produced a root facing away
from the camera. The next restore turned it back, so front and back
alternated every frame while held and through recovery: the avatar
looked see-through, split into front and back shells, and was left
facing away about half the time after recovery (reported with a VRoid
model). step() now saves and restore() copies the Euler angles, which
undoes step() exactly.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The defaults folded a held avatar over. Joint stiffness 12 barely beat
gravity 9.8, so held joints bent about 45 degrees (more on VRoid models,
whose upper chest takes its direction from a bust bone), and threshold
ragdoll started after 350 px of ordinary dragging and pinned every joint
at the 60-degree limit. On a VRoid skeleton a chest grab bent the head
46 degrees and stretched the top's mesh to 2.9x; with body follow spring
40 and no ragdoll no bone bends more than 16 degrees and nothing
stretches. Those are now the defaults; ragdoll stays available as an
opt-in. Stored profiles keep their own values.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…d body

Dragging, throwing or walking the avatar moves her window, not her
skeleton, and VRoid springs are centred on the skeleton, so they never
felt the motion: she moved as one rigid piece. AvatarInertia tracks the
window's position each frame and adds its acceleration (plus a little
air resistance) to every spring joint's world-space gravity for that
update only: springs lag when she starts moving, stream behind while she
moves and bounce when she stops. A jump between places is not motion.
The new pickup.bounce setting (0-2, default 1, "Hair & cloth bounce when
moved" in Pickup, ragdoll & recovery) scales it; 1 is about a third of
physical force, since a brisk drag of a 1.6 m avatar shown 720 px tall
is a 20 g jolt. On a VRoid model a brisk drag at 1 swings hair and bust
about 30 degrees, and they settle within a few seconds; at 2 the hair
still settles (no spring is left caught on a collider).

The held body was too stiff to bounce. Damping 0.8 (was 2) and drag
inertia 1.2 (was 0.5) let her wobble after a drag stops and after
release, and the joint limit drops to 35 degrees (was 60), so she
bounces without folding over.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…n VRM 0.x

Her face never moved. With emotion off it sat on a blank neutral preset,
surprise never resolved on VRoid models (their preset is 'Surprised'),
and blinks snapped shut on a fixed timer.

- avatar_face.mjs: expression names resolve robustly (exact, any case,
  VRM 0.x aliases). Face parts built from VRoid Fcl_* morphs (brows up,
  worried or angry, wide or squinting eyes, small or O mouth, fang)
  register when the model has them, within the 64-name capability limit.
  A layered mixer crossfades the base face (emotion, or a faint resting
  smile when there is none) against a reaction or a hit rule's
  expression without overshooting, and corrects blinks for expressions
  that already close the eyes. Blinks close fast, hold, open slower,
  sometimes double, and fire on events.
- Reactions on today's events: a press blinks with a brow pop, a grab
  startles into surprise and eases into a worried, breathing face,
  release brings a relief blink and smile, a hair or bust tug winces.
  The API (react, sequence, blink) is what the hold state machine will
  drive.
- MotionEngine now works in canonical VRM 1.0 axes and flips once for a
  VRM 0.x rig (negating x and z, as three-vrm-animation does for .vrma),
  reading the rest pose through the same flip, so .vrma clips are never
  flipped twice. On a VRoid model the idle arms were raised 63 degrees
  and nods went up first; now the arms hang down, nods go down and the
  wave raises the hand. VRM 1.0 models are unchanged.
- Settings: face.restingSmile and face.intensity in the avatar studio.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ng and a carry swing

Every desktop companion studied (Desktop Mate, Mate Engine, VPet,
Shimeji) holds the character in a designed pose and gets its bounce
from springs and the face; none bends every joint under gravity, which
is what made her fold like a limp doll.

- avatar_hold.mjs: HoldBehaviour, a pure state machine (pressed, poked,
  grabbed, dangling, carried, dizzy, protesting, resigned, tugged,
  recovering) driven by the cursor's motion. Picking her up is a short
  scene: a startle and surprise, a worried breathing dangle with glances
  at the holder, alarm when carried fast, dizziness when shaken, a sulk
  with kicks after 6 s, a sigh and a limp hang after 18 s, a wince on a
  hair tug. HoldFace drives the face layer; it exposes a look target for
  the gaze.
- avatar_held_pose.mjs: authored poses in VRM 1.0 axes (tuck, reach,
  swing, limp, startle, kicks, alarm, settle, breathing), tuned on a
  VRoid model to keep elbow and armpit stretch near walking's; the legs
  mirror smoothly to the trailing side.
- AvatarPickup is now a carry rig: the scruff hold slides her so the
  cursor holds her upper back (kept through a wheel resize and into the
  saved position); one pendulum on the hips (1 Hz, damping ratio 0.28,
  35-degree soft limit, air lean) runs before the springs, so the hair
  swings with her; legs and arms lag, the head stays level, and release
  settles her on her feet with no bounce or sliding. The per-bone
  gravity torque, the root swing and the linear 2 s blend are gone;
  opt-in ragdoll is a limp profile. She never faces away.
- MotionEngine takes the held pose and its crossfade from the hold.
  AvatarInertia.shift keeps a resize from reading as motion. The scruff
  slide and drag geometry live in avatar_geometry.mjs with tests.
- Settings: one shared pickup and reactions field list for the studio
  and settings search; the old pickup keys (gravity, damping, bodySpring,
  jointLimit, inertia, maxSwing) are dropped, so stored values cannot
  bring the fold back.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…el smoke test

- avatar_drop.mjs: release. A flick at or above the throw speed (900 px/s
  by default) throws her with the cursor's velocity, even off the
  taskbar; she falls under gravity and air drag, bounces off the work
  area's walls (re-measured from her outline every frame, so her arms
  stay on screen), and lands on the floor line of the display she is on
  with her shoe tips planted. A gentle release sets her down in place,
  gliding onto the floor when her feet are close to it (this fixes the
  foot-grab drop), and never leaves her below it. Pressing her mid-fall
  catches her. The position is saved once she has landed and the camera
  has settled, and on unmount mid-flight.
- HoldBehaviour gains falling, landing and recovering: arms flung out and
  eyes wide in the air, a wince at a wall bonk, eyes squeezed shut and
  knees bent on landing, a squash that springs back with a small
  overshoot, then a pout at the cursor after a throw or a sigh and a look
  back after a soft drop. No skating after contact.
- Gaze: saccades with fixation flicks instead of gliding (gaze.saccades,
  on by default), timed by amplitude; a glance up at the holder on grab,
  then the viewer, occasional holder glances, the floor while falling;
  the head joins past the eyes' range; big shifts blink at most every
  2 s. No look target means normal tracking in every state.
- Settings: release style, throw speed, fall gravity, landing squash and
  quick eye jumps join the shared list (studio and search);
  recoverySeconds keeps its key, now "Landing and recovery time".
- avatar_vrm_smoke.test.mjs: runs under npm test only when RIKO_TEST_VRM
  names a .vrm file; checks resting arms, the dangling silhouette, a
  decaying carry swing, a throw that lands and recovers, the face, no
  face-away frames and joint limits.

Known: on brisk drags and shakes the camera safety net still shrinks her
(to 73-85% for 0.6-1.6 s); a wider canvas or a compensated pan would fix
it but needs an on-screen check.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…timate, and macOS/Linux activity

V175: GPU telemetry read only nvidia-smi, so Metal, ROCm, Intel and AMD
always showed "nvidia-smi unavailable", and no estimate could project.
- riko_devices, a sixth bridge export: ggml's devices as JSON (name,
  description, backend, type, PCI id, free and total), from the same
  queries llama.cpp's own load uses, so it covers CUDA, HIP, Vulkan and
  Metal (whose figure is this process's share of its working set). Safe
  from any thread once riko_create has run. It is optional at runtime:
  an older bridge (no export) falls back cleanly; newly built bundles,
  the release check and the native smoke require it.
- resources/gpu_sources.py: telemetry sources in order (the bridge's
  devices, nvidia-smi, amd-smi or rocm-smi and amdgpu sysfs on Linux,
  any-vendor DXGI plus PDH through ctypes on Windows instead of a
  PowerShell call every 3 s), merged per device; integrated GPUs take
  ggml's shared budget, not their small dedicated slice. Process
  identity on macOS via libproc. The UI keys devices by uuid or index,
  shows the device the model runs on and labels unified memory; the
  Discord /resources command follows.
V316: the estimate matches runtime.main_gpu through ggml's device list
(not nvidia-smi's PCI order), budgets torch models where they actually
run (no CUDA context on a Mac), names the compute row neutrally, and
has a unified-memory mode: the budget is the smaller of Metal's working
set and 75% of RAM, with no projection when no layers are offloaded.
V180: computer awareness was Windows-only and claimed 'enabled' with no
data. runtime/activity.py: an adapter per platform (Windows as before;
macOS idle via CoreGraphics and the frontmost app via lsappinfo, no
permission needed; Linux idle over D-Bus with retries; a null adapter
otherwise), capabilities in the snapshot, unsupported toggles disabled
in the initiative settings, and perception status from the last
successful sample. Runtime observations ignore timestamps and count
idle in whole minutes, so they no longer churn the prompt.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…aths, MCP, memory, logs)

V148: Settings no longer returns secret values. Each secret field comes
back masked (a key set only in RIKO_API_KEY shows empty with a note);
saving the untouched mask keeps the stored value, clearing it still
works after the confirmation, and the confirmation dialog shows a
fingerprint, never the key. The path check returns only what the UI
needs.
V149: remote Markdown images become click-to-open links instead of
loading silently (a prompt-injection exfiltration channel); local and
/api/media images are unchanged. index.html gets a Content-Security-
Policy (self, data:, blob:, the loopback backend on any port; Vite's
HMR socket only while it serves the page), a doctype and a charset.
Checked in Electron from file://: scripts, styles, KaTeX fonts and
images load with no violations.
V146: kernel/safe_paths.py checks paths as text before any filesystem
call: network and device paths, Windows drive-relative paths,
alternate streams, reserved names and lexical escapes are refused, then
the resolved path is re-checked for symlinks. Media, avatar and
animation import, the avatar model route, effect assets, the path check
and the resource estimate all go through it; tests run the Windows
cases on every OS.
V153: MCP servers get a minimal environment (PATH and a safe set plus
their declared env, or inherit_env by choice) instead of the backend's
whole one; their tool descriptions, parameters, outputs and errors
reach the model framed as untrusted and trimmed; MCP tools ask for
approval by default unless allowed in Settings or the server sets
"require_approval": false.
V154: recalled memories are framed as recalled data that may contain
other people's text and is never an instruction.
V105: the console log handler (and uvicorn's) redacts secrets too; the
packaged backend-launch.log is capped at 5 MiB with two backups.
V157: character_config.yaml.previous is written 0600 like the config.
V132: .gitignore covers user data and secret-bearing copies at any
depth (persistent_memories, mcp.json, .env files, settings temps and
backups, logs, models, wake words) with nothing tracked becoming
ignored; character_files/main_sample.wav stays tracked by an explicit
exception pending its licence and consent.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…d an opt-in echo gate

V051: Julia-1 ran whatever Python its Hugging Face repo held, imported
as 'julia' from sys.path[0], with two warmups racing to define it.
Both call sites now download the pinned commit a85b127 (configurable),
check its 18 code files against a SHA-256 manifest (kernel/model_pins.py),
and import it under a unique riko_julia_<hash> name through a finder
that compiles only the checked source bytes (never bytecode, extension
modules or unlisted files); sys.path is untouched, the two call sites
share one load, and a cached snapshot still loads offline. The probe's
identity keys are unchanged.
V150: every GitHub action is pinned to a full commit SHA (tag in a
comment); the Linux and Windows Vulkan SDK downloads are checked against
SHA-256 sums recorded from LunarG before they are extracted or run, and
the Windows installer's exit code is checked; stale comments and the
release README are corrected, with the Windows code-signing options
written down. electron-builder, Python dependencies, the ROCm image and
llama.cpp were already pinned.
V073: Riko's own voice through laptop speakers interrupted her after
1.5 s and was saved as the user speaking over her. voice.barge_in adds
opt-in policies: echo_gate learns her echo level at the start of every
reply (from the exact blocks SpeechQueue writes) and lets an
interruption through only when the microphone is clearly louder than
her echo; stop_only ignores voice during playback and relies on Stop.
Gated echo fragments are not transcribed. The default, full_duplex, is
unchanged. A proper echo canceller is documented as the follow-up.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Wave 6: opt-in ROCm, backend-neutral telemetry, security hardening, supply-chain pins; plus app and avatar fixes
Measured with the UI audit's offscreen harness, HEAD against this
build, back to back on the same stub (CPU is % of one core):

- Chat: Message, StreamChat and FormattedText are memoized and the
  markdown plugins hoisted, so a streamed token re-renders only the
  streaming message: 8.2 ms and 184 components per token at 40 messages
  -> 1.8 ms and 12; in the dock 8.4 ms -> 0.4 ms (it no longer renders
  the list there).
- Snapshots: runtime_store.mjs keeps every unchanged part of a snapshot
  as the previous object and useRuntime(select) subscribes to fields, so
  a state.snapshot re-renders what changed: 9.7-13.8 ms and up to 336
  components -> about 0.5 ms and 1. The reducer keeps its state when
  nothing it holds changed; only model and chat errors stick as the chat
  error (cleared by the next completed turn), others are an 8 s notice.
- Avatar: avatar_frame_rate.mjs paces the loop: about 30 fps standing
  still, the display rate while she moves, is held, talks or her eyes
  follow the pointer (and 1 s after, while springs settle), paused when
  asleep and settled. One full matrix update per frame instead of four.
  Idle at 120 Hz 20% -> 7.8% of a core; asleep 13.7% -> 0.4%. A setting
  (Settings > Graphics: adaptive or always full). An exception in one
  frame no longer stops the loop.
- Traffic: hit-test IPC and popup interactivity only on change (116/s
  -> 0.1/s with the cursor away; dock hover 58/s -> 0.1/s); pointer POSTs
  only when the near flag flips, with a 1 s keep-alive.
- Idle CSS border animations stop when only the mic is waiting (reduce-
  motion still applies): the full chat and dock no longer burn 2-4% idle.
- Bugs: the avatar mounts after the first snapshot (no 'Avatar model
  could not be loaded' flash, no Mita.vrm fetch); capabilities and the
  whiteboard capture are re-sent after a backend restart; an error
  boundary per route shows a Reload card instead of a blank window;
  whiteboard bounds no longer snap back mid-save; a display change during
  a post is sent once it ends.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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.

2 participants