Skip to content
Merged
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
13 changes: 6 additions & 7 deletions crates/tracedecay-runtime-core/src/db/access/path_layout.rs
Original file line number Diff line number Diff line change
Expand Up @@ -46,14 +46,13 @@ pub(super) fn database_lock_root(database_path: &Path, fallback_parent: &Path) -

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
let data_root = if parent.file_name().is_some_and(|name| name == "branches")
|| (parent
.file_name()
.is_some_and(|name| name == "source-sessions.db" || name == "target-sessions.db")
.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 {
Expand Down
16 changes: 8 additions & 8 deletions src/daemon/scheduler.rs
Original file line number Diff line number Diff line change
Expand Up @@ -321,24 +321,24 @@ impl DaemonEngine {
}

pub(super) async fn shutdown_automation_schedulers_with_deadline(&self, deadline: Duration) {
let scheduler_handles: Vec<JoinHandle<()>> = match timeout(
let Ok(scheduler_handles) = timeout(
deadline,
self.store_administration.with_writer(|| async {
let mut schedulers = self
.store_administration
.automation_schedulers()
.lock()
.await;
schedulers.drain().map(|(_, handle)| handle.task).collect()
schedulers
.drain()
.map(|(_, handle)| handle.task)
.collect::<Vec<JoinHandle<()>>>()
}),
)
.await
{
Ok(handles) => handles,
Err(_) => {
log_daemon_event("daemon_shutdown", &[("outcome", "timeout".to_string())]);
return;
}
else {
log_daemon_event("daemon_shutdown", &[("outcome", "timeout".to_string())]);
return;
};
let _child_shutdown = crate::sessions::codex_app_server::begin_codex_app_server_shutdown();
for handle in &scheduler_handles {
Expand Down
3 changes: 2 additions & 1 deletion src/migrate/consolidate/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -402,6 +402,8 @@ async fn synthesized_branch_metadata_change_invalidates_confirmation_token() {

#[tokio::test]
async fn hook_analytics_append_after_plan_preserves_bytes_without_invalidating_confirmation() {
use std::io::Write;

let fixture = fixture().await;
let source = layout_for_id(&fixture.project, &fixture.profile, &fixture.source_id).unwrap();
let telemetry_path = source.data_root.join("hook_analytics.jsonl");
Expand All @@ -411,7 +413,6 @@ async fn hook_analytics_append_after_plan_preserves_bytes_without_invalidating_c
let options = fixture.options();

let planned = plan(&options).await.unwrap();
use std::io::Write;
fs::OpenOptions::new()
.append(true)
.open(&telemetry_path)
Expand Down
38 changes: 24 additions & 14 deletions src/tracedecay/lifecycle.rs
Original file line number Diff line number Diff line change
Expand Up @@ -229,11 +229,7 @@ impl TraceDecay {
// stay behind the rare paths that actually compare stores. Resolving a
// layout is on every open, including fail-closed clients that must not
// touch the store at all.
let (
candidates,
selected_manifest_matches_exact_root,
candidates_match_exact_root,
) =
let (candidates, selected_manifest_matches_exact_root, candidates_match_exact_root) =
storage::matching_legacy_profile_layouts(project_root, &profile_root, selected_id)?;
if selected.is_some()
&& !candidates.is_empty()
Expand Down Expand Up @@ -265,6 +261,9 @@ impl TraceDecay {
)
}

// The four flags mirror the resolver evidence computed by the sole
// caller; a params struct would outlive this soon-to-move code.
#[allow(clippy::fn_params_excessive_bools)]
async fn choose_identity_layout(
project_root: &Path,
selected: Option<StoreLayout>,
Expand All @@ -280,14 +279,27 @@ impl TraceDecay {
// This resolver uses bounded presence probes only; the subsequent
// serving open performs full integrity validation and fails closed.
// Legacy duplicates stay untouched, while an empty or unreadable
// selected store still reaches the fail-closed diagnostics.
// selected store still reaches the fail-closed diagnostics. An
// exact-root candidate with real content is a genuine identity
// cutover: it keeps the fail-closed conflict diagnostics instead of
// being silently outranked.
if (selected_manifest_matches_exact_root
|| (selected_via_exact_registry_alias && !candidates_match_exact_root))
&& !candidates.is_empty()
&& let Some(selected) = selected.as_ref()
&& let Some(selected_layout) = selected.as_ref()
&& store_identity_has_bounded_population_evidence(selected_layout).await
{
if store_identity_has_bounded_population_evidence(selected).await {
return Ok(Some(selected.clone()));
let mut exact_candidate_is_populated = false;
if candidates_match_exact_root {
for candidate in &candidates {
if store_identity_has_bounded_population_evidence(candidate).await {
exact_candidate_is_populated = true;
break;
Comment on lines +292 to +294

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 Treat unreadable exact candidates as conflicts

When an exact-root candidate retains real auxiliary data but its graph database is missing or corrupt, store_identity_has_bounded_population_evidence returns false immediately without checking sessions or artifacts; it likewise returns false when a populated graph has any unreadable auxiliary path. The new loop therefore classifies these non-pristine candidates as empty and returns the selected layout at line 298, bypassing the inventory-based identity-cutover conflict that would preserve fail-closed behavior. Treat probe errors as conflicting evidence, or otherwise distinguish a genuinely pristine/junk candidate from an unreadable one before taking the fast path.

Useful? React with 👍 / 👎.

}
}
}
if !exact_candidate_is_populated {
return Ok(Some(selected_layout.clone()));
}
}
if candidates.len() > 1 {
Expand Down Expand Up @@ -1567,9 +1579,8 @@ async fn store_identity_has_bounded_population_evidence(layout: &StoreLayout) ->
tree_has_files(&layout.lcm_payload_root),
tree_has_files(&layout.response_handle_root),
);
let (automation_files, payload_files, response_files) = match tree_presence {
(Ok(automation), Ok(payloads), Ok(responses)) => (automation, payloads, responses),
_ => return false,
let (Ok(automation_files), Ok(payload_files), Ok(response_files)) = tree_presence else {
return false;
};

graph_is_populated
Expand All @@ -1591,11 +1602,10 @@ fn branch_inventory(data_root: &Path) -> std::result::Result<usize, ()> {
let path = data_root.join(storage::BRANCH_META_FILENAME);
match std::fs::symlink_metadata(&path) {
Err(error) if error.kind() == std::io::ErrorKind::NotFound => Ok(0),
Err(_) => Err(()),
Ok(metadata) if metadata.file_type().is_file() => branch_meta::load_branch_meta(data_root)
.map(|meta| meta.branches.len())
.ok_or(()),
Ok(_) => Err(()),
Err(_) | Ok(_) => Err(()),
}
}

Expand Down
11 changes: 7 additions & 4 deletions tests/storage_suite/storage_resolver_test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1902,9 +1902,7 @@ async fn linked_worktree_exact_registry_alias_ignores_duplicate_shared_legacy_ma
store_kind: "code_project".to_string(),
storage_mode: "profile_sharded".to_string(),
store_relpath: format!("projects/{project_id}"),
manifest_relpath: Some(format!(
"projects/{project_id}/{STORE_MANIFEST_FILENAME}"
)),
manifest_relpath: Some(format!("projects/{project_id}/{STORE_MANIFEST_FILENAME}")),
last_verified_at: None,
last_write_at: None,
})
Expand Down Expand Up @@ -2033,7 +2031,12 @@ async fn linked_worktree_exact_manifest_overrides_canonical_exact_registry_alias

let canonical = TraceDecay::init(&project).await.unwrap();
canonical.index_all().await.unwrap();
let canonical_project_id = canonical.store_layout().identity.project_id.clone().unwrap();
let canonical_project_id = canonical
.store_layout()
.identity
.project_id
.clone()
.unwrap();
canonical.close();

git(
Expand Down
Loading