Skip to content

PLAN-v3 Phase 2: harden local state and split the orchestrator - #9

Merged
Kisahime merged 1 commit into
mainfrom
cursor/v3-phase-2-0e13
Oct 10, 2026
Merged

Kisahime merged 1 commit into
mainfrom
cursor/v3-phase-2-0e13

Conversation

@Kisahime

Copy link
Copy Markdown
Owner

#8 merged into main (ac00a5a) before this PR was opened. CI on #8 was already green (both test jobs). This PR targets main and contains only Phase 2.

V3-P2-01 Atomic writes for booth and identity

save_booth() and save_identity() use atomic_write_text(). A corrupt file is logged and copied to a .bad sibling, the same way memory already does.

Tested by making os.replace raise (the old booth.json stays intact) and by loading a corrupt booth.json / streamer.json (logged, copied to *.bad, defaults used).

V3-P2-02 Piper temp cleanup

An abandoned persistent request keeps its output path in a pending-delete set. The placeholder is removed immediately, and the path stays tracked so a wav Piper writes after the interrupt is deleted on the ready line, the next synth, or close. Temps stay in the system temp directory rather than data/tmp/, so constructing a PiperTTS in tests does not wipe a repo folder.

Tested with a fake Piper that writes the wav after interrupt(); the next synth leaves no stray file.

V3-P2-03 Manual mood across restarts

BoothState stores mood (the profile base) and mood_override (a manual change). effective_mood() prefers the override. Applying a profile clears the override. Loading a booth that has no override still applies the profile mood.

Tested: profile "Focus game" plus manual hype, a new orchestrator on the same booth file is still hype, and switching profile again returns to chill with an empty override.

V3-P2-04 Shoutout lines

chat.shoutout_line (default go check out {user}) is the chat line when the target is not the last raider. A shoutout of the last raider still says {target}, welcome in. chat.shoutout_speak (default true) controls whether that line is spoken. The chat post still happens when speech is off.

Tested: !so with no raid posts the configured line and speaks it; a raider shoutout keeps the welcome; shoutout_speak: false posts and does not speak.

V3-P2-05 Persona control command

With chat.control_command empty, the control command is ! plus the first token of the persona name, lowercased. !pixel stays an alias. A set control_command is used as written and the persona name is not also derived. The streamer-save hint uses the live persona name. The smalltalk fallback default is Cohost, and the chat path passes the persona name.

chat_action() still treats only !pixel as a control verb when it is called on its own. Live chat goes through the orchestrator, which passes the full command set. That is what !nova mute uses.

Tested: persona Nova, !nova mute and !pixel mute both work for a mod. In the dashboard, saving streamer Luna with persona Nova shows Nova will treat Luna as the streamer. The Twitch nick (the preview uses pixelbot) is account data, not the persona string.

V3-P2-06 Split the orchestrator

Behaviour moved, unchanged, into:

  • src/core/chat_flow.py — pick, rate, decide, perform
  • src/core/event_flow.py — batching, priority, react, recap
  • src/core/voice_flow.py — utterance to reply, level loop
  • src/core/twitch_link.py — connect, disconnect, watchdog, shoutout, markers
  • src/core/queue.py — queue ids and snapshots
  • src/core/lifecycle.py — startup, profiles, mood, and settings

lifecycle.py is the extra module. Startup and the settings setters did not belong in the five flows named in the plan, and keeping them in the facade would have blown the 500-line cap. Orchestrator is a thin facade (388 lines). main.py and the existing tests still call the same methods.

_spawn is still the only asyncio.create_task in orchestrator.py, with the line task = asyncio.create_task(coro, name=name). Names tests patch on src.orchestrator (update_config, user_id, shoutout, create_marker, ROOT, asyncio.sleep) are looked up at call time.

Tested by the existing suite with no call-site edits.

V3-P2-07 Hermetic tests and a coverage floor

conftest.py sets COHOST_SKIP_SECRET_CHECK=1 and points every already-imported src.* module's ROOT at a temp tree with a minimal config and the persona fixture.

A deliberately broken repo config.yaml still builds a booth: _booth_port returns 8080 and create_app(prepared=...) comes up. Two tests still open the shipped config.yaml by an explicit path (test_shipped_config_and_overlay_hooks, test_shipped_config_keeps_optional_feeds_off) so those shipped values stay locked. They are the exception, and they fail if that file is actually broken.

CI and pyproject.toml run pytest --cov=src --cov-fail-under=72. Measured coverage on this change is 74% (5410 statements). The floor is that number minus 2.

V3-P2-08 Configurable logging

logging.level defaults to INFO. logging.debug_modules (for example ["cohost.mic"]) turns DEBUG on only for those loggers. The dashboard button "Debug log for 10 min" POSTs to token-protected /api/logging/debug and the task restores the previous level after 600 seconds.

Tested: debug_modules: [cohost.mic] records the mic line and not the tts line. The toggle test fast-forwards the 600s sleep and checks the level returns to INFO. In the browser, the button gains an active border and the hint reads Debug log on for 10 minutes.; a second click reads Debug log off.

Debug log button on
Nova streamer hint
debug_log_toggle_and_nova_hint.mp4

Tests

ruff check clean. pytest 113 passed. Coverage 74.12% against a floor of 72.

Needs owner

No new owner items. The V3-P0 checklist is unchanged and still unticked.

To show artifacts inline, enable in settings.

Open in Web Open in Cursor 

Atomic booth and identity saves, a mood override that survives restart, shoutout lines, persona control commands, Piper interrupt cleanup, hermetic test roots, and a coverage floor. Orchestrator behaviour moves into src/core behind a thin facade.

Co-authored-by: Kisahime <Kisahime@users.noreply.github.com>
@Kisahime
Kisahime marked this pull request as ready for review October 10, 2026 21:10
@Kisahime
Kisahime merged commit 4ccae7e into main Oct 10, 2026
2 checks passed
@Kisahime
Kisahime deleted the cursor/v3-phase-2-0e13 branch October 10, 2026 21:10
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