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

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -14,7 +14,7 @@ use rusqlite::Connection;
use serde::{Deserialize, Serialize};
use xai_sqlite_journal::JournalMode;
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum WorktreeKind {
Session,
@ -38,14 +38,33 @@ impl WorktreeKind {
}
pub fn from_str_lossy(s: &str) -> Self {
Self::from_str_exact(s).unwrap_or(Self::Manual)
}
/// Exact known kind key. Unknown → None (unlike [`Self::from_str_lossy`]).
pub fn from_str_exact(s: &str) -> Option<Self> {
match s {
"session" => Self::Session,
"ab" => Self::Ab,
"pool" => Self::Pool,
"fork" => Self::Fork,
"manual" => Self::Manual,
"subagent" => Self::Subagent,
_ => Self::Manual,
"session" => Some(Self::Session),
"ab" => Some(Self::Ab),
"pool" => Some(Self::Pool),
"fork" => Some(Self::Fork),
"manual" => Some(Self::Manual),
"subagent" => Some(Self::Subagent),
_ => None,
}
}
/// Config key parse: trim + case-insensitive; unknown → None.
pub fn from_str_opt(s: &str) -> Option<Self> {
let t = s.trim();
if let Some(k) = Self::from_str_exact(t) {
return Some(k);
}
// Only allocate lowercase when needed.
if t.bytes().any(|b| b.is_ascii_uppercase()) {
Self::from_str_exact(&t.to_ascii_lowercase())
} else {
None
}
}
}
@ -306,6 +325,35 @@ impl WorktreeDb {
pub fn sweep_dead(&self) -> Result<u64> {
queries::sweep_dead(&self.conn)
}
/// Read a value from the `meta` table. `Ok(None)` when the key is absent.
pub fn get_meta(&self, key: &str) -> Result<Option<String>> {
match self
.conn
.query_row(schema::GET_META, [key], |row| row.get(0))
{
Ok(v) => Ok(Some(v)),
Err(rusqlite::Error::QueryReturnedNoRows) => Ok(None),
Err(e) => Err(e).with_context(|| format!("failed to read meta key {key}")),
}
}
/// Insert or replace a `meta` table value.
pub fn set_meta(&self, key: &str, value: &str) -> Result<()> {
self.conn
.execute(schema::UPSERT_META, rusqlite::params![key, value])
.with_context(|| format!("failed to write meta key {key}"))?;
Ok(())
}
/// Test-only: run raw SQL (e.g. drop tables to force fail-closed paths).
#[cfg(test)]
pub(crate) fn execute_batch_for_test(&self, sql: &str) -> Result<()> {
self.conn
.execute_batch(sql)
.context("execute_batch_for_test failed")?;
Ok(())
}
}
/// Derive a worktree ID from its destination path: `<basename>-<hash of full path>`

View file

@ -154,6 +154,21 @@ pub struct RebuildReport {
pub already_tracked: u64,
}
fn managed_worktree_roots(grok_home: &Path) -> [PathBuf; 2] {
[grok_home.join("worktrees"), grok_home.join("worktree_pool")]
.map(|root| dunce::canonicalize(&root).unwrap_or(root))
}
/// True when `path` is under a managed root (`worktrees/` or `worktree_pool/`).
/// Prefer already-canonical `path`; roots are canonicalized inside.
pub fn path_under_managed_worktree_roots(path: &Path, grok_home: &Path) -> bool {
path_under_roots(path, &managed_worktree_roots(grok_home))
}
fn path_under_roots(path: &Path, roots: &[PathBuf]) -> bool {
roots.iter().any(|root| path.starts_with(root))
}
pub fn rebuild_worktree_db(
db: &crate::db::WorktreeDb,
grok_home: &Path,
@ -163,16 +178,29 @@ pub fn rebuild_worktree_db(
discovered: discovery.found.len() as u64,
..Default::default()
};
let now = now_epoch_secs();
let roots = managed_worktree_roots(grok_home);
for wt in discovery.found {
let path = dunce::canonicalize(&wt.path).unwrap_or_else(|_| wt.path.clone());
// Refuse symlink escape outside managed roots.
if !path_under_roots(&path, &roots) {
tracing::warn!(
path = %path.display(),
"rebuild skipped path outside grok worktrees/worktree_pool"
);
continue;
}
let id = id_from_path(&path);
let path_str = path.to_string_lossy();
if db.get_by_id(&id)?.is_some() || db.get(&path_str)?.is_some() {
report.already_tracked += 1;
continue;
}
db.register(&wt.into_record())?;
let mut rec = wt.into_record();
// Touch so same-pass age GC does not reclaim solely from old FS mtime.
rec.last_accessed_at = Some(now);
db.register(&rec)?;
report.registered += 1;
}
@ -327,4 +355,45 @@ mod tests {
assert_eq!(deser.registered, 3);
assert_eq!(deser.already_tracked, 2);
}
#[test]
fn rebuild_sets_last_accessed_at() {
let tmp = tempfile::TempDir::new().unwrap();
let grok_home = tmp.path();
let wt = grok_home.join("worktrees/repo/sess");
make_fake_standalone_worktree(&wt);
let db = crate::db::WorktreeDb::open_in_memory().unwrap();
rebuild_worktree_db(&db, grok_home).unwrap();
let rec = db.get(&wt.to_string_lossy()).unwrap().expect("registered");
assert!(
rec.last_accessed_at.is_some(),
"rebuild must touch last_accessed_at for same-pass age safety"
);
}
#[cfg(unix)]
#[test]
fn rebuild_skips_symlink_escape_outside_managed_roots() {
let tmp = tempfile::TempDir::new().unwrap();
let grok_home = tmp.path().join("grok");
let outside = tmp.path().join("outside-real");
make_fake_standalone_worktree(&outside);
let link_parent = grok_home.join("worktrees/repo");
std::fs::create_dir_all(&link_parent).unwrap();
std::os::unix::fs::symlink(&outside, link_parent.join("escaped")).unwrap();
let db = crate::db::WorktreeDb::open_in_memory().unwrap();
let report = rebuild_worktree_db(&db, &grok_home).unwrap();
assert_eq!(report.discovered, 1);
assert_eq!(report.registered, 0, "symlink escape must not register");
assert!(
db.list(&crate::db::ListFilter::default())
.unwrap()
.is_empty()
);
assert!(!path_under_managed_worktree_roots(
&dunce::canonicalize(&outside).unwrap(),
&grok_home
));
}
}

View file

@ -1207,7 +1207,9 @@ mod tests {
rehydrate_worktree_from_ref(&dest, &repo_path, &snap, Some("subagent-42")).unwrap();
// Filter to OUR record by path: concurrent open_default writers may add
// other subagent rows since GROK_HOME is process-global.
// other subagent rows since GROK_HOME is process-global. Match the
// canonical path register_worktree stores (/var → /private/var on macOS).
let dest_canon = dunce::canonicalize(&dest).unwrap_or_else(|_| dest.clone());
let db = crate::db::WorktreeDb::open(&fx.home).unwrap();
let mine: Vec<_> = db
.list(&crate::db::ListFilter {
@ -1216,7 +1218,7 @@ mod tests {
})
.unwrap()
.into_iter()
.filter(|r| r.path == dest)
.filter(|r| r.path == dest || r.path == dest_canon)
.collect();
assert_eq!(mine.len(), 1, "exactly one rehydrated subagent record");
assert_eq!(mine[0].kind, crate::db::WorktreeKind::Subagent);

View file

@ -9,6 +9,8 @@
//! 6. SQLite metadata tracking (behind `metadata` feature)
mod api;
#[cfg(feature = "metadata")]
mod auto_gc;
#[cfg(target_os = "linux")]
pub mod btrfs;
mod copy;
@ -31,6 +33,8 @@ pub use api::cleanup_orphaned_btrfs_snapshots;
#[cfg(target_os = "linux")]
pub use api::cleanup_orphaned_overlay_snapshots;
#[cfg(feature = "metadata")]
pub use api::gc::effective_max_age;
#[cfg(feature = "metadata")]
pub use api::gc::{GcOptions, GcReport, gc_worktrees, gc_worktrees_with_delegate};
pub use api::{
BtrfsDelegate, BtrfsMode, CleanupReport, CopyReport, CreationMode, DelegateSnapshotResult,
@ -39,6 +43,17 @@ pub use api::{
cleanup_worktrees_in_with_delegate, remove_worktree, remove_worktree_with_delegate,
};
#[cfg(feature = "metadata")]
pub use auto_gc::{
AutoGcOptions, AutoGcOutcome, AutoGcReport, DEFAULT_MAX_AGE_SECS, DEFAULT_MIN_INTERVAL_SECS,
DEFAULT_REBUILD_MIN_INTERVAL_SECS, ENV_AUTO_GC, ENV_AUTO_GC_DRY_RUN, ENV_AUTO_GC_MAX_AGE,
ENV_AUTO_GC_REBUILD, MAX_AGE_SECS_MAX, MAX_AGE_SECS_MIN, META_LAST_AUTO_GC_AT,
META_LAST_AUTO_REBUILD_AT, MIN_INTERVAL_SECS_MAX, MIN_INTERVAL_SECS_MIN,
ResolvedWorktreeAutoGc, WorktreeAutoGcLayer, age_expiry_allowed, build_auto_gc_options,
clamp_max_age_secs, clamp_min_interval_secs, default_max_age_by_kind, env_auto_gc_disabled,
env_auto_gc_dry_run, env_auto_gc_max_age, env_auto_gc_rebuild, maybe_auto_gc,
maybe_auto_gc_default, process_cwd_scan_available, resolve_worktree_auto_gc_from_layers,
};
#[cfg(feature = "metadata")]
pub use db::{
DbStats, ListFilter, WorktreeDb, WorktreeKind, WorktreeRecord, WorktreeStatus, id_from_path,
now_epoch_secs, repo_name_from_path, resolve_grok_home,