diff --git a/ops/dashboard/src/nanobot_ops_dashboard/app.py b/ops/dashboard/src/nanobot_ops_dashboard/app.py index a86a1c2..4c9f987 100644 --- a/ops/dashboard/src/nanobot_ops_dashboard/app.py +++ b/ops/dashboard/src/nanobot_ops_dashboard/app.py @@ -1395,14 +1395,30 @@ def _discover_subagent_requests(cfg: DashboardConfig, stale_after_seconds: int = else: state_root = local_state_root selected_source = 'local' - source_skew_state = 'skewed' if canonical_has_activity and local_has_activity and canonical_state_root != local_state_root else 'aligned' - source_skew_reasons = ['local_and_canonical_subagent_roots_present'] if source_skew_state == 'skewed' else [] + dual_roots_available = bool(canonical_has_activity and local_has_activity and canonical_state_root != local_state_root) + if dual_roots_available and selected_source == 'eeepc': + source_skew_state = 'dual_roots_available' + source_skew_severity = 'informational' + source_skew_reasons = ['local_and_canonical_subagent_roots_present'] + elif dual_roots_available: + source_skew_state = 'skewed' + source_skew_severity = 'warning' + source_skew_reasons = ['local_and_canonical_subagent_roots_present'] + else: + source_skew_state = 'aligned' + source_skew_severity = 'none' + source_skew_reasons = [] request_dir = state_root / 'subagents' / 'requests' result_dir = state_root / 'subagents' / 'results' now = time.time() requests: list[dict] = [] if canonical_remote and isinstance(remote_payload, dict): requests = [dict(item) for item in remote_payload.get('requests', []) if isinstance(item, dict)] + for request in requests: + raw_status = request.get('request_status') or request.get('status') or 'queued' + request.setdefault('raw_request_status', raw_status) + request.setdefault('request_status', raw_status) + request.setdefault('effective_status', request.get('status') or raw_status) elif request_dir.exists(): for path in sorted(request_dir.glob('*.json'), key=lambda p: p.stat().st_mtime, reverse=True): payload = _json_file(path) @@ -1424,6 +1440,8 @@ def _discover_subagent_requests(cfg: DashboardConfig, stale_after_seconds: int = 'profile': payload.get('profile'), 'status': status, 'request_status': status, + 'raw_request_status': status, + 'effective_status': status, 'age_seconds': age, 'source_artifact': payload.get('source_artifact'), }) @@ -1495,13 +1513,17 @@ def _discover_subagent_requests(cfg: DashboardConfig, stale_after_seconds: int = or (results_by_task_id.get(str(request.get('task_id'))) if request.get('task_id') else None) ) if isinstance(materialized_result, dict): - request['status'] = str(materialized_result.get('status') or 'completed').lower() + resolved_status = str(materialized_result.get('status') or 'completed').lower() + request.setdefault('raw_request_status', request.get('request_status') or request.get('status') or 'queued') + request['status'] = resolved_status + request['effective_status'] = resolved_status request['materialized_result_path'] = materialized_result.get('path') - request['materialized_result_status'] = materialized_result.get('status') + request['materialized_result_status'] = resolved_status if materialized_result.get('terminal_reason'): request['terminal_reason'] = materialized_result.get('terminal_reason') elif request.get('request_status') in {'queued', 'pending'} and request.get('age_seconds', 0) >= stale_after_seconds: request['status'] = 'stale' + request['effective_status'] = 'stale' stale_count = sum(1 for item in requests if item.get('request_status') in {'queued', 'pending'} and not item.get('materialized_result_path') and item.get('age_seconds', 0) >= stale_after_seconds) queued_count = sum(1 for item in requests if item.get('request_status') in {'queued', 'pending'} and not item.get('materialized_result_path')) blocked_count = sum(1 for item in results if str(item.get('status') or '').lower() in {'blocked', 'terminal_blocked'}) @@ -1528,8 +1550,32 @@ def _result_age_seconds(item: dict) -> int | None: if _result_age_seconds(item) is None or _result_age_seconds(item) >= 6 * 60 * 60 ) fresh_result_count = max(0, result_count - stale_result_count) + summary = { + 'total_requests': len(requests), + 'stale_request_count': stale_count, + 'queued_request_count': queued_count, + 'result_count': result_count, + 'blocked_result_count': blocked_count, + 'stale_result_count': stale_result_count, + 'fresh_result_count': fresh_result_count, + 'latest_result_age_seconds': (results[0].get('age_seconds') if results else ((rollup or {}).get('latest_result') or {}).get('age_seconds') if isinstance((rollup or {}).get('latest_result'), dict) else None), + 'freshness_state': 'fresh' if fresh_result_count else ('stale' if stale_result_count else state), + 'freshness_window_seconds': 6 * 60 * 60, + 'sources': [selected_source] if requests or results or isinstance(rollup, dict) else [], + 'state': state, + 'reason': reason, + } return { 'schema_version': 'subagent-visibility-v1', + 'state': summary['state'], + 'status': summary['state'], + 'total_requests': summary['total_requests'], + 'stale_request_count': summary['stale_request_count'], + 'queued_request_count': summary['queued_request_count'], + 'result_count': summary['result_count'], + 'blocked_result_count': summary['blocked_result_count'], + 'stale_result_count': summary['stale_result_count'], + 'fresh_result_count': summary['fresh_result_count'], 'source': { 'selected': selected_source, 'state_root': str(state_root), @@ -1541,26 +1587,14 @@ def _result_age_seconds(item: dict) -> int | None: }, 'source_skew': { 'state': source_skew_state, + 'severity': source_skew_severity, + 'authoritative_source': selected_source, 'reasons': source_skew_reasons, }, 'requests': requests, 'results': results, 'subagent_rollup': rollup, - 'summary': { - 'total_requests': len(requests), - 'stale_request_count': stale_count, - 'queued_request_count': queued_count, - 'result_count': result_count, - 'blocked_result_count': blocked_count, - 'stale_result_count': stale_result_count, - 'fresh_result_count': fresh_result_count, - 'latest_result_age_seconds': (results[0].get('age_seconds') if results else ((rollup or {}).get('latest_result') or {}).get('age_seconds') if isinstance((rollup or {}).get('latest_result'), dict) else None), - 'freshness_state': 'fresh' if fresh_result_count else ('stale' if stale_result_count else state), - 'freshness_window_seconds': 6 * 60 * 60, - 'sources': [selected_source] if requests or results or isinstance(rollup, dict) else [], - 'state': state, - 'reason': reason, - }, + 'summary': summary, 'latest_request': requests[0] if requests else ((rollup or {}).get('latest_request') if isinstance(rollup, dict) else None), 'latest_result': results[0] if results else ((rollup or {}).get('latest_result') if isinstance(rollup, dict) else None), 'latest_telemetry': (rollup or {}).get('latest_telemetry') if isinstance(rollup, dict) else None, diff --git a/ops/dashboard/tests/test_app.py b/ops/dashboard/tests/test_app.py index 92c956b..77bb9b8 100644 --- a/ops/dashboard/tests/test_app.py +++ b/ops/dashboard/tests/test_app.py @@ -1699,8 +1699,18 @@ def test_app_api_subagents_prefers_materialized_blocked_result_over_stale_queued assert summary['queued_request_count'] == 0 assert summary['result_count'] == 1 assert summary['blocked_result_count'] == 1 + assert payload['state'] == summary['state'] + assert payload['status'] == summary['state'] + assert payload['total_requests'] == summary['total_requests'] == 1 + assert payload['result_count'] == summary['result_count'] == 1 + assert payload['queued_request_count'] == summary['queued_request_count'] == 0 + assert payload['stale_request_count'] == summary['stale_request_count'] == 0 + assert payload['blocked_result_count'] == summary['blocked_result_count'] == 1 + assert payload['fresh_result_count'] == summary['fresh_result_count'] assert payload['subagent_rollup']['blocked_result_count'] == 1 assert payload['requests'][0]['request_status'] == 'queued' + assert payload['requests'][0]['raw_request_status'] == 'queued' + assert payload['requests'][0]['effective_status'] == 'blocked' assert payload['requests'][0]['status'] == 'blocked' assert payload['requests'][0]['materialized_result_status'] == 'blocked' assert payload['requests'][0]['materialized_result_path'] == str(result_path) diff --git a/ops/dashboard/tests/test_dashboard_truth_audit_gaps.py b/ops/dashboard/tests/test_dashboard_truth_audit_gaps.py index f795918..578fc67 100644 --- a/ops/dashboard/tests/test_dashboard_truth_audit_gaps.py +++ b/ops/dashboard/tests/test_dashboard_truth_audit_gaps.py @@ -3297,8 +3297,13 @@ def test_subagent_visibility_prefers_canonical_eeepc_state_over_stale_local(tmp_ assert visibility['source']['selected'] == 'eeepc' assert visibility['source']['local_state_root'] == str(local_state) assert visibility['source']['canonical_state_root'] == str(canonical_state) - assert visibility['source_skew']['state'] == 'skewed' + assert visibility['source_skew']['state'] == 'dual_roots_available' + assert visibility['source_skew']['severity'] == 'informational' + assert visibility['source_skew']['authoritative_source'] == 'eeepc' assert visibility['latest_request']['request_id'] == request_id + assert visibility['latest_request']['raw_request_status'] == 'queued' + assert visibility['latest_request']['effective_status'] == 'blocked' + assert visibility['latest_request']['status'] == 'blocked' assert visibility['latest_result']['request_id'] == request_id assert visibility['latest_result']['verification_task_id'] == request_id assert visibility['summary']['sources'] == ['eeepc'] @@ -3365,7 +3370,9 @@ def test_subagent_visibility_uses_remote_canonical_state_when_not_local(tmp_path assert visibility['latest_request']['request_id'] == request_id assert visibility['latest_result']['request_id'] == request_id assert visibility['summary']['sources'] == ['eeepc'] - assert visibility['source_skew']['state'] == 'skewed' + assert visibility['source_skew']['state'] == 'dual_roots_available' + assert visibility['source_skew']['severity'] == 'informational' + assert visibility['source_skew']['authoritative_source'] == 'eeepc'