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
2 changes: 1 addition & 1 deletion crates/tracedecay-global-db/src/observation/schema.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ const OBSERVATION_UNIFIED_IDENTITY_MIGRATION: &str = "observations-unified-ident
/// observation identity. The store's other authorities stay admissible; its
/// session features are refused until the store is reset.
pub(crate) const OBSERVATIONS_PREDATE_UNIFIED_IDENTITY: crate::registered::RefusedAuthorityV1 =
crate::registered::RefusedAuthorityV1 {
crate::registered::RefusedAuthorityV1::Shape {
authority: "observations",
reason: "observation rows predate the unified observation identity and cannot be read; reset the profile so ingestion can rebuild them from host transcripts",
};
Expand Down
68 changes: 51 additions & 17 deletions crates/tracedecay-global-db/src/registered.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ use std::future::Future;
use std::path::Path;
use std::sync::{Arc, OnceLock, RwLock, Weak};

use crate::schema_stages::RegisteredSchemaAttachmentV1;
use tracedecay_domain::errors::TraceDecayError;
use tracedecay_runtime_core::{
db::{
Expand Down Expand Up @@ -32,18 +33,38 @@ type SessionRelationGraphStateV1 = RwLock<
)>,
>;

/// An authority inside an admitted store whose persisted rows this binary
/// refuses to read. The store serves its other authorities; every feature
/// that reads the refused one gets [`Self::error`] until the store is reset.
/// A session authority inside an admitted store whose persisted shape this
/// binary refuses to read. The store serves its other authorities; every
/// session feature gets [`Self::error`] until the store is reset.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub(crate) struct RefusedAuthorityV1 {
pub(crate) authority: &'static str,
pub(crate) reason: &'static str,
pub(crate) enum RefusedAuthorityV1 {
/// Rows or tables whose shape nothing converts.
Shape {
authority: &'static str,
reason: &'static str,
},
/// A recorded schema version other than the one this binary writes.
Version {
component: &'static str,
found_version: Option<i64>,
required_version: i64,
},
}

impl RefusedAuthorityV1 {
pub(crate) fn error(self) -> TraceDecayError {
TraceDecayError::reset_required(self.authority, self.reason)
match self {
Self::Shape { authority, reason } => TraceDecayError::reset_required(authority, reason),
Self::Version {
component,
found_version,
required_version,
} => TraceDecayError::ProfileResetRequired {
component,
found_version,
required_version,
},
}
}
}

Expand Down Expand Up @@ -98,12 +119,17 @@ impl RegisteredGlobalDbOwnerV1 {
) -> tracedecay_domain::errors::Result<Self> {
let temporary = database.issue_lease().map_err(registered_owner_error)?;
let registered = RegisteredGlobalDb::from_owned_database(temporary);
let (_, refused_authority) =
super::schema_stages::ensure_attached_registered_schema(&registered.database).await?;
// A store refused for reset is never converged: its reset deletes it.
if refused_authority.is_none() {
super::schema_stages::converge_attached_registered_schema(&registered.database).await?;
}
let refused_authority =
match super::schema_stages::ensure_attached_registered_schema(&registered.database)
.await?
{
RegisteredSchemaAttachmentV1::Admitted(_) => {
super::schema_stages::converge_attached_registered_schema(&registered.database)
.await?;
None
}
RegisteredSchemaAttachmentV1::SessionsRefused(refused) => Some(refused),
};
drop(registered);
Ok(Self {
database,
Expand All @@ -114,16 +140,24 @@ impl RegisteredGlobalDbOwnerV1 {
}

/// Returns the resumable convergence plan for an already admitted schema
/// without retaining an unowned client lease.
/// without retaining an unowned client lease. A store admitted in its
/// typed reset-required state has no plan: its reset deletes it.
#[hotpath::measure(future = true, label = "global_db.registered.admit_daemon")]
pub async fn admit_and_attach_for_daemon(
database: DatabaseOwnerV1,
) -> tracedecay_domain::errors::Result<(Self, super::schema_stages::RegisteredSchemaConvergence)>
{
) -> tracedecay_domain::errors::Result<(
Self,
Option<super::schema_stages::RegisteredSchemaConvergence>,
)> {
let temporary = database.issue_lease().map_err(registered_owner_error)?;
let registered = RegisteredGlobalDb::from_owned_database(temporary);
let (convergence, refused_authority) =
super::schema_stages::ensure_attached_registered_schema(&registered.database).await?;
match super::schema_stages::ensure_attached_registered_schema(&registered.database)
.await?
{
RegisteredSchemaAttachmentV1::Admitted(convergence) => (Some(convergence), None),
RegisteredSchemaAttachmentV1::SessionsRefused(refused) => (None, Some(refused)),

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 Block remote replay for refused session stores

When a project store hits one of the newly softened LCM/workflow/git refusals, this still constructs a normal mounted owner. mount_project_session_store then unconditionally registers its weak issuer as a remote-replay target, while issue_target_lease never checks reset_required() before dispatching RemoteObservationReplay. A remote capture arriving before the operator resets the store can therefore receive a committed receipt and be written into a database that wipe --stale subsequently deletes, even though ordinary session calls correctly refuse the same store. Keep refused owners out of the replay/fence target or enforce the refusal when issuing replay leases.

AGENTS.md reference: AGENTS.md:L209-L211

Useful? React with 👍 / 👎.

};
drop(registered);
Ok((
Self {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,8 @@ use tracedecay_runtime_core::db::TestDatabaseRuntimeScope;
use crate::tests::harness::open_registered_test_database_fixture;

/// A store recorded at the whole-projection Git evidence schema (version 5)
/// is refused with the typed reset and left byte-for-byte untouched: nothing
/// converts or copies the older shape.
/// is admitted in its typed reset-required state and left byte-for-byte
/// untouched: nothing converts or copies the older shape.
#[tokio::test]
async fn an_older_git_correlation_schema_is_refused_without_mutation() {
crate::register_registered_schema_installer();
Expand All @@ -33,19 +33,23 @@ async fn an_older_git_correlation_schema_is_refused_without_mutation() {
.unwrap();
let before = fs::read(&database_path).unwrap();

let error = match open_registered_test_database_fixture(&database_path, scope()).await {
Ok(_) => panic!("an older Git correlation schema must not be admitted"),
Err(error) => error,
};

assert!(matches!(
error,
TraceDecayError::ProfileResetRequired {
component: "git correlation",
found_version: Some(5),
required_version: 6,
}
));
let (lease, owner) = open_registered_test_database_fixture(&database_path, scope())
.await
.expect("the store's other authorities stay admissible");
for refusal in [lease.reset_required(), owner.reset_required()] {
assert!(
matches!(
refusal,
Some(TraceDecayError::ProfileResetRequired {
component: "git correlation",
found_version: Some(5),
required_version: 6,
})
),
"an older Git correlation schema refuses session features: {refusal:?}"
);
}
drop((lease, owner));
assert_eq!(
fs::read(&database_path).unwrap(),
before,
Expand Down
28 changes: 15 additions & 13 deletions crates/tracedecay-global-db/src/registered/workflow_schema_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -56,25 +56,27 @@ async fn assert_workflow_schema_reset_without_mutation(malformed_schema: String)
drop(connection);
drop(database);

let error = match open_registered_test_database_fixture(
let (lease, owner) = open_registered_test_database_fixture(
&database_path,
TestDatabaseRuntimeScope::ProjectSessions {
project_id: ProjectId::new("project.workflow-schema").unwrap(),
},
)
.await
{
Ok(_) => panic!("malformed workflow schema must not be completed"),
Err(error) => error,
};

assert!(matches!(
error,
TraceDecayError::ResetRequired {
ref authority,
..
} if authority == "workflow"
));
.expect("the store's other authorities stay admissible");
for refusal in [lease.reset_required(), owner.reset_required()] {
assert!(
matches!(
refusal,
Some(TraceDecayError::ResetRequired {
ref authority,
..
}) if authority == "workflow"
),
"a malformed workflow schema refuses session features: {refusal:?}"
);
}
drop((lease, owner));
assert_eq!(
fs::read(&database_path).unwrap(),
before_bytes,
Expand Down
Loading
Loading