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
Original file line number Diff line number Diff line change
Expand Up @@ -758,20 +758,6 @@ impl ExtendedPrimitivePort for TraceDecayExtendedPrimitivePortV1 {
if !matches!(current.coverage, DiagnosticQueryCoverage::Complete) {
return diagnostics_unavailable(finished_at, OmissionReason::Unavailable);
}
let Some(identity) = self
.diagnostic_identity
.resolve(self.source_runtime.project_root().to_path_buf())
.await
else {
return diagnostics_unavailable(finished_at, OmissionReason::Unavailable);
};
let scope = context.request.scope();
if identity.repository() != &scope.repository_id
|| identity.worktree() != Some(&scope.worktree_id)
|| identity.reference() != scope.reference.as_ref()
{
return diagnostics_unavailable(finished_at, OmissionReason::Stale);
}
let document_path = match &request.scope {
super::super::runtime::DiagnosticsPrimitiveScope::Workspace => None,
super::super::runtime::DiagnosticsPrimitiveScope::File(path) => {
Expand All @@ -784,12 +770,6 @@ impl ExtendedPrimitivePort for TraceDecayExtendedPrimitivePortV1 {
OmissionReason::Unavailable,
);
};
if identity.file(&path).is_none() {
return diagnostics_unavailable(
finished_at,
OmissionReason::Unavailable,
);
}
Some(path)
}
super::super::runtime::DiagnosticsPrimitiveScope::Package(_) => {
Expand All @@ -809,9 +789,32 @@ impl ExtendedPrimitivePort for TraceDecayExtendedPrimitivePortV1 {
return diagnostics_unavailable(finished_at, OmissionReason::Unavailable);
}
};
if current_index.code_generation_id != *identity.generation_id() {
if current_index.code_generation_id != current_generation {
return diagnostics_unavailable(finished_at, OmissionReason::Stale);
}
// The code index retains the published generation; an identity
// it cannot resolve is one whose source proof moved and is
// being renewed, so the publication is stale, not absent.
let Some(identity) = self
.diagnostic_identity
.resolve(self.source_runtime.project_root().to_path_buf())
.await
else {
return diagnostics_unavailable(finished_at, OmissionReason::Stale);
};
let scope = context.request.scope();
if identity.repository() != &scope.repository_id
|| identity.worktree() != Some(&scope.worktree_id)
|| identity.reference() != scope.reference.as_ref()
{
return diagnostics_unavailable(finished_at, OmissionReason::Stale);
}
if document_path
.as_deref()
.is_some_and(|path| identity.file(path).is_none())
{
return diagnostics_unavailable(finished_at, OmissionReason::Unavailable);
}
if current_generation != *identity.generation_id() {
return diagnostics_unavailable(finished_at, OmissionReason::Stale);
}
Expand Down
14 changes: 12 additions & 2 deletions crates/tracedecay-application/src/primitives/symbol_graph.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1324,6 +1324,16 @@ async fn complete_or_failed<T: Send>(
page.related_edge_count = related_edge_count;
page.support_gaps = gaps;
let receipt = graph.cost.receipt();
let Some(finished_at) = i64::try_from(receipt.wall_micros)
.ok()
.and_then(|wall| context.observed_at.0.checked_add(wall))
.map(UtcMicros)
else {
return failed(
context,
"graph read end time is outside the timestamp domain",
);
};
let budget = OperationBudgetUsage {
elapsed_micros: receipt.wall_micros,
..OperationBudgetUsage::default()
Expand All @@ -1332,14 +1342,14 @@ async fn complete_or_failed<T: Send>(
if page.support_gaps.is_empty() {
SymbolGraphPortOutcome::Completed {
page,
finished_at: context.observed_at,
finished_at,
budget,
cost,
}
} else {
SymbolGraphPortOutcome::Partial {
page,
finished_at: context.observed_at,
finished_at,
budget,
cost,
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3691,7 +3691,10 @@ impl CodeIndexSchedulerRegistryV1 {
) -> Option<tracedecay_application::diagnostics_publication::CodeIndexPublicationIdentityV1>
{
let root = canonical_existing_identity(&project_root).ok()?;
let root_generation = self.latest_text_serving_for_root(&root).await?;
// Identity, not serving: this reads only the retained generation's
// manifest and snapshot. The TypeScript producer publishes at the
// seal, so a warming lexical artifact must not hide that publication.
let root_generation = self.retained_text_owner_for_root(&root).await?;
let scope = match scope {
Some(scope) => scope,
None => {
Expand All @@ -3706,7 +3709,7 @@ impl CodeIndexSchedulerRegistryV1 {
.ok()?
}
};
let (current, fresh) = self.latest_text_serving_freshness_for_scope(&scope).await?;
let (current, fresh) = self.retained_text_owner_freshness_for_scope(&scope).await?;
if !fresh
|| root_generation.metadata().manifest().generation_id
!= current.metadata().manifest().generation_id
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -406,6 +406,7 @@ impl CodeIndexSchedulerRegistryV1 {
repository_id,
worktree_id,
serving_generation,
graph_activation_enabled,
flights,
hints,
wake,
Expand All @@ -428,13 +429,26 @@ impl CodeIndexSchedulerRegistryV1 {
worktree.repository_id.clone(),
worktree.worktree_id.clone(),
Arc::clone(&worktree.serving_generation),
worktree.graph_activation.policy().is_enabled(),
Arc::clone(&worktree.ignored_dependency_admissions),
Arc::clone(&worktree.hints),
Arc::clone(&worktree.wake),
Arc::clone(&worktree.epoch),
Arc::clone(&worktree.pending_wake),
)
};
// A ready generation may serve from its text owner with no decoded
// seat; the whole decode runs only on demand. Admission builds on the
// decoded generation, so a refusal for an empty seat demands it and
// the caller's retry finds it seated.
if graph_activation_enabled
&& serving_generation
.read()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.is_none()
{
self.request_complete_generation(&project_root).await;
}
let (flight, owns_flight) = {
let mut active = flights
.lock()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -266,14 +266,14 @@ impl CodeIndexSchedulerRegistryV1 {
) -> Result<CodeIndexReadinessWaitReadV1, CodeIndexFreshnessReadFailureV1> {
let deadline = tokio::time::Instant::now() + budget;
if target != CodeIndexReadinessTargetV1::Fresh
&& self
&& let Some(reading) = self
.dashboard_freshness_read(project_root)
.await?
.is_some_and(|freshness| {
freshness.readiness(target) == CodeIndexReadinessV1::Reached
})
.filter(|freshness| freshness.readiness(target) == CodeIndexReadinessV1::Reached)
{
return Ok(CodeIndexReadinessWaitReadV1::Reached);
return Ok(CodeIndexReadinessWaitReadV1::Reached {
reading: Box::new(reading),
});
}
let mut signals = CodeIndexOwnerSignalsV1::subscribe(self, project_root).await;
// The caller's budget bounds the sweep, and an unproven source cannot
Expand Down Expand Up @@ -307,7 +307,9 @@ impl CodeIndexSchedulerRegistryV1 {
if let Some(freshness) = last.as_ref() {
match freshness.readiness(target) {
CodeIndexReadinessV1::Reached => {
return Ok(CodeIndexReadinessWaitReadV1::Reached);
return Ok(CodeIndexReadinessWaitReadV1::Reached {
reading: Box::new(freshness.clone()),
});
}
CodeIndexReadinessV1::Unreachable { reason } => {
return Ok(CodeIndexReadinessWaitReadV1::Unreachable { reason });
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3036,7 +3036,8 @@ async fn sealed_publication_identity_answers_before_the_generation_seats() {
.expect("publication projection gate stays armed");

assert!(
CodeIndexPublicationIdentityPortV1::resolve(&registry, fixture.path().to_path_buf())
registry
.latest_text_serving_for_root(fixture.path())
.await
.is_none(),
"no generation serves while its text projection is held"
Expand All @@ -3061,16 +3062,26 @@ async fn sealed_publication_identity_answers_before_the_generation_seats() {
.is_none(),
"a caller that handled the sealed generation learns of no newer seal"
);
// A diagnostics read validates the seal-time publication against this
// identity, so it must name the sealed generation before text serves.
let read_identity =
CodeIndexPublicationIdentityPortV1::resolve(&registry, fixture.path().to_path_buf())
.await
.expect("the retained generation answers identity while its text projection is held");
assert_eq!(read_identity.generation_id(), sealed.generation_id());

release_projection
.send(())
.expect("release publication projection");
let serving =
wait_until_serving_seat(&registry, fixture.path(), Duration::from_secs(10), || {
CodeIndexPublicationIdentityPortV1::resolve(&registry, fixture.path().to_path_buf())
registry.latest_text_serving_for_root(fixture.path())
})
.await;
assert_eq!(serving.generation_id(), sealed.generation_id());
assert_eq!(
&serving.metadata().manifest().generation_id,
sealed.generation_id()
);
registry.shutdown().await;
}

Expand Down Expand Up @@ -6709,7 +6720,7 @@ async fn readiness_wait_reaches_ready_exactly_when_the_held_graph_publishes() {
assert!(
matches!(
reached,
tracedecay_contracts::code_index_freshness::CodeIndexReadinessWaitReadV1::Reached
tracedecay_contracts::code_index_freshness::CodeIndexReadinessWaitReadV1::Reached { .. }
),
"{reached:?}"
);
Expand All @@ -6736,7 +6747,7 @@ async fn readiness_wait_reaches_ready_exactly_when_the_held_graph_publishes() {
assert!(
matches!(
graph_ready,
tracedecay_contracts::code_index_freshness::CodeIndexReadinessWaitReadV1::Reached
tracedecay_contracts::code_index_freshness::CodeIndexReadinessWaitReadV1::Reached { .. }
),
"{graph_ready:?}"
);
Expand Down Expand Up @@ -6799,7 +6810,7 @@ async fn fresh_wait_on_a_current_index_reaches_inside_one_second() {
assert!(
matches!(
outcome,
tracedecay_contracts::code_index_freshness::CodeIndexReadinessWaitReadV1::Reached
tracedecay_contracts::code_index_freshness::CodeIndexReadinessWaitReadV1::Reached { .. }
),
"{outcome:?}"
);
Expand Down Expand Up @@ -6831,7 +6842,7 @@ async fn fresh_wait_catches_an_unreported_save_and_returns_after_its_reindex() {
assert!(
matches!(
outcome,
tracedecay_contracts::code_index_freshness::CodeIndexReadinessWaitReadV1::Reached
tracedecay_contracts::code_index_freshness::CodeIndexReadinessWaitReadV1::Reached { .. }
),
"{outcome:?}"
);
Expand Down Expand Up @@ -6878,7 +6889,7 @@ async fn fresh_wait_verifies_the_source_while_a_pass_holds_the_scheduler() {
assert!(
matches!(
quiet,
tracedecay_contracts::code_index_freshness::CodeIndexReadinessWaitReadV1::Reached
tracedecay_contracts::code_index_freshness::CodeIndexReadinessWaitReadV1::Reached { .. }
),
"{quiet:?}"
);
Expand All @@ -6903,7 +6914,7 @@ async fn fresh_wait_verifies_the_source_while_a_pass_holds_the_scheduler() {
assert!(
matches!(
reindexed,
tracedecay_contracts::code_index_freshness::CodeIndexReadinessWaitReadV1::Reached
tracedecay_contracts::code_index_freshness::CodeIndexReadinessWaitReadV1::Reached { .. }
),
"{reindexed:?}"
);
Expand Down
6 changes: 5 additions & 1 deletion crates/tracedecay-contracts/src/code_index_freshness.rs
Original file line number Diff line number Diff line change
Expand Up @@ -708,7 +708,11 @@ impl CodeIndexWorktreeFreshnessV1 {
/// What a readiness wait observed when it ended.
#[derive(Clone, Debug)]
pub enum CodeIndexReadinessWaitReadV1 {
Reached,
/// `reading` is the reading that satisfied the target; a response built
/// on the wait describes this reading, not a later one.
Reached {
reading: Box<CodeIndexWorktreeFreshnessV1>,
},
/// The budget elapsed; `last` is the last reading, `None` while the root
/// was never mounted.
TimedOut {
Expand Down
30 changes: 22 additions & 8 deletions crates/tracedecay-mcp/src/handlers/info/status.rs
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ pub fn readiness_wait_outcome(
read: CodeIndexReadinessWaitReadV1,
) -> CodeIndexReadinessWaitOutcomeV1 {
match read {
CodeIndexReadinessWaitReadV1::Reached => CodeIndexReadinessWaitOutcomeV1::Reached,
CodeIndexReadinessWaitReadV1::Reached { .. } => CodeIndexReadinessWaitOutcomeV1::Reached,
CodeIndexReadinessWaitReadV1::TimedOut { last } => {
CodeIndexReadinessWaitOutcomeV1::TimedOut {
last_state: last
Expand Down Expand Up @@ -363,14 +363,16 @@ fn code_index_freshness_status(

/// Computes `tracedecay_status`. `server_stats` is the serving MCP server's
/// request counters; `wait` is the readiness wait the owner held the read
/// for, when the request asked for one.
/// for, when the request asked for one, and `reached_freshness` the reading
/// that satisfied it, which the payload reports instead of a later reading.
#[hotpath::measure(label = "mcp.info.status.total")]
pub async fn compute_status(
ctx: &McpToolContext<'_>,
request: &StatusSurfaceRequestV1,
server_stats: Option<Value>,
scope_prefix: Option<&str>,
wait: Option<CodeIndexReadinessWaitOutcomeV1>,
reached_freshness: Option<CodeIndexWorktreeFreshnessV1>,
) -> Result<StatusResultV1> {
if request.admission_only {
return Ok(StatusResultV1::Admission(StatusAdmissionV1 {
Expand All @@ -385,11 +387,20 @@ pub async fn compute_status(
// commonly push status over the response-frame budget, and the truncated
// body is not something the caller should reassemble into context. Opt in
// when the full diagnostic section is the thing being asked for.
let freshness_payload = hotpath::future!(
ctx.freshness(),
label = "mcp.info.status.code_index_freshness"
)
.await;
let freshness_payload = match reached_freshness {
Some(reading) => Some(
tracedecay_contracts::code_index_freshness::CodeIndexFreshnessPayloadV1::from_scheduler_read(
Some(reading),
),
),
None => {
hotpath::future!(
ctx.freshness(),
label = "mcp.info.status.code_index_freshness"
)
.await
}
};
let (code_index_freshness, code_index_freshness_warning, retrieval_serving) =
code_index_freshness_status(freshness_payload.as_ref());
let github_source = match github_source_status_v1(ctx.project_root()) {
Expand Down Expand Up @@ -1081,6 +1092,7 @@ mod tests {
coverage: CodeIndexFreshnessCoverageV1::PartialRefreshInProgress,
..Default::default()
};
let reached_reading = Box::new(seated_graph_pending.clone());
for (last, expected) in [
(Some(Box::new(seated_graph_pending)), "current"),
(Some(Box::new(rebuilding)), "warming"),
Expand All @@ -1096,7 +1108,9 @@ mod tests {
}
assert_eq!(
serde_json::to_value(readiness_wait_outcome(
CodeIndexReadinessWaitReadV1::Reached
CodeIndexReadinessWaitReadV1::Reached {
reading: reached_reading
}
))
.expect("outcome serializes"),
serde_json::json!({ "outcome": "reached" })
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -388,6 +388,9 @@ impl CodeLexicalArtifactReaderV1 {
)
.map_err(|error| map_reader_open_error(path, error))?;
verify_named_path_identity(path, &file)?;
// An unserved revision's receipt has another shape; classify the
// revision before decoding it so an old artifact is Incompatible.
verify_artifact_state_revision(&connection, control)?;
let receipt_bytes: Vec<u8> = connection
.query_row(
"SELECT receipt FROM artifact_state WHERE singleton = 1",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1558,7 +1558,7 @@ async fn first_index_serves_graph_reads_without_decoding_the_generation() {
assert!(
matches!(
reached,
tracedecay_contracts::code_index_freshness::CodeIndexReadinessWaitReadV1::Reached
tracedecay_contracts::code_index_freshness::CodeIndexReadinessWaitReadV1::Reached { .. }
),
"the first index must reach fresh with graph serving: {reached:?}"
);
Expand Down
Loading
Loading