Tracking issue for the multi-agent code audit of 855f5cc (branch cleanup).
Interactive report: https://claude.ai/code/artifact/180f5fbd-9010-4371-b893-247894fcfbc1
16 reviewers swept ~42k lines of first-party code across the agent, autonomy engine, MCP tool modules, dashboard, face recognition, speech services, and infrastructure, plus 3 cross-cutting lenses (security, concurrency, cross-service contracts). Each of the 170 raw findings was handed to 2 independent agents prompted to refute it; 19 were refuted and 10 drew a split verdict , leaving 127 unique confirmed issues (14 duplicates merged — 12 were found independently by 2–3 reviewers working blind, noted on those issues).
Severity
Count
Meaning
High
20
Wrong behavior on a path a real user hits
Medium
67
Edge-case bug, leak, or robustness gap
Low
40
Dead code or worthwhile improvement
Filter with the audit label, plus severity:*, area:*, and category:*.
High · 20
Agent API
WS client disconnect mid-turn abandons orchestrator.run() and poisons the pooled session with dangling tool_calls #39 — WS client disconnect mid-turn abandons orchestrator.run() and poisons the pooled session with dangling tool_calls (chat.py:307) · 3 reviewers
get_logger() re-runs dictConfig on every call, which strips the log-stream ring-buffer handler after startup — /ws/logs dies on the first chat turn #42 — get_logger() re-runs dictConfig on every call, which strips the log-stream ring-buffer handler after startup — /ws/logs dies on the first chat turn (logger.py:116)
Autonomy engine
resume_confirmed_run has no atomic state transition — approved act runs can execute twice #43 — resume_confirmed_run has no atomic state transition — approved act runs can execute twice (engine.py:611)
Quiet-hours 'defer' creates one scheduled run per suppressed fire; all dispatch concurrently at quiet end, bypassing the global rate limit #44 — Quiet-hours 'defer' creates one scheduled run per suppressed fire; all dispatch concurrently at quiet end, bypassing the global rate limit (engine.py:375)
Autonomy handlers
Watch condition gate calls a nonexistent tool, so any watch item with a condition never notifies #45 — Watch condition gate calls a nonexistent tool, so any watch item with a condition never notifies (watch.py:80)
Face recognition
DELETE /api/people never removes face image files: relative paths unlinked against CWD instead of SNAPSHOT_DIR #40 — DELETE /api/people never removes face image files: relative paths unlinked against CWD instead of SNAPSHOT_DIR (people.py:318) · 2 reviewers
Confirm / rescan / camera-enrollment re-embed paths ignore camera fov_type — no panoramic tiling #55 — Confirm / rescan / camera-enrollment re-embed paths ignore camera fov_type — no panoramic tiling (detections.py:185)
Frontend: chat/lib
XSS: assistant LLM output rendered as raw HTML via {@html} with unsanitized marked #50 — XSS: assistant LLM output rendered as raw HTML via {@html } with unsanitized marked (+page.svelte:468)
Mid-turn summary_reset appended after the assistant placeholder makes the entire turn's response invisible #51 — Mid-turn summary_reset appended after the assistant placeholder makes the entire turn's response invisible (chat.ts:164)
Frontend: routes
Editing an existing reminder silently replaces its schedule with a one-shot cron ~30 minutes from now #52 — Editing an existing reminder silently replaces its schedule with a one-shot cron ~30 minutes from now (ReminderTimePicker.svelte:152)
Edit-and-save wipes config keys the form does not model (attach_snapshot, gather.presence, scene_description, etc.) #53 — Edit-and-save wipes config keys the form does not model (attach_snapshot, gather.presence, scene_description, etc.) (AgendaForm.svelte:230)
Switching trigger type on edit leaves the old trigger active — item ends up firing on both cron and event #54 — Switching trigger type on edit leaves the old trigger active — item ends up firing on both cron and event (AgendaForm.svelte:335)
Infra + config
DEBUG_LOGGING=0 enables debug logging (string truthiness inversion) #41 — DEBUG_LOGGING=0 enables debug logging (string truthiness inversion) (shared_config.py:30) · 2 reviewers
LokiHandler.emit does synchronous HTTP POST per log record inside async services #57 — LokiHandler.emit does synchronous HTTP POST per log record inside async services (logger.py:62)
Lens: security
Wildcard CORS on a fully unauthenticated agent API enables drive-by cross-origin home control #58 — Wildcard CORS on a fully unauthenticated agent API enables drive-by cross-origin home control (selene_agent.py:314)
MCP: HA + general
get_weather_forecast reads wrong env var (TIMEZONE vs CURRENT_TIMEZONE), so every dated forecast query fails #46 — get_weather_forecast reads wrong env var (TIMEZONE vs CURRENT_TIMEZONE), so every dated forecast query fails (mcp_server.py:413)
ha_trigger_script spreads variables at the top level of script.turn_on service data, which Home Assistant rejects #47 — ha_trigger_script spreads variables at the top level of script.turn_on service data, which Home Assistant rejects (mcp_server.py:1205)
MCP: vision + device
github_read_file/github_list_dir can read .git/config, exposing the embedded GITHUB_TOKEN #48 — github_read_file/github_list_dir can read .git/config, exposing the embedded GITHUB_TOKEN (github_mcp_server.py:292)
_wrap_untrusted is trivially escapable — issue text containing </UNTRUSTED_USER_TEXT> breaks out of the wrapper #49 — _wrap_untrusted is trivially escapable — issue text containing </UNTRUSTED_USER_TEXT> breaks out of the wrapper (github_mcp_server.py:121)
TTS / STT
transcribe_file constructs a brand-new FasterWhisperASR (full WhisperModel GPU load) on every request, synchronously on the event loop #56 — transcribe_file constructs a brand-new FasterWhisperASR (full WhisperModel GPU load) on every request, synchronously on the event loop (main.py:84)
Medium · 67
Agent API
verify=bool(config.HAOS_USE_SSL) misparses the env string — 'false'/'0' enable verification, and the unset default silently disables TLS verification #63 — verify=bool(config.HAOS_USE_SSL) misparses the env string — 'false'/'0' enable verification, and the unset default silently disables TLS verification (homeassistant.py:41) · 2 reviewers
confirmation_token leaked via GET /api/autonomy/runs and the /ws/autonomy/runs feed despite deliberate stripping elsewhere #72 — confirmation_token leaked via GET /api/autonomy/runs and the /ws/autonomy/runs feed despite deliberate stripping elsewhere (autonomy.py:109)
/ws/autonomy/runs holds a pooled asyncpg connection for the socket's entire lifetime — a handful of clients can exhaust the shared 10-connection pool #73 — /ws/autonomy/runs holds a pooled asyncpg connection for the socket's entire lifetime — a handful of clients can exhaust the shared 10-connection pool (autonomy.py:332)
admin_purge guardrail accepts {"pending_l4_approval": false}, which deletes essentially the entire memory store #74 — admin_purge guardrail accepts {"pending_l4_approval": false}, which deletes essentially the entire memory store (memory.py:417)
Agent core
_pool_lock held across slow awaits: eviction can run a 15s summary LLM call and hydration runs sync Qdrant I/O, serializing all chat traffic #64 — _pool_lock held across slow awaits: eviction can run a 15s summary LLM call and hydration runs sync Qdrant I/O, serializing all chat traffic (session_pool.py:174) · 2 reviewers
LRU eviction ignores in-flight turns: flushes a mid-turn snapshot, pops the lock, and allows a duplicate orchestrator for the same session #65 — LRU eviction ignores in-flight turns: flushes a mid-turn snapshot, pops the lock, and allows a duplicate orchestrator for the same session (session_pool.py:193) · 2 reviewers
No MCP reconnect/restart: a crashed tool subprocess permanently disables its tools while is_connected() keeps reporting True #67 — No MCP reconnect/restart: a crashed tool subprocess permanently disables its tools while is_connected() keeps reporting True (mcp_client_manager.py:120)
MCP contexts entered in per-server tasks but exited from the lifespan task; failed connects leak the spawned subprocess #68 — MCP contexts entered in per-server tasks but exited from the lifespan task; failed connects leak the spawned subprocess (mcp_client_manager.py:91)
/v1/chat/completions silently discards all request history except the last user message #69 — /v1/chat/completions silently discards all request history except the last user message (selene_agent.py:429)
_summarize_and_reset discards the in-memory history even when the DB persist failed #70 — _summarize_and_reset discards the in-memory history even when the DB persist failed (orchestrator.py:478)
DEBUG_LOGGING=0 (or "false") enables debug logging — env value truthiness bug #71 — DEBUG_LOGGING=0 (or "false") enables debug logging — env value truthiness bug (config.py:5)
Autonomy engine
Failed notification still starts the cooldown window, suppressing subsequent alerts that were never delivered #75 — Failed notification still starts the cooldown window, suppressing subsequent alerts that were never delivered (engine.py:474)
Runs parked awaiting_confirmation are invisible to the global hourly rate limit #76 — Runs parked awaiting_confirmation are invisible to the global hourly rate limit (db.py:715)
Confirmation token is unenforced (None skips validation) and leaked via /autonomy/runs and the WS feed #77 — Confirmation token is unenforced (None skips validation) and leaked via /autonomy/runs and the WS feed (engine.py:613)
MQTT consumer serializes full handler execution — one slow LLM turn delays every queued sensor event #78 — MQTT consumer serializes full handler execution — one slow LLM turn delays every queued sensor event (mqtt_listener.py:169)
DST fall-back: daily crons in the repeated hour fire twice (verified with container's croniter 6.2.3) #79 — DST fall-back: daily crons in the repeated hour fire twice (verified with container's croniter 6.2.3) (schedule.py:34)
Autonomy handlers
Failed one-shot reminder delivery silently reschedules the reminder for next year #80 — Failed one-shot reminder delivery silently reschedules the reminder for next year (reminder.py:191)
memory_review always reports status ok and permanently skips clustering windows after any failure #81 — memory_review always reports status ok and permanently skips clustering windows after any failure (memory_review.py:446)
memory_review performs blocking sync I/O (requests + sync QdrantClient) on the agent's main event loop #82 — memory_review performs blocking sync I/O (requests + sync QdrantClient) on the agent's main event loop (memory_review.py:38)
Speaker 'no one home' downgrade rail is dead code: presence payload is a JSON string, never a dict #83 — Speaker 'no one home' downgrade rail is dead code: presence payload is a JSON string, never a dict (watch_llm.py:296)
execute_approved does not re-check AUTONOMY_ACT_ENABLED at confirm time #84 — execute_approved does not re-check AUTONOMY_ACT_ENABLED at confirm time (act.py:463)
_propose_l4 ignores the AUTONOMY_MEMORY_LLM_CALL_CAP budget #85 — _propose_l4 ignores the AUTONOMY_MEMORY_LLM_CALL_CAP budget (memory_review.py:313)
Face recognition
FIFO eviction never deletes the evicted image file (relative path unlinked against /app) #109 — FIFO eviction never deletes the evicted image file (relative path unlinked against /app) (pipeline.py:156)
Rebuild-embeddings orphan cleanup deletes Qdrant points created during the job #110 — Rebuild-embeddings orphan cleanup deletes Qdrant points created during the job (admin.py:190)
Rescan-unknowns can terminate without examining most unknowns (newest-200 window + zero-match break) #111 — Rescan-unknowns can terminate without examining most unknowns (newest-200 window + zero-match break) (admin.py:418)
capture_burst does not catch asyncio.TimeoutError — a slow frame aborts the whole burst and bypasses HASnapshotError handling #112 — capture_burst does not catch asyncio.TimeoutError — a slow frame aborts the whole burst and bypasses HASnapshotError handling (ha_snapshot.py:77)
Blocking GPU inference and sync network I/O run directly on the event loop in several async paths #113 — Blocking GPU inference and sync network I/O run directly on the event loop in several async paths (people.py:209)
Frontend: chat/lib
disconnect() never resets isProcessing — navigating away from /chat mid-turn permanently disables input on return #99 — disconnect() never resets isProcessing — navigating away from /chat mid-turn permanently disables input on return (chat.ts:291)
Memory CRUD mutations ignore HTTP errors; addL4 discards user input even when the POST failed #100 — Memory CRUD mutations ignore HTTP errors; addL4 discards user input even when the POST failed (+page.svelte:153)
LogStream reconnects after component destroy — zombie /ws/logs socket persists for the page lifetime #101 — LogStream reconnects after component destroy — zombie /ws/logs socket persists for the page lifetime (LogStream.svelte:51)
runNow poll loop can never detect completion — refresh() overwrites lastRunTime before the comparison #102 — runNow poll loop can never detect completion — refresh() overwrites lastRunTime before the comparison (+page.svelte:169)
Frontend: routes
WebSocket onclose schedules a reconnect after onDestroy, leaking a zombie /ws/autonomy/runs connection per page visit #103 — WebSocket onclose schedules a reconnect after onDestroy, leaking a zombie /ws/autonomy/runs connection per page visit (+page.svelte:183)
Advanced-cron sync effect deletes spaces as the user types, making manual cron entry unusable #104 — Advanced-cron sync effect deletes spaces as the user types, making manual cron entry unusable (ReminderTimePicker.svelte:158)
One-shot crons are built from the browser's local clock but interpreted in the server's CURRENT_TIMEZONE #105 — One-shot crons are built from the browser's local clock but interpreted in the server's CURRENT_TIMEZONE (ReminderTimePicker.svelte:74)
Microphone stream, MediaRecorder, and timer are never cleaned up on unmount #106 — Microphone stream, MediaRecorder, and timer are never cleaned up on unmount (+page.svelte:74)
TTS stream is not cancelled on unmount — audio keeps playing and the NDJSON fetch stays open after navigation #107 — TTS stream is not cancelled on unmount — audio keeps playing and the NDJSON fetch stays open after navigation (+page.svelte:99)
Invalid MQTT payload-match JSON is silently dropped, saving a watch that matches ANY payload #108 — Invalid MQTT payload-match JSON is silently dropped, saving a watch that matches ANY payload (AgendaForm.svelte:221)
Infra + config
Documented /v2/* vLLM passthrough always returns 404 (no /v2 -> /v1 rewrite) #115 — Documented /v2/* vLLM passthrough always returns 404 (no /v2 -> /v1 rewrite) (nginx.conf:46)
vLLM and vllm-vision run with no API key on LAN-published ports, contradicting .env/docs #116 — vLLM and vllm-vision run with no API key on LAN-published ports, contradicting .env/docs (compose.yaml:152)
Postgres, Qdrant, and embeddings published to all LAN interfaces though only compose-internal consumers exist #117 — Postgres, Qdrant, and embeddings published to all LAN interfaces though only compose-internal consumers exist (compose.yaml:12)
Static upstream blocks for vllm/tts/stt never re-resolve DNS — 502s after container recreate #118 — Static upstream blocks for vllm/tts/stt never re-resolve DNS — 502s after container recreate (nginx.conf:6)
Lens: concurrency
/health memory_stats is permanently dead: asyncio is never imported, so asyncio.get_running_loop() raises NameError swallowed by the bare except #59 — /health memory_stats is permanently dead: asyncio is never imported, so asyncio.get_running_loop() raises NameError swallowed by the bare except (selene_agent.py:543) · 3 reviewers
LokiHandler.emit performs a synchronous requests.post (timeout=5) per log record on the event-loop thread #60 — LokiHandler.emit performs a synchronous requests.post (timeout=5) per log record on the event-loop thread (logger.py:62) · 2 reviewers
Per-turn retrieval does synchronous requests.post + sync QdrantClient query on the event loop, freezing the whole agent #62 — Per-turn retrieval does synchronous requests.post + sync QdrantClient query on the event loop, freezing the whole agent (retrieval.py:74) · 2 reviewers
_tick creates fire-and-forget tasks with no saved reference; _dispatch_deferred has no exception guard and skips the _running_items check #66 — _tick creates fire-and-forget tasks with no saved reference; _dispatch_deferred has no exception guard and skips the _running_items check (engine.py:245) · 2 reviewers
process_event runs sync Qdrant queries and cv2.imwrite JPEG encodes directly on the FastAPI event loop #125 — process_event runs sync Qdrant queries and cv2.imwrite JPEG encodes directly on the FastAPI event loop (pipeline.py:436)
Lens: contracts
Uploaded audio temp file is only deleted on the success path — failed transcriptions leak files in /tmp #61 — Uploaded audio temp file is only deleted on the success path — failed transcriptions leak files in /tmp (main.py:167) · 2 reviewers
Lens: security
SSRF: unauthenticated vision proxy fetches an arbitrary caller-supplied URL #122 — SSRF: unauthenticated vision proxy fetches an arbitrary caller-supplied URL (vision.py:131)
Path traversal into the authenticated Home Assistant REST API via unsanitized camera segment #123 — Path traversal into the authenticated Home Assistant REST API via unsanitized camera segment (ha_snapshot.py:59)
OCR/vision tool results are injected into the tool-calling context without untrusted-text fencing #124 — OCR/vision tool results are injected into the tool-calling context without untrusted-text fencing (orchestrator.py:927)
MCP: HA + general
queue_prompt ignores HTTP errors and missing prompt_id — ComfyUI rejection turns into a 120 s poll on /history/None and a misleading TimeoutError #86 — queue_prompt ignores HTTP errors and missing prompt_id — ComfyUI rejection turns into a 120 s poll on /history/None and a misleading TimeoutError (comfyui_tools.py:71)
brave_search and search_wikipedia inject raw untrusted web content into tool results without the repo's UNTRUSTED_USER_TEXT wrapping #87 — brave_search and search_wikipedia inject raw untrusted web content into tool results without the repo's UNTRUSTED_USER_TEXT wrapping (mcp_server.py:497)
_normalize_volume maps value=1 to 100% volume although the tool schema defines value as integer percent 0-100 #88 — _normalize_volume maps value=1 to 100% volume although the tool schema defines value as integer percent 0-100 (ha_media_controller.py:49)
ha_toggle_automation coerces enabled with bool(), so string "false" enables the automation and a missing value silently disables it #89 — ha_toggle_automation coerces enabled with bool(), so string "false" enables the automation and a missing value silently disables it (mcp_server.py:922)
MCP: media + memory
MassAgent.connect() hangs forever if start_listening fails before signaling init_ready #90 — MassAgent.connect() hangs forever if start_listening fails before signaling init_ready (mass_client.py:74)
Snapshot request/response has no correlation: late MQTT messages resolve the next request with stale URLs; concurrent calls clobber the shared future #91 — Snapshot request/response has no correlation: late MQTT messages resolve the next request with stale URLs; concurrent calls clobber the shared future (mcp_server.py:126)
L4 cache invalidation in _delete_memory is a cross-process no-op — deleted persistent memories keep being injected into every prompt #92 — L4 cache invalidation in _delete_memory is a cross-process no-op — deleted persistent memories keep being injected into every prompt (qdrant_mcp_server.py:459)
_get_embedding uses blocking requests.post with no timeout — a hung embeddings service wedges the whole memory module #93 — _get_embedding uses blocking requests.post with no timeout — a hung embeddings service wedges the whole memory module (qdrant_mcp_server.py:94)
MCP: vision + device
Bootstrap clone/fetch runs synchronously in __init__ before stdio serving — can exceed the manager's 30s connect timeout and permanently disable all GitHub tools #94 — Bootstrap clone/fetch runs synchronously in init before stdio serving — can exceed the manager's 30s connect timeout and permanently disable all GitHub tools (github_mcp_server.py:129)
_one_shot_cron encodes an absolute instant as a year-less cron — 'at' times >12 months out fire a year early; 'at' within the current minute fires a year late #95 — _one_shot_cron encodes an absolute instant as a year-less cron — 'at' times >12 months out fire a year early; 'at' within the current minute fires a year late (mcp_server.py:47)
cancel_reminder deletes any agenda item by id without checking kind='reminder' #96 — cancel_reminder deletes any agenda item by id without checking kind='reminder' (mcp_server.py:335)
face_enroll_person creates the person row before validating the source — bad source leaves orphan zero-image people in the gallery #97 — face_enroll_person creates the person row before validating the source — bad source leaves orphan zero-image people in the gallery (face_mcp_server.py:282)
github_get_issue silently truncates comments to GitHub's first page (30) #98 — github_get_issue silently truncates comments to GitHub's first page (30) (github_mcp_server.py:387)
TTS / STT
_model_lock is released between conds prep and per-sentence generate, so concurrent streams cross-contaminate voices — contradicting the documented invariant #114 — _model_lock is released between conds prep and per-sentence generate, so concurrent streams cross-contaminate voices — contradicting the documented invariant (streaming.py:201)
Tests / quality
"half_life_days" implements e-folding decay, not halving; tests pin the contradictory behavior #119 — "half_life_days" implements e-folding decay, not halving; tests pin the contradictory behavior (memory_math.py:26)
Running pytest triggers a real consolidation run that mutates production Qdrant memories #120 — Running pytest triggers a real consolidation run that mutates production Qdrant memories (test_integration_memory_review.py:137)
Mid-stream WS tests assert immediately after send_json with no synchronization — flaky, and one variant cannot fail #121 — Mid-stream WS tests assert immediately after send_json with no synchronization — flaky, and one variant cannot fail (test_chat_header_device_name.py:163)
Low · 40
Agent API
Metrics endpoints accept unbounded/negative limit and days query params — negative limit becomes an unhandled 500, huge limit dumps the whole table #129 — Metrics endpoints accept unbounded/negative limit and days query params — negative limit becomes an unhandled 500, huge limit dumps the whole table (metrics.py:11)
/ws/logs subscribes only after replaying the snapshot — records logged during the replay are silently dropped #130 — /ws/logs subscribes only after replaying the snapshot — records logged during the replay are silently dropped (logs.py:23)
Agent core
get_max_model_len negative-caches forever after a single transient failure, permanently disabling context-size summarization #127 — get_max_model_len negative-caches forever after a single transient failure, permanently disabling context-size summarization (vllm.py:164)
invalidate_cache() fired during an in-flight _render is overwritten, caching a stale L4 block until the next mutation #128 — invalidate_cache() fired during an in-flight _render is overwritten, caching a stale L4 block until the next mutation (l4_context.py:48)
Autonomy engine
Confirmation deep-link falls back to the docker-internal URL, producing an unusable approve link by default #131 — Confirmation deep-link falls back to the docker-internal URL, producing an unusable approve link by default (engine.py:574)
ensure_default_agenda silently overwrites config and autonomy_level of system items on every boot #132 — ensure_default_agenda silently overwrites config and autonomy_level of system items on every boot (db.py:238)
Zone LISTEN loop cannot detect a silently dead Postgres connection, contrary to its docstring #133 — Zone LISTEN loop cannot detect a silently dead Postgres connection, contrary to its docstring (sensor_events.py:173)
Source-mismatch check in match() is a no-op — the event's source is never validated against the spec #134 — Source-mismatch check in match() is a no-op — the event's source is never validated against the spec (trigger_match.py:64)
_connect_with_backoff's retry loop never retries — connect_async performs no network I/O #135 — _connect_with_backoff's retry loop never retries — connect_async performs no network I/O (mqtt_listener.py:151)
Autonomy handlers
recent_visitors prompt-size cap never applies because the tool result is a string #136 — recent_visitors prompt-size cap never applies because the tool result is a string (watch_llm.py:180)
Contradictory conditional: both branches of the status expression yield 'error' #137 — Contradictory conditional: both branches of the status expression yield 'error' (briefing.py:113)
watch.py _make_notifier is never called #138 — watch.py _make_notifier is never called (watch.py:46)
Frontend: routes
Image-generation poll loop is never cancelled on unmount #146 — Image-generation poll loop is never cancelled on unmount (+page.svelte:49)
Default 'At…' date mixes UTC date with local time, yielding yesterday's date in UTC-positive timezones #147 — Default 'At…' date mixes UTC date with local time, yielding yesterday's date in UTC-positive timezones (ReminderTimePicker.svelte:32)
Activity chart keys mix local midnight with toISOString UTC dates, shifting/omitting days in UTC-positive timezones #148 — Activity chart keys mix local midnight with toISOString UTC dates, shifting/omitting days in UTC-positive timezones (+page.svelte:57)
selectDomain responses can arrive out of order, showing entities for the wrong domain #149 — selectDomain responses can arrive out of order, showing entities for the wrong domain (+page.svelte:52)
Clearing an item's Name on edit is silently ignored #150 — Clearing an item's Name on edit is silently ignored (AgendaForm.svelte:337)
Infra + config
Per-request trace_id used as a Loki stream label causes unbounded label cardinality #153 — Per-request trace_id used as a Loki stream label causes unbounded label cardinality (logger.py:44)
HAOS_USE_SSL = True is dead and contradicts the effective agent default #154 — HAOS_USE_SSL = True is dead and contradicts the effective agent default (shared_config.py:45)
ComfyUI GPU index baked into the image (--default-device 3) instead of env pinning; collides with face-recognition/vLLM on GPU 3 #155 — ComfyUI GPU index baked into the image (--default-device 3) instead of env pinning; collides with face-recognition/vLLM on GPU 3 (Dockerfile:63)
Lens: concurrency
Synthesized tool_call ids collide across sessions (second-granularity timestamp), cross-wiring the companion pending-upload registry #164 — Synthesized tool_call ids collide across sessions (second-granularity timestamp), cross-wiring the companion pending-upload registry (orchestrator.py:669)
Lens: contracts
chatSync() is dead code and its contract is stale: no X-Session-Id support and ChatResponse omits session_id #126 — chatSync() is dead code and its contract is stale: no X-Session-Id support and ChatResponse omits session_id (api.ts:154) · 2 reviewers
SYSTEM_PROMPT instructs the LLM to call 'query_multimodal_ai' but the registered tool is 'query_multimodal_api' #165 — SYSTEM_PROMPT instructs the LLM to call 'query_multimodal_ai' but the registered tool is 'query_multimodal_api' (config.py:198)
Lens: security
git clone/fetch stderr is logged even though the remote URL embeds the GitHub token #163 — git clone/fetch stderr is logged even though the remote URL embeds the GitHub token (github_mcp_server.py:64)
MCP: HA + general
_ws_call receive loop has no timeout — a stalled HA WebSocket leaks a hung task and open connection in the MCP subprocess #139 — _ws_call receive loop has no timeout — a stalled HA WebSocket leaks a hung task and open connection in the MCP subprocess (mcp_server.py:153)
_control_climate reports total failure even when earlier service calls in the batch already succeeded #140 — _control_climate reports total failure even when earlier service calls in the batch already succeeded (mcp_server.py:1193)
_resolve_device silently picks the first substring match when several media players match #141 — _resolve_device silently picks the first substring match when several media players match (ha_media_controller.py:191)
#142 — wiki_tools main example calls async functions without await, printing coroutine objects (wiki_tools.py:186)
MCP: media + memory
on_message fails the pending snapshot future on errors from unrelated topics #143 — on_message fails the pending snapshot future on errors from unrelated topics (mcp_server.py:85)
HA wake/launch map lookup is case-sensitive while client-name resolution is case-insensitive #144 — HA wake/launch map lookup is case-sensitive while client-name resolution is case-insensitive (plex_client.py:207)
MCP: vision + device
Vision HTTP timeout (180s) exceeds the MCP client-side tool cap (120s default) — long calls always surface as generic MCP timeouts while GPU work continues #145 — Vision HTTP timeout (180s) exceeds the MCP client-side tool cap (120s default) — long calls always surface as generic MCP timeouts while GPU work continues (server.py:42)
TTS / STT
Re-uploading an existing voice never invalidates the old conds cache entry, pinning stale Conditionals in GPU memory #151 — Re-uploading an existing voice never invalidates the old conds cache entry, pinning stale Conditionals in GPU memory (main.py:268)
srt/vtt responses fabricate a fixed 00:00:00–00:00:10 cue and verbose_json returns duration=None, segments=[] #152 — srt/vtt responses fabricate a fixed 00:00:00–00:00:10 cue and verbose_json returns duration=None, segments=[] (main.py:176)
Tests / quality
test_reminder_rejects_empty_body cannot fail and its name contradicts actual behavior #156 — test_reminder_rejects_empty_body cannot fail and its name contradicts actual behavior (test_reminder_handler.py:122)
Upload tests resolve an asyncio.Future across two event loops from different threads #157 — Upload tests resolve an asyncio.Future across two event loops from different threads (test_companion_upload.py:65)
Unused 'websocket' dependency — obsolete package that can shadow websocket-client #158 — Unused 'websocket' dependency — obsolete package that can shadow websocket-client (pyproject.toml:27)
frozen_now fixture is unused by every test #159 — frozen_now fixture is unused by every test (conftest.py:7)
Untested surface: stateless /v1/chat/completions OpenAI-compat endpoint has zero tests #160 — Untested surface: stateless /v1/chat/completions OpenAI-compat endpoint has zero tests (selene_agent.py:421)
Untested surface: SessionOrchestratorPool.get_or_create mint/hydrate/LRU-eviction path #161 — Untested surface: SessionOrchestratorPool.get_or_create mint/hydrate/LRU-eviction path (session_pool.py:160)
Untested surface: MCPClientManager — the single chokepoint for all 68 tools #162 — Untested surface: MCPClientManager — the single chokepoint for all 68 tools (mcp_client_manager.py:1)
Tracking issue for the multi-agent code audit of
855f5cc(branchcleanup).Interactive report: https://claude.ai/code/artifact/180f5fbd-9010-4371-b893-247894fcfbc1
16 reviewers swept ~42k lines of first-party code across the agent, autonomy engine, MCP tool modules, dashboard, face recognition, speech services, and infrastructure, plus 3 cross-cutting lenses (security, concurrency, cross-service contracts). Each of the 170 raw findings was handed to 2 independent agents prompted to refute it; 19 were refuted and 10 drew a split verdict, leaving 127 unique confirmed issues (14 duplicates merged — 12 were found independently by 2–3 reviewers working blind, noted on those issues).
Filter with the
auditlabel, plusseverity:*,area:*, andcategory:*.High · 20
Agent API
chat.py:307) · 3 reviewerslogger.py:116)Autonomy engine
engine.py:611)engine.py:375)Autonomy handlers
watch.py:80)Face recognition
people.py:318) · 2 reviewersdetections.py:185)Frontend: chat/lib
+page.svelte:468)chat.ts:164)Frontend: routes
ReminderTimePicker.svelte:152)AgendaForm.svelte:230)AgendaForm.svelte:335)Infra + config
shared_config.py:30) · 2 reviewerslogger.py:62)Lens: security
selene_agent.py:314)MCP: HA + general
mcp_server.py:413)mcp_server.py:1205)MCP: vision + device
github_mcp_server.py:292)github_mcp_server.py:121)TTS / STT
main.py:84)Medium · 67
Agent API
homeassistant.py:41) · 2 reviewersautonomy.py:109)autonomy.py:332)memory.py:417)Agent core
session_pool.py:174) · 2 reviewerssession_pool.py:193) · 2 reviewersmcp_client_manager.py:120)mcp_client_manager.py:91)selene_agent.py:429)orchestrator.py:478)config.py:5)Autonomy engine
engine.py:474)db.py:715)engine.py:613)mqtt_listener.py:169)schedule.py:34)Autonomy handlers
reminder.py:191)memory_review.py:446)memory_review.py:38)watch_llm.py:296)act.py:463)memory_review.py:313)Face recognition
pipeline.py:156)admin.py:190)admin.py:418)ha_snapshot.py:77)people.py:209)Frontend: chat/lib
chat.ts:291)+page.svelte:153)LogStream.svelte:51)+page.svelte:169)Frontend: routes
+page.svelte:183)ReminderTimePicker.svelte:158)ReminderTimePicker.svelte:74)+page.svelte:74)+page.svelte:99)AgendaForm.svelte:221)Infra + config
nginx.conf:46)compose.yaml:152)compose.yaml:12)nginx.conf:6)Lens: concurrency
selene_agent.py:543) · 3 reviewerslogger.py:62) · 2 reviewersretrieval.py:74) · 2 reviewersengine.py:245) · 2 reviewerspipeline.py:436)Lens: contracts
main.py:167) · 2 reviewersLens: security
vision.py:131)ha_snapshot.py:59)orchestrator.py:927)MCP: HA + general
comfyui_tools.py:71)mcp_server.py:497)ha_media_controller.py:49)enabledwith bool(), so string "false" enables the automation and a missing value silently disables it #89 — ha_toggle_automation coercesenabledwith bool(), so string "false" enables the automation and a missing value silently disables it (mcp_server.py:922)MCP: media + memory
mass_client.py:74)mcp_server.py:126)qdrant_mcp_server.py:459)qdrant_mcp_server.py:94)MCP: vision + device
github_mcp_server.py:129)mcp_server.py:47)mcp_server.py:335)face_mcp_server.py:282)github_mcp_server.py:387)TTS / STT
streaming.py:201)Tests / quality
memory_math.py:26)test_integration_memory_review.py:137)test_chat_header_device_name.py:163)Low · 40
Agent API
metrics.py:11)logs.py:23)Agent core
vllm.py:164)l4_context.py:48)Autonomy engine
engine.py:574)db.py:238)sensor_events.py:173)trigger_match.py:64)mqtt_listener.py:151)Autonomy handlers
watch_llm.py:180)briefing.py:113)watch.py:46)Frontend: routes
+page.svelte:49)ReminderTimePicker.svelte:32)+page.svelte:57)+page.svelte:52)AgendaForm.svelte:337)Infra + config
logger.py:44)shared_config.py:45)--default-device 3) instead of env pinning; collides with face-recognition/vLLM on GPU 3 #155 — ComfyUI GPU index baked into the image (--default-device 3) instead of env pinning; collides with face-recognition/vLLM on GPU 3 (Dockerfile:63)Lens: concurrency
orchestrator.py:669)Lens: contracts
api.ts:154) · 2 reviewersconfig.py:198)Lens: security
github_mcp_server.py:64)MCP: HA + general
mcp_server.py:153)mcp_server.py:1193)ha_media_controller.py:191)wiki_tools.py:186)MCP: media + memory
mcp_server.py:85)plex_client.py:207)MCP: vision + device
server.py:42)TTS / STT
main.py:268)main.py:176)Tests / quality
test_reminder_handler.py:122)test_companion_upload.py:65)pyproject.toml:27)conftest.py:7)selene_agent.py:421)session_pool.py:160)mcp_client_manager.py:1)