From d69d7799e8f6b3ad56b1739879828d7ccb5355b0 Mon Sep 17 00:00:00 2001 From: Yvette Carlisle Date: Thu, 11 Jun 2026 23:02:22 +0800 Subject: [PATCH] {"schema":"decodex/commit/1","summary":"Fix retained attention queue echo projection","authority":"manual"} --- apps/decodex/src/orchestrator/status.rs | 47 +++++++++---- .../tests/operator/status/queue.rs | 70 +++++++++++++++++++ docs/reference/operator-control-plane.md | 5 ++ docs/spec/runtime.md | 5 ++ 4 files changed, 115 insertions(+), 12 deletions(-) diff --git a/apps/decodex/src/orchestrator/status.rs b/apps/decodex/src/orchestrator/status.rs index 0220b6037..cf1f2f4f0 100644 --- a/apps/decodex/src/orchestrator/status.rs +++ b/apps/decodex/src/orchestrator/status.rs @@ -653,6 +653,7 @@ where &mut snapshot, )?; apply_terminal_history_ledger_outcomes(&mut snapshot); + suppress_terminal_attention_queue_echoes(&mut snapshot); refresh_worktree_ownership( &mut snapshot, Some(workflow.frontmatter().tracker().resolved_completed_state()), @@ -972,6 +973,28 @@ fn apply_terminal_history_ledger_outcomes(snapshot: &mut OperatorStatusSnapshot) }); } +fn suppress_terminal_attention_queue_echoes(snapshot: &mut OperatorStatusSnapshot) { + let terminal_attention_keys = snapshot + .history_lanes + .iter() + .filter(|lane| history_ledger_outcome_requires_attention(&lane.ledger_outcome)) + .map(history_lane_group_key) + .collect::>(); + + if terminal_attention_keys.is_empty() { + return; + } + + snapshot.queued_candidates.retain(|candidate| { + let candidate_key = + operator_issue_attention_key(&candidate.issue_id, Some(&candidate.issue_identifier)); + let is_terminal_attention_echo = + candidate.reason == "issue_needs_attention" && terminal_attention_keys.contains(&candidate_key); + + !is_terminal_attention_echo + }); +} + fn history_ledger_outcome_supersedes_local_attempts( outcome: &OperatorHistoryLedgerOutcome, ) -> bool { @@ -1091,18 +1114,6 @@ fn worktree_ownership( audit_required: false, }; } - - if worktree_has_queued_attention_owner(worktree, snapshot) { - return WorktreeOwnership { - kind: "queued_attention", - reason: String::from( - "Intake Queue owns this worktree because the issue needs operator attention.", - ), - next_action: None, - audit_required: false, - }; - } - if let Some(lane) = worktree_history_attention_owner(worktree, snapshot) { return WorktreeOwnership { kind: "retained_attention", @@ -1124,6 +1135,18 @@ fn worktree_ownership( audit_required: false, }; } + + if worktree_has_queued_attention_owner(worktree, snapshot) { + return WorktreeOwnership { + kind: "queued_attention", + reason: String::from( + "Intake Queue owns this worktree because the issue needs operator attention.", + ), + next_action: None, + audit_required: false, + }; + } + if let Some(hygiene) = &worktree.hygiene { return WorktreeOwnership { kind: "post_land_cleanup", diff --git a/apps/decodex/src/orchestrator/tests/operator/status/queue.rs b/apps/decodex/src/orchestrator/tests/operator/status/queue.rs index 36d4f39af..06351d501 100644 --- a/apps/decodex/src/orchestrator/tests/operator/status/queue.rs +++ b/apps/decodex/src/orchestrator/tests/operator/status/queue.rs @@ -387,6 +387,76 @@ fn live_operator_status_snapshot_prioritizes_needs_attention_over_shared_claim() ); } +#[test] +fn live_operator_status_snapshot_deduplicates_terminal_retained_attention_queue_echo() { + let (_temp_dir, config, workflow) = temp_project_layout(); + let state_store = StateStore::open_in_memory().expect("state store should open"); + let issue = sample_issue_with_sort_fields( + "issue-xy-922", + "XY-922", + "Todo", + &["decodex:needs-attention"], + Some(2), + "2026-06-11T09:08:00Z", + ); + let local_comments = retained_partial_progress_linear_execution_history_comments(&issue); + let tracker = FakeTracker::new(vec![issue.clone()]); + + tracker + .issue_comments + .borrow_mut() + .insert(issue.id.clone(), local_comments.clone()); + state_store + .upsert_worktree( + TEST_SERVICE_ID, + &issue.id, + "xy/profit-pilot-xy-922", + &config.worktree_root().join(&issue.identifier).display().to_string(), + ) + .expect("retained worktree should record"); + state_store + .record_run_attempt("xy-355-attempt-1-1777527013", &issue.id, 1, "failed") + .expect("failed run attempt should record"); + + seed_local_linear_execution_events(&state_store, &local_comments); + + let snapshot = orchestrator::build_live_operator_status_snapshot( + &tracker, + &config, + &workflow, + &state_store, + 10, + ) + .expect("snapshot should build"); + let project = snapshot.projects.first().expect("project summary should exist"); + let lane = snapshot + .history_lanes + .iter() + .find(|lane| lane.issue_key == "XY-922") + .expect("terminal retained lane should render from run ledger"); + let worktree = snapshot.worktrees.first().expect("retained worktree should render"); + + assert!( + snapshot.queued_candidates.iter().all(|candidate| candidate.issue_identifier != "XY-922"), + "terminal retained attention should not remain as an intake queue candidate" + ); + assert_eq!(project.queued_candidate_count, 0); + assert_eq!(project.attention_count, 1); + assert_eq!(project.retained_worktree_count, 1); + assert_eq!( + lane.ledger_outcome.needs_attention_reason.as_deref(), + Some("Decodex retained validation-ready partial progress for manual review.") + ); + assert_eq!(worktree.ownership, "retained_attention"); + assert!( + worktree + .recovery_next_action + .as_deref() + .is_some_and(|next_action| next_action.contains("validation-ready partial progress")), + "retained worktree next action should come from the terminal run ledger" + ); +} + #[test] fn live_operator_status_snapshot_blocks_active_plus_queued_label_without_local_claim() { let (_temp_dir, config, workflow) = temp_project_layout(); diff --git a/docs/reference/operator-control-plane.md b/docs/reference/operator-control-plane.md index 97537b52a..640b795b4 100644 --- a/docs/reference/operator-control-plane.md +++ b/docs/reference/operator-control-plane.md @@ -563,6 +563,11 @@ rate-limited, or unavailable. - Completed lanes without Decodex Linear execution ledger records are reported as `missing` / `execution_ledger_missing`. Tracker terminal state, local attempt success, and non-ledger comments never satisfy the Run Ledger outcome contract. +- When a terminal Run Ledger attention record exists for an issue that still has both + the service queue label and `decodex:needs-attention`, the operator snapshot treats + the queue label as stale echo state. The issue remains visible through Run Ledger + attention and retained-worktree ownership instead of appearing again as intake + backlog. ## Current Non-Goals diff --git a/docs/spec/runtime.md b/docs/spec/runtime.md index 9e4123ed8..524c132a0 100644 --- a/docs/spec/runtime.md +++ b/docs/spec/runtime.md @@ -707,6 +707,11 @@ After a process restart, recent-run history, active lease ownership, retained po currently owns it. A retained worktree for that same issue must be projected as retained attention, not neutral cleanup-only hygiene, so monitors do not need to parse `history_lanes` to discover a human-required terminal outcome. +- If that same issue still carries the service queue label plus the configured + `needs_attention_label`, the terminal Run Ledger attention outcome must own the + operator projection. Status must not also render the issue as an intake queue + candidate, because the queue label is then a stale echo of the retained terminal + lane rather than dispatchable backlog. - If Linear still has `decodex:active:` on an issue that also remains queued, but the local runtime cannot prove a matching active lease, status must classify the queued row as blocked with reason `linear_active_label_present`; it must not treat the issue as ready intake. If the retained marker or private execution event rows for that run are missing, status must surface `evidence_missing` in the recovery details. If the retained worktree has tracked changes, that dirty worktree remains owned by queued recovery/attention instead of being hidden as cleanup-only state. - Operator status snapshots must expose worktree provenance in both JSON and human text output. A cleanup-only worktree with `provenance_source = "legacy_unknown"` must set