Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
850414b
fix(migrate): authorize staged databases
HashemKhalifa Aug 8, 2026
a556506
fix(db): restrict consolidation authority
HashemKhalifa Aug 8, 2026
ab7be60
perf(storage): avoid full counts on exact roots
HashemKhalifa Aug 10, 2026
133ca5c
perf(storage): trust healthy exact root among duplicates
HashemKhalifa Aug 10, 2026
ac0b0c9
perf(storage): check the serving branch graph
HashemKhalifa Aug 10, 2026
726ce54
perf(mcp): reuse active selected project
HashemKhalifa Aug 10, 2026
25b67d1
perf(session): bound correlation presence checks
HashemKhalifa Aug 11, 2026
caba6eb
fix(daemon): bound maintenance shutdown
HashemKhalifa Aug 11, 2026
fa93535
fix(daemon): abort watchers before joins
HashemKhalifa Aug 11, 2026
a299410
perf(storage): bound conflict inventory
HashemKhalifa Aug 11, 2026
c8dd768
docs(changeset): cover bounded inventory
HashemKhalifa Aug 11, 2026
6e92ed4
fix(storage): fail closed on unreadable inventory
HashemKhalifa Aug 11, 2026
2467ff9
perf(storage): avoid duplicate recovery scans
HashemKhalifa Aug 11, 2026
82876ea
fix(storage): isolate branch recovery markers
HashemKhalifa Aug 11, 2026
043a047
fix(storage): reject empty recovery stores
HashemKhalifa Aug 11, 2026
71b19fc
fix(storage): harden recovery validation
HashemKhalifa Aug 11, 2026
a818fcd
fix(storage): validate auxiliary health first
HashemKhalifa Aug 11, 2026
a79f70c
fix(storage): trust exact worktree registry aliases
HashemKhalifa Aug 12, 2026
df96e9b
fix(storage): preserve exact-root precedence
HashemKhalifa Aug 12, 2026
91944bc
fix(storage): honor aliases with repository markers
HashemKhalifa Aug 12, 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
5 changes: 5 additions & 0 deletions .changeset/bounded-daemon-maintenance-shutdown.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"tracedecay": patch
---

Keep daemon shutdown bounded while background maintenance is active, use lightweight read-only presence probes during exact-root store selection and identity-conflict reporting, and avoid repeating successful full integrity scans during crash recovery.
5 changes: 5 additions & 0 deletions .changeset/consolidation-maintenance-telemetry.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"tracedecay": patch
---

Authorize migration-owned staged session databases under the exclusive maintenance scope, and omit only volatile hook analytics from confirmation fingerprints while preserving its bytes in backups and consolidated artifacts.
5 changes: 5 additions & 0 deletions .changeset/exact-root-inventory-fast-path.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"tracedecay": patch
---

Avoid redundant same-project selector opens and full session-table row counts while resolving a healthy exact-root project store, including branch-scoped graphs and preserved duplicate manifests, keeping graph calls responsive when session history lives on slower storage.
5 changes: 5 additions & 0 deletions .changeset/session-correlation-presence-fast-path.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"tracedecay": patch
---

Keep `tracedecay_sessions_for` responsive on large session stores by using bounded row-presence probes for empty-index reporting instead of full correlation-table counts.
4 changes: 4 additions & 0 deletions crates/tracedecay-migrate/src/consolidate/files.rs
Original file line number Diff line number Diff line change
Expand Up @@ -214,6 +214,10 @@ pub(super) fn is_sqlite_sidecar(relative: &Path) -> bool {
.is_some_and(|value| value.ends_with("-shm") || value.ends_with("-wal"))
}

pub(super) fn is_volatile_hook_telemetry(relative: &Path) -> bool {
relative == Path::new("hook_analytics.jsonl")
}

pub(super) fn is_sqlite_database(relative: &Path) -> bool {
relative
.extension()
Expand Down
10 changes: 8 additions & 2 deletions crates/tracedecay-migrate/src/consolidate/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ pub use files::{
};
use files::{
excluded_source_artifact, is_coordination_lock, is_reference_artifact, is_runtime_lock,
is_sqlite_database, is_sqlite_sidecar, tree_stats,
is_sqlite_database, is_sqlite_sidecar, is_volatile_hook_telemetry, tree_stats,
};
use finalize::{cut_over_markers, register_destination, verify_destination};
use preflight::{acquire_store_locks, ensure_profile_offline, preflight_disk_space};
Expand Down Expand Up @@ -993,7 +993,13 @@ fn fingerprint_inputs(
}
}
for (relative, path) in files {
if is_runtime_lock(&relative) || is_sqlite_sidecar(&relative) {
if is_runtime_lock(&relative)
|| is_sqlite_sidecar(&relative)
|| is_volatile_hook_telemetry(&relative)
{
// Hooks may append this telemetry between dry-run and apply. It is
// omitted only from the confirmation fingerprint; backup and
// artifact merge still copy and checksum every telemetry byte.
Comment on lines +996 to +1002

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Reconcile volatile telemetry when resuming consolidation

If consolidation is interrupted after DestinationReady or ArtifactsMerged and a hook then appends to either input's hook_analytics.jsonl, excluding this file keeps the old confirmation token valid on retry. The ledger resumes after the relevant artifact-copy phase, so the prepared destination is not refreshed from the target and an already-completed source artifact merge is not rerun; the newly appended rows are therefore absent from the consolidated store after marker cutover even though the retry succeeds. Keep the token stable if desired, but resumptions must recopy or append-merge this volatile file before cutover rather than skipping change detection without reconciling it.

Useful? React with 👍 / 👎.

continue;
}
hash.update(relative.to_string_lossy().as_bytes());
Expand Down
8 changes: 8 additions & 0 deletions crates/tracedecay-runtime-core/src/db/access/path_layout.rs
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,14 @@ fn profile_project_root(database_path: &Path) -> Option<&Path> {
let parent = database_path.parent()?;
let data_root = if parent.file_name().is_some_and(|name| name == "branches") {
parent.parent()?
} else if parent
.file_name()
.is_some_and(|name| name == ".consolidation-input")
&& database_path
.file_name()
.is_some_and(|name| name == "source-sessions.db" || name == "target-sessions.db")
{
parent.parent()?
} else {
parent
};
Expand Down
65 changes: 65 additions & 0 deletions crates/tracedecay-runtime-core/src/db/access/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -794,6 +794,71 @@ fn maintenance_scope_requires_and_inherits_exclusive_profile_lease() {
drop(lifecycle);
}

#[test]
fn consolidation_input_session_databases_inherit_profile_maintenance_scope() {
let _lock = SCOPE_TEST_LOCK.lock().unwrap();
let temp = tempfile::tempdir().unwrap();
let expected_profile = platform_identity_key(&temp.path().canonicalize().unwrap());
let expected_lock_parent = expected_profile.join(".tracedecay-database-locks");
let lifecycle = crate::lifecycle_lease::acquire_exclusive_for_profile(
temp.path(),
"consolidation maintenance test",
)
.unwrap();
let scope =
enter_maintenance_database_scope(&lifecycle, temp.path(), "consolidation maintenance test")
.unwrap();

for file_name in ["source-sessions.db", "target-sessions.db"] {
let path = temp
.path()
.join("projects/p1/.consolidation-input")
.join(file_name);
let identity = DatabaseIdentity::for_path(&path).unwrap();
assert_eq!(
identity.profile_root,
expected_profile,
"{}",
path.display()
);
assert_eq!(
identity.access_lock_path.parent(),
Some(expected_lock_parent.as_path()),
"{}",
path.display()
);
assert!(
!identity.allows_ambient_profile_scope,
"{} must require its exact profile authority",
path.display()
);
let authority =
DatabaseAuthority::for_runtime(&path, "merge consolidation sessions").unwrap();
assert_eq!(authority.role(), DatabaseAuthorityRole::Maintenance);
}

let arbitrary = temp
.path()
.join("projects/p1/.consolidation-input/arbitrary.db");
let arbitrary_identity = DatabaseIdentity::for_path(&arbitrary).unwrap();
assert_ne!(arbitrary_identity.profile_root, expected_profile);
let arbitrary_authority =
DatabaseAuthority::for_runtime(&arbitrary, "unowned consolidation input").unwrap();
assert_eq!(arbitrary_authority.role(), DatabaseAuthorityRole::Test);

let unrelated = temp
.path()
.join("projects/p1/other-input/source-sessions.db");
let unrelated_identity = DatabaseIdentity::for_path(&unrelated).unwrap();
assert_ne!(unrelated_identity.profile_root, expected_profile);
let unrelated_authority =
DatabaseAuthority::for_runtime(&unrelated, "unrelated nested database").unwrap();
assert_eq!(unrelated_authority.role(), DatabaseAuthorityRole::Test);

drop(scope);
drop(lifecycle);
}

#[test]
fn daemon_scopes_are_isolated_by_profile() {
let _lock = SCOPE_TEST_LOCK.lock().unwrap();
Expand Down
88 changes: 86 additions & 2 deletions crates/tracedecay-runtime-core/src/db/connection.rs
Original file line number Diff line number Diff line change
Expand Up @@ -105,13 +105,36 @@ impl Database {
/// Returns `(Self, migrated)` where `migrated` is `true` if schema
/// migrations were applied during open.
pub async fn open(db_path: &Path, authority: &DatabaseAuthority) -> Result<(Self, bool)> {
Self::open_inner(db_path, authority, false).await
}

/// Opens an existing database while revalidating a cached writable
/// connection before reuse. Dirty-store recovery uses this entry point so
/// an in-process handle cannot bypass validation of the on-disk recovery
/// set before its marker is cleared.
#[doc(hidden)]
pub async fn open_revalidating_cached(
db_path: &Path,
authority: &DatabaseAuthority,
) -> Result<(Self, bool)> {
Self::open_inner(db_path, authority, true).await
}

async fn open_inner(
db_path: &Path,
authority: &DatabaseAuthority,
revalidate_cached: bool,
) -> Result<(Self, bool)> {
let authority = authority.hold_for(db_path, "open")?;
let slot = database_slot(authority.canonical_database_path());
let mut open = slot.lock().await;
if let Some(inner) = open.upgrade() {
if !inner.writable {
return Err(integrity::read_only_upgrade_error(db_path, "open"));
}
if revalidate_cached {
integrity::validate_read_only(db_path).await?;
}
return Ok((Self { inner }, false));
}
let is_fresh = std::fs::metadata(db_path)
Expand Down Expand Up @@ -164,6 +187,30 @@ impl Database {
db_path: &Path,
authority: &DatabaseAuthority,
) -> Result<(Self, bool)> {
let database = Self::open_read_only_inner(db_path, authority, true).await?;
Ok((database, false))
}

/// Opens an existing database for a bounded read-only presence probe.
///
/// This validates the SQLite header and applies read-only PRAGMAs, but it
/// deliberately does not run `PRAGMA quick_check`: callers must issue only
/// bounded queries such as `SELECT 1 ... LIMIT 1`. A subsequent serving
/// open still performs the normal full integrity validation and fails
/// closed before exposing data.
#[doc(hidden)]
pub async fn open_read_only_for_presence_probe(
db_path: &Path,
authority: &DatabaseAuthority,
) -> Result<Self> {
Self::open_read_only_inner(db_path, authority, false).await
}

async fn open_read_only_inner(
db_path: &Path,
authority: &DatabaseAuthority,
validate_integrity: bool,
) -> Result<Self> {
let authority = authority.hold_for(db_path, "open_read_only")?;
integrity::validate_sqlite_header(db_path, "open_read_only", false)?;
let db = Builder::new_local(db_path)
Expand All @@ -182,7 +229,9 @@ impl Database {

let file_size = std::fs::metadata(db_path).map_or(0, |m| m.len());
pragmas::apply_read_only(&conn, file_size).await?;
integrity::validate(&conn, "open_read_only").await?;
if validate_integrity {
integrity::validate(&conn, "open_read_only").await?;
}

let inner = Arc::new(DatabaseInner {
conn,
Expand All @@ -191,7 +240,7 @@ impl Database {
_authority: authority,
_slot: None,
});
Ok((Self { inner }, false))
Ok(Self { inner })
}

/// Returns a reference to the underlying libsql connection.
Expand Down Expand Up @@ -536,6 +585,41 @@ mod tests {
assert_eq!(platform_safe_mmap_size(0), 0);
}

#[tokio::test]
async fn presence_probe_open_is_read_only_without_full_integrity_validation() {
let temp = tempfile::tempdir().unwrap();
let path = temp.path().join("graph.db");
let authority = DatabaseAuthority::acquire_test(&path, "presence probe").unwrap();
let (seed, _) = Database::initialize(&path, &authority).await.unwrap();
seed.conn()
.execute("CREATE TABLE probe_rows (id INTEGER)", ())
.await
.unwrap();
seed.conn()
.execute("INSERT INTO probe_rows (id) VALUES (1)", ())
.await
.unwrap();
drop(seed);

let probe = Database::open_read_only_for_presence_probe(&path, &authority)
.await
.unwrap();
let mut rows = probe
.conn()
.query("SELECT 1 FROM probe_rows LIMIT 1", ())
.await
.unwrap();
assert!(rows.next().await.unwrap().is_some());
assert!(
probe
.conn()
.execute("INSERT INTO probe_rows (id) VALUES (2)", ())
.await
.is_err(),
"presence probes must remain read-only"
);
}

#[tokio::test]
async fn repeated_authorized_opens_share_one_physical_connection() {
let temp = tempfile::tempdir().unwrap();
Expand Down
Loading