From 954c0ca6413893f431fc53668c9932c10cf69da6 Mon Sep 17 00:00:00 2001 From: Yvette Carlisle Date: Mon, 11 May 2026 21:48:06 +0800 Subject: [PATCH 1/5] {"schema":"decodex/commit/1","summary":"Polish operator dashboard controls","authority":"manual"} --- apps/decodex/src/agent/app_server.rs | 39 +- apps/decodex/src/agent/app_server/tests.rs | 51 ++ .../src/orchestrator/operator_dashboard.html | 744 ++++++++++++++---- .../decodex/src/orchestrator/operator_http.rs | 196 +++-- .../tests/operator/status/dashboard.rs | 163 +++- .../tests/operator/status/http.rs | 252 ++++-- docs/reference/operator-control-plane.md | 13 +- 7 files changed, 1122 insertions(+), 336 deletions(-) diff --git a/apps/decodex/src/agent/app_server.rs b/apps/decodex/src/agent/app_server.rs index a432bdf..757016e 100644 --- a/apps/decodex/src/agent/app_server.rs +++ b/apps/decodex/src/agent/app_server.rs @@ -1110,13 +1110,22 @@ fn protocol_account_detail(payload_value: Option<&Value>) -> Option { &[ &["params", "planType"], &["params", "chatgptPlanType"], + &["params", "rateLimits", "planType"], &["planType"], &["chatgptPlanType"], + &["rateLimits", "planType"], ], ); let status = string_at_paths( value, - &[&["params", "status"], &["params", "refreshStatus"], &["status"], &["refreshStatus"]], + &[ + &["params", "status"], + &["params", "refreshStatus"], + &["params", "rateLimits", "rateLimitReachedType"], + &["status"], + &["refreshStatus"], + &["rateLimits", "rateLimitReachedType"], + ], ); match (plan, status) { @@ -1207,17 +1216,15 @@ fn thread_status_waiting_reason(payload_value: Option<&Value>) -> Option None } -fn protocol_rate_limit_status(event_type: &str, payload: &str) -> Option { +fn protocol_rate_limit_status(_event_type: &str, payload: &str) -> Option { let payload_value = serde_json::from_str::(payload).ok()?; - find_string_field(&payload_value, &["rateLimitReachedType", "rate_limit_reached_type"]) - .or_else(|| { + find_string_field(&payload_value, &["rateLimitReachedType", "rate_limit_reached_type"]).or_else( + || { find_string_field(&payload_value, &["codexErrorInfo", "codex_error_info"]) .filter(|value| value.to_ascii_lowercase().contains("limit")) - }) - .or_else(|| { - event_type.to_ascii_lowercase().contains("ratelimit").then(|| event_type.to_owned()) - }) + }, + ) } fn child_tool_call_event( @@ -1519,9 +1526,9 @@ fn find_string_field(value: &Value, keys: &[&str]) -> Option { Value::Object(entries) => { for (key, nested) in entries { if keys.iter().any(|candidate| *candidate == key) - && let Some(text) = nested.as_str() + && let Some(text) = string_like_json_value(nested) { - return Some(text.to_owned()); + return Some(text); } } @@ -1532,6 +1539,18 @@ fn find_string_field(value: &Value, keys: &[&str]) -> Option { } } +fn string_like_json_value(value: &Value) -> Option { + match value { + Value::String(text) if !text.is_empty() => Some(text.clone()), + Value::Number(number) => Some(number.to_string()), + Value::Bool(value) => Some(value.to_string()), + Value::Object(entries) => ["kind", "type"] + .iter() + .find_map(|key| entries.get(*key).and_then(string_like_json_value)), + _ => None, + } +} + fn json_number_to_i64(value: &Value) -> Option { value.as_i64().or_else(|| value.as_u64().and_then(|number| i64::try_from(number).ok())) } diff --git a/apps/decodex/src/agent/app_server/tests.rs b/apps/decodex/src/agent/app_server/tests.rs index 14b6c8f..3fa9924 100644 --- a/apps/decodex/src/agent/app_server/tests.rs +++ b/apps/decodex/src/agent/app_server/tests.rs @@ -1118,6 +1118,57 @@ fn recorder_summarizes_high_value_protocol_activity() { })); } +#[test] +fn recorder_summarizes_v2_account_rate_limit_notifications() { + let temp_dir = TempDir::new().expect("tempdir should create"); + let state_store = StateStore::open_in_memory().expect("state store should open"); + let marker_path = temp_dir.path().to_path_buf(); + let mut recorder = RunRecorder::new(&state_store, "run-1", 1, Some(&marker_path)); + + recorder + .record( + "account/rateLimits/updated", + r#"{"method":"account/rateLimits/updated","params":{"rateLimits":{"planType":"pro","rateLimitReachedType":"workspace_member_usage_limit_reached","primary":{"usedPercent":100}}}}"#, + ) + .expect("rate limit protocol event should record"); + + let marker = state::read_run_activity_marker_snapshot(temp_dir.path()) + .expect("marker snapshot should load") + .expect("marker snapshot should exist"); + let summary = marker.protocol_activity().expect("protocol activity should be captured"); + let event = summary.recent_events.first().expect("recent rate limit event should render"); + + assert_eq!(summary.rate_limit_status.as_deref(), Some("workspace_member_usage_limit_reached")); + assert_eq!(event.category, "rate_limit"); + assert_eq!(event.detail.as_deref(), Some("pro/workspace_member_usage_limit_reached")); +} + +#[test] +fn recorder_does_not_treat_rate_limit_update_method_as_limit_status() { + let temp_dir = TempDir::new().expect("tempdir should create"); + let state_store = StateStore::open_in_memory().expect("state store should open"); + let marker_path = temp_dir.path().to_path_buf(); + let mut recorder = RunRecorder::new(&state_store, "run-1", 1, Some(&marker_path)); + + recorder + .record( + "account/rateLimits/updated", + r#"{"method":"account/rateLimits/updated","params":{"rateLimits":{"planType":"pro","rateLimitReachedType":null,"primary":{"usedPercent":12}}}}"#, + ) + .expect("rate limit update event should record"); + + let marker = state::read_run_activity_marker_snapshot(temp_dir.path()) + .expect("marker snapshot should load") + .expect("marker snapshot should exist"); + let summary = marker.protocol_activity().expect("protocol activity should be captured"); + + assert_eq!(summary.rate_limit_status, None); + assert_eq!( + summary.recent_events.first().and_then(|event| event.detail.as_deref()), + Some("pro") + ); +} + #[test] fn recorder_summarizes_wrapped_account_protocol_activity() { let temp_dir = TempDir::new().expect("tempdir should create"); diff --git a/apps/decodex/src/orchestrator/operator_dashboard.html b/apps/decodex/src/orchestrator/operator_dashboard.html index 3eb0435..749fdc2 100644 --- a/apps/decodex/src/orchestrator/operator_dashboard.html +++ b/apps/decodex/src/orchestrator/operator_dashboard.html @@ -8,10 +8,9 @@ content="Delivery progress console for Decodex retained lanes." /> Decodex - + + +