Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
64 commits
Select commit Hold shift + click to select a range
92d53ff
fix(sqlite): reserve new sidecar namespace and preserve failed files
ScriptedAlchemy Nov 8, 2026
1738c8e
fix(sqlite): reject incomplete databases on existing attach
ScriptedAlchemy Nov 8, 2026
a3c0a16
fix(sqlite): preserve aborted initialization and live worker pins
ScriptedAlchemy Nov 8, 2026
2e9b650
style(sqlite): satisfy pinned formatting checks
ScriptedAlchemy Nov 8, 2026
e6ea022
fix(sqlite): refuse short attachments without dropping POSIX locks
ScriptedAlchemy Nov 8, 2026
83d0221
fix(sqlite): reject partial header pages before descriptor pin
ScriptedAlchemy Nov 8, 2026
761cc49
fix(sqlite): reject partial-page databases before SQLite opens
ScriptedAlchemy Nov 8, 2026
9a6e93d
style(sqlite): format external lock regression
ScriptedAlchemy Nov 8, 2026
45d0ed2
test(sqlite): cover factory refusal and retry sidecar integrity
ScriptedAlchemy Nov 8, 2026
b154e6b
fix(sqlite): validate existing stores through SQLite VFS
ScriptedAlchemy Nov 8, 2026
02783ec
test(sqlite): prove rejected headers retain POSIX writer locks
ScriptedAlchemy Nov 8, 2026
935d3d4
style(sqlite): normalize lock regression imports
ScriptedAlchemy Nov 8, 2026
fcd316b
style(sqlite): satisfy pinned rustfmt for integrity tests
ScriptedAlchemy Nov 8, 2026
58d49b8
test(sqlite): preserve writer locks on valid-store admission
ScriptedAlchemy Nov 8, 2026
3e5fbc7
fix(sqlite): keep Windows sidecar imports at module scope
ScriptedAlchemy Nov 8, 2026
76ed787
test(sqlite): materialize existing-store fixtures before attachment
ScriptedAlchemy Nov 8, 2026
a4d0871
test(sqlite): initialize remaining existing-store test fixtures
ScriptedAlchemy Nov 8, 2026
7506de7
test(sqlite): distinguish incomplete namespace from absent fixture
ScriptedAlchemy Nov 8, 2026
af51d02
test(sqlite): preserve writer locks during existing-store admission
ScriptedAlchemy Nov 8, 2026
6fb6bbb
test(sqlite): keep FIFO preflight regression nonblocking
ScriptedAlchemy Nov 8, 2026
eb2573a
fix(sqlite): use path-only Linux pins to preserve writer locks
ScriptedAlchemy Nov 8, 2026
b931d06
test(runtime): exercise queued attachment with a valid store
ScriptedAlchemy Nov 8, 2026
ecfd8f6
test(automation): bind scheduler fixtures to backend identities
ScriptedAlchemy Nov 8, 2026
26311e5
fix(storage): publish complete SQLite stores after schema installation
ScriptedAlchemy Nov 8, 2026
b491915
fix(sqlite): admit first schema commits in active legacy WALs
ScriptedAlchemy Nov 8, 2026
4b399ed
fix(sqlite): compare resolved writer paths on Unix
ScriptedAlchemy Nov 8, 2026
2dc0f59
fix(storage): retire SQLite staging under the durable directory lease
ScriptedAlchemy Nov 8, 2026
06f75fb
style(sqlite): import the WAL probe dependency at module scope
ScriptedAlchemy Nov 8, 2026
918efc1
fix(sqlite): satisfy Clippy on initialization cleanup
ScriptedAlchemy Nov 9, 2026
c70df88
fix(sqlite): bind staging cleanup to pinned directory
ScriptedAlchemy Nov 9, 2026
b9621a7
fix(storage): verify the staging directory before retirement
ScriptedAlchemy Nov 9, 2026
66560cf
test(sqlite): inspect live legacy WALs across processes
ScriptedAlchemy Nov 9, 2026
8f9b5db
fix(sqlite): inspect completed handoffs without a writable lease
ScriptedAlchemy Nov 9, 2026
2ff2385
fix(storage): preserve initialization leases during scoped reset
ScriptedAlchemy Nov 9, 2026
55cc61c
fix(sqlite): retire exact graph verification marker on reset
ScriptedAlchemy Nov 9, 2026
945dde8
fix(storage): retire complete graph data during offline wipes
ScriptedAlchemy Nov 9, 2026
995cb20
fix(storage): publish only owned staged SQLite generations
ScriptedAlchemy Nov 9, 2026
e397264
fix(storage): retain SQLite leaf pins through publication
ScriptedAlchemy Nov 9, 2026
2253825
fix(storage): verify the complete pending SQLite handoff
ScriptedAlchemy Nov 9, 2026
140531b
fix(storage): resume a stopped SQLite schema installation
ScriptedAlchemy Nov 9, 2026
df991d4
fix(sqlite): verify prepared contents before recovery publication
ScriptedAlchemy Nov 9, 2026
a7f6305
style(sqlite): clarify prepared-content generation witness
ScriptedAlchemy Nov 9, 2026
a0b3709
fix(storage): recover provable interrupted SQLite transfers
ScriptedAlchemy Nov 9, 2026
f29253d
fix(storage): integrate content-bound schema recovery
ScriptedAlchemy Nov 9, 2026
ef03ad6
test(fs): retain the inode pin when allocation generations are absent
ScriptedAlchemy Nov 9, 2026
d7fe73d
fix(storage): retain SQLite reservations through worker startup
ScriptedAlchemy Nov 9, 2026
622aa10
simplify(storage): use one physical initialization entry point
ScriptedAlchemy Nov 9, 2026
90f5e7a
fix(storage): preserve other writers when Darwin attachments close
ScriptedAlchemy Nov 9, 2026
10e09e6
style(storage): group the Unix attachment test import
ScriptedAlchemy Nov 9, 2026
d8f057b
fix(storage): integrate verified reader and attachment lock lifetimes
ScriptedAlchemy Nov 9, 2026
f2d1ab2
Merge master into fix/4513-reserve-sqlite-sidecars-20261108
ScriptedAlchemy Nov 9, 2026
b6c7cd3
Merge master into fix/4513-reserve-sqlite-sidecars-20261108
ScriptedAlchemy Nov 9, 2026
b3c9629
test(sqlite): include reserved journals in reset journeys
ScriptedAlchemy Nov 9, 2026
185181f
fix(sqlite): await writer capacity to finalize prospective schema
ScriptedAlchemy Nov 9, 2026
4535896
test(sqlite): qualify unpublished stores through real admission
ScriptedAlchemy Nov 9, 2026
ebbf982
fix(sqlite): close staging readers before final WAL checkpoint
ScriptedAlchemy Nov 9, 2026
77e7658
style(sqlite): format initialization reader shutdown barrier
ScriptedAlchemy Nov 9, 2026
bc5cbc1
fix(sqlite): close publication pins before releasing handoff
ScriptedAlchemy Nov 9, 2026
714474f
style(sqlite): format initialization handoff regressions
ScriptedAlchemy Nov 9, 2026
89f84bd
fix(sqlite): claim reader shutdown ownership atomically
ScriptedAlchemy Nov 10, 2026
d80c1b6
style(sqlite): format initialization reader shutdown check
ScriptedAlchemy Nov 10, 2026
73b01ef
fix(storage): confine publication renames to pinned parents
ScriptedAlchemy Nov 10, 2026
12f5b30
fix(sqlite): refuse unverifiable worker VFS identity
ScriptedAlchemy Nov 10, 2026
b1f093e
style(sqlite): satisfy exact-head Bazel rustfmt gates
ScriptedAlchemy Nov 10, 2026
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
106 changes: 75 additions & 31 deletions crates/tracedecay-cli/src/commands/stale_store_reset.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ use tracedecay_runtime_core::storage::{

use super::storage::{
ProfileOfflineAuthority, RestoreWording, join_outcome_and_restore, remove_fixed_profile_path,
take_profile_offline,
remove_store_family, take_profile_offline,
};

const OPERATION: &str = "wipe --stale";
Expand Down Expand Up @@ -189,7 +189,7 @@ fn print_project_registrations(profile_root: &Path) -> Result<()> {

/// What a reset deletes inside a store's directory.
enum StoreMembers {
/// A database and every entry named after it.
/// The SQLite family and its registered store artifacts.
DatabaseFamily(&'static str),
/// These exact directories.
Directories(&'static [&'static str]),
Expand Down Expand Up @@ -229,34 +229,6 @@ fn store_location(
})
}

/// Removes `database` and every entry named after it in `directory`: its
/// SQLite sidecars and spools (`<name>.db…`), its host-admission directory
/// (`.<name>.db…`), and its session relation graph (`<name>.grafeo…`).
fn remove_store_family(directory: &Path, database: &str) -> Result<usize> {
let stem = database.strip_suffix(".db").unwrap_or(database);
let owned_prefix = format!("{stem}.");
let hidden_prefix = format!(".{database}.");
let entries = std::fs::read_dir(directory).map_err(|error| TraceDecayError::Config {
message: format!("failed to list '{}': {error}", directory.display()),
})?;
let mut members = Vec::new();
for entry in entries {
let entry = entry.map_err(|error| TraceDecayError::Config {
message: format!("failed to list '{}': {error}", directory.display()),
})?;
let name = entry.file_name().to_string_lossy().into_owned();
if name.starts_with(&owned_prefix) || name.starts_with(&hidden_prefix) {
members.push(name);
}
}
let mut removed = 0;
for name in &members {
removed += usize::from(remove_fixed_profile_path(directory, name)?);
verify_store_path_absent(&directory.join(name))?;
}
Ok(removed)
}

#[cfg(test)]
mod tests {
use super::*;
Expand All @@ -270,6 +242,10 @@ mod tests {
"sessions.db-wal",
"sessions.db-shm",
"sessions.grafeo",
"sessions.verified",
"user-sessions.verified",
"tracedecay.verified",
"sessions.grafeo.corruption-lock",
"tracedecay.db",
"tracedecay.db-wal",
"tracedecay.grafeo",
Expand All @@ -279,19 +255,25 @@ mod tests {
"global.db-wal",
"global.db-shm",
"profile-identity.json",
"sessions.unrelated",
".sessions.db.notes",
] {
std::fs::write(root.join(file), file).expect("write store file");
}
for dir in [
".sessions.db.host-admission",
"sessions.db.delivery-settlement-spool-v1",
"sessions.grafeo.wal",
"sessions.grafeo.spill",
"sessions.spill",
"sessions.sealed",
"code-index-v1",
] {
std::fs::create_dir(root.join(dir)).expect("create store directory");
std::fs::write(root.join(dir).join("entry"), dir).expect("write store entry");
}

assert_eq!(remove_store_family(root, "sessions.db").unwrap(), 6);
assert_eq!(remove_store_family(root, "sessions.db").unwrap(), 10);
assert_eq!(remove_store_family(root, GLOBAL_DB_FILENAME).unwrap(), 3);

let mut kept: Vec<String> = std::fs::read_dir(root)
Expand All @@ -302,17 +284,79 @@ mod tests {
assert_eq!(
kept,
[
".sessions.db.notes",
"code-index-v1",
"profile-identity.json",
"sessions.grafeo.corruption-lock",
"sessions.spill",
"sessions.unrelated",
"store_manifest.json",
"tracedecay.db",
"tracedecay.db-wal",
"tracedecay.grafeo",
"tracedecay.verified",
"user-sessions.db",
"user-sessions.verified",
]
);
}

#[test]
fn profile_sessions_reset_removes_only_its_graph_verification_marker() {
let directory = tempfile::TempDir::new().unwrap();
let root = directory.path();
for name in [
"sessions.db",
"sessions.verified",
"user-sessions.db",
"user-sessions.verified",
] {
std::fs::write(root.join(name), name).unwrap();
}

assert_eq!(
remove_store_family(root, USER_SESSIONS_DB_FILENAME).unwrap(),
2
);
assert!(!root.join("user-sessions.db").exists());
assert!(!root.join("user-sessions.verified").exists());
assert_eq!(
std::fs::read(root.join("sessions.db")).unwrap(),
b"sessions.db"
);
assert_eq!(
std::fs::read(root.join("sessions.verified")).unwrap(),
b"sessions.verified"
);
}

#[test]
fn unexpected_graph_verification_marker_kind_preserves_the_store() {
let directory = tempfile::TempDir::new().unwrap();
let root = directory.path();
std::fs::write(root.join("sessions.db"), b"store").unwrap();
let marker = root.join("sessions.verified");
std::fs::create_dir(&marker).unwrap();
std::fs::write(marker.join("foreign"), b"preserve").unwrap();

assert!(remove_store_family(root, SESSIONS_DB_FILENAME).is_err());
assert_eq!(std::fs::read(root.join("sessions.db")).unwrap(), b"store");
assert_eq!(std::fs::read(marker.join("foreign")).unwrap(), b"preserve");
}

#[test]
fn unexpected_sqlite_member_kind_preserves_the_whole_family() {
let directory = tempfile::TempDir::new().unwrap();
let root = directory.path();
std::fs::write(root.join("sessions.db"), b"main").unwrap();
let foreign = root.join("sessions.db-wal");
std::fs::create_dir(&foreign).unwrap();
std::fs::write(foreign.join("entry"), b"keep").unwrap();
assert!(remove_store_family(root, "sessions.db").is_err());
assert_eq!(std::fs::read(root.join("sessions.db")).unwrap(), b"main");
assert_eq!(std::fs::read(foreign.join("entry")).unwrap(), b"keep");
}

#[test]
fn a_store_that_is_not_resettable_on_its_own_refuses_the_whole_reset() {
let store = |label: &str, remedy: &str| StoreResetRequiredV1 {
Expand Down
159 changes: 114 additions & 45 deletions crates/tracedecay-cli/src/commands/storage.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,11 @@ use tracedecay_maintenance::retention::footprint::{
MAX_FOOTPRINT_ENTRIES, WalkTerminationV1, WalkedEntryV1, walk_regular_files_with_budget,
};
use tracedecay_runtime_core::config::{GLOBAL_DB_FILENAME, ProfileRoot};
use tracedecay_runtime_core::storage::{USER_MEMORY_DB_FILENAME, USER_SESSIONS_DB_FILENAME};
use tracedecay_runtime_core::shard_runtime::registry::lock_offline_sqlite_family_for_reset;
use tracedecay_runtime_core::storage::{
SESSIONS_DB_FILENAME, USER_MEMORY_DB_FILENAME, USER_SESSIONS_DB_FILENAME,
graph_store_data_paths,
};

use tracedecay_global_db::profile_registry_maintenance::{
ProfileRegistryMaintenanceRuntime, remove_store_directory, verify_store_path_absent,
Expand All @@ -33,27 +37,109 @@ const PROFILE_SQLITE_DATABASES: [&str; 3] = [
USER_SESSIONS_DB_FILENAME,
USER_MEMORY_DB_FILENAME,
];
const PROFILE_DATABASE_PATHS: [&str; 14] = [
"projects",
"remote",
"agent_managed",
"user-sessions.grafeo",
"user-sessions.grafeo.wal",
"user-memory.grafeo",
"user-memory.grafeo.wal",
".user-sessions.db.host-admission",
tracedecay_hooks::PROFILE_HOOK_ADMISSIONS_DIR,
tracedecay_runtime_core::storage::RESPONSE_HANDLES_DIRECTORY,
"lcm-payloads",
"hook_analytics.jsonl",
"hook_analytics.jsonl.lock",
"maintenance",
#[derive(Clone, Copy)]
enum StoreMemberKind {
File,
Directory,
}

use StoreMemberKind::{Directory, File};

const PROFILE_DATABASE_PATHS: [(&str, StoreMemberKind); 9] = [
("projects", Directory),
("remote", Directory),
("agent_managed", Directory),
(tracedecay_hooks::PROFILE_HOOK_ADMISSIONS_DIR, Directory),
(
tracedecay_runtime_core::storage::RESPONSE_HANDLES_DIRECTORY,
Directory,
),
("lcm-payloads", Directory),
("hook_analytics.jsonl", File),
("hook_analytics.jsonl.lock", File),
("maintenance", Directory),
];

fn sqlite_family_member(database: &Path, suffix: &str) -> PathBuf {
let mut member = database.as_os_str().to_os_string();
member.push(suffix);
member.into()
fn lock_offline_store_family(
directory: &Path,
database: &str,
) -> tracedecay_domain::errors::Result<Option<tracedecay_private_fs::FileLease>> {
let path = directory.join(database);
lock_offline_sqlite_family_for_reset(&path)
.map_err(|error| wipe_io("lock offline SQLite family", &path, &error))
}

/// Exact data files and directories belonging to the relational shard.
/// Sidecars and projections go before the SQLite main so an interrupted
/// reset can still rediscover its stale authority.
fn sqlite_store_data_members(database: &str) -> Vec<(PathBuf, StoreMemberKind)> {
let sessions = database == SESSIONS_DB_FILENAME || database == USER_SESSIONS_DB_FILENAME;
let mut members = Vec::new();
if sessions || database == USER_MEMORY_DB_FILENAME {
let (files, directories) =
graph_store_data_paths(&Path::new(database).with_extension("grafeo"));
members.extend(directories.into_iter().map(|path| (path, Directory)));
members.extend(files.into_iter().map(|path| (path, File)));
}
if sessions {
members.push((format!(".{database}.host-admission").into(), Directory));
members.push((
format!("{database}.delivery-settlement-spool-v1").into(),
Directory,
));
}
members.extend(
["-wal", "-shm", "-journal", ""]
.map(|suffix| (PathBuf::from(format!("{database}{suffix}")), File)),
);
members
}

/// Under the exclusive offline lease, refuse every ambiguous member before
/// the first removal, including a directory in the place of a regular file.
fn remove_checked_profile_members(
directory: &Path,
members: &[(PathBuf, StoreMemberKind)],
) -> tracedecay_domain::errors::Result<usize> {
for (name, kind) in members {
let path = directory.join(name);
tracedecay_runtime_core::storage::reject_symlink_components(&path, "offline store data")
.map_err(|error| wipe_io("validate store data", &path, &error))?;
match std::fs::symlink_metadata(&path) {
Ok(meta)
if match kind {
File => meta.is_file(),
Directory => meta.is_dir(),
} => {}
Err(error) if error.kind() == io::ErrorKind::NotFound => {}
Ok(_) => {
return Err(tracedecay_domain::errors::TraceDecayError::Config {
message: format!(
"unexpected file type for offline store data '{}'",
path.display()
),
});
}
Err(error) => return Err(wipe_io("inspect offline store data", &path, &error)),
}
}
let mut removed = 0;
for (name, _) in members {
removed += usize::from(remove_fixed_profile_path(directory, name)?);
verify_store_path_absent(&directory.join(name))?;
}
Ok(removed)
}

/// The offline lifecycle authority admits deletion of the stale store
/// itself. Similar names, private initialization data and stable coordination
/// locks are not members of its data family.
pub(super) fn remove_store_family(
directory: &Path,
database: &str,
) -> tracedecay_domain::errors::Result<usize> {
let _initialization = lock_offline_store_family(directory, database)?;
remove_checked_profile_members(directory, &sqlite_store_data_members(database))
}

fn wipe_io(
Expand Down Expand Up @@ -121,7 +207,7 @@ fn validate_complete_wipe_profile_root(

pub(super) fn remove_fixed_profile_path(
profile_root: &Path,
name: &str,
name: impl AsRef<Path>,
) -> tracedecay_domain::errors::Result<bool> {
use tracedecay_private_fs::framed_log::{DirectorySyncPolicy, sync_directory};

Expand Down Expand Up @@ -318,32 +404,15 @@ pub(crate) fn take_profile_offline(
fn wipe_complete_profile_database_state(
profile_root: &Path,
) -> tracedecay_domain::errors::Result<usize> {
let mut removed = 0usize;
for name in PROFILE_DATABASE_PATHS {
removed += usize::from(remove_fixed_profile_path(profile_root, name)?);
}
let mut leases = Vec::new();
let mut members = PROFILE_DATABASE_PATHS
.map(|(name, is_directory)| (PathBuf::from(name), is_directory))
.to_vec();
for database in PROFILE_SQLITE_DATABASES {
let database = profile_root.join(database);
for suffix in ["-wal", "-shm", "-journal", ""] {
let member = sqlite_family_member(&database, suffix);
removed += usize::from(
tracedecay_runtime_core::storage::PrivateStoreIo::remove_file_durable(&member)
.map_err(|error| {
wipe_io("remove profile SQLite family member", &member, &error)
})?,
);
}
leases.push(lock_offline_store_family(profile_root, database)?);
members.extend(sqlite_store_data_members(database));
}
for name in PROFILE_DATABASE_PATHS {
verify_store_path_absent(&profile_root.join(name))?;
}
for database in PROFILE_SQLITE_DATABASES {
let database = profile_root.join(database);
for suffix in ["-wal", "-shm", "-journal", ""] {
verify_store_path_absent(&sqlite_family_member(&database, suffix))?;
}
}
Ok(removed)
remove_checked_profile_members(profile_root, &members)
}

/// Handles the `wipe` and `wipe --all` commands.
Expand Down
Loading
Loading