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
23 changes: 21 additions & 2 deletions apps/decodex/src/orchestrator/operator_dashboard.html
Original file line number Diff line number Diff line change
Expand Up @@ -4278,7 +4278,10 @@ <h2 id="recent-title">Run History</h2>
if (candidate.classification === "closed") {
return "tone-muted";
}
if (candidate.reason === "issue_needs_attention") {
if (
candidate.reason === "issue_needs_attention" ||
candidate.reason === "linear_active_label_present"
) {
return "tone-blocked";
}
return "tone-wait";
Expand Down Expand Up @@ -4342,6 +4345,8 @@ <h2 id="recent-title">Run History</h2>
return "Automation is disabled for this issue.";
case "issue_needs_attention":
return "";
case "linear_active_label_present":
return "Active ownership is still present; reconcile before dispatch.";
case "non_startable_state":
return `${candidate.state} cannot start.`;
case "terminal_state":
Expand Down Expand Up @@ -4389,6 +4394,9 @@ <h2 id="recent-title">Run History</h2>
if (candidate.reason === "issue_needs_attention") {
return "needs attention";
}
if (candidate.reason === "linear_active_label_present") {
return "active label present";
}
if (candidate.attention?.retry_budget_attempt_count != null) {
return "auto retry paused";
}
Expand Down Expand Up @@ -4431,6 +4439,7 @@ <h2 id="recent-title">Run History</h2>
return Boolean(
candidate.attention ||
candidate.reason === "issue_needs_attention" ||
candidate.reason === "linear_active_label_present" ||
candidate.reason === "retry_budget_exhausted",
);
}
Expand Down Expand Up @@ -4950,6 +4959,9 @@ <h2 id="recent-title">Run History</h2>
if (reason === "needs_attention_label") {
return "needs-attention label set";
}
if (reason === "linear_active_label_present") {
return "active label present";
}

return `blocked by ${sentenceToken(reason)}`;
}
Expand Down Expand Up @@ -5513,6 +5525,8 @@ <h2 id="recent-title">Run History</h2>
return "start identity unavailable";
case "process_start_identity_mismatch":
return "process identity changed";
case "process_identity_mismatch":
return "process identity changed";
default:
return reason ? reason.replaceAll("_", " ") : "unknown";
}
Expand All @@ -5528,6 +5542,8 @@ <h2 id="recent-title">Run History</h2>
return "protocol active";
case "process_stopped":
return "process stopped";
case "process_identity_mismatch":
return "process identity mismatch";
case "not_running":
return "not running";
default:
Expand All @@ -5551,6 +5567,8 @@ <h2 id="recent-title">Run History</h2>
return "no lease; protocol active";
case "process_stopped":
return "no lease; stopped process";
case "process_identity_mismatch":
return "no lease; process identity mismatch";
default:
return "no lease";
}
Expand Down Expand Up @@ -9384,7 +9402,8 @@ <h4>${escapeHtml(title)}</h4>
const attentionWorktree = candidate.attention?.worktree_path;

return (
candidate.reason === "issue_needs_attention" &&
(candidate.reason === "issue_needs_attention" ||
candidate.reason === "linear_active_label_present") &&
(attentionWorktree === worktree.worktree_path ||
candidate.issue_id === worktree.issue_id ||
candidate.issue_identifier === worktree.issue_id)
Expand Down
113 changes: 106 additions & 7 deletions apps/decodex/src/orchestrator/status.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,10 @@ use crate::pull_request::{self, PullRequestLandingGateView};
use crate::worktree;
use crate::worktree::MergedWorktreeCleanupDebt;

const QUEUE_REASON_LINEAR_ACTIVE_LABEL_PRESENT: &str = "linear_active_label_present";
const ATTENTION_ERROR_EVIDENCE_MISSING: &str = "evidence_missing";
const EXECUTION_LIVENESS_PROCESS_IDENTITY_MISMATCH: &str = "process_identity_mismatch";

#[derive(Clone, Copy, Debug, Eq, PartialEq)]
enum RetainedCloseoutPrMergeGate {
Merged,
Expand Down Expand Up @@ -673,7 +677,10 @@ fn worktree_has_queued_attention_owner(
snapshot: &OperatorStatusSnapshot,
) -> bool {
snapshot.queued_candidates.iter().any(|candidate| {
candidate.reason == "issue_needs_attention"
matches!(
candidate.reason.as_str(),
"issue_needs_attention" | QUEUE_REASON_LINEAR_ACTIVE_LABEL_PRESENT
)
&& (candidate
.attention
.as_ref()
Expand Down Expand Up @@ -1807,6 +1814,13 @@ where
if state_store.issue_has_active_shared_claim(project.service_id(), &issue.id)? {
return Ok(("claimed", "shared_claim_present"));
}
if tracker::issue_has_label_with_server_confirmation(
tracker,
issue,
&tracker::automation_active_label(project.service_id()),
)? {
return Ok(("blocked", QUEUE_REASON_LINEAR_ACTIVE_LABEL_PRESENT));
}
if tracker_policy.terminal_states().iter().any(|state| state == &issue.state.name) {
return Ok(("closed", "terminal_state"));
}
Expand Down Expand Up @@ -1849,7 +1863,12 @@ fn operator_queued_issue_attention_status<T>(
where
T: IssueTracker,
{
if !matches!(reason, "issue_needs_attention" | "retry_budget_exhausted") {
if !matches!(
reason,
"issue_needs_attention"
| "retry_budget_exhausted"
| QUEUE_REASON_LINEAR_ACTIVE_LABEL_PRESENT
) {
return Ok(None);
}

Expand All @@ -1861,12 +1880,27 @@ where
let retry_budget_attempts = state_retry_attempts.max(marker_retry_attempts);
let retry_budget_attempt_count = (retry_budget_attempts > 0).then_some(retry_budget_attempts);
let retry_budget_max_attempts = i64::from(workflow.frontmatter().execution().max_attempts());
let auto_retry_blocked_reason =
(reason == "issue_needs_attention").then(|| String::from("needs_attention_label"));
let auto_retry_blocked_reason = match reason {
"issue_needs_attention" => Some(String::from("needs_attention_label")),
QUEUE_REASON_LINEAR_ACTIVE_LABEL_PRESENT => {
Some(String::from(QUEUE_REASON_LINEAR_ACTIVE_LABEL_PRESENT))
},
_ => None,
};
let attention_record =
operator_queued_issue_latest_attention_record(tracker, project, state_store, issue);
let attention_error_class =
attention_record.as_ref().and_then(|record| record.error_class.clone());
let private_evidence_missing = operator_queued_issue_private_evidence_missing(
project,
state_store,
issue,
marker.as_ref(),
reason,
)?;
let attention_error_class = if private_evidence_missing {
Some(String::from(ATTENTION_ERROR_EVIDENCE_MISSING))
} else {
attention_record.as_ref().and_then(|record| record.error_class.clone())
};
let attention_next_action =
attention_record.as_ref().and_then(|record| record.next_action.clone());
let attempt_status = marker
Expand Down Expand Up @@ -1925,6 +1959,30 @@ where
}))
}

fn operator_queued_issue_private_evidence_missing(
project: &ServiceConfig,
state_store: &StateStore,
issue: &TrackerIssue,
marker: Option<&RunActivityMarker>,
reason: &str,
) -> crate::prelude::Result<bool> {
if reason != QUEUE_REASON_LINEAR_ACTIVE_LABEL_PRESENT {
return Ok(false);
}

let Some(marker) = marker else {
return Ok(true);
};
let events = state_store.list_private_execution_events(
project.service_id(),
&issue.id,
marker.run_id(),
marker.attempt_number(),
)?;

Ok(events.is_empty())
}

fn operator_queued_issue_latest_attention_record<T>(
tracker: &T,
project: &ServiceConfig,
Expand Down Expand Up @@ -2003,6 +2061,33 @@ fn operator_queued_issue_attention_summary(
worktree_has_tracked_changes: bool,
attention_error_class: Option<&str>,
) -> String {
if reason == QUEUE_REASON_LINEAR_ACTIVE_LABEL_PRESENT {
if worktree_has_tracked_changes {
return String::from(
"Linear active ownership is still present with retained worktree changes; inspect the patch and reconcile the lane before dispatch.",
);
}
if attention_error_class == Some(ATTENTION_ERROR_EVIDENCE_MISSING) {
return if marker.is_some() {
String::from(
"Linear active ownership is still present but private execution evidence is missing; inspect the retained marker and reconcile before dispatch.",
)
} else {
String::from(
"Linear active ownership is still present but the retained marker or private execution evidence is missing; reconcile before dispatch.",
)
};
}
if marker.is_some() {
return String::from(
"Linear active ownership is still present alongside queue intake; inspect the retained marker before dispatch.",
);
}

return String::from(
"Linear active ownership is still present without a matching local active lease; reconcile before dispatch.",
);
}
if worktree_has_tracked_changes {
if retry_budget_attempts > 0 {
return format!(
Expand Down Expand Up @@ -4275,6 +4360,10 @@ fn operator_run_execution_liveness(
return String::from("process_alive");
}
if timing.process_alive == Some(false) {
if process_liveness_reason_is_identity_mismatch(timing.process_liveness_reason.as_deref()) {
return String::from(EXECUTION_LIVENESS_PROCESS_IDENTITY_MISMATCH);
}

return String::from("process_stopped");
}
if matches!(app_server_state.thread_status.as_deref(), Some("active"))
Expand All @@ -4289,6 +4378,10 @@ fn operator_run_execution_liveness(
String::from("not_captured")
}

fn process_liveness_reason_is_identity_mismatch(reason: Option<&str>) -> bool {
matches!(reason, Some("host_boot_id_mismatch" | "process_start_identity_mismatch"))
}

fn operator_run_child_agent_activity(
marker: Option<&RunActivityMarker>,
now_unix_epoch: i64,
Expand Down Expand Up @@ -5015,7 +5108,10 @@ fn rendered_worktree_role<'a>(
return "post_review_lane";
}
if snapshot.queued_candidates.iter().any(|candidate| {
candidate.reason == "issue_needs_attention"
matches!(
candidate.reason.as_str(),
"issue_needs_attention" | QUEUE_REASON_LINEAR_ACTIVE_LABEL_PRESENT
)
&& (candidate
.attention
.as_ref()
Expand Down Expand Up @@ -5453,6 +5549,9 @@ fn operator_run_queue_lease_summary(run: &OperatorRunStatus) -> String {
"thread_active" => String::from("not_held (thread_active keeps lane visible)"),
"protocol_observed" => String::from("not_held (protocol_observed keeps lane visible)"),
"process_stopped" => String::from("not_held (process_stopped needs attention)"),
EXECUTION_LIVENESS_PROCESS_IDENTITY_MISMATCH => {
String::from("not_held (process_identity_mismatch needs attention)")
},
_ => String::from("not_held"),
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1412,6 +1412,7 @@ fn operator_dashboard_prioritizes_needs_attention_reason_over_retry_count() {
"facts.push([\"Auto retry\", autoRetryBlockedReasonText(attention.auto_retry_blocked_reason)]);"
));
assert!(response.contains("return \"needs-attention label set\";"));
assert!(response.contains("return \"active label present\";"));
assert!(reason_text.contains("return \"auto retry paused\";"));
assert!(response.contains("function queuedCandidateInlineReason(candidate)"));
assert!(response.contains("displayTextRepeats(reason, sentenceToken(candidate.attention.attention_error_class))"));
Expand Down
Loading