feat: web switch for her inner voice (Dolphin/3090 | Qwen-32B/MI50 | Off)

Her introspection (reflect/think) voice is now switchable live from the web
settings, read each cycle by the dream loop — so Brian can flip it off the 3090
before gaming without touching config or restarting.

- memory: runtime key/value settings table + get_setting/set_setting.
- self_state: INTROSPECTION_MODES (dolphin=local/dolphin3:8b, mi50=Qwen-32B,
  off=paused) + introspection_target()/set_introspection_mode(); default "dolphin".
  reflect() resolves from the live setting and SKIPS entirely when off.
- thoughts.think(): same resolution + skip-when-off.
- server: GET/POST /settings/introspection.
- index.html: "Inner Voice (introspection)" selector in Settings, applies instantly.
- tests: routing (dolphin/mi50), off-skip for think + reflect. Suite 77, ruff clean.

Default = Dolphin on the 3090 (richer voice). Flip to MI50 or Off in Settings
before gaming — that was the GPU-contention culprit.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-22 19:16:35 +00:00
parent a705e573a9
commit a7966e4bab
7 changed files with 139 additions and 8 deletions
+38 -3
View File
@@ -136,6 +136,36 @@ Respond with ONLY a JSON object — the same shape as the draft, plus "self_crit
}"""
# Her introspection (reflect/think) voice — switchable live from the web settings.
# "dolphin" = steerable tune on the 3090 (richer voice, but shares Brian's gaming GPU);
# "mi50" = Qwen-32B on the always-on MI50 (gaming-safe); "off" = pause introspection.
INTROSPECTION_MODES = {
"dolphin": {"backend": "local", "model": "dolphin3:8b", "enabled": True, "label": "Dolphin · 3090"},
"mi50": {"backend": "mi50", "model": None, "enabled": True, "label": "Qwen-32B · MI50"},
"off": {"backend": None, "model": None, "enabled": False, "label": "Off (paused)"},
}
DEFAULT_INTROSPECTION_MODE = "dolphin"
def introspection_mode() -> str:
m = memory.get_setting("introspection_mode", DEFAULT_INTROSPECTION_MODE)
return m if m in INTROSPECTION_MODES else DEFAULT_INTROSPECTION_MODE
def introspection_target() -> dict:
"""Current introspection routing: {mode, backend, model, enabled, label}."""
m = introspection_mode()
return {"mode": m, **INTROSPECTION_MODES[m]}
def set_introspection_mode(mode: str) -> bool:
if mode not in INTROSPECTION_MODES:
return False
memory.set_setting("introspection_mode", mode)
logbus.log("info", "introspection mode set", mode=mode)
return True
def load() -> dict:
"""Current self-state, or a copy of the default (not persisted until reflect).
@@ -240,9 +270,14 @@ def reflect(backend: Backend | None = None, session_id: str | None = None,
produces (reflections, the critique, and any deliberate journal note) is also
appended to her permanent journal, tagged with `source`.
"""
cfg = config.load()
backend = backend or cfg.introspection_backend # her voice (may differ from consolidation)
model = model or cfg.introspection_model
# Resolve her introspection voice from the live setting (web-switchable), unless a
# backend was passed explicitly. If introspection is switched off, skip entirely.
if backend is None and model is None:
tgt = introspection_target()
if not tgt["enabled"]:
logbus.log("info", "reflection skipped — introspection off")
return load()
backend, model = tgt["backend"], tgt["model"]
state = load()
state.setdefault("reflections", [])
state.setdefault("metacognition", [])