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 0006), the rewritten README.md, and four app-icon concepts (docs/assets/icon-concepts/). The engineering work below is the substantive part.


Source material - what Mete proposed

Three artifacts from Mete:

  1. Addressee Detection report (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.

  1. 6-area memory model - `vehicle / fuel / driver / people / episodes /

patterns`, all following the pattern *"no sensor → ask the driver at a natural moment → store with source + confidence → speak by confidence → never fabricate."*

  1. Event-bus architecture - 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*.


1. Echo guard - stop EXCAR interrupting itself

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.


2. Barge-in hardening - sustained energy, not transients

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.


3. AddresseeDetector - "was this meant for me?"

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.


4. Memory engine-off summarisation - make persistent memory actually work

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.


Decision Log - what we chose, and the roads not taken

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).

  • ✅ Chosen: fall back to wake-word + follow-up. Online → Haiku; offline → say

"EXCAR" + 15s follow-up window. Works in tunnels/garages; aligns with Mete's "never miss a command."

  • ✗ Respond-to-all-owner-speech offline - simplest, but the original

passenger-interruption problem returns whenever offline.

  • ✗ Stay silent unless wake word, no follow-up - safest, least conversational.

D2 - Addressee classifier scope (cost/latency).

  • ✅ Chosen: owner speech only (after diarization). Targets the real gap

(owner→passenger); no Haiku spend on passenger utterances.

  • ✗ Run on every utterance - simpler flow, but a Haiku call + ~100 ms on every

passenger sentence too.

D3 - Memory refactor rollout.

  • ✅ Chosen: incremental. Ship engine-off summarisation first (makes memory

work end-to-end), then split into typed stores + add confidence in later commits. Low risk, each step testable.

  • ✗ Full 6-store refactor now - cleaner end state, but one large change with a wide

test surface, and it delays the high-leverage summarisation fix.

D4 - Engine-off summariser brain (car may be offline at shutdown).

  • ✅ Chosen: lazy, on next boot, local-preferred. Mark the day pending at

shutdown; summarise when a brain is reachable, preferring the local model. No data loss on an offline shutdown.

  • ✗ Local model only - no internet needed, but the GGUF must always be present/loaded.
  • ✗ Cloud Haiku, require online - simplest, but loses summaries when the car shuts

down in a tunnel/garage.


Bugs & notes

  • Self-interrupt (latent). Root cause: barge-in mic + speaker run concurrently

with no self-voice filter and a flat threshold. Fix: echo-adaptive threshold (§1). Acknowledged in Mete's report as symptom 2.3.

  • Two pipeline tests were already red before this session - Session 6's

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.

  • Safe-by-default. 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).


Open items (conscious deferrals)

  • Event-bus (Mete's 3rd proposal). A pub/sub events/ + behaviors/ layer. It

is 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.

  • **Typed memory stores + universal {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 (needs

gpsd route/time clustering) - net-new stores.

  • PRIVATE/SHARED episode tagging - privacy-aware memory.

Credits

  • Concept & proposals: Mete Selçuk Şimşek
  • Engineering: Selim Fedakâr
  • Tech Lead: Atilla Kaan Alkan (AI research)

This session built on Mete's addressee-detection and memory proposals; the technical direction and review sit with Atilla as tech lead.