mirror of
https://github.com/instructkr/claw-code.git
synced 2026-04-13 19:44:53 +08:00
Keep finished lanes from leaving stale reminders armed
The next repo-local sweep target was ROADMAP #66: reminder/cron state could stay enabled after the associated lane had already finished, which left stale nudges firing into completed work. The fix teaches successful lane persistence to disable matching enabled cron entries and record which reminder ids were shut down on the finished event. Constraint: Preserve existing cron/task registries and add the shutdown behavior only on the successful lane-finished path Rejected: Add a separate reminder-cleanup command that operators must remember to run | leaves the completion leak unfixed at the source Confidence: high Scope-risk: narrow Reversibility: clean Directive: If cron-matching heuristics change later, update `disable_matching_crons`, its regression, and the ROADMAP closeout together Tested: cargo fmt --all --check; cargo clippy --workspace --all-targets -- -D warnings; cargo test --workspace; architect review APPROVE Not-tested: Cross-process cron/reminder persistence beyond the in-memory registry used in this repo
This commit is contained in:
@@ -3764,7 +3764,8 @@ fn persist_agent_terminal_state(
|
||||
.push(LaneEvent::failed(iso8601_now(), &blocker));
|
||||
} else {
|
||||
next_manifest.current_blocker = None;
|
||||
let finished_summary = build_lane_finished_summary(&next_manifest, result);
|
||||
let mut finished_summary = build_lane_finished_summary(&next_manifest, result);
|
||||
finished_summary.data.disabled_cron_ids = disable_matching_crons(&next_manifest, result);
|
||||
next_manifest.lane_events.push(
|
||||
LaneEvent::finished(iso8601_now(), finished_summary.detail).with_data(
|
||||
serde_json::to_value(&finished_summary.data)
|
||||
@@ -3846,6 +3847,8 @@ struct LaneFinishedSummaryData {
|
||||
selection_outcome: Option<SelectionOutcome>,
|
||||
#[serde(rename = "artifactProvenance", skip_serializing_if = "Option::is_none")]
|
||||
artifact_provenance: Option<ArtifactProvenance>,
|
||||
#[serde(rename = "disabledCronIds", skip_serializing_if = "Vec::is_empty")]
|
||||
disabled_cron_ids: Vec<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
@@ -3928,6 +3931,7 @@ fn build_lane_finished_summary(
|
||||
review_rationale: review_outcome.and_then(|outcome| outcome.rationale),
|
||||
selection_outcome: extract_selection_outcome(raw_summary.unwrap_or_default()),
|
||||
artifact_provenance,
|
||||
disabled_cron_ids: Vec::new(),
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -4200,6 +4204,46 @@ fn normalize_diff_stat(value: &str) -> String {
|
||||
trimmed.to_string()
|
||||
}
|
||||
|
||||
fn disable_matching_crons(manifest: &AgentOutput, result: Option<&str>) -> Vec<String> {
|
||||
let tokens = cron_match_tokens(manifest, result);
|
||||
if tokens.is_empty() {
|
||||
return Vec::new();
|
||||
}
|
||||
|
||||
let mut disabled = Vec::new();
|
||||
for entry in global_cron_registry().list(true) {
|
||||
let haystack = format!(
|
||||
"{} {}",
|
||||
entry.prompt,
|
||||
entry.description.as_deref().unwrap_or_default()
|
||||
)
|
||||
.to_ascii_lowercase();
|
||||
if tokens.iter().any(|token| haystack.contains(token))
|
||||
&& global_cron_registry().disable(&entry.cron_id).is_ok()
|
||||
{
|
||||
disabled.push(entry.cron_id);
|
||||
}
|
||||
}
|
||||
disabled.sort();
|
||||
disabled
|
||||
}
|
||||
|
||||
fn cron_match_tokens(manifest: &AgentOutput, result: Option<&str>) -> Vec<String> {
|
||||
let mut tokens = extract_roadmap_items(manifest.description.as_str())
|
||||
.into_iter()
|
||||
.chain(extract_roadmap_items(result.unwrap_or_default()))
|
||||
.map(|item| item.to_ascii_lowercase())
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
if tokens.is_empty() && !manifest.name.trim().is_empty() {
|
||||
tokens.push(manifest.name.trim().to_ascii_lowercase());
|
||||
}
|
||||
|
||||
tokens.sort();
|
||||
tokens.dedup();
|
||||
tokens
|
||||
}
|
||||
|
||||
fn derive_agent_state(
|
||||
status: &str,
|
||||
result: Option<&str>,
|
||||
@@ -5985,7 +6029,7 @@ mod tests {
|
||||
use super::{
|
||||
agent_permission_policy, allowed_tools_for_subagent, classify_lane_failure,
|
||||
derive_agent_state, execute_agent_with_spawn, execute_tool, final_assistant_text,
|
||||
maybe_commit_provenance, mvp_tool_specs, permission_mode_from_plugin,
|
||||
global_cron_registry, maybe_commit_provenance, mvp_tool_specs, permission_mode_from_plugin,
|
||||
persist_agent_terminal_state, push_output_block, run_task_packet, AgentInput, AgentJob,
|
||||
GlobalToolRegistry, LaneEventName, LaneFailureClass, ProviderRuntimeClient,
|
||||
SubagentToolExecutor,
|
||||
@@ -7945,6 +7989,43 @@ mod tests {
|
||||
"deadbee"
|
||||
);
|
||||
|
||||
let cron = global_cron_registry().create(
|
||||
"*/10 * * * *",
|
||||
"roadmap-nudge-10min for ROADMAP #66",
|
||||
Some("ROADMAP #66 reminder"),
|
||||
);
|
||||
let reminder = execute_agent_with_spawn(
|
||||
AgentInput {
|
||||
description: "Close ROADMAP #66 reminder shutdown".to_string(),
|
||||
prompt: "Finish the cron shutdown fix".to_string(),
|
||||
subagent_type: Some("Explore".to_string()),
|
||||
name: Some("cron-closeout".to_string()),
|
||||
model: None,
|
||||
},
|
||||
|job| {
|
||||
persist_agent_terminal_state(
|
||||
&job.manifest,
|
||||
"completed",
|
||||
Some("Completed ROADMAP #66 after verification."),
|
||||
None,
|
||||
)
|
||||
},
|
||||
)
|
||||
.expect("reminder agent should succeed");
|
||||
|
||||
let reminder_manifest = std::fs::read_to_string(&reminder.manifest_file)
|
||||
.expect("reminder manifest should exist");
|
||||
let reminder_manifest_json: serde_json::Value =
|
||||
serde_json::from_str(&reminder_manifest).expect("reminder manifest json");
|
||||
assert_eq!(
|
||||
reminder_manifest_json["laneEvents"][1]["data"]["disabledCronIds"][0],
|
||||
cron.cron_id
|
||||
);
|
||||
let disabled_entry = global_cron_registry()
|
||||
.get(&cron.cron_id)
|
||||
.expect("cron should still exist");
|
||||
assert!(!disabled_entry.enabled);
|
||||
|
||||
let spawn_error = execute_agent_with_spawn(
|
||||
AgentInput {
|
||||
description: "Spawn error task".to_string(),
|
||||
|
||||
Reference in New Issue
Block a user