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 @@ -2769,11 +2769,36 @@ impl LatestCodeTextGenerationV1 {
self.install_artifact_owners(reader, reader_reservation)?;
self.publish_text_progress_snapshot(ready_progress);
if needs_clone_successor {
self.text_projection_build.retain_clone_successor_retry()?;
return self
.begin_clone_successor(descriptor, prior, sealed_identity, source, control)
.map(TextHeadOpenOutcomeV1::BuildCloneSuccessor)
.map(Some);
// `begin_clone_successor` copies the whole prior lexical
// artifact with the slot lock released, so this wake's
// head-open claim has to span it. Parking
// `CloneSuccessorPending` before the copy published a
// takeable state mid-claim: `advance_artifact_text_serving`
// leaves its park loop on that state, so a concurrent wake
// took a second `HeadOpening` on top of this open and both
// drove the same staging database. Whichever open resolved
// second then found the slot already reset and failed the
// clone lane closed. A successful begin resolves the claim
// to `BuildingCloneSuccessor` anyway, so only a failed one
// needs the retry marker: the owners installed above would
// otherwise let the next wake short-circuit on a plain
// `Idle` and never owe the successor again.
return match self.begin_clone_successor(
descriptor,
prior,
sealed_identity,
source,
Comment on lines +2786 to +2790

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Preserve backfilling status while holding the clone claim

When opening a published artifact without clone fingerprints, the owners are already ready while this corpus-sized copy runs under HeadOpening. clone_successor_progress() classifies HeadOpening as Idle, so concurrent dashboard/MCP freshness reads report the missing successor as Partial rather than Backfilling for the duration of the copy; the existing status test confirms that an idle missing successor maps to Partial. Keep the state untakeable while still identifying this as clone backfill, or teach the progress reader to distinguish this clone-opening claim.

AGENTS.md reference: AGENTS.md:L9-L12

Useful? React with 👍 / 👎.

control,
) {
Ok(build) => Ok(Some(TextHeadOpenOutcomeV1::BuildCloneSuccessor(build))),
Err(error) => {
// The claim still owns `HeadOpening`, so this only
// parks the marker; the begin failure is the one
// worth reporting.
let _ = self.text_projection_build.retain_clone_successor_retry();

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 Propagate the retry-state contract failure

If retain_clone_successor_retry() observes anything other than HeadOpening—the exact ownership invariant this patch is intended to protect—it returns a Contract error, but this let _ discards that evidence and reports the unrelated begin_clone_successor error instead. In the Idle case, the claim guard also leaves the slot idle, so later wakes see ready owners and can permanently skip the owed successor. Propagate or combine the parking failure rather than suppressing it.

AGENTS.md reference: AGENTS.md:L152-L154

Useful? React with 👍 / 👎.

Err(error)
}
};
}
drop(source);
Ok(Some(TextHeadOpenOutcomeV1::Served))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -942,6 +942,88 @@ fn lexical_readiness_leaves_the_clone_successor_uncopied() {
assert_eq!(revision, 16);
}

/// Only one wake at a time may own a head-open claim.
///
/// `open_published_text_artifact` used to park `CloneSuccessorPending`
/// before `begin_clone_successor` copied the whole prior artifact, and
/// `advance_artifact_text_serving` leaves its park loop on that state. A
/// concurrent wake took a second `HeadOpening` on top of the first open,
/// both drove the same staging database, and whichever open resolved second
/// found the slot already reset and refused with `clone-successor retry
/// requires an active head-open claim`. The clone lanes report that refusal
/// as a non-retryable `search_failed`.
#[test]
fn concurrent_wakes_never_overlap_the_clone_successor_head_open() {
let sources = (0..24)
.map(|index| {
(
format!("src/module_{index}.rs"),
format!(
"pub fn alpha_{index}() {{ one(); two(); three(); four(); five(); six(); seven(); eight(); nine(); ten(); }}\npub fn beta_{index}() {{ one(); two(); three(); four(); five(); six(); seven(); eight(); nine(); ten(); }}\n"
),
)
})
.collect::<Vec<_>>();
let files = sources
.iter()
.map(|(path, contents)| (path.as_str(), contents.as_str()))
.collect::<Vec<_>>();
let fixture = GitFixture::new(&files);
let store = TempDir::new().expect("store root");
let mut scheduler = scheduler(
&fixture,
store.path().to_path_buf(),
Arc::new(SharedCodeIndexBytePoolV1::default()),
);
published(scheduler.reconcile_now().expect("publish generation"));
let latest = scheduler.latest_complete().expect("latest generation");
while !latest.query_owners_are_ready() {
latest
.advance_text_serving(1)
.expect("advance lexical build");
}
assert!(
matches!(
&*latest.text_projection_build.lock_slot(),
super::super::CodeTextProjectionSlotV1::CloneSuccessorPending
),
"the successor must still be owed when the wakes start"
);

let workers = (0..4)
.map(|_| {
let latest = latest.clone();
thread::spawn(move || {
let mut advances = 0_usize;
while latest.text_projection_needs_work() && advances < 400 {
latest.advance_text_serving(1)?;
advances += 1;
}
Ok(())
})
})
.collect::<Vec<_>>();
for worker in workers {
worker
.join()
.expect("wake thread joins")
.unwrap_or_else(|error: RetrievalPortError| {
panic!("a concurrent wake failed the text projection: {error}")
});
}

assert!(!latest.text_projection_needs_work());
let revision: i64 = rusqlite::Connection::open(active_text_artifact_path(store.path()))
.expect("open finished artifact")
.query_row(
"SELECT format_revision FROM artifact_state WHERE singleton = 1",
[],
|row| row.get(0),
)
.expect("read finished revision");
assert_eq!(revision, 16, "the clone successor must have sealed");
}

#[test]
fn clone_status_distinguishes_unavailable_backfill_partial_ready_and_stale() {
let fixture = GitFixture::new(&[(
Expand Down
Loading