Synced from monorepo

Synced from monorepo

Changes:
- Shell: accept target response id on rewind execute
- Shell: stamp response id on chat user message chunks
- Worktree: optional rebuild and stale git registration cleanup in auto-GC
- Worktree: kind-aware auto-GC TTLs and config knobs
- Worktree: macOS process CWD scan and Unix PID liveness for GC guards
- Worktree: automatic throttled GC on startup (Linux age-based; non-Linux dead-only)
- Pager: add `[ui].combine_queued_prompts` to batch queued follow-ups
- Shell: stop overwriting user skills
- Tools: read markdown in `skills/` directories untruncated
- `/usage` shows per-session token and dollar usage in the TUI
- Security: prompt on environment-dumping `ps` variants
- Security: always-safe `kubectl` no longer runs arbitrary kubeconfig credential plugins without permission
- Tools: make scheduler deletion durable
- Shell: add relocation storage primitives
- Shell: give side model calls their own conversation ids
- Fix five workflow-runtime bugs (budget, pause, cancel, reconnect)
- Security: peel `env -S` / `--split-string` operands in the Bash permission gate (managed deny/ask)
- Pager: expose doctor in the TUI
- Security: block unauthorized RCE via abused safe commands
- Pager idle watcher cue: "1 subagent still running" instead of "watching · 1 subagent"
- Security: block `rg --pre` arbitrary code execution in auto-mode
- Voice: diagnose silent-mic failures (macOS permission) and add doctor/terminal-setup Voice section
- App builder deployer: `allow_forking` and `show_built_with_grok`
- Pager: stop stacking duplicate "Worked for" markers on parked turns
- Shell: support `max` as a distinct reasoning effort tier
- Tools: serialize background `/loop` fires on the whole work unit
- Shell: add working-directory relocation state primitives
- Proto: `ClientToolResult` and `ChatConfig` client-side tools
- Shell: model providers
- Chat: select App Builder product on the Build path
- Shell: attach author identity to feedback when the deployment opts in
- Doctor: fix for SSH wrap setup
- Workflow authoring skills: create-workflow and import-claude-workflow docs
- Add read-only grok doctor
- Sandbox: apply Landlock without a controlling TTY
- Pager: recover image paste over grok wrap on headless remotes
- Pager: make actions screen-mode aware
- Shell: resume sessions when the working directory moves
- Pager: centralize terminal diagnostics
- Workspace: gate inline shell file access
- Pager: centralize terminal probes
- Pager: edit minimal prompts in an external editor
- Pager: standardize backgrounding on Ctrl+B
- Shell: recap rides the parent turn's prompt cache
- Tools: add scheduler lifecycle version clock

Source-Revision: 0f4d7c91b8b2b408333f6de1e8a76cb8eaa71899
This commit is contained in:
grokkybara[bot] 2026-07-21 18:10:23 +00:00
commit 3af4d5d398
556 changed files with 56609 additions and 21892 deletions

View file

@ -7,7 +7,7 @@
//! dedicated std thread and forwards PCM chunks through a sync channel.
use std::sync::Arc;
use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
use std::sync::atomic::{AtomicBool, AtomicU16, AtomicUsize, Ordering};
use std::sync::mpsc::TrySendError;
use std::thread::{self, JoinHandle};
use std::time::Duration;
@ -23,9 +23,15 @@ pub struct CaptureHandle {
stop: Arc<AtomicBool>,
thread: Option<JoinHandle<()>>,
bridge: tokio::task::JoinHandle<()>,
peak: Arc<AtomicU16>,
}
impl CaptureHandle {
/// Session peak of device-delivered PCM (see [`meter_and_send`]).
pub fn peak_meter(&self) -> Arc<AtomicU16> {
Arc::clone(&self.peak)
}
/// Stop capture and wait for the thread to exit.
///
/// Dropping a `CaptureHandle` also stops capture (see the `Drop` impl), but
@ -76,9 +82,11 @@ pub fn spawn_pcm_capture(
let stop = Arc::new(AtomicBool::new(false));
let stop_flag = Arc::clone(&stop);
let peak = Arc::new(AtomicU16::new(0));
let peak_cb = Arc::clone(&peak);
let (ready_tx, ready_rx) = std::sync::mpsc::sync_channel::<Result<(), VoiceError>>(1);
let thread = thread::spawn(move || {
run_capture_loop(sample_rate, sync_tx, stop_flag, ready_tx);
run_capture_loop(sample_rate, sync_tx, stop_flag, peak_cb, ready_tx);
});
// Wait briefly for the device to actually open (mirrors the STT
@ -106,9 +114,33 @@ pub fn spawn_pcm_capture(
stop,
thread: Some(thread),
bridge,
peak,
})
}
/// Default cpal input device, or a config error when the host has none.
fn default_input_device() -> Result<cpal::Device, VoiceError> {
cpal::default_host()
.default_input_device()
.ok_or_else(|| VoiceError::Config("no default input audio device".into()))
}
/// Default input device without opening a stream ([`crate::probe::input_device_info`]).
pub fn input_device_info() -> Result<crate::probe::InputDeviceInfo, VoiceError> {
let device = default_input_device()?;
let name = device.name().unwrap_or_else(|_| "<unknown>".to_string());
let detail = match device.default_input_config() {
Ok(c) => format!(
"{} Hz, {} ch, {:?}",
c.sample_rate().0,
c.channels(),
c.sample_format()
),
Err(e) => format!("default config unavailable: {e}"),
};
Ok(crate::probe::InputDeviceInfo { name, detail })
}
/// Record mono PCM16 LE for a fixed duration (probe / diagnostics).
pub fn capture_pcm_for_duration(
sample_rate: u32,
@ -119,7 +151,14 @@ pub fn capture_pcm_for_duration(
let stop_flag = Arc::clone(&stop);
let (ready_tx, ready_rx) = std::sync::mpsc::sync_channel::<Result<(), VoiceError>>(1);
let thread = thread::spawn(move || {
run_capture_loop(sample_rate, sync_tx, stop_flag, ready_tx);
// Duration probe does not read the session peak.
run_capture_loop(
sample_rate,
sync_tx,
stop_flag,
Arc::new(AtomicU16::new(0)),
ready_tx,
);
});
// Surface device-open failures before recording instead of returning empty.
@ -159,6 +198,7 @@ struct CaptureStreamParams<'a> {
target_rate: u32,
sync_tx: std::sync::mpsc::SyncSender<Vec<u8>>,
stop: Arc<AtomicBool>,
peak: Arc<AtomicU16>,
/// Count of PCM chunks dropped because the channel was full. Logged off the
/// audio thread by `run_capture_loop`.
dropped: Arc<AtomicUsize>,
@ -168,6 +208,7 @@ fn run_capture_loop(
sample_rate: u32,
sync_tx: std::sync::mpsc::SyncSender<Vec<u8>>,
stop: Arc<AtomicBool>,
peak: Arc<AtomicU16>,
ready_tx: std::sync::mpsc::SyncSender<Result<(), VoiceError>>,
) {
let dropped = Arc::new(AtomicUsize::new(0));
@ -179,6 +220,7 @@ fn run_capture_loop(
sample_rate,
sync_tx,
Arc::clone(&stop),
peak,
Arc::clone(&dropped),
) {
Ok(v) => {
@ -202,12 +244,10 @@ fn open_capture_stream(
sample_rate: u32,
sync_tx: std::sync::mpsc::SyncSender<Vec<u8>>,
stop: Arc<AtomicBool>,
peak: Arc<AtomicU16>,
dropped: Arc<AtomicUsize>,
) -> Result<(cpal::Stream, String), VoiceError> {
let host = cpal::default_host();
let device = host
.default_input_device()
.ok_or_else(|| VoiceError::Config("no default input audio device".into()))?;
let device = default_input_device()?;
let device_name = device.name().unwrap_or_else(|_| "<unknown>".to_string());
@ -244,6 +284,7 @@ fn open_capture_stream(
target_rate: sample_rate,
sync_tx,
stop,
peak,
dropped,
};
@ -350,6 +391,7 @@ where
target_rate,
sync_tx,
stop,
peak,
dropped,
} = params;
let channels = in_channels as usize;
@ -371,17 +413,7 @@ where
if pcm.is_empty() {
return;
}
let bytes: Vec<u8> = pcm.iter().flat_map(|s| s.to_le_bytes()).collect();
// Never block the real-time audio thread: shed load if the
// consumer is behind. Dropped chunks are counted and logged by
// `run_capture_loop`.
match sync_tx.try_send(bytes) {
Ok(()) => {}
Err(TrySendError::Full(_)) => {
dropped.fetch_add(1, Ordering::Relaxed);
}
Err(TrySendError::Disconnected(_)) => {}
}
meter_and_send(&pcm, &peak, &sync_tx, &dropped);
},
|err| {
tracing::warn!(error = %err, "voice capture stream error");
@ -393,6 +425,25 @@ where
Ok(stream)
}
/// Meter then non-blocking send. Peak is updated **before** load-shed so the
/// silence guard sees what the mic delivered, not what survived backpressure.
fn meter_and_send(
pcm: &[i16],
peak: &AtomicU16,
sync_tx: &std::sync::mpsc::SyncSender<Vec<u8>>,
dropped: &AtomicUsize,
) {
peak.fetch_max(crate::pcm::peak_abs_i16(pcm), Ordering::Relaxed);
let bytes: Vec<u8> = pcm.iter().flat_map(|s| s.to_le_bytes()).collect();
match sync_tx.try_send(bytes) {
Ok(()) => {}
Err(TrySendError::Full(_)) => {
dropped.fetch_add(1, Ordering::Relaxed);
}
Err(TrySendError::Disconnected(_)) => {}
}
}
fn frames_to_mono_i16<T>(data: &[T], channels: usize) -> Vec<i16>
where
T: Sample,
@ -447,6 +498,18 @@ fn resample_mono_i16(samples: &[i16], input_rate: u32, output_rate: u32) -> Vec<
mod tests {
use super::*;
#[test]
fn meter_and_send_meters_shed_chunks() {
let (tx, _rx) = std::sync::mpsc::sync_channel::<Vec<u8>>(1);
let peak = AtomicU16::new(0);
let dropped = AtomicUsize::new(0);
meter_and_send(&[100], &peak, &tx, &dropped); // fills the channel
meter_and_send(&[-9_000], &peak, &tx, &dropped); // shed, but metered
assert_eq!(dropped.load(Ordering::Relaxed), 1);
assert_eq!(peak.load(Ordering::Relaxed), 9_000);
}
#[test]
fn resample_halves_rate() {
let input: Vec<i16> = (0..48).map(|i| (i * 100) as i16).collect();

View file

@ -17,8 +17,8 @@
//! pipeline and probe are backend-agnostic.
use std::io::Read;
use std::process::{Child, ChildStdout, Command, Stdio};
use std::sync::atomic::{AtomicBool, Ordering};
use std::process::{Child, Command, Stdio};
use std::sync::atomic::{AtomicBool, AtomicU16, Ordering};
use std::sync::{Arc, Mutex};
use std::thread::{self, JoinHandle};
use std::time::{Duration, Instant};
@ -125,17 +125,22 @@ fn binary_on_path(name: &str) -> bool {
})
}
/// Spawn the chosen recorder with stdout/stderr piped, and confirm it didn't
/// exit immediately (no device, audio server down). On success the child is
/// running with `stdout` available for reading.
fn spawn_recorder(sample_rate: u32) -> Result<(Recorder, Child), VoiceError> {
let recorder = detect_recorder().ok_or_else(|| {
/// The detected recorder, or a `VoiceError` naming the packages to install.
fn require_recorder() -> Result<Recorder, VoiceError> {
detect_recorder().ok_or_else(|| {
VoiceError::Config(
"no microphone recorder found on PATH: install pipewire (pw-record), \
pulseaudio-utils (parec), or alsa-utils (arecord)"
.into(),
)
})?;
})
}
/// Spawn the chosen recorder with stdout/stderr piped, and confirm it didn't
/// exit immediately (no device, audio server down). On success the child is
/// running with `stdout` available for reading.
fn spawn_recorder(sample_rate: u32) -> Result<(Recorder, Child), VoiceError> {
let recorder = require_recorder()?;
let mut child = Command::new(recorder.program())
.args(recorder.args(sample_rate))
@ -177,9 +182,15 @@ pub struct CaptureHandle {
child: Option<Child>,
stop: Arc<AtomicBool>,
reader: Option<JoinHandle<()>>,
peak: Arc<AtomicU16>,
}
impl CaptureHandle {
/// Session peak of recorder-delivered PCM (metered before load-shed).
pub fn peak_meter(&self) -> Arc<AtomicU16> {
Arc::clone(&self.peak)
}
/// Stop capture: kill the recorder, reap it, and join the reader thread so
/// the input device is released before returning.
///
@ -237,8 +248,11 @@ pub fn spawn_pcm_capture(
let stop = Arc::new(AtomicBool::new(false));
let stop_reader = Arc::clone(&stop);
let peak = Arc::new(AtomicU16::new(0));
let peak_reader = Arc::clone(&peak);
let device = recorder.program();
let reader = thread::spawn(move || forward_pcm(stdout, pcm_tx, stop_reader, device));
let reader =
thread::spawn(move || forward_pcm(stdout, pcm_tx, stop_reader, peak_reader, device));
tracing::info!(
recorder = recorder.program(),
@ -250,6 +264,7 @@ pub fn spawn_pcm_capture(
child: Some(child),
stop,
reader: Some(reader),
peak,
})
}
@ -275,10 +290,12 @@ fn drain_stderr(child: &mut Child, device: &'static str) {
/// Forward raw PCM from the recorder's stdout to the async STT sender until the
/// recorder stops (EOF on kill), the consumer goes away, or `stop` is set.
/// Generic over the reader for tests; production passes the child's stdout.
fn forward_pcm(
mut stdout: ChildStdout,
mut stdout: impl Read,
pcm_tx: async_mpsc::Sender<Vec<u8>>,
stop: Arc<AtomicBool>,
peak: Arc<AtomicU16>,
device: &'static str,
) {
let mut buf = vec![0u8; READ_CHUNK];
@ -291,6 +308,8 @@ fn forward_pcm(
// EOF: the recorder closed stdout (killed by teardown or exited).
Ok(0) => break,
Ok(n) => {
// Before try_send: shed chunks must still move the peak meter.
peak.fetch_max(crate::pcm::peak_abs_i16_le(&buf[..n]), Ordering::Relaxed);
// Never park this thread on the channel: `stop()` joins it, so a
// send that waits on a stalled STT consumer would turn teardown
// into a hang. Shed load instead when the consumer is behind —
@ -319,6 +338,15 @@ fn forward_pcm(
}
}
/// Recorder that would be spawned, without recording ([`crate::probe::input_device_info`]).
pub fn input_device_info() -> Result<crate::probe::InputDeviceInfo, VoiceError> {
let recorder = require_recorder()?;
Ok(crate::probe::InputDeviceInfo {
name: recorder.program().to_string(),
detail: "system recorder; uses the audio server's default input".to_string(),
})
}
/// Record mono PCM16 LE for a fixed duration (probe / diagnostics).
pub fn capture_pcm_for_duration(
sample_rate: u32,
@ -379,6 +407,30 @@ pub fn capture_pcm_for_duration(
mod tests {
use super::*;
#[test]
fn forward_pcm_meters_shed_chunks() {
// One loud sample per READ_CHUNK read; capacity 1 forces the second
// read to shed. Both must register in the peak meter.
let mut pcm = vec![0u8; 2 * READ_CHUNK];
pcm[..2].copy_from_slice(&5_000i16.to_le_bytes());
pcm[READ_CHUNK..READ_CHUNK + 2].copy_from_slice(&(-9_000i16).to_le_bytes());
let (tx, mut rx) = async_mpsc::channel::<Vec<u8>>(1);
let peak = Arc::new(AtomicU16::new(0));
forward_pcm(
std::io::Cursor::new(pcm),
tx,
Arc::new(AtomicBool::new(false)),
Arc::clone(&peak),
"test",
);
assert_eq!(peak.load(Ordering::Relaxed), 9_000);
let first = rx.try_recv().expect("first chunk forwarded");
assert_eq!(crate::pcm::peak_abs_i16_le(&first), 5_000);
assert!(rx.try_recv().is_err(), "second chunk shed (channel full)");
}
#[test]
fn arecord_args_are_raw_s16_mono() {
let args = Recorder::Arecord.args(16_000);

View file

@ -10,9 +10,11 @@
#[cfg(not(target_os = "linux"))]
mod capture;
#[cfg(not(target_os = "linux"))]
pub use capture::{CaptureHandle, capture_pcm_for_duration, spawn_pcm_capture};
pub use capture::{CaptureHandle, capture_pcm_for_duration, input_device_info, spawn_pcm_capture};
#[cfg(target_os = "linux")]
mod capture_linux;
#[cfg(target_os = "linux")]
pub use capture_linux::{CaptureHandle, capture_pcm_for_duration, spawn_pcm_capture};
pub use capture_linux::{
CaptureHandle, capture_pcm_for_duration, input_device_info, spawn_pcm_capture,
};