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:
Yeachan-Heo
2026-04-12 12:52:27 +00:00
parent e75d67dfd3
commit 6b4bb4ac26
2 changed files with 84 additions and 3 deletions

View File

@@ -504,7 +504,7 @@ Model name prefix now wins unconditionally over env-var presence. Regression tes
65. **Backlog-scanning team lanes emit opaque stops, not structured selection outcomes****done (verified 2026-04-12):** completed lane persistence in `rust/crates/tools/src/lib.rs` now recognizes backlog-scan selection summaries and records structured `selectionOutcome` metadata on `lane.finished`, including `chosenItems`, `skippedItems`, `action`, and optional `rationale`, while preserving existing non-selection and review-lane behavior. Regression coverage locks the structured backlog-scan payload alongside the earlier quality-floor and review-verdict paths. **Original filing below.**
66. **Completion-aware reminder shutdown missing**dogfooded 2026-04-12. Ultraclaw batch completed and was reported as done, but 10-minute cron reminder (`roadmap-nudge-10min`) kept firing into channel as if work still pending. Reminder/cron state not coupled to terminal task state. **Fix shape:** (a) cron jobs should check task completion state before firing; (b) or, provide explicit `cron.remove` on task completion; (c) or, reminders should include "work complete" detection and auto-expire. Blocker: none. Source: gaebal-gajae dogfood analysis 2026-04-12.
66. **Completion-aware reminder shutdown missing****done (verified 2026-04-12):** completed lane persistence in `rust/crates/tools/src/lib.rs` now disables matching enabled cron reminders when the associated lane finishes successfully, and records the affected cron ids in `lane.finished.data.disabledCronIds`. Regression coverage locks the path where a ROADMAP-linked reminder is disabled on successful completion while leaving incomplete work untouched. **Original filing below.**
67. **Scoped review lanes do not emit structured verdicts****done (verified 2026-04-12):** completed lane persistence in `rust/crates/tools/src/lib.rs` now recognizes review-style `APPROVE`/`REJECT`/`BLOCKED` results and records structured `reviewVerdict`, `reviewTarget`, and `reviewRationale` metadata on the `lane.finished` event while preserving existing non-review lane behavior. Regression coverage locks both the normal completion path and a scoped review-lane completion payload. **Original filing below.**

View File

@@ -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(),