fix: better validate for dev_brain and 50k reminder not triggering rebuild

This commit is contained in:
Georg Bauer
2026-07-27 23:08:47 +02:00
parent d371d9d659
commit 90a445cafd
2 changed files with 119 additions and 20 deletions

View File

@@ -72,12 +72,13 @@ sources:
- project: Registered project name
path: src/example.rs
symbol: optional_symbol
revision: exact-clean-git-revision
# Use hash instead of revision when the worktree is dirty or is not Git.
revision: full-or-unique-short-clean-git-revision
# Use `git rev-parse HEAD`, or a unique lowercase hex prefix of at least 7 characters.
# Use hash instead when the worktree is dirty or is not Git.
---
```
Each source has exactly one evidence version: `revision` or a lowercase SHA-256 `hash`. Paths are project-relative and may not escape the registered project.
Each source has exactly one evidence version: `revision` or a lowercase SHA-256 `hash`. A revision is the current clean commit's full object ID or a unique lowercase hexadecimal prefix of at least 7 characters. Paths are project-relative and may not escape the registered project.
## Compilation
@@ -1081,9 +1082,9 @@ fn validate_source(source: &SourceRecord, projects: &[RegisteredProject]) -> Res
));
}
if let Some(revision) = &source.revision {
return Ok(
git_state(&project.root).is_some_and(|(current, clean)| clean && current == *revision)
);
return Ok(git_state(&project.root).is_some_and(|(current, clean)| {
clean && git_revision_matches(&project.root, revision, &current)
}));
}
let expected = source.hash.as_deref().unwrap();
if expected.len() != 64
@@ -1100,6 +1101,29 @@ fn validate_source(source: &SourceRecord, projects: &[RegisteredProject]) -> Res
&& !git_state(&project.root).is_some_and(|(_, clean)| clean))
}
fn git_revision_matches(root: &Path, revision: &str, current: &str) -> bool {
if revision == current {
return true;
}
if revision.len() < 7
|| revision.len() >= current.len()
|| !revision
.bytes()
.all(|byte| byte.is_ascii_digit() || (b'a'..=b'f').contains(&byte))
{
return false;
}
Command::new("git")
.args(["-C"])
.arg(root)
.args(["rev-parse", "--verify"])
.arg(format!("{revision}^{{commit}}"))
.output()
.ok()
.filter(|output| output.status.success())
.is_some_and(|output| String::from_utf8_lossy(&output.stdout).trim() == current)
}
fn git_state(root: &Path) -> Option<(String, bool)> {
let revision = Command::new("git")
.args(["-C"])
@@ -1560,7 +1584,7 @@ mod tests {
}
#[test]
fn clean_git_revisions_become_stale_when_the_worktree_changes() {
fn clean_full_and_short_git_revisions_become_stale_when_the_worktree_changes() {
let fixture = Fixture::new();
for arguments in [
vec!["init"],
@@ -1586,11 +1610,21 @@ mod tests {
);
}
let revision = git_state(&fixture.project).unwrap().0;
assert!(git_revision_matches(
&fixture.project,
&revision[..7],
&revision
));
assert!(!git_revision_matches(
&fixture.project,
&revision[..6],
&revision
));
let topic = fixture
.topic("verified", &"0".repeat(64), "Revision-backed knowledge.")
.replace(
&format!("hash: {}", "0".repeat(64)),
&format!("revision: {revision}"),
&format!("revision: {}", &revision[..7]),
);
let mut brain = fixture.brain();
write_topic(&fixture, &mut brain, &topic).unwrap();

View File

@@ -568,14 +568,15 @@ impl Generator {
mut progress: impl FnMut(u32, u32, Option<f32>),
mut phase: impl FnMut(&'static str),
) -> Result<GenerationOutput, String> {
let history = messages
.split_last()
.map_or(messages, |(_, history)| history);
let checkpoint_present = checkpoint.is_file();
let selected = self.select_checkpoint(
checkpoint,
conversation_tag(&settings.system_prompt, settings.reasoning_mode, history),
)?;
let selected = self.select_checkpoint(checkpoint, |tag| {
checkpoint_matches_prefix(
tag,
&settings.system_prompt,
settings.reasoning_mode,
messages,
)
})?;
if checkpoint_present && !selected.found {
phase(checkpoint_rebuild_activity(selected.incompatible));
}
@@ -788,7 +789,10 @@ impl Generator {
return Ok(self.checkpoint.clone());
}
if let Some(entry) = store.find(key, self.executor.context()) {
if self.select_checkpoint(&entry.checkpoint, entry.tag)?.found {
if self
.select_checkpoint(&entry.checkpoint, |tag| tag == entry.tag)?
.found
{
store.touch(&entry)?;
self.last_store_tokens = entry.tokens;
return Ok(Some(entry.checkpoint));
@@ -955,10 +959,10 @@ impl Generator {
fn select_checkpoint(
&mut self,
checkpoint: &Path,
expected_tag: [u8; 32],
matches: impl Fn([u8; 32]) -> bool,
) -> Result<CheckpointSelection, String> {
let resident_hit = self.activate_resident(checkpoint.to_owned())?;
if resident_hit && self.executor.checkpoint_tag() == expected_tag {
if resident_hit && matches(self.executor.checkpoint_tag()) {
self.checkpoint = Some(checkpoint.to_owned());
self.metrics.kv_lookup(KvLookup::MemoryHit);
return Ok(CheckpointSelection {
@@ -976,7 +980,7 @@ impl Generator {
incompatible: false,
});
}
let found = self.executor.checkpoint_tag() == expected_tag;
let found = matches(self.executor.checkpoint_tag());
self.metrics.kv_lookup(if found {
KvLookup::MemoryHit
} else {
@@ -995,7 +999,7 @@ impl Generator {
.load_checkpoint(checkpoint, &mut |bytes| self.metrics.kv_read_bytes(bytes));
self.metrics
.kv_read_finished(started.elapsed(), loaded.is_err());
let found = matches!(loaded, Ok(true)) && self.executor.checkpoint_tag() == expected_tag;
let found = matches!(loaded, Ok(true)) && matches(self.executor.checkpoint_tag());
let incompatible = loaded.is_err();
let lookup = match loaded {
Ok(true) if found => KvLookup::DiskHit,
@@ -1527,6 +1531,20 @@ fn conversation_tag(system: &str, reasoning: ReasoningMode, messages: &[ChatTurn
Sha256::digest(conversation_key(system, reasoning, messages)).into()
}
#[cfg(target_os = "macos")]
fn checkpoint_matches_prefix(
checkpoint: [u8; 32],
system: &str,
reasoning: ReasoningMode,
messages: &[ChatTurn],
) -> bool {
// ponytail: appended control messages are few; carry incremental hashes if
// scanning a genuinely changed, very long history becomes measurable.
(0..messages.len())
.rev()
.any(|end| checkpoint == conversation_tag(system, reasoning, &messages[..end]))
}
#[cfg(target_os = "macos")]
fn resident_key(directory: &Path, tag: [u8; 32]) -> PathBuf {
let mut name = String::with_capacity(64);
@@ -1735,6 +1753,53 @@ mod sampling_tests {
assert!(conversation_key("System", ReasoningMode::High, &messages).starts_with(&prefix));
}
#[test]
fn checkpoint_tag_accepts_an_unchanged_prefix_before_reminders() {
let mut messages = vec![ChatTurn {
user: true,
tool: false,
system: false,
skip_previous_eos: false,
reasoning: None,
reasoning_complete: true,
content: "Question".into(),
}];
let checkpoint = conversation_tag("System", ReasoningMode::High, &messages);
messages.extend([
ChatTurn {
user: false,
tool: true,
system: false,
skip_previous_eos: false,
reasoning: None,
reasoning_complete: true,
content: "Tool result".into(),
},
ChatTurn {
user: false,
tool: false,
system: true,
skip_previous_eos: false,
reasoning: None,
reasoning_complete: true,
content: "System prompt reminder".into(),
},
]);
assert!(checkpoint_matches_prefix(
checkpoint,
"System",
ReasoningMode::High,
&messages,
));
assert!(!checkpoint_matches_prefix(
checkpoint,
"Changed",
ReasoningMode::High,
&messages,
));
}
#[test]
fn bootstrap_key_is_the_prefix_before_dynamic_session_context() {
let system = "System\n\nProject instructions from AGENTS.md:\n\nkeep this";