Echo guard, barge-in hardening, the addressee detector, engine-off memory summarisation.
Date: 2026-06-11 → 2026-06-12 (Session 7) Theme: Make EXCAR socially aware about *who* it answers and *when* it stays quiet, fix a real self-interrupt bug, and make persistent memory actually work end-to-end - building on collaborator Mete Selçuk Şimşek's proposals.
This session also produced the retroactive build journal (reports 00–06), the rewritten README.md, and four app-icon concepts (docs/assets/icon-concepts/). The engineering work below is the substantive part.
Three artifacts from Mete:
EXCAR_Addressee_Detection_Report_1.docx) -one root cause (no gate between "someone spoke" and "it was meant for me") with three symptoms: passenger speech triggers a reply, no barge-in, echo feedback.
patterns`, all following the pattern *"no sensor → ask the driver at a natural moment → store with source + confidence → speak by confidence → never fabricate."*
events/ (types + bus) and behaviors/(subscribers), with a scenario map (phone call → stay silent, passenger present → stay silent, engine start → greet, good/bad mood → adjust tone).
This session implemented #1 and the write-path of #2. #3 (event-bus) and the full typed-store refactor of #2 are deliberately deferred - see *Open items*.
What. While EXCAR speaks, the barge-in mic also hears EXCAR. Before this, the flat RMS ≥ 0.035 trigger meant EXCAR's own TTS could set the interrupt and cut its own sentence. Now the barge-in threshold rises above our own current playback level while speaking.
Why. Mete's symptom 2.3. It was a latent correctness bug, not just polish - the barge-in monitor and the speaker run at the same time with no self-voice filter.
How. _speak_worker publishes the RMS of each synthesized sentence to self._tts_rms; _respond sets self._speaking for the turn. The barge-in monitor, while self._speaking is set, uses threshold = max(base * 1.5, tts_rms * 1.3) so speaker bleed can't trip it but a closer, louder human voice can. (core/pipeline.py)
Purpose. EXCAR finishes its own sentences; only a real human interrupts it.
What. Barge-in now requires speech to stay above threshold for audio.barge_in_min_ms (default 250 ms ≈ 4 mic blocks), not a single block.
Why. Mete's risk table: a flat single-block trigger fires on potholes, horns, and music spikes. Sustained energy rejects those.
How. A consecutive-block counter (run) in _barge_in_monitor; it resets on any quiet block and only sets the interrupt once run reaches the required count. barge_in and barge_in_min_ms are now config knobs. (core/pipeline.py, config.py, config/config.yaml)
Purpose. Interruptible without being twitchy.
What. A new core/addressee.py: a claude-haiku-4-5 classifier that, given the utterance plus recent turns, returns directed: true|false with a confidence. Three-valued and fail-soft: True / False / None (undecided).
Why. Local Voice ID (diarization.is_owner) rejects a *passenger's* voice but can't catch the owner talking to a passenger - that's the owner's voice (passes diarization) and isn't filler (passes the intent gate), yet isn't a command. That exact gap is what Mete's classifier targets. The two are complementary, not competing.
How. Runs in process_utterance only after diarization confirms the owner (cost/latency saver). On None (offline / disabled / API error) the pipeline falls back to the wake-word + follow-up gate, so EXCAR never goes mute when the cloud is unreachable. Wired via AddresseeConfig + factory.build_addressee_detector. Off by default (addressee.enabled: false) - opt-in because it needs Haiku. (core/addressee.py, core/pipeline.py, config.py, factory.py, config/config.yaml, tests/test_addressee.py)
Purpose. EXCAR answers commands, not side-conversations - locally first, with cloud nuance when online.
What. The read path (get_recent_summaries) already existed since Session 2, but nothing ever *wrote* <date>.summary.txt. Added the write path: each drive is summarised at engine-off, and any day left unsummarised is caught up on the next boot.
Why. Mete's "every drive is an episode; summarised at engine-off and stored." Without the write path, persistent memory was half-wired and never surfaced.
How. PersistentMemory.summarise_pending(summariser, include_today) summarises each day still missing a summary. app.py wires it: a daemon thread catches up past days on boot; an atexit + SIGTERM handler summarises today's drive at engine-off (systemd stop or Ctrl+C). The summariser (factory.build_summariser) prefers the local model (works offline - the car often is, at engine-off) and falls back to cloud Haiku. A drive that ends offline is summarised lazily on the next boot when a brain is reachable. (memory/persistent.py, app.py, factory.py, tests/test_persistent_summary.py)
Purpose. EXCAR opens each drive remembering the last one.
Recorded so the team can revisit if they disagree. For each: the chosen option and the rejected alternatives.
D1 - Offline addressee behaviour (how EXCAR decides "for me?" when Haiku is unreachable).
"EXCAR" + 15s follow-up window. Works in tunnels/garages; aligns with Mete's "never miss a command."
passenger-interruption problem returns whenever offline.
D2 - Addressee classifier scope (cost/latency).
(owner→passenger); no Haiku spend on passenger utterances.
passenger sentence too.
D3 - Memory refactor rollout.
work end-to-end), then split into typed stores + add confidence in later commits. Low risk, each step testable.
test surface, and it delays the high-leverage summarisation fix.
D4 - Engine-off summariser brain (car may be offline at shutdown).
shutdown; summarise when a brain is reachable, preferring the local model. No data loss on an offline shutdown.
down in a tunnel/garage.
with no self-voice filter and a flat threshold. Fix: echo-adaptive threshold (§1). Acknowledged in Mete's report as symptom 2.3.
uncommitted intent_gate filters single-word utterances, and the fixtures used "selam" (one word). Fixed the fixtures to a 2-word phrase ("selam dostum"), which is what the orchestration tests actually mean to exercise.
addressee.enabled defaults to false, so existing behaviour is unchanged until opted in. With no detector, process_utterance behaves exactly as before.
Verification: 37 tests pass; ruff/mypy clean on all new code (the repo's pre-existing strict-mode debt in untouched files is out of scope).
events/ + behaviors/ layer. Itis orthogonal to this work - a *dispatch* organisation, while our gates are the *detection*. Our addressee/diarization already cover its "passenger present → stay silent" scenarios. Worth a deliberate team decision: adopt incrementally (our gates emit events) or defer. Not yet built.
{value, source, confidence, updated_at} record** (D3 step 2): split vehicle/driver out of user_profile.json, add confidence to fuel.
people.json (needs diarization/enrollment) and patterns.json (needsgpsd route/time clustering) - net-new stores.
This session built on Mete's addressee-detection and memory proposals; the technical direction and review sit with Atilla as tech lead.