memory.py and marauder_cart.py were subprocess wrappers around marauder
CLI — redundant now that the opencode chat agent has native EEMS tools.
Also remove custom TOOLS dict, EEMS context injection at session start,
marauder_cart.create() in calibration_done, and anthropic dep/env vars.
Inline slug() as _slug() in calibration.py.
The sin:4098 sidecar service has been dead since the last reboot, and the
real binding mechanism is the chat-persona.ts opencode plugin (now in
madcat-plugin) that reads the operator cart file directly. The sidecar
HTTP round-trip was vestigial.
Changes:
- Remove SIDECAR_URL env, _sidecar_get_binding(), _sidecar_bind(),
_session_id_for_user() — all dead code paths.
- Add _slug_from_cart(cart): derives canonical PERSONAS slug from a cart's
persona_name (case-insensitive) or voice prefix fallback.
- Simplify _pick_system_prompt(cart): cart.system_prompt (calibrated) →
BT default. No more sidecar override layer.
- / route: bound_slug from _slug_from_cart(cart) instead of sidecar lookup.
- /api/persona POST: mutates the cart file in-place (preserves calibrated
UI prefs), creates a minimal cart for fresh operators. The opencode plugin
re-reads the cart on every turn, so switches take effect on the very next
message — no session reconnect.
- /api/persona/current GET: reads from cart, returns {slug, display, voice,
bound}.
- WS handler: re-loads cart at the start of each turn for live persona
switching; voice falls back cart.voice → TTS_VOICE.
Operator cart at /home/madcat/.local/share/chat-saiden/operators/<email>.json
is now the single source of truth.
- POST /session/:id/prompt_async fires the prompt (204 immediate)
- GET /event SSE stream picks up message.part.updated with real text deltas
- Filters events by session ID, computes delta from cumulative text
- Breaks on message completed or session idle
- tts.py: replace piper subprocess with HTTP POST to madcat-tts /v1/audio/speech (chatterbox voice cloning)
- chat.js: replace whisper server upload with browser Web Speech API (webkitSpeechRecognition)
- chat.css: style persona picker — appearance:none select, themed with CSS vars, mobile responsive
- main.py: default TTS voice → bt7274-en
Part 1 — Transport swap:
- Replace anthropic.AsyncAnthropic streaming with httpx SSE client
calling opencode's OpenAI-compat /v1/chat/completions on sin:4096
- Auth: basic auth opencode:$OPENCODE_PASSWORD
- Env: OPENCODE_URL (default http://sin:4096), OPENCODE_PASSWORD
- Sidecar binding (sin:4098) consulted per message to resolve active
persona; voice read from binding → cart → env default
- Helper _session_id_for_user: deterministic sha256 slug per email
so sidecar binding survives WebSocket reconnects
- anthropic dep retained in pyproject.toml (not removed — P4 may use it)
Part 2 — Persona switcher:
- PERSONAS dict: bt7274, friday, samantha (slug → voice/backend/prompt)
- POST /api/persona — bind persona via sidecar, maps slug → full config
- GET /api/persona/current — return current binding
- GET /api/personas — list available personas
- chat.html: persona <select> in topnav with server-rendered active state
- chat.js: onChange → fetch /api/persona, update __personaName +
status badge + system message in conversation feed
TODO: add CSS polish for .topnav__persona-wrap (inherits base styles for now)