fix: better validate for dev_brain and 50k reminder not triggering rebuild
This commit is contained in:
@@ -72,12 +72,13 @@ sources:
|
|||||||
- project: Registered project name
|
- project: Registered project name
|
||||||
path: src/example.rs
|
path: src/example.rs
|
||||||
symbol: optional_symbol
|
symbol: optional_symbol
|
||||||
revision: exact-clean-git-revision
|
revision: full-or-unique-short-clean-git-revision
|
||||||
# Use hash instead of revision when the worktree is dirty or is not Git.
|
# 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
|
## Compilation
|
||||||
|
|
||||||
@@ -1081,9 +1082,9 @@ fn validate_source(source: &SourceRecord, projects: &[RegisteredProject]) -> Res
|
|||||||
));
|
));
|
||||||
}
|
}
|
||||||
if let Some(revision) = &source.revision {
|
if let Some(revision) = &source.revision {
|
||||||
return Ok(
|
return Ok(git_state(&project.root).is_some_and(|(current, clean)| {
|
||||||
git_state(&project.root).is_some_and(|(current, clean)| clean && current == *revision)
|
clean && git_revision_matches(&project.root, revision, ¤t)
|
||||||
);
|
}));
|
||||||
}
|
}
|
||||||
let expected = source.hash.as_deref().unwrap();
|
let expected = source.hash.as_deref().unwrap();
|
||||||
if expected.len() != 64
|
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))
|
&& !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)> {
|
fn git_state(root: &Path) -> Option<(String, bool)> {
|
||||||
let revision = Command::new("git")
|
let revision = Command::new("git")
|
||||||
.args(["-C"])
|
.args(["-C"])
|
||||||
@@ -1560,7 +1584,7 @@ mod tests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[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();
|
let fixture = Fixture::new();
|
||||||
for arguments in [
|
for arguments in [
|
||||||
vec!["init"],
|
vec!["init"],
|
||||||
@@ -1586,11 +1610,21 @@ mod tests {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
let revision = git_state(&fixture.project).unwrap().0;
|
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
|
let topic = fixture
|
||||||
.topic("verified", &"0".repeat(64), "Revision-backed knowledge.")
|
.topic("verified", &"0".repeat(64), "Revision-backed knowledge.")
|
||||||
.replace(
|
.replace(
|
||||||
&format!("hash: {}", "0".repeat(64)),
|
&format!("hash: {}", "0".repeat(64)),
|
||||||
&format!("revision: {revision}"),
|
&format!("revision: {}", &revision[..7]),
|
||||||
);
|
);
|
||||||
let mut brain = fixture.brain();
|
let mut brain = fixture.brain();
|
||||||
write_topic(&fixture, &mut brain, &topic).unwrap();
|
write_topic(&fixture, &mut brain, &topic).unwrap();
|
||||||
|
|||||||
@@ -568,14 +568,15 @@ impl Generator {
|
|||||||
mut progress: impl FnMut(u32, u32, Option<f32>),
|
mut progress: impl FnMut(u32, u32, Option<f32>),
|
||||||
mut phase: impl FnMut(&'static str),
|
mut phase: impl FnMut(&'static str),
|
||||||
) -> Result<GenerationOutput, String> {
|
) -> Result<GenerationOutput, String> {
|
||||||
let history = messages
|
|
||||||
.split_last()
|
|
||||||
.map_or(messages, |(_, history)| history);
|
|
||||||
let checkpoint_present = checkpoint.is_file();
|
let checkpoint_present = checkpoint.is_file();
|
||||||
let selected = self.select_checkpoint(
|
let selected = self.select_checkpoint(checkpoint, |tag| {
|
||||||
checkpoint,
|
checkpoint_matches_prefix(
|
||||||
conversation_tag(&settings.system_prompt, settings.reasoning_mode, history),
|
tag,
|
||||||
)?;
|
&settings.system_prompt,
|
||||||
|
settings.reasoning_mode,
|
||||||
|
messages,
|
||||||
|
)
|
||||||
|
})?;
|
||||||
if checkpoint_present && !selected.found {
|
if checkpoint_present && !selected.found {
|
||||||
phase(checkpoint_rebuild_activity(selected.incompatible));
|
phase(checkpoint_rebuild_activity(selected.incompatible));
|
||||||
}
|
}
|
||||||
@@ -788,7 +789,10 @@ impl Generator {
|
|||||||
return Ok(self.checkpoint.clone());
|
return Ok(self.checkpoint.clone());
|
||||||
}
|
}
|
||||||
if let Some(entry) = store.find(key, self.executor.context()) {
|
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)?;
|
store.touch(&entry)?;
|
||||||
self.last_store_tokens = entry.tokens;
|
self.last_store_tokens = entry.tokens;
|
||||||
return Ok(Some(entry.checkpoint));
|
return Ok(Some(entry.checkpoint));
|
||||||
@@ -955,10 +959,10 @@ impl Generator {
|
|||||||
fn select_checkpoint(
|
fn select_checkpoint(
|
||||||
&mut self,
|
&mut self,
|
||||||
checkpoint: &Path,
|
checkpoint: &Path,
|
||||||
expected_tag: [u8; 32],
|
matches: impl Fn([u8; 32]) -> bool,
|
||||||
) -> Result<CheckpointSelection, String> {
|
) -> Result<CheckpointSelection, String> {
|
||||||
let resident_hit = self.activate_resident(checkpoint.to_owned())?;
|
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.checkpoint = Some(checkpoint.to_owned());
|
||||||
self.metrics.kv_lookup(KvLookup::MemoryHit);
|
self.metrics.kv_lookup(KvLookup::MemoryHit);
|
||||||
return Ok(CheckpointSelection {
|
return Ok(CheckpointSelection {
|
||||||
@@ -976,7 +980,7 @@ impl Generator {
|
|||||||
incompatible: false,
|
incompatible: false,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
let found = self.executor.checkpoint_tag() == expected_tag;
|
let found = matches(self.executor.checkpoint_tag());
|
||||||
self.metrics.kv_lookup(if found {
|
self.metrics.kv_lookup(if found {
|
||||||
KvLookup::MemoryHit
|
KvLookup::MemoryHit
|
||||||
} else {
|
} else {
|
||||||
@@ -995,7 +999,7 @@ impl Generator {
|
|||||||
.load_checkpoint(checkpoint, &mut |bytes| self.metrics.kv_read_bytes(bytes));
|
.load_checkpoint(checkpoint, &mut |bytes| self.metrics.kv_read_bytes(bytes));
|
||||||
self.metrics
|
self.metrics
|
||||||
.kv_read_finished(started.elapsed(), loaded.is_err());
|
.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 incompatible = loaded.is_err();
|
||||||
let lookup = match loaded {
|
let lookup = match loaded {
|
||||||
Ok(true) if found => KvLookup::DiskHit,
|
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()
|
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")]
|
#[cfg(target_os = "macos")]
|
||||||
fn resident_key(directory: &Path, tag: [u8; 32]) -> PathBuf {
|
fn resident_key(directory: &Path, tag: [u8; 32]) -> PathBuf {
|
||||||
let mut name = String::with_capacity(64);
|
let mut name = String::with_capacity(64);
|
||||||
@@ -1735,6 +1753,53 @@ mod sampling_tests {
|
|||||||
assert!(conversation_key("System", ReasoningMode::High, &messages).starts_with(&prefix));
|
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]
|
#[test]
|
||||||
fn bootstrap_key_is_the_prefix_before_dynamic_session_context() {
|
fn bootstrap_key_is_the_prefix_before_dynamic_session_context() {
|
||||||
let system = "System\n\nProject instructions from AGENTS.md:\n\nkeep this";
|
let system = "System\n\nProject instructions from AGENTS.md:\n\nkeep this";
|
||||||
|
|||||||
Reference in New Issue
Block a user