Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .alfonso/release-notes/next-notes.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
- aft_zoom: when the call graph store is momentarily busy, zoom now reports the call graph as building (retry shortly), as `aft_callgraph` does, instead of unavailable.
- gh shim (manifest 19): `gh workflow disable` and `gh workflow enable` run under the operator bypass, like `run cancel`, and are refused as undeclared on older manifests.
- Backups: the daemon writes its ENGRAM backup catalog (`engram-catalog.json` in the storage root) at startup, so a fresh machine is covered without a hand-written file. It declares only what exists right now: `aft.db` (captured page by page) and the trusted-filter project list as device-local, and undo history, checkpoints, background task output, logs, diagnostics, caches and generated shims as excluded from backup. Undo history keeps its earlier entry ids, so ENGRAM records the switch to excluded on the same entries, and an id stays with its folder across restarts. The catalog is written only when the storage folder is named `aft`, since ENGRAM looks nowhere else.
- Storage on several volumes: legacy folder moves, state-file repair and filter-folder repair fall back to copying (or keep the original and say so) when a storage folder sits on another volume, retention retires a cache folder that is itself a mount point by emptying it on its own volume, and the storage migration checks free space on each destination volume.

## Sandbox profiles

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
{
"targeted_mechanism": "none",
"kind": "harness",
"slice_class": "engine_unwired",
"fixtures": [
"crates/aft/tests/integration/tool_call_parity_test.rs"
],
"fenced_files": [
"crates/aft/src/lib.rs"
],
"note": "The only fenced change is a module declaration in lib.rs for the cross-volume rename helper. Ranking and routing are unchanged; benchmark rows must remain byte-identical."
}
58 changes: 53 additions & 5 deletions crates/aft/src/backup.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2980,8 +2980,10 @@ impl BackupStore {
if harness_backups.exists() {
let _ = std::fs::remove_dir(&harness_backups);
}
match std::fs::rename(&root_backups, &harness_backups) {
Ok(()) => {
// The harness folder and the root normally share a volume, but a move
// that crosses volumes falls back to a copy instead of failing.
match crate::cross_volume::move_path(&root_backups, &harness_backups) {
Ok(_) => {
crate::slog_info!(
"moved legacy root backups into harness namespace: {}",
harness_backups.display()
Expand All @@ -2993,18 +2995,36 @@ impl BackupStore {
harness_backups.display(),
error
);
if create_private_durable_dir(&harness_backups).is_err() {
if let Err(error) = create_private_durable_dir(&harness_backups) {
crate::slog_warn!(
"legacy root backups kept in {}: cannot create {}: {}",
root_backups.display(),
harness_backups.display(),
error
);
return;
}
if let Ok(entries) = std::fs::read_dir(&root_backups) {
for entry in entries.flatten() {
let source = entry.path();
let target = harness_backups.join(entry.file_name());
if !target.exists() {
let _ = std::fs::rename(source, target);
if target.exists() {
continue;
}
if let Err(error) = crate::cross_volume::move_path(&source, &target) {
// A backup that cannot be moved stays in the root
// `backups/` folder; log the move failure instead
// of dropping it.
crate::slog_warn!(
"legacy backup {} kept in place: cannot move it to {}: {}",
source.display(),
target.display(),
error
);
}
}
}
// Removes the legacy folder only when every backup left it.
let _ = std::fs::remove_dir(&root_backups);
}
}
Expand Down Expand Up @@ -7064,6 +7084,34 @@ mod tests {
let _ = fs::remove_dir_all(&dir_b);
}

#[test]
fn legacy_root_backups_move_into_a_harness_folder_on_another_volume() {
let storage = tempfile::tempdir().unwrap();
let root_backups = storage.path().join("backups");
fs::create_dir_all(root_backups.join("session-a")).unwrap();
fs::write(root_backups.join("session-a/meta.json"), b"{}").unwrap();
let harness = crate::harness::Harness::Opencode;
let harness_dir = storage.path().join(harness.storage_segment());
fs::create_dir_all(&harness_dir).unwrap();
// Every rename from the root into the harness folder now fails with EXDEV.
let _volume =
crate::walk_boundary::simulated_volumes::simulate(&harness_dir, u64::MAX - 21);

let mut store = BackupStore::new();
store.set_storage_dir_for_harness(storage.path().to_path_buf(), harness, 72);
store.repair_root_backups_if_needed();

assert_eq!(
fs::read(harness_dir.join("backups/session-a/meta.json")).unwrap(),
b"{}",
"the backup history must reach the harness folder"
);
assert!(
!root_backups.exists(),
"the legacy folder is removed after the copy"
);
}

#[test]
fn restore_last_operation_restores_all_top_entries_for_same_op() {
let path_a = temp_file("op_restore_a.txt", "a1");
Expand Down
62 changes: 57 additions & 5 deletions crates/aft/src/bash_background/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1350,8 +1350,10 @@ pub fn repair_legacy_root_tasks(storage_root: &std::path::Path, harness: crate::
let _ = std::fs::remove_dir(&harness_tasks);
}

match std::fs::rename(&root_tasks, &harness_tasks) {
Ok(()) => crate::slog_info!(
// The harness folder and the root normally share a volume, but a move
// that crosses volumes falls back to a copy instead of failing.
match crate::cross_volume::move_path(&root_tasks, &harness_tasks) {
Ok(_) => crate::slog_info!(
"moved legacy root bash tasks into harness namespace: {}",
harness_tasks.display()
),
Expand All @@ -1361,18 +1363,36 @@ pub fn repair_legacy_root_tasks(storage_root: &std::path::Path, harness: crate::
harness_tasks.display(),
error
);
if crate::private_storage::create_dir_all(&harness_tasks).is_err() {
if let Err(error) = crate::private_storage::create_dir_all(&harness_tasks) {
crate::slog_warn!(
"legacy root bash tasks kept in {}: cannot create {}: {}",
root_tasks.display(),
harness_tasks.display(),
error
);
return;
}
if let Ok(entries) = std::fs::read_dir(&root_tasks) {
for entry in entries.flatten() {
let source = entry.path();
let target = harness_tasks.join(entry.file_name());
if !target.exists() {
let _ = std::fs::rename(source, target);
if target.exists() {
continue;
}
if let Err(error) = crate::cross_volume::move_path(&source, &target) {
// A task that cannot be moved stays readable in the
// root `bash-tasks/` folder; log the move failure
// instead of dropping it.
crate::slog_warn!(
"legacy bash task {} kept in place: cannot move it to {}: {}",
source.display(),
target.display(),
error
);
}
}
}
// Removes the legacy folder only when every task left it.
let _ = std::fs::remove_dir(&root_tasks);
}
}
Expand Down Expand Up @@ -1624,3 +1644,35 @@ mod shell_resolution_tests {
);
}
}

#[cfg(test)]
mod legacy_root_task_repair_tests {
use std::fs;

use crate::harness::Harness;
use crate::walk_boundary::simulated_volumes::simulate;

#[test]
fn legacy_root_tasks_move_into_a_harness_folder_on_another_volume() {
let storage = tempfile::tempdir().unwrap();
let root_tasks = storage.path().join("bash-tasks");
fs::create_dir_all(root_tasks.join("task-1")).unwrap();
fs::write(root_tasks.join("task-1/meta.json"), b"{\"id\":1}").unwrap();
let harness_dir = storage.path().join(Harness::Opencode.storage_segment());
fs::create_dir_all(&harness_dir).unwrap();
// Every rename from the root into the harness folder now fails with EXDEV.
let _volume = simulate(&harness_dir, u64::MAX - 20);

super::repair_legacy_root_tasks(storage.path(), Harness::Opencode);

assert_eq!(
fs::read(harness_dir.join("bash-tasks/task-1/meta.json")).unwrap(),
b"{\"id\":1}",
"the task must reach the harness folder"
);
assert!(
!root_tasks.exists(),
"the legacy folder is removed after the copy"
);
}
}
55 changes: 43 additions & 12 deletions crates/aft/src/callgraph_store/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12485,24 +12485,29 @@ fn sweep_orphaned_build_temps_store_wide(callgraph_dir: &Path) {
};
let mut skipped_foreign_mounts = 0usize;

// Root-keyed layout: every `<storage>/callgraph/<key>` directory.
// Root-keyed layout: every `<storage>/callgraph/<key>` directory. The
// callgraph folder may be a separate volume mounted under the storage
// root, so its children are compared against the callgraph folder's own
// device. Skipping that whole folder as "foreign" would leave every other
// key's orphaned build temporaries on it forever.
let root_keyed_dir = storage_root.join(domain);
if root_keyed_dir.is_dir() {
if boundary.should_descend(&root_keyed_dir).unwrap_or(false) {
if let Ok(entries) = std::fs::read_dir(&root_keyed_dir) {
for entry in entries.flatten() {
let path = entry.path();
if path.is_dir() {
if boundary.should_descend(&path).unwrap_or(false) {
sweep_orphaned_build_temps(&path);
} else {
skipped_foreign_mounts += 1;
match crate::walk_boundary::DeviceBoundary::for_root(&root_keyed_dir) {
Ok(domain_boundary) => {
if let Ok(entries) = std::fs::read_dir(&root_keyed_dir) {
for entry in entries.flatten() {
let path = entry.path();
if path.is_dir() {
if domain_boundary.should_descend(&path).unwrap_or(false) {
sweep_orphaned_build_temps(&path);
} else {
skipped_foreign_mounts += 1;
}
}
}
}
}
} else {
skipped_foreign_mounts += 1;
Err(_) => skipped_foreign_mounts += 1,
}
}

Expand Down Expand Up @@ -24954,6 +24959,32 @@ mod cold_build_insert_tests {
);
}

#[test]
fn orphan_temp_sweep_reaches_a_callgraph_folder_mounted_from_another_volume() {
let storage = tempdir().unwrap();
let storage_root = storage.path();
let callgraph = storage_root.join("callgraph");
let dead_dir = callgraph.join("deadkey");
fs::create_dir_all(&dead_dir).unwrap();
let orphan = "deadkey.g100.1.sqlite.tmp.1.200";
write_build_temp_with_age(
&dead_dir,
orphan,
ORPHANED_BUILD_TEMP_MIN_AGE + Duration::from_secs(60),
);
let live_dir = callgraph.join("livekey");
fs::create_dir_all(&live_dir).unwrap();
// The callgraph folder is a separate volume from the storage root.
let _volume = crate::walk_boundary::simulated_volumes::simulate(&callgraph, u64::MAX - 40);

sweep_orphaned_build_temps_store_wide(&live_dir);

assert!(
!dead_dir.join(orphan).exists(),
"another key's orphan on the mounted callgraph folder must be reclaimed"
);
}

#[test]
fn orphan_temp_sweep_negative_control_age_predicate_is_what_spares_fresh() {
// NEGATIVE CONTROL, mutation-proved: forcing the age predicate to accept
Expand Down
50 changes: 46 additions & 4 deletions crates/aft/src/commands/state.rs
Original file line number Diff line number Diff line change
Expand Up @@ -320,12 +320,32 @@ fn repair_root_scoped_harness_file(
}

if let Some(parent) = harness_path.parent() {
if crate::private_storage::create_dir_all(parent).is_err() {
return harness_path;
if let Err(error) = crate::private_storage::create_dir_all(parent) {
crate::slog_warn!(
"keeping {} at the storage root: cannot create {}: {}",
root_path.display(),
parent.display(),
error
);
return root_path;
}
}
// The harness folder and the root normally share a volume, but a move
// that crosses volumes falls back to a copy instead of failing. If the
// move still fails, keep using the root file rather than pointing at a
// harness path that holds nothing.
match crate::cross_volume::move_path(&root_path, &harness_path) {
Ok(_) => harness_path,
Err(error) => {
crate::slog_warn!(
"keeping {} at the storage root: cannot move it to {}: {}",
root_path.display(),
harness_path.display(),
error
);
root_path
}
}
let _ = fs::rename(root_path, &harness_path);
harness_path
}

fn legacy_host_path(ctx: &AppContext, key: &str) -> Option<PathBuf> {
Expand Down Expand Up @@ -451,4 +471,26 @@ mod tests {
let path = temp.path().join("trusted-filter-projects.json");
assert_eq!(std::fs::read_to_string(&path).unwrap(), "[]");
}

#[test]
fn root_state_file_moves_into_a_harness_folder_on_another_volume() {
let temp = tempfile::tempdir().unwrap();
let ctx = test_context();
ctx.update_config(|config| {
config.storage_dir = Some(temp.path().to_path_buf());
});
std::fs::write(temp.path().join("last_announced_version"), "0.40.0").unwrap();
let harness_dir = temp.path().join("opencode");
std::fs::create_dir_all(&harness_dir).unwrap();
// Every rename from the root into the harness folder now fails with EXDEV.
let _volume =
crate::walk_boundary::simulated_volumes::simulate(&harness_dir, u64::MAX - 22);

let path = legacy_harness_path(&ctx, Harness::Opencode, "last_announced_version")
.expect("known key");

assert_eq!(path, harness_dir.join("last_announced_version"));
assert_eq!(std::fs::read_to_string(&path).unwrap(), "0.40.0");
assert!(!temp.path().join("last_announced_version").exists());
}
}
35 changes: 34 additions & 1 deletion crates/aft/src/compress/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1544,7 +1544,18 @@ pub(crate) fn repair_legacy_user_filter_dir(storage_dir: &Path, harness: Harness
if target.exists() {
continue;
}
let _ = fs::rename(entry.path(), target);
// The harness folder and the root normally share a volume, but a move
// that crosses volumes falls back to a copy instead of failing.
if let Err(error) = crate::cross_volume::move_path(&entry.path(), &target) {
// A filter that cannot be moved stays in the root `filters/`
// folder; log the move failure instead of dropping it.
crate::slog_warn!(
"legacy filter {} kept in place: cannot move it to {}: {}",
entry.path().display(),
target.display(),
error
);
}
}

if fs::read_dir(&legacy_dir)
Expand Down Expand Up @@ -1661,6 +1672,28 @@ mod tests {
);
assert!(!storage.join("filters/root-only.toml").exists());
}

#[test]
fn repair_legacy_user_filter_dir_moves_filters_into_a_harness_folder_on_another_volume() {
let temp = tempfile::tempdir().unwrap();
let storage = temp.path();
fs::create_dir_all(storage.join("filters")).unwrap();
fs::create_dir_all(storage.join("opencode/filters")).unwrap();
fs::write(storage.join("filters/root-only.toml"), "root").unwrap();
// Every rename from the root into the harness folder now fails with EXDEV.
let _volume = crate::walk_boundary::simulated_volumes::simulate(
&storage.join("opencode"),
u64::MAX - 23,
);

repair_legacy_user_filter_dir(storage, Harness::Opencode);

assert_eq!(
fs::read_to_string(storage.join("opencode/filters/root-only.toml")).unwrap(),
"root"
);
assert!(!storage.join("filters").exists());
}
}

#[cfg(test)]
Expand Down
Loading
Loading