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:
parent
a881e6703f
commit
3af4d5d398
556 changed files with 56609 additions and 21892 deletions
|
|
@ -56,12 +56,13 @@ xai-test-utils = { workspace = true }
|
|||
# Only used by tests behind #[cfg(feature = "metadata")] in db/tests.rs.
|
||||
rusqlite = { version = "0.37", features = ["bundled"] }
|
||||
|
||||
[target.'cfg(target_os = "linux")'.dev-dependencies]
|
||||
[target.'cfg(unix)'.dependencies]
|
||||
libc = { workspace = true }
|
||||
|
||||
[target.'cfg(target_os = "linux")'.dev-dependencies]
|
||||
nix = { version = "0.30", features = ["fs"] }
|
||||
|
||||
[target.'cfg(target_os = "linux")'.dependencies]
|
||||
libc = { workspace = true }
|
||||
nix = { version = "0.30", features = ["fs"] }
|
||||
serde = { workspace = true, features = ["derive"] }
|
||||
serde_json = { workspace = true }
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
2379
crates/codegen/xai-fast-worktree/src/auto_gc.rs
Normal file
2379
crates/codegen/xai-fast-worktree/src/auto_gc.rs
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -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>`
|
||||
|
|
|
|||
|
|
@ -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
|
||||
));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
Loading…
Reference in a new issue