Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
47 changes: 35 additions & 12 deletions apps/decodex/src/orchestrator/status.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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()),
Expand Down Expand Up @@ -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::<HashSet<_>>();

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 {
Expand Down Expand Up @@ -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",
Expand All @@ -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",
Expand Down
70 changes: 70 additions & 0 deletions apps/decodex/src/orchestrator/tests/operator/status/queue.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
5 changes: 5 additions & 0 deletions docs/reference/operator-control-plane.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
5 changes: 5 additions & 0 deletions docs/spec/runtime.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:<service-id>` 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
Expand Down