diff --git a/changelog.d/1299-voice-panel-settings.md b/changelog.d/1299-voice-panel-settings.md
index 60a68e7d3..1a95a6ed4 100644
--- a/changelog.d/1299-voice-panel-settings.md
+++ b/changelog.d/1299-voice-panel-settings.md
@@ -1,3 +1,5 @@
- Add a shared Voice Preview and Settings panel with browser-local preferences,
- inactivity controls and accessible floating provider controls on compact layouts
+ inactivity controls and accessible floating provider controls on compact layouts;
+ recover inactivity after confirmed audio-less replies, release retired audio
+ observers and keep cost warnings visible outside Settings
(manjunath22466, #1299).
diff --git a/docs/CURRENT-STATE.md b/docs/CURRENT-STATE.md
index 7907a9640..4f58da8b9 100644
--- a/docs/CURRENT-STATE.md
+++ b/docs/CURRENT-STATE.md
@@ -588,6 +588,17 @@ Preview preference. Unverified speech captions use the localized Recent
speech label; ordinary user captions retain YOU. Caption attribution does not
control tool dispatch.
+Voice inactivity waits for local input, output and actions to become quiet.
+A matching OpenAI response that explicitly completes without audio can restore
+idle eligibility; completion of an audio response does not prove playback has
+drained. Missing or ambiguous output evidence remains conservative. Gemini
+retires its per-audio observer wrappers on close, replacement or session end,
+so repeated Stop/Start does not retain old audio instances until the panel closes.
+The panel keeps the model tier and regular cost estimate in Voice Settings;
+warning, capped and unknown cost states also appear in the dock heading when
+Settings is closed. The existing hard cost cap still stops the session and
+announces the reason.
+
Automatic Voice Preview uses output from the active mounted provider, including late replies that may answer an older request. Manual panel choices remain until genuine local input rearms automatic presentation. Preview stays off when disabled, remains hidden in Cockpit, and retires on Stop or provider replacement. Display events do not grant command authority.
Contacts nearest-aircraft follow-ups retain the snapshot's distance basis even
@@ -5160,7 +5171,7 @@ and unreachable upstream (502/504) separately from road geometry.
- **Contact-match ties break on hex ascending.** `track_entity` is a mutation fulfilling "follow that one", and the model's observed answer to a non-ok track result is to retry with guesses rather than ask, so the lookup always commits rather than returning an ambiguity. What it owes the caller is stability: hex is unique and always present, so the same query resolves to the same contact for as long as both are loaded.
- **Visual grounding**: at `local` view scale with no structured identity, the client captures the Cesium canvas (≤1200px JPEG, black-frame detection, double-render for freshness) and sends it as `input_image` with a strict "do not invent labels" instruction.
- **Context window**: only the latest viewport screenshot stays in context — the client deletes the prior image item (`conversation.item.delete`) before adding a new one (images are the most expensive item, re-billed every turn). Text history is bounded by the **server-side** `truncation: { type: 'retention_ratio', retention_ratio, token_limits.post_instructions }` set in `/api/realtime/token` (cache-friendly batched truncation). There is intentionally **no** client-side per-turn conversation-item cap — deleting from the front of history each turn busts the Realtime prompt cache. A spatially-aware summarize-and-prune policy is specced for a future iteration.
-- **Model tier + spend guard** (`src/voice/voiceCost.js`, August 2026): the voice heading row carries a `STD`/`MINI` toggle and a running session-cost readout (`~$0.42`).
+- **Model tier + spend guard** (`src/voice/voiceCost.js`, August 2026): hosts without the voice panel keep the `STD`/`MINI` toggle and running session-cost readout (`~$0.42`) in the heading. Panel hosts place these controls in Voice Settings and mirror warning, capped or unknown cost states in the heading.
- **Tier selection.** `standard` = `gpt-realtime-2` (default), `mini` = `gpt-realtime-2.1-mini` (~3× cheaper per audio token). The client sends `?tier=` to `/api/realtime/token`; the endpoint resolves it through the shared registry, so an unknown, empty, or hostile value falls back to `standard` rather than reaching OpenAI as a model id. Responses echo `X-GEV-Voice-Tier` / `X-GEV-Voice-Model` (plus `X-GEV-Voice-Tier-Fallback: 1` when a bogus tier was downgraded). Persisted at `godsEyeView.voiceCost.tier`.
- **Applies NEXT session.** The model is fixed when the ephemeral token is minted, so a live session always keeps the model it connected with; toggling mid-session only records the preference (the button title says so). The cost tracker's lifetime is the session's lifetime and its model binding is immutable from `start()` to `stop()` — rebuilding it on toggle would erase accrued spend and let repeated toggles bypass the cap. The tracker may only be replaced once the session is FULLY SETTLED (`isVoiceSessionSettled()`: not active **and** no data channel **and** no peer connection) — `!isActive()` alone is not enough, because the `error` status reports inactive while the transport can still deliver a late `response.done`. The toggle itself reads and writes only the persisted preference, never the live tracker.
- **Env overrides.** `OPENAI_REALTIME_MODEL` / `OPENAI_REALTIME_MODEL_MINI` remain authoritative per tier, so a drifted upstream model id is a `.env` fix rather than a code change. Because an override can point a tier at any model, the client prices against the model id the server actually echoed, **not** the tier it requested. An unrecognised id is billed at the most expensive known rates plus one console warning — under-metering is what lets a cap be overrun.
diff --git a/scripts/format-scope.json b/scripts/format-scope.json
index e60413624..53e581063 100644
--- a/scripts/format-scope.json
+++ b/scripts/format-scope.json
@@ -1089,5 +1089,13 @@
"src/dockerTrustedPeers.test.mjs",
"src/cameraGlobeRotation.test.mjs",
"src/voice/globeResetConfirmation.test.mjs",
- "src/hudDms.test.mjs"
+ "src/hudDms.test.mjs",
+ "src/voice/panel/preferences.test.mjs",
+ "src/voice/panel/host.test.mjs",
+ "src/voice/panel/manualPanel.test.mjs",
+ "src/voice/panel/nativeActivity.test.mjs",
+ "src/voice/panel/inactivity.test.mjs",
+ "src/voice/panel/actionObserver.test.mjs",
+ "src/voice/panel/testDom.mjs",
+ "src/voice/realtimeCostPanel.test.mjs"
]
diff --git a/src/ui/styles/voice-cost.css b/src/ui/styles/voice-cost.css
index 9d655e484..4ca1d558d 100644
--- a/src/ui/styles/voice-cost.css
+++ b/src/ui/styles/voice-cost.css
@@ -62,6 +62,15 @@
color: #ffc46b;
}
+.gev-voice-cost-warning {
+ margin-left: auto;
+ flex: 0 0 auto;
+}
+
+.gev-voice-cost-warning[data-level='unknown'] {
+ color: #ffc46b;
+}
+
.gev-voice-cost-value[data-level='cap'] {
color: #ff6b6b;
font-weight: 600;
diff --git a/src/voice/control.js b/src/voice/control.js
index af9d37cbc..df40be104 100644
--- a/src/voice/control.js
+++ b/src/voice/control.js
@@ -49,7 +49,7 @@ export function createVoiceControl({ reset = false, panel = false } = {}) {