Markerul opac [tts-lang:en] depindea de regula din VOICE_MODE.md, aflată
la ~30k caractere distanță în system prompt — modelul o rata pe ~1 din 5
turnuri. Răspunsul în română era apoi respins integral de pocket-tts
(English-only) și turnul rămânea fără audio.
- src/router.py: markerul poartă instrucțiunea inline (un singur token,
acoperit în continuare de strip-ul anti-jailbreak); 5/5 EN la repro
- src/voice/tts_stream.py: dacă modelul tot scapă română pe o voce
pockettts, blocul cade pe Supertonic (M2, ro) în loc de tăcere;
coada TTS transportă (clause, voice, lang) per clauză
- tools/tts.py: looks_romanian public (folosit de tts_stream)
- personality/VOICE_MODE.md: regula gated pe prefixul [tts-lang:en
- tests: 4 teste noi fallback + expectații marker actualizate
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Instrucțiunea „engleză pentru audio" nu ajungea la model: VOICE_MODE.md nu
avea nicio regulă de limbă, iar condiția din TOOLS.md („când generez pentru
o voce pockettts") era neevaluabilă — modelul nu știa ce voce e activă.
Rezultat: mirroring pe română → filtrul de diacritice bloca sinteza → tăcere.
- tools/tts.py: helper public engine_for_voice() (catalog → tts.default_engine)
- src/router.py: injectează [tts-lang:en] în prefixul voice când vocea activă
e pockettts (config citit fresh — vede swap-ul live /voice setvoice);
anti-jailbreak strip extins la [tts-lang:...]
- personality/VOICE_MODE.md: regulă de limbă gated pe marker
- tests/test_router.py: 5 teste noi pentru marker + strip
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Voice utterances and text messages on the same Discord channel now share
one Claude session, and Echo's voice replies are mirrored back into the
text channel. Replaces the old voice:<id> session-key split.
Changes:
- src/adapters/_text_chunks.py: new leaf module for split_message
(used by both discord_bot and voice pipeline)
- src/router.py: drop voice: prefix from session_key; add [voice] marker;
strip leading [speaker:/[voice] tokens from user input (anti-jailbreak);
remove dead double-clear of voice: key
- src/claude_session.py: include personality/VOICE_MODE.md unconditionally
(rules become per-turn-aware via [speaker:] prefix instead of session flag)
- src/voice/pipeline.py: VoiceSession splits text_channel_id +
voice_channel_id; resolve text channel per-send (no stale refs); mirror
Echo's reply text into the text channel after route_message returns
- src/adapters/discord_voice.py: /voice join passes both channel ids
- src/adapters/discord_bot.py: import split_message from leaf module
- personality/VOICE_MODE.md: rewrite as per-turn dynamic rules;
add synthesis instructions for text turns after voice turns
Tests:
- tests/test_router.py: 4 new cases (plain channel_id, anti-jailbreak,
text-adapter regression, no-double-clear)
- tests/test_pipeline_mirror.py: new — Echo reply mirror chunking,
empty guard, mirror_enabled=False, send-raises resilience
- tests/test_voice_session_channel_ids.py: new — split-attr contract
+ metrics payload schema
- tests/test_voice_session_cleanup.py: update for new kwargs
Plan: /home/moltbot/.claude/plans/vreau-ca-tot-textul-greedy-rivest.md
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Squashed branch: voice/dave-recv → master. Closes Pas 12 (DAVE E2E) and lands
voice-mode UX polish + verbal voice control on top of the Pas 1-10 scaffolding
already on master.
## DAVE E2E receive-side decrypt (e4f3177)
Vendored fork: discord-ext-voice-recv 0.5.3a+echo.dave1. Patches the receive
pipeline to handle Discord's mandatory DAVE encryption on voice gateway v=8.
- `_maybe_dave_decrypt`: uses davey.can_passthrough(user_id) as primary gate,
falls through to dave.decrypt for DAVE-epoch peers, drops on decrypt failure
without killing the reader thread.
- VAD fix: silero-vad v5+ requires exactly 512 samples; our 100ms window
(1600 samples) was silently raising ValueError → STT never fired. Now slice
into 512-sample chunks.
- Whisper: bumped beam_size 1→5 and added RO initial_prompt.
- Tests: 11 DAVE unit tests + 2 callback integration tests + contract test
with fork-version guard.
## Voice UX polish (d1bc77e)
- Killed the 3s "mă gândesc" filler (always collided with Claude p50 4-7s).
- Barge-in via `ttsq.clear()` at top of `on_segment_done`.
- DTX silence-flush poller (200ms tick) — Discord stops sending RTP packets
when silent, so the inline silence-check in sink.write() never fired for
trailing audio; background thread handles it.
- `EchoStreamingAudioSource.read()` non-blocking — old `get_frame(timeout=0.1)`
wrecked Discord's 20ms cadence and the client interpreted bursts as
stuttering (Marius heard "4 de minute" instead of full sentence).
- RO time expansion: 23:09 → "douăzeci și trei și nouă minute".
- Supertonic Unicode sanitize centralized in tools/tts.py.
- Whisper local_files_only=True — no HF metadata GET on each startup.
- Diagnostic logging through sink → VAD → Claude stream → TTS chain.
## Voice mode iteration (e589e48)
- `personality/VOICE_MODE.md` — voice-tailored system prompt (short, no
markdown, no abbreviations, time without seconds, distances in
"mii"/"milioane"); plumbed via build_system_prompt(voice_mode=True).
- Isolated voice session key `voice:<channel_id>` — voice doesn't share
context with text adapter on the same channel; auto-applied without
/clear ceremony. /clear drops both keys.
- Metric units + Romanian thousands (normalize.py): "384.000 km" →
"trei sute optzeci și patru de mii de kilometri" with feminine-correct
pluralization and "de" particle for ≥20.
- `/voice setvoice <M1-F5>` slash command with native autocomplete; swaps
live + persists voice.default_voice to config.json.
- Verbal voice change (src/voice/voice_commands.py + 29 tests) — "schimbă
vocea pe M5", "voce em cinci", with permissive substring fallback for
Whisper-mangled forms like "Mâcinci"=M5 and "unul cinci"=M5. Whisper
initial_prompt now lists voice vocabulary to bias STT toward clean
outputs.
- Fast barge-in: VAD ≥2 consecutive windows (~200ms) on Marius's user
while Echo has pending TTS frames → cut him off mid-sentence so user
doesn't wait the full silence + STT cycle. Acoustic echo bleed-through
still requires headphones (no AEC).
## Test suite
130 voice + router tests pass (test_voice_recv_dave, test_voice_session_cleanup,
test_voice_adapter_contract, test_voice_normalize, test_voice_commands,
test_router).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Fix arhitectural general (beneficiu și pentru text adapters), nu doar voice.
src/claude_session.py:
- _session_locks: dict[str, threading.Lock] cu bootstrap lock pentru
lazy creation thread-safe.
- _get_session_lock(channel_id) helper.
- send_message() body wrapped în with _get_session_lock(channel_id).
- threading.Lock (NU asyncio.Lock) — send_message e sync subprocess.run
blocking; asyncio.Lock nu protejează cod sync rulat via to_thread.
- Per-channel granularity preserved — different channels run în paralel.
- send_message() public signature unchanged.
src/router.py:
- route_message(): dacă adapter_name == "discord-voice", prepend
[speaker:<user_name>] prefix (Config.get("voice.user_name", "user")).
- Original text variable left untouched for downstream paths.
- Text adapters: zero behavior change.
- route_message() public signature unchanged.
tests/test_claude_session_mutex.py — 6 tests REGRESSION-CRITICAL:
- same channel serializes (concurrent → mutex serializes, no overlap)
- same channel lock identity (same dict entry per channel_id)
- different channels run in parallel (overlap MUST fire)
- 3 channels all overlap
- contested acquire blocks then proceeds (policy: blocking, not fail-fast)
- lock released on subprocess exception (no deadlock on crash)
Acquisition policy: BLOCKING acquire bound by claude --timeout (5min default)
nu fail-fast — adapters already serialize via asyncio.to_thread queue, un
non-blocking acquire ar surface transient busy errors.
Test results: 82 passed (51 existing + 31 new). 2 PRE-EXISTING failures în
TestPromptInjectionProtection (stale assertion vs current prompt text) —
out of scope, recomand ticket separat.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Two structural fixes that together let users manage feature-branch
work without manual intervention:
Approval guard — `/plan/start` returns 409 `already_committed` if the
project status is approved/running/complete, unless the body opts in
with `force=true`. Frontend now renders "Re-planifică" instead of
"Planifică" on approved cards and gates it behind a confirm dialog
that threads `force=true` through. Prevents an accidental click from
wiping `status=approved` and burning a fresh planning subprocess.
Worktree awareness — projects can now declare that they target a
feature branch on an existing Gitea repo, not a repo-per-slug clone.
Three optional fields added to approved-tasks.json: `repo` (default
= slug), `branch` (feature branch to create), `base_branch` (default
main). Wired through `/p` flag parser in router.py, the dashboard
Propose modal's new "Avansat" section, and the night-execute prompt
which clones {repo} and creates {branch} from {base_branch} before
running ralph.
CLAUDE.md updated with both flows + the new schema fields.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Echo Core devine planning agent: poartă o conversație multi-fază cu Marius
folosind skill-urile gstack (/office-hours → /plan-ceo-review →
/plan-eng-review → /plan-design-review opt) și produce final-plan.md în
~/workspace/<slug>/scripts/ralph/, gata să fie consumat de Ralph PRD
generator (W3) noaptea.
Decizii arhitecturale (din eng review + spike findings):
- PlanningSession ca clasă SEPARATĂ de chat-ul main (NU mode=string param)
— separation explicit. claude_session.py rămâne strict pentru chat;
planning trăiește în src/planning_session.py + src/planning_orchestrator.py.
Inheritance literală nu se aplică (claude_session.py expune funcții
module-level, nu o clasă) — separation e satisfacută prin module distinct.
- Fresh subprocess PER skill phase, NU single resumed session — phase-urile
coordinează via disk artifacts (gstack convention în
~/.gstack/projects/<slug>/). Avoids context window growth.
- --max-turns 20 default + retry pe error_max_turns la --max-turns 30.
Spike a arătat că prompt-uri complexe pot exploda turn budget-ul.
- approved-tasks.json schema extins cu planning_session_id + final_plan_path
(Status flow: pending → planning → approved → running → complete).
- State separat în sessions/planning.json (NU active.json), keyed pe
(adapter, channel_id) pentru re-resume la restart echo-core.
Trigger-e:
- Discord: slash command /plan <slug> [descriere] cu autocomplete pe pending,
buton "🧠 Planifică" în RalphProjectView, și /cancel slash command.
- Telegram: /plan + /cancel commands, plus buton "🧠 Planifică" în
ralph project keyboard.
- Router: state-aware routing — dacă chat-ul e în planning, mesajele plain
trec la PlanningOrchestrator.respond() prin --resume; /cancel revine la
status pending; /advance / "Continuă faza" advance fază nouă (fresh
subprocess); /finalize sau "Dau drumul" promote la status approved.
Discord defer pattern: toate butoanele noi (PlanningActiveView,
PlanningFinalView, "🧠 Planifică") apelează await
interaction.response.defer(ephemeral=True) ÎNAINTE de orice IO — evită
"Interaction failed" pe IO >3s.
UX strings warm + colaborativ (per design review): "🧠 Pornesc planning
pentru ...", "Răspunde aici", "Continuă faza", "Dau drumul tonight",
"Anulează" — niciun "Submit/Approve/Cancel" generic.
Tests: 23 noi (test_planning_session, test_planning_orchestrator,
test_router_planning) — toate pass. Mock pe _run_claude pentru a evita
subprocess Claude real în CI.
Files new:
prompts/planning_agent.md
src/planning_session.py
src/planning_orchestrator.py
tests/test_planning_session.py
tests/test_planning_orchestrator.py
tests/test_router_planning.py
Files modified:
src/claude_session.py — _run_claude(cwd=...) optional + surface subtype/is_error
src/router.py — state-aware routing, start_planning_session, planning_advance/approve/cancel, _ralph_propose schema cu planning_session_id + final_plan_path
src/adapters/discord_bot.py — /plan + /cancel slash commands; planning views imported
src/adapters/discord_views.py — PlanningActiveView, PlanningFinalView, "Planifică" button în RalphProjectView, _split_chunks helper
src/adapters/telegram_bot.py — /plan + /cancel handlers, callback_ralph extins cu plan/planadvance/plancancel/planapprove, planning keyboards
Status testelor pe modulele atinse: 75 passed, 0 failed
(test_claude_session security_section preexistent — neatins).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Restructurează comenzile Ralph într-un dispatcher unificat (_try_ralph_dispatch)
care suportă atât comenzile noi scurte (/p /a /l /k) cât și aliasurile legacy
(!propose !approve !status !stop). Pe Discord adaugă slash commands native cu
autocomplete dinamic pentru pending (/a) și running (/k). Pe Telegram apar în
meniul /. WhatsApp le parsează ca text plain.
Activează cron jobs morning-report (08:30) și evening-report (21:00) și adaugă
night-execute (23:00) pentru execuția autonomă a proiectelor aprobate.
Foundation pentru W1 din planul "Echo Core conversational planning agent".
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Fast commands for git, email, calendar, notes, search, reminders, and
diagnostics — all execute instantly without Claude CLI. Incremental
embeddings indexing in heartbeat (1h cooldown) + inline indexing after
/note, /jurnal, /email save. Fix Ollama URL (localhost → 10.0.20.161),
fix email_process.py KB path (kb/ → memory/kb/).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Replace all ~/clawd and ~/.clawdbot paths with ~/echo-core equivalents
in tools (git_commit, ralph_prd_generator, backup_config, lead-gen)
- Update personality files: TOOLS.md repo/paths, AGENTS.md security audit cmd
- Migrate HANDOFF.md architectural decisions to docs/architecture.md
- Tighten credentials/ dir to 700, add to .gitignore
- Add .claude/ and *.pid to .gitignore
- Various adapter, router, and session improvements from prior work
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
/model (show/change), /restart (owner), /logs, set_session_model API, model reset on /clear. 20 new tests (161 total).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>