# Push-to-talk and session settings — 0.4.0

Voice input always targets the device's selected `agent:pi:<id>` or
`agent:codex:<id>`. Server Overview never chooses a session implicitly. Legacy
`pi:<id>` state is interpreted as `agent:pi:<id>` without manual migration.

## Device interaction

Buttons are debounced for 30 ms. A press held for 600 ms emits one long-press
event; its release never also navigates a page. Priority is Approval → voice
confirmation → settings → normal long press → normal short press.

- Normal: short A/B next/previous page; hold A to speak; release A to finish.
- Approval: A approve / B reject, regardless of hold duration. No timer decides.
- Transcript: A SEND / B CANCEL. Agent, session name and transcript are shown.
  Long transcripts automatically page every four seconds; no touch is required.
- Settings: hold B to open; A cycles models; B selects. Codex then cycles only
  the selected model's advertised efforts. B applies; hold B exits. Pi hides
  effort. A single model shows NO ALTERNATIVES. Successful updates show the
  authoritative model for one second. Failure retains the original setting.

A target change, connection loss or Approval cancels unfinished voice input.
Approval first stops capture, then plays the existing alert. Cancelled transcripts
are never restored. Errors never turn into Agent input.

## Audio and transport

The existing I2S0 TX/RX handles and ES8311 codec are reused. The implementation
was checked against M5Stack's [official hal_audio.cpp](https://github.com/m5stack/M5StopWatch-UserDemo/blob/6b4aa125288b6fe9dca661f10159f6e1e5ee785c/main/hal/hal_audio.cpp):
44,100 Hz, 16-bit mono PCM, input gain 30 dB, `esp_codec_dev_read()`.
No dynamic sample-rate switch, second driver, microphone monitoring or TTS.

A codec mode mutex excludes playback during recording. Short start/end tones
bracket capture; output is muted while reading the microphone. RX is drained
before speech capture to remove stale data from playback. Capture runs in its
own task and pushes 3,528-byte (40 ms) PCM frames into an eight-frame queue.
The WSS sender separately encodes 4,704 base64 bytes plus JSON. Queue overflow
cancels the request rather than dropping audio. Capture stops at 20 seconds.

Only the existing authenticated `/device/ws` is used. Gateway bounds incoming
frames at 8,192 bytes, PCM at 1,764,000 bytes, each chunk at 4,410 bytes, and total
chunk count at 1,000. Sequence starts at zero and increments without gaps.
The Gateway independently ends capture after 20 seconds. One request per device
connection owns voice ID, target, agent, session, transcript and state.

State: RECORDING → TRANSCRIBING → AWAITING_CONFIRMATION → SENDING → SENT;
any incomplete request can become CANCELLED or FAILED. A SEND decision is
consumed once before calling the existing `AgentAdapter.send_input` control
layer. Pi/Codex resume the same session through their native SDK/API.

## STT and privacy

`SpeechToTextProvider.transcribe(wav_bytes)` is separate from WSS handling.
The initial adapter posts multipart WAV directly to an OpenAI-compatible
`/audio/transcriptions` endpoint. There is no resampling. HTTP timeout is 25 s,
with a 30 s overall transcription bound. Redirects are disabled.

Set these only in the repo-external `~/.config/esp32-device-gateway/gateway.env`
with mode `0600`, then restart the Gateway:

```text
GATEWAY_STT_PROVIDER=openai-compatible
GATEWAY_STT_BASE_URL=https://your-provider.example/v1
GATEWAY_STT_API_KEY=<authorized-secret>
GATEWAY_STT_MODEL=<transcription-model>
```

**音频会发送给配置的 STT provider。** Gateway audio/WAV/transcript buffers are
memory only. They are not written to disk, logged, or included in Admin's device
last-event payload. Once confirmed, Agent input is subject to that Agent's normal
session persistence. Credentials are never returned to Device/Admin.

Without a configured provider capture/transport remain available, but STT displays
VOICE ERROR / Transcription failed / B CLOSE. Fake STT is used only by tests.
Run `server/.venv/bin/python scripts/smoke-stt.py --wav /tmp/public-test.wav --expected 'public test phrase' --key-term phrase`
for an explicitly configured real provider; an absent provider reports SKIPPED.

## Session settings

Adapters expose `get_settings`, `list_models`, and `update_settings`; UI code
never calls Pi SDK or Codex protocol directly. RUNNING / WAITING_APPROVAL returns
409 session busy. Model IDs and efforts are validated against native metadata.

Pi 0.85.1 uses `ModelRegistry.getAvailable()` and `session.setModel(...,
{persist:false})`; `persist:false` avoids changing global defaults and still
records the model in the same session through official SDK persistence. For an
existing TUI the extension uses official `pi.setModel`. A dormant session is
resumed by the SDK when its settings are opened/updated; no TUI is restarted.
Configured credentials determine the SDK catalog, but catalog presence alone
is not proof that credentials are valid for inference.

Codex 0.154.0 uses `model/list` and experimental `thread/settings/update`.
The locally generated schema has no `thread/settings/read`; `thread/read`
provides `model` / `reasoningEffort`. Update returns `{}`, so the adapter reads
back authoritative settings. A dormant thread is resumed before updating. All
native calls preserve the original session ID. A shared daemon is not restarted.
Effort strings come directly from metadata, including nonstandard values.
The official update affects subsequent turns; Gateway adds no independent
settings persistence and does not edit Agent files. The real smoke verifies a
subsequent turn and readback after unsubscribing; daemon-restart durability before
any subsequent turn is not claimed.

Admin has the complete native model catalog. Device gets at most six, ordered
by current model, recently used project/session models, native defaults, then
native catalog order, deduplicated. There is no hand-maintained favorites list.

## Verification

`./scripts/test-server.sh` includes host C++ gesture/Approval tests, Pi and Admin
JS tests, Registry/adapter/voice regression, HTTP + WS smoke, and actual TLS WSS
synthetic-device voice confirmation to both adapters with **fake STT**.
`./scripts/build.sh m5stack-stopwatch` builds the firmware. Real settings proof is
`scripts/smoke-settings.py`; real approval proof is `scripts/smoke-approvals.py`.
Physical microphone quality, feedback, button timing, volume and notifications
still require the device. No Touch, OTA, always-on microphone, extra agents,
rewind, database, multi-VPS or general workflow system is added.
