fix code queue mgr trace summary contract
This commit is contained in:
@@ -3,7 +3,7 @@
|
||||
use chrono::{DateTime, SecondsFormat, Utc};
|
||||
use postgres::{Client, NoTls};
|
||||
use serde_json::{json, Map, Value};
|
||||
use std::collections::{HashSet, VecDeque};
|
||||
use std::collections::{BTreeSet, HashSet, VecDeque};
|
||||
use std::env;
|
||||
use std::error::Error;
|
||||
use std::fs::{create_dir_all, OpenOptions};
|
||||
@@ -641,6 +641,139 @@ fn output_max_seq(task: &TaskMeta) -> i64 {
|
||||
task.last_output_seq.max(from_output)
|
||||
}
|
||||
|
||||
fn trace_countable_outputs(task: &TaskMeta) -> Vec<Value> {
|
||||
output_array(task)
|
||||
.into_iter()
|
||||
.filter(|item| !(item.get("channel").and_then(Value::as_str) == Some("system") && item.get("method").and_then(Value::as_str) == Some("enqueue")))
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn configured_step_count(task: &TaskMeta) -> Option<i64> {
|
||||
task.task_json.as_ref().and_then(|value| value.get("stepCount")).and_then(Value::as_i64)
|
||||
}
|
||||
|
||||
fn raw_trace_step_count(task: &TaskMeta) -> i64 {
|
||||
let output_count = trace_countable_outputs(task).len() as i64;
|
||||
let output_count = if output_count > 0 { output_count } else { task.output_count };
|
||||
configured_step_count(task).unwrap_or(output_count).max(output_count)
|
||||
}
|
||||
|
||||
fn trace_stats_contract(scope_id: &str, step_count: i64, output_max_seq: i64) -> Value {
|
||||
json!({
|
||||
"scopeId": scope_id,
|
||||
"source": "oa-event-flow",
|
||||
"sourceHint": "raw-trace-fallback",
|
||||
"stepCount": step_count,
|
||||
"llmStepCount": 0,
|
||||
"toolCallCount": step_count,
|
||||
"editedFileCount": 0,
|
||||
"commandCount": 0,
|
||||
"firstSeq": if step_count > 0 { json!(1) } else { Value::Null },
|
||||
"lastSeq": if output_max_seq > 0 { json!(output_max_seq) } else { Value::Null },
|
||||
"computedAt": now_iso(),
|
||||
"incomplete": true
|
||||
})
|
||||
}
|
||||
|
||||
fn trace_execution_contract(scope_id: &str, step_count: i64, output_max_seq: i64) -> Value {
|
||||
let trace_stats = if step_count > 0 || output_max_seq > 0 {
|
||||
trace_stats_contract(scope_id, step_count, output_max_seq)
|
||||
} else {
|
||||
Value::Null
|
||||
};
|
||||
json!({
|
||||
"stepCount": step_count,
|
||||
"llmStepCount": 0,
|
||||
"toolCallCount": step_count,
|
||||
"editedFileCount": 0,
|
||||
"commandCount": 0,
|
||||
"traceStats": trace_stats,
|
||||
"statsSource": if step_count > 0 || output_max_seq > 0 { "raw-trace-fallback" } else { "raw-trace-empty" },
|
||||
"traceStatsState": if step_count > 0 || output_max_seq > 0 { "degraded" } else { "empty" },
|
||||
"traceStatsReason": if step_count > 0 || output_max_seq > 0 { "oa-event-flow-stats-unavailable-raw-trace-present" } else { "no-trace-steps-yet" },
|
||||
"statsUnavailable": true,
|
||||
"statsSyncing": step_count > 0 || output_max_seq > 0,
|
||||
"rawTraceStepCount": step_count
|
||||
})
|
||||
}
|
||||
|
||||
fn attempt_index_from_value(value: &Value) -> Option<i64> {
|
||||
value
|
||||
.get("index")
|
||||
.or_else(|| value.get("attempt"))
|
||||
.or_else(|| value.get("attemptIndex"))
|
||||
.and_then(Value::as_i64)
|
||||
.filter(|index| *index > 0)
|
||||
}
|
||||
|
||||
fn output_attempt_index(task: &TaskMeta, item: &Value) -> i64 {
|
||||
if let Some(index) = item
|
||||
.get("attempt")
|
||||
.or_else(|| item.get("attemptIndex"))
|
||||
.and_then(Value::as_i64)
|
||||
.filter(|index| *index > 0)
|
||||
{
|
||||
return index;
|
||||
}
|
||||
let seq = seq_from_output(item);
|
||||
for attempt in attempts_array(task) {
|
||||
let Some(index) = attempt_index_from_value(&attempt) else { continue };
|
||||
let start = attempt.get("outputStartSeq").and_then(Value::as_i64).unwrap_or(i64::MIN);
|
||||
let end = attempt.get("outputEndSeq").and_then(Value::as_i64).unwrap_or(i64::MAX);
|
||||
if seq >= start && seq <= end {
|
||||
return index;
|
||||
}
|
||||
}
|
||||
task.current_attempt.max(1)
|
||||
}
|
||||
|
||||
fn attempt_indexes_for_trace(task: &TaskMeta, outputs: &[Value]) -> Vec<i64> {
|
||||
let mut indexes = BTreeSet::new();
|
||||
for attempt in attempts_array(task) {
|
||||
if let Some(index) = attempt_index_from_value(&attempt) {
|
||||
indexes.insert(index);
|
||||
}
|
||||
}
|
||||
if task.current_attempt > 0 {
|
||||
indexes.insert(task.current_attempt);
|
||||
}
|
||||
for item in outputs {
|
||||
indexes.insert(output_attempt_index(task, item));
|
||||
}
|
||||
indexes.into_iter().collect()
|
||||
}
|
||||
|
||||
fn attempt_trace_window(task: &TaskMeta, outputs: &[Value], index: i64) -> Value {
|
||||
let existing = attempts_array(task)
|
||||
.into_iter()
|
||||
.find(|attempt| attempt_index_from_value(attempt) == Some(index));
|
||||
let attempt_outputs: Vec<&Value> = outputs.iter().filter(|item| output_attempt_index(task, item) == index).collect();
|
||||
let step_count = attempt_outputs.len() as i64;
|
||||
let output_max = attempt_outputs.iter().map(|item| seq_from_output(item)).max().unwrap_or(0);
|
||||
let scope_id = format!("task:{}:attempt:{}", task.id, index);
|
||||
let mut record = existing.and_then(|value| value.as_object().cloned()).unwrap_or_else(Map::new);
|
||||
record.insert("index".to_string(), json!(index));
|
||||
record.entry("mode".to_string()).or_insert_with(|| {
|
||||
if index == task.current_attempt {
|
||||
task.current_mode.as_ref().map(|value| json!(value)).unwrap_or(Value::Null)
|
||||
} else {
|
||||
Value::Null
|
||||
}
|
||||
});
|
||||
record.insert("stepCount".to_string(), json!(step_count));
|
||||
record.insert("retainedStepCount".to_string(), json!(step_count));
|
||||
record.insert("outputMaxSeq".to_string(), json!(output_max));
|
||||
record.insert("traceStats".to_string(), if step_count > 0 { trace_stats_contract(&scope_id, step_count, output_max) } else { Value::Null });
|
||||
record.insert("statsSource".to_string(), json!(if step_count > 0 { "raw-trace-fallback" } else { "raw-trace-empty" }));
|
||||
record.insert("traceStatsState".to_string(), json!(if step_count > 0 { "degraded" } else { "empty" }));
|
||||
record.insert("traceStatsReason".to_string(), json!(if step_count > 0 { "oa-event-flow-stats-unavailable-raw-trace-present" } else { "no-trace-steps-yet" }));
|
||||
record.insert("statsUnavailable".to_string(), json!(true));
|
||||
record.insert("statsSyncing".to_string(), json!(step_count > 0));
|
||||
record.insert("rawTraceStepCount".to_string(), json!(step_count));
|
||||
record.insert("execution".to_string(), trace_execution_contract(&scope_id, step_count, output_max));
|
||||
Value::Object(record)
|
||||
}
|
||||
|
||||
fn task_scheduler_heartbeat(task: &TaskMeta) -> Option<Value> {
|
||||
task.task_json
|
||||
.as_ref()
|
||||
@@ -912,6 +1045,9 @@ fn task_list_response(_state: &AppState, task: &TaskMeta, lite: bool) -> Value {
|
||||
let final_text = final_response(task);
|
||||
let agent_port = code_agent_port(&task.model);
|
||||
let preview_limit = if lite { 360 } else { 2000 };
|
||||
let raw_step_count = raw_trace_step_count(task);
|
||||
let execution = trace_execution_contract(&format!("task:{}", task.id), raw_step_count, output_max_seq(task));
|
||||
let trace_stats = execution.get("traceStats").cloned().unwrap_or(Value::Null);
|
||||
json!({
|
||||
"id": task.id,
|
||||
"queueId": task.queue_id,
|
||||
@@ -927,10 +1063,16 @@ fn task_list_response(_state: &AppState, task: &TaskMeta, lite: bool) -> Value {
|
||||
"displayPromptChars": display_prompt.chars().count(),
|
||||
"promptEditable": queued_prompt_editable(task),
|
||||
"finalResponseChars": final_text.chars().count(),
|
||||
"stepCount": task.task_json.as_ref().and_then(|value| value.get("stepCount")).and_then(Value::as_i64).unwrap_or(task.last_output_seq),
|
||||
"stepCount": raw_step_count,
|
||||
"llmStepCount": task.task_json.as_ref().and_then(|value| value.get("llmStepCount")).and_then(Value::as_i64).unwrap_or(task.last_output_seq),
|
||||
"traceStats": Value::Null,
|
||||
"statsSource": "code-queue-mgr-rust",
|
||||
"traceStats": trace_stats,
|
||||
"statsSource": execution.get("statsSource").cloned().unwrap_or_else(|| json!("raw-trace-empty")),
|
||||
"traceStatsState": execution.get("traceStatsState").cloned().unwrap_or_else(|| json!("empty")),
|
||||
"traceStatsReason": execution.get("traceStatsReason").cloned().unwrap_or_else(|| json!("no-trace-steps-yet")),
|
||||
"statsUnavailable": true,
|
||||
"statsSyncing": raw_step_count > 0,
|
||||
"rawTraceStepCount": raw_step_count,
|
||||
"execution": execution,
|
||||
"summaryOnly": true,
|
||||
"referenceTaskIds": task.reference_task_ids,
|
||||
"referenceInjection": task.reference_injection,
|
||||
@@ -986,6 +1128,14 @@ fn task_meta_response(state: &AppState, task: &TaskMeta) -> Value {
|
||||
} else {
|
||||
task.base_prompt.clone()
|
||||
};
|
||||
let trace_outputs = trace_countable_outputs(task);
|
||||
let raw_step_count = raw_trace_step_count(task);
|
||||
let execution = trace_execution_contract(&format!("task:{}", task.id), raw_step_count, output_max_seq(task));
|
||||
let trace_stats = execution.get("traceStats").cloned().unwrap_or(Value::Null);
|
||||
let attempts = attempt_indexes_for_trace(task, &trace_outputs)
|
||||
.into_iter()
|
||||
.map(|index| attempt_trace_window(task, &trace_outputs, index))
|
||||
.collect::<Vec<_>>();
|
||||
map.insert("prompt".to_string(), json!(task.prompt));
|
||||
map.insert("basePrompt".to_string(), json!(task.base_prompt));
|
||||
map.insert("displayPrompt".to_string(), json!(display_prompt));
|
||||
@@ -995,7 +1145,7 @@ fn task_meta_response(state: &AppState, task: &TaskMeta) -> Value {
|
||||
"promptHistory".to_string(),
|
||||
task.task_json.as_ref().and_then(|value| value.get("promptHistory")).cloned().unwrap_or_else(|| json!([])),
|
||||
);
|
||||
map.insert("attempts".to_string(), json!(attempts_array(task)));
|
||||
map.insert("attempts".to_string(), json!(attempts));
|
||||
map.insert(
|
||||
"nextMode".to_string(),
|
||||
task.task_json.as_ref().and_then(|value| value.get("nextMode")).cloned().unwrap_or(Value::Null),
|
||||
@@ -1009,6 +1159,16 @@ fn task_meta_response(state: &AppState, task: &TaskMeta) -> Value {
|
||||
map.insert("output".to_string(), json!([]));
|
||||
map.insert("events".to_string(), json!([]));
|
||||
map.insert("summaryOnly".to_string(), json!(false));
|
||||
map.insert("stepCount".to_string(), json!(raw_step_count));
|
||||
map.insert("llmStepCount".to_string(), json!(task.task_json.as_ref().and_then(|value| value.get("llmStepCount")).and_then(Value::as_i64).unwrap_or(task.last_output_seq)));
|
||||
map.insert("traceStats".to_string(), trace_stats);
|
||||
map.insert("statsSource".to_string(), execution.get("statsSource").cloned().unwrap_or_else(|| json!("raw-trace-empty")));
|
||||
map.insert("traceStatsState".to_string(), execution.get("traceStatsState").cloned().unwrap_or_else(|| json!("empty")));
|
||||
map.insert("traceStatsReason".to_string(), execution.get("traceStatsReason").cloned().unwrap_or_else(|| json!("no-trace-steps-yet")));
|
||||
map.insert("statsUnavailable".to_string(), json!(true));
|
||||
map.insert("statsSyncing".to_string(), json!(raw_step_count > 0));
|
||||
map.insert("rawTraceStepCount".to_string(), json!(raw_step_count));
|
||||
map.insert("execution".to_string(), execution);
|
||||
base
|
||||
}
|
||||
|
||||
@@ -2010,6 +2170,7 @@ fn trace_step_detail(task: &TaskMeta, url: &RequestUrl) -> Value {
|
||||
|
||||
fn task_summary(task: &TaskMeta) -> Value {
|
||||
let outputs = output_array(task);
|
||||
let trace_outputs = trace_countable_outputs(task);
|
||||
let tool_outputs: Vec<Value> = outputs
|
||||
.iter()
|
||||
.filter(|item| matches!(item.get("channel").and_then(Value::as_str), Some("command" | "tool" | "diff" | "error")))
|
||||
@@ -2035,6 +2196,13 @@ fn task_summary(task: &TaskMeta) -> Value {
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
let agent_port = code_agent_port(&task.model);
|
||||
let raw_step_count = raw_trace_step_count(task);
|
||||
let execution = trace_execution_contract(&format!("task:{}", task.id), raw_step_count, output_max_seq(task));
|
||||
let trace_stats = execution.get("traceStats").cloned().unwrap_or(Value::Null);
|
||||
let attempts = attempt_indexes_for_trace(task, &trace_outputs)
|
||||
.into_iter()
|
||||
.map(|index| attempt_trace_window(task, &trace_outputs, index))
|
||||
.collect::<Vec<_>>();
|
||||
json!({
|
||||
"id": task.id,
|
||||
"queueId": task.queue_id,
|
||||
@@ -2068,7 +2236,7 @@ fn task_summary(task: &TaskMeta) -> Value {
|
||||
"referenceInjection": task.reference_injection,
|
||||
"lastAssistantMessage": last_assistant_message(task),
|
||||
"toolSummary": { "count": tool_outputs.len(), "returned": tool_outputs.len().saturating_sub(start), "limit": 160, "truncated": start > 0, "items": tool_items },
|
||||
"attempts": attempts_array(task),
|
||||
"attempts": attempts,
|
||||
"lastJudge": task.last_judge,
|
||||
"lastError": task.last_error,
|
||||
"cancelRequested": task.task_json.as_ref().and_then(|value| value.get("cancelRequested")).and_then(Value::as_bool).unwrap_or(false),
|
||||
@@ -2077,34 +2245,66 @@ fn task_summary(task: &TaskMeta) -> Value {
|
||||
"transcriptCount": outputs.len() + json_array_len(task.task_json.as_ref(), "promptHistory", 0) as usize,
|
||||
"transcriptMaxSeq": output_max_seq(task),
|
||||
"finalResponse": final_response(task),
|
||||
"stepCount": task.task_json.as_ref().and_then(|value| value.get("stepCount")).and_then(Value::as_i64).unwrap_or(task.last_output_seq),
|
||||
"stepCount": raw_step_count,
|
||||
"llmStepCount": task.task_json.as_ref().and_then(|value| value.get("llmStepCount")).and_then(Value::as_i64).unwrap_or(task.last_output_seq),
|
||||
"statsSource": "code-queue-mgr-rust-postgres"
|
||||
"traceStats": trace_stats,
|
||||
"statsSource": execution.get("statsSource").cloned().unwrap_or_else(|| json!("raw-trace-empty")),
|
||||
"traceStatsState": execution.get("traceStatsState").cloned().unwrap_or_else(|| json!("empty")),
|
||||
"traceStatsReason": execution.get("traceStatsReason").cloned().unwrap_or_else(|| json!("no-trace-steps-yet")),
|
||||
"statsUnavailable": true,
|
||||
"statsSyncing": raw_step_count > 0,
|
||||
"rawTraceStepCount": raw_step_count,
|
||||
"execution": execution
|
||||
})
|
||||
}
|
||||
|
||||
fn trace_summary(task: &TaskMeta) -> Value {
|
||||
let outputs = output_array(task);
|
||||
let outputs = trace_countable_outputs(task);
|
||||
let step_count = raw_trace_step_count(task);
|
||||
let output_max_seq = output_max_seq(task);
|
||||
let execution = trace_execution_contract(&format!("task:{}", task.id), step_count, output_max_seq);
|
||||
let trace_stats = execution.get("traceStats").cloned().unwrap_or(Value::Null);
|
||||
let attempts = attempt_indexes_for_trace(task, &outputs)
|
||||
.into_iter()
|
||||
.map(|index| attempt_trace_window(task, &outputs, index))
|
||||
.collect::<Vec<_>>();
|
||||
json!({
|
||||
"id": task.id,
|
||||
"taskId": task.id,
|
||||
"queueId": task.queue_id,
|
||||
"status": task.status,
|
||||
"providerId": task.provider_id,
|
||||
"executionMode": task.execution_mode,
|
||||
"executionModeInfo": execution_mode_info(&task.execution_mode),
|
||||
"model": task.model,
|
||||
"stepCount": task.task_json.as_ref().and_then(|value| value.get("stepCount")).and_then(Value::as_i64).unwrap_or(outputs.len() as i64),
|
||||
"cwd": task.cwd,
|
||||
"reasoningEffort": task.reasoning_effort,
|
||||
"stepCount": step_count,
|
||||
"llmStepCount": 0,
|
||||
"toolCallCount": step_count,
|
||||
"editedFileCount": 0,
|
||||
"commandCount": 0,
|
||||
"retainedStepCount": outputs.len(),
|
||||
"outputMaxSeq": output_max_seq(task),
|
||||
"outputMaxSeq": output_max_seq,
|
||||
"schedulerHeartbeat": task_scheduler_heartbeat(task).unwrap_or(Value::Null),
|
||||
"statsSource": "code-queue-mgr-rust-postgres",
|
||||
"attempts": attempts_array(task),
|
||||
"traceStats": trace_stats,
|
||||
"statsSource": execution.get("statsSource").cloned().unwrap_or_else(|| json!("raw-trace-empty")),
|
||||
"traceStatsState": execution.get("traceStatsState").cloned().unwrap_or_else(|| json!("empty")),
|
||||
"traceStatsReason": execution.get("traceStatsReason").cloned().unwrap_or_else(|| json!("no-trace-steps-yet")),
|
||||
"statsUnavailable": true,
|
||||
"statsSyncing": step_count > 0,
|
||||
"rawTraceStepCount": step_count,
|
||||
"execution": execution,
|
||||
"attempts": attempts,
|
||||
"prompt": { "initialPreview": preview(&task.prompt, 1600), "basePreview": preview(&task.base_prompt, 1600), "chars": task.prompt.chars().count(), "lines": task.prompt.lines().count() },
|
||||
"lastAssistantMessage": last_assistant_message(task),
|
||||
"createdAt": task.created_at,
|
||||
"updatedAt": task.updated_at,
|
||||
"finalResponse": final_response(task),
|
||||
"lastJudge": task.last_judge,
|
||||
"lastError": task.last_error,
|
||||
"currentAttempt": task.current_attempt,
|
||||
"currentMode": task.current_mode,
|
||||
"maxAttempts": task.max_attempts,
|
||||
"startedAt": task.started_at,
|
||||
"finishedAt": task.finished_at,
|
||||
@@ -3038,4 +3238,58 @@ mod tests {
|
||||
Some("mgr-visible")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn trace_summary_contract_exposes_retry_window_and_raw_fallback() {
|
||||
let mut task = test_task("trace summary retry contract".to_string());
|
||||
task.id = "codex_trace_contract_rust".to_string();
|
||||
task.status = "running".to_string();
|
||||
task.current_attempt = 2;
|
||||
task.current_mode = Some("retry".to_string());
|
||||
task.max_attempts = 99;
|
||||
task.attempt_count = 1;
|
||||
task.last_output_seq = 21;
|
||||
task.task_json = Some(json!({
|
||||
"status": "running",
|
||||
"stepCount": Value::Null,
|
||||
"llmStepCount": Value::Null,
|
||||
"attempts": [
|
||||
{
|
||||
"index": 1,
|
||||
"mode": "initial",
|
||||
"startedAt": "2026-05-19T00:00:10.000Z",
|
||||
"finishedAt": "2026-05-19T00:02:00.000Z",
|
||||
"judge": { "decision": "retry", "reason": "try again" }
|
||||
}
|
||||
],
|
||||
"output": [
|
||||
{ "seq": 1, "at": "2026-05-19T00:00:00.000Z", "channel": "system", "method": "enqueue", "text": "queued" },
|
||||
{ "seq": 20, "attempt": 2, "at": "2026-05-19T00:06:00.000Z", "channel": "command", "method": "item/started", "text": "attempt 2 / 99\npnpm test" },
|
||||
{ "seq": 21, "attempt": 2, "at": "2026-05-19T00:06:20.000Z", "channel": "tool", "method": "item/completed", "text": "src/components/microservices/code-queue-mgr/src-rs/main.rs" }
|
||||
],
|
||||
"events": [],
|
||||
"promptHistory": []
|
||||
}));
|
||||
|
||||
let summary = trace_summary(&task);
|
||||
assert_eq!(summary.get("currentAttempt").and_then(Value::as_i64), Some(2));
|
||||
assert_eq!(summary.get("statsSource").and_then(Value::as_str), Some("raw-trace-fallback"));
|
||||
assert_eq!(summary.get("traceStatsState").and_then(Value::as_str), Some("degraded"));
|
||||
assert_eq!(
|
||||
summary.get("traceStatsReason").and_then(Value::as_str),
|
||||
Some("oa-event-flow-stats-unavailable-raw-trace-present")
|
||||
);
|
||||
assert_eq!(summary.get("statsUnavailable").and_then(Value::as_bool), Some(true));
|
||||
assert_eq!(summary.get("statsSyncing").and_then(Value::as_bool), Some(true));
|
||||
assert!(summary.get("stepCount").and_then(Value::as_i64).unwrap_or(0) > 0);
|
||||
|
||||
let attempts = summary.get("attempts").and_then(Value::as_array).expect("attempts array");
|
||||
let attempt2 = attempts
|
||||
.iter()
|
||||
.find(|attempt| attempt.get("index").and_then(Value::as_i64) == Some(2))
|
||||
.expect("attempt 2 visible");
|
||||
assert_eq!(attempt2.get("stepCount").and_then(Value::as_i64), Some(2));
|
||||
assert_eq!(attempt2.get("statsSource").and_then(Value::as_str), Some("raw-trace-fallback"));
|
||||
assert_eq!(attempt2.pointer("/execution/traceStatsState").and_then(Value::as_str), Some("degraded"));
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user