Close decision assistant H16 F17 G16 D18

This commit is contained in:
Codex
2026-05-13 10:04:42 +08:00
parent 07687e326f
commit ff1e908c6f
5 changed files with 533 additions and 17 deletions
+18 -3
View File
@@ -2,7 +2,7 @@
## Current Goal
Automation execution-layer PXX remains closed and its known scope stays frozen. Under explicit user direction, the active product phase is a dense `Decision Assistant` operator workspace with grouped recall, broader replay baselines, note templates, quick-save shortcuts, and concrete OKX announcements/system-status read-only sources. This run closed H15/F16/G15/D17 by adding handoff lifecycle / operator resume queue fields, digest proof follow-up queue and resolved states, template guardrail resolution / preferred-template decay fields, and setup-family promotion queue / demotion history hints. Local validation passed, the current working tree was deployed, and remote smoke confirmed the latest Decision Assistant fields on the live host.
Automation execution-layer PXX remains closed and its known scope stays frozen. Under explicit user direction, the active product phase is a dense `Decision Assistant` operator workspace with grouped recall, broader replay baselines, note templates, quick-save shortcuts, and concrete OKX announcements/system-status read-only sources. This run closed H16/F17/G16/D18 by adding handoff resume persistence / resolved-lane state, digest proof follow-up resolution history / stale queue aging, template preference recovery / guardrail audit trails, and setup-family promotion decision history / review-lane aging. Local validation passed, the current working tree was deployed, and remote smoke confirmed the latest Decision Assistant fields on the live host.
## Current Branch
@@ -88,6 +88,10 @@ Automation execution-layer PXX remains closed and its known scope stays frozen.
- latest_decision_assistant_digest_followup_slice: proof_escalation_followup_resolved_states
- latest_decision_assistant_template_guardrail_resolution_slice: guardrail_resolution_preferred_decay
- latest_decision_assistant_replay_promotion_queue_slice: setup_family_promotion_queue_demotion_history
- latest_decision_assistant_handoff_resume_persistence_slice: resume_queue_persistence_resolved_lanes
- latest_decision_assistant_digest_resolution_history_slice: proof_followup_resolution_history_queue_aging
- latest_decision_assistant_template_recovery_slice: preference_recovery_guardrail_audit
- latest_decision_assistant_replay_review_lane_slice: promotion_decision_history_review_lane_aging
- latest_decision_assistant_summary_status: implemented
- latest_decision_assistant_change_summary_status: implemented
- latest_decision_assistant_evidence_status: implemented
@@ -160,10 +164,14 @@ Automation execution-layer PXX remains closed and its known scope stays frozen.
- latest_decision_assistant_digest_followup_status: implemented
- latest_decision_assistant_template_guardrail_resolution_status: implemented
- latest_decision_assistant_replay_promotion_queue_status: implemented
- latest_decision_assistant_handoff_resume_persistence_status: implemented
- latest_decision_assistant_digest_resolution_history_status: implemented
- latest_decision_assistant_template_recovery_status: implemented
- latest_decision_assistant_replay_review_lane_status: implemented
- latest_decision_assistant_workbench_status: visible_local_smoke
- latest_remote_decision_assistant_deploy_status: deployed_current_head
- latest_remote_decision_assistant_smoke_status: passed
- latest_remote_decision_assistant_review_packet_status: handoff_lifecycle_proof_followup_guardrail_resolution_promotion_queue_visible
- latest_remote_decision_assistant_review_packet_status: resume_persistence_proof_resolution_template_recovery_review_lane_visible
- latest_p7_selector_choice: continue_periodic_analysis_decision_monitoring_with_maturity_rollup
- latest_p14_slice: webhook_transport_retry
- latest_p15_pxx_tasklist_status: ready
@@ -242,6 +250,12 @@ Automation execution-layer PXX remains closed and its known scope stays frozen.
## Completed In This Run
- Added handoff resume persistence / resolved-lane fields (`resume_persistence_key`, `resume_persistence_status`, `resolved_lane_state`, `resolved_lane_prompt`, `resume_queue_persistence_status`, and `resolved_lane_state_counts`) plus cockpit visibility.
- Added digest proof follow-up resolution history / stale queue aging (`proof_follow_up_age_band`, `proof_follow_up_stale_queue`, `proof_resolution_history`, `stale_proof_follow_up_count`, and `proof_follow_up_age_counts`).
- Added template preference recovery / guardrail audit fields (`preference_recovery_status`, `preference_recovery_prompt`, `guardrail_audit_trail`, `preference_recovery_queue`, and `guardrail_audit_trail_count`).
- Added setup-family promotion decision history / review-lane aging (`promotion_decision_history`, `promotion_decision_history_count`, `review_lane_age_status`, `review_lane_age_score`, and `review_lane_aging_hint`).
- Passed focused validation: `unittest tests.unit.test_operator_workspace_service`, `node --check src/web/app.js`, `unittest tests.unit.test_operator_workspace_service tests.unit.test_api_static_server tests.unit.test_api_server`, full `unittest discover -s tests/unit` (713 tests), and `scripts/run_script_static_checks.py`.
- Deployed the current working tree to `http://192.168.3.90:8000`; deploy smoke passed 11 endpoints, `run_remote_ops_smoke.py` passed 9 checks, and remote field smoke confirmed resume persistence, proof resolution history, template recovery/audit, and review-lane aging fields.
- Added handoff lifecycle / operator resume queue fields (`lifecycle_state`, `resume_queue_rank`, `resume_action_label`, `handoff_lifecycle_summary`, `operator_resume_queue`, and `next_resume_*`) plus cockpit resume visibility.
- Added digest proof follow-up queue and resolved-state fields (`proof_follow_up_state`, `proof_follow_up_action_label`, `proof_resolved_state`, `proof_follow_up_queue`, `resolved_proof_count`, and `proof_resolution_state_counts`).
- Added template guardrail resolution / preferred-template decay fields (`guardrail_resolution_status`, `guardrail_resolution_prompt`, `preferred_template_decay`, `guardrail_resolution_queue`, and `preferred_template_decay_status`).
@@ -531,11 +545,12 @@ Automation execution-layer PXX remains closed and its known scope stays frozen.
- P24 deploy passed through `scripts/deploy_with_password.py` with 11 HTTP smoke endpoints.
- P24 remote ops smoke passed on `http://192.168.3.90:8000` with 9 passed checks, warnings=[], blockers=[].
- PXX final validation passed: 709 unit tests, 73 script static checks, `node --check src/web/app.js`, P24 remote ops smoke passed, and closure gate passed.
- Latest H16/F17/G16/D18 validation passed: 713 unit tests, 73 script static checks, `node --check src/web/app.js`, deploy smoke passed 11 endpoints, remote ops smoke passed 9 checks, and remote field smoke returned `status=passed`.
- Real-live order submission remains intentionally disabled because the OKX order adapter is still not a real-live submitter.
## Exact Next Step
Continue the Decision Assistant dense operator workspace with the next bounded slice: H16 handoff resume queue persistence / resolved lane state, F17 digest proof follow-up resolution history / stale queue aging, G16 template preference recovery / guardrail audit trail, and D18 setup-family promotion decision history / review lane aging. PXX remains closed; do not create P25/P26 automatically, and do not enable real trading.
Continue the Decision Assistant dense operator workspace with the next bounded slice: H17 handoff resume history / stale persistence cleanup, F18 digest proof resolution SLA / repeated-stale proof suppression, G17 template recovery recommendations / audit filtering, and D19 setup-family review-lane SLA / promotion backlog prioritization. PXX remains closed; do not create P25/P26 automatically, and do not enable real trading.
## Validation Commands Used Recently
+21 -5
View File
@@ -88,6 +88,10 @@
- [x] Decision Assistant F16 digest proof escalation follow-up queue / resolved proof states slice complete
- [x] Decision Assistant G15 template guardrail resolution / preferred-template decay slice complete
- [x] Decision Assistant D17 setup-family promotion queue / demotion history slice complete
- [x] Decision Assistant H16 handoff resume queue persistence / resolved lane state slice complete
- [x] Decision Assistant F17 digest proof follow-up resolution history / stale queue aging slice complete
- [x] Decision Assistant G16 template preference recovery / guardrail audit trail slice complete
- [x] Decision Assistant D18 setup-family promotion decision history / review lane aging slice complete
## Next Up
@@ -107,10 +111,14 @@
- [x] F16 deepen digest recall with proof escalation follow-up queue / resolved proof states
- [x] G15 continue review template ergonomics with template guardrail resolution / preferred-template decay
- [x] D17 extend replay comparison with setup-family promotion queue / demotion history
- [ ] H16 continue compact operator workspace polish with handoff resume queue persistence / resolved lane state
- [ ] F17 deepen digest recall with proof follow-up resolution history / stale queue aging
- [ ] G16 continue review template ergonomics with template preference recovery / guardrail audit trail
- [ ] D18 extend replay comparison with setup-family promotion decision history / review lane aging
- [x] H16 continue compact operator workspace polish with handoff resume queue persistence / resolved lane state
- [x] F17 deepen digest recall with proof follow-up resolution history / stale queue aging
- [x] G16 continue review template ergonomics with template preference recovery / guardrail audit trail
- [x] D18 extend replay comparison with setup-family promotion decision history / review lane aging
- [ ] H17 continue compact operator workspace polish with handoff resume history / stale persistence cleanup
- [ ] F18 deepen digest recall with proof resolution SLA / repeated-stale proof suppression
- [ ] G17 continue review template ergonomics with template recovery recommendations / audit filtering
- [ ] D19 extend replay comparison with setup-family review-lane SLA / promotion backlog prioritization
- [x] P8 local closure complete
- [x] P9 local closure complete
- [x] P10 local closure complete
@@ -206,6 +214,10 @@
- [x] latest_decision_assistant_digest_followup_slice: proof_escalation_followup_resolved_states
- [x] latest_decision_assistant_template_guardrail_resolution_slice: guardrail_resolution_preferred_decay
- [x] latest_decision_assistant_replay_promotion_queue_slice: setup_family_promotion_queue_demotion_history
- [x] latest_decision_assistant_handoff_resume_persistence_slice: resume_queue_persistence_resolved_lanes
- [x] latest_decision_assistant_digest_resolution_history_slice: proof_followup_resolution_history_queue_aging
- [x] latest_decision_assistant_template_recovery_slice: preference_recovery_guardrail_audit
- [x] latest_decision_assistant_replay_review_lane_slice: promotion_decision_history_review_lane_aging
- [x] latest_decision_assistant_summary_status: implemented
- [x] latest_decision_assistant_change_summary_status: implemented
- [x] latest_decision_assistant_evidence_status: implemented
@@ -275,10 +287,14 @@
- [x] latest_decision_assistant_digest_followup_status: implemented
- [x] latest_decision_assistant_template_guardrail_resolution_status: implemented
- [x] latest_decision_assistant_replay_promotion_queue_status: implemented
- [x] latest_decision_assistant_handoff_resume_persistence_status: implemented
- [x] latest_decision_assistant_digest_resolution_history_status: implemented
- [x] latest_decision_assistant_template_recovery_status: implemented
- [x] latest_decision_assistant_replay_review_lane_status: implemented
- [x] latest_decision_assistant_workbench_status: visible_local_smoke
- [x] latest_remote_decision_assistant_deploy_status: deployed_current_head
- [x] latest_remote_decision_assistant_smoke_status: passed
- [x] latest_remote_decision_assistant_review_packet_status: handoff_lifecycle_proof_followup_guardrail_resolution_promotion_queue_visible
- [x] latest_remote_decision_assistant_review_packet_status: resume_persistence_proof_resolution_template_recovery_review_lane_visible
- [x] latest_p7_selector_choice: continue_periodic_analysis_decision_monitoring_with_maturity_rollup
- [x] latest_p14_slice: webhook_transport_retry
- [x] latest_p15_pxx_tasklist_status: ready
@@ -1090,6 +1090,10 @@ def _build_decision_assistant_review_entrypoints(
item["guardrail_resolution_prompt"] = outcome.get("guardrail_resolution_prompt")
item["preferred_template_decay"] = outcome.get("preferred_template_decay")
item["preferred_template_decay_reason"] = outcome.get("preferred_template_decay_reason")
item["preference_recovery_status"] = outcome.get("preference_recovery_status")
item["preference_recovery_prompt"] = outcome.get("preference_recovery_prompt")
item["guardrail_audit_trail"] = outcome.get("guardrail_audit_trail") or []
item["guardrail_audit_trail_count"] = outcome.get("guardrail_audit_trail_count") or 0
return {
"status": "active",
"note_count": len(recent_notes),
@@ -1401,6 +1405,16 @@ def _score_template_outcome(*, template_id: str, usage: dict, notes: list[dict])
outcome_status=outcome_status,
reuse_count=reuse_count,
)
preference_recovery_status = _template_preference_recovery_status(
negative_count=negative_count,
positive_count=positive_count,
actionable_count=action_count,
)
guardrail_audit_trail = _template_guardrail_audit_trail(
template_id=template_id,
feedback_trend_history=feedback_trend_history,
negative_count=negative_count,
)
return {
"template_id": template_id,
"reuse_count": reuse_count,
@@ -1435,6 +1449,13 @@ def _score_template_outcome(*, template_id: str, usage: dict, notes: list[dict])
positive_count=positive_count,
actionable_count=action_count,
),
"preference_recovery_status": preference_recovery_status,
"preference_recovery_prompt": _template_preference_recovery_prompt(
template_id=template_id,
preference_recovery_status=preference_recovery_status,
),
"guardrail_audit_trail": guardrail_audit_trail[:5],
"guardrail_audit_trail_count": len(guardrail_audit_trail),
}
@@ -1485,6 +1506,57 @@ def _template_preferred_decay_reason(*, negative_count: int, positive_count: int
return "no_feedback_decay_signal"
def _template_preference_recovery_status(*, negative_count: int, positive_count: int, actionable_count: int) -> str:
if negative_count:
if positive_count + actionable_count >= negative_count:
return "recovery_in_progress"
return "recovery_required"
if positive_count or actionable_count:
return "recovered_or_stable"
return "awaiting_feedback"
def _template_preference_recovery_prompt(*, template_id: str, preference_recovery_status: str) -> str:
template_label = template_id.replace("_", " ") or "template"
if preference_recovery_status == "recovery_required":
return f"Collect a recovery note before {template_label} can be preferred again."
if preference_recovery_status == "recovery_in_progress":
return f"Review the latest useful feedback before restoring {template_label} preference."
if preference_recovery_status == "recovered_or_stable":
return f"{template_label} preference is stable; keep capturing outcome evidence after reuse."
return f"{template_label} still needs outcome feedback before preference can be scored."
def _template_guardrail_audit_trail(*, template_id: str, feedback_trend_history: list[dict], negative_count: int) -> list[dict]:
trail = []
for index, item in enumerate(feedback_trend_history[:5], start=1):
signal = str(item.get("signal") or "reuse_only")
trail.append(
{
"template_id": template_id,
"event": "negative_guardrail" if signal == "negative" else "feedback_signal",
"signal": signal,
"label": item.get("label"),
"created_ts": item.get("created_ts"),
"audit_index": index,
"resolution_status": "resolution_required" if signal == "negative" else "evidence_captured",
}
)
if negative_count and not any(item.get("event") == "negative_guardrail" for item in trail):
trail.append(
{
"template_id": template_id,
"event": "negative_guardrail",
"signal": "negative",
"label": "Negative feedback",
"created_ts": None,
"audit_index": len(trail) + 1,
"resolution_status": "resolution_required",
}
)
return trail
def _build_template_feedback_loop(*, template_id: str, outcome_status: str, reuse_count: int) -> dict:
template_label = template_id.replace("_", " ") or "template"
if outcome_status == "review_before_reuse":
@@ -1566,6 +1638,22 @@ def _build_template_feedback_aggregation(
for item in template_outcomes.values()
if item.get("guardrail_resolution_status") == "resolution_required"
]
preference_recovery_queue = [
{
"template_id": str(item.get("template_id") or ""),
"status": item.get("preference_recovery_status"),
"prompt": item.get("preference_recovery_prompt"),
"decay": item.get("preferred_template_decay"),
"guardrail_status": item.get("guardrail_resolution_status"),
}
for item in template_outcomes.values()
if item.get("preference_recovery_status") in {"recovery_required", "recovery_in_progress", "awaiting_feedback"}
]
guardrail_audit_trail = [
row
for item in template_outcomes.values()
for row in list(item.get("guardrail_audit_trail") or [])
]
rated_template_count = sum(
1
for item in template_outcomes.values()
@@ -1622,6 +1710,13 @@ def _build_template_feedback_aggregation(
"preferred_template_decay_max": max(
[float(item.get("decay") or 0.0) for item in guardrail_resolution_queue] or [0.0]
),
"preference_recovery_queue": preference_recovery_queue[:5],
"preference_recovery_count": len(preference_recovery_queue),
"next_preference_recovery_template_id": (
preference_recovery_queue[0].get("template_id") if preference_recovery_queue else None
),
"guardrail_audit_trail": guardrail_audit_trail[:5],
"guardrail_audit_trail_count": len(guardrail_audit_trail),
}
@@ -1865,10 +1960,42 @@ def _build_decision_assistant_digest_history_summary(history: list[dict]) -> dic
"prompt": group.get("proof_escalation_prompt"),
"evidence_anchor": group.get("refresh_evidence_anchor"),
"resolved_state": group.get("proof_resolved_state"),
"age_seconds": group.get("proof_follow_up_age_seconds"),
"age_band": group.get("proof_follow_up_age_band"),
"stale_queue": group.get("proof_follow_up_stale_queue"),
}
for group in groups
if group.get("proof_follow_up_state") in {"follow_up_required", "watch"}
]
stale_proof_follow_ups = [
group
for group in groups
if bool(group.get("proof_follow_up_stale_queue"))
]
proof_follow_up_age_counts = {}
for group in groups:
age_band = str(group.get("proof_follow_up_age_band") or "unknown")
proof_follow_up_age_counts[age_band] = int(proof_follow_up_age_counts.get(age_band) or 0) + 1
proof_resolution_history = [
row
for group in groups
for row in list(group.get("proof_resolution_history") or [])
]
proof_follow_up_with_age = [
group
for group in groups
if group.get("proof_follow_up_state") in {"follow_up_required", "watch"}
and group.get("proof_follow_up_age_seconds") is not None
]
oldest_proof_follow_up = dict(
sorted(
proof_follow_up_with_age,
key=lambda row: int(row.get("proof_follow_up_age_seconds") or 0),
reverse=True,
)[0]
if proof_follow_up_with_age
else {}
)
proof_resolution_state_counts = {}
for group in groups:
state = str(group.get("proof_resolved_state") or "unknown")
@@ -1912,6 +2039,12 @@ def _build_decision_assistant_digest_history_summary(history: list[dict]) -> dic
"resolved_proof_count": proof_resolution_state_counts.get("resolved_fresh", 0),
"proof_resolution_state_counts": proof_resolution_state_counts,
"next_proof_follow_up_prompt": proof_follow_up_queue[0].get("prompt") if proof_follow_up_queue else None,
"stale_proof_follow_up_count": len(stale_proof_follow_ups),
"proof_follow_up_age_counts": proof_follow_up_age_counts,
"proof_resolution_history": proof_resolution_history[:5],
"proof_resolution_history_count": len(proof_resolution_history),
"oldest_proof_follow_up_kind": oldest_proof_follow_up.get("kind"),
"oldest_proof_follow_up_age_seconds": oldest_proof_follow_up.get("proof_follow_up_age_seconds"),
}
@@ -1949,6 +2082,21 @@ def _build_decision_assistant_digest_history_groups(history: list[dict], *, reco
freshness_proof_status=freshness_proof_status,
refresh_evidence_anchor=refresh_evidence_anchor,
)
proof_follow_up_age_band = _digest_history_proof_follow_up_age_band(
age=age,
proof_follow_up_state=proof_follow_up_state,
)
proof_follow_up_stale_queue = _digest_history_proof_follow_up_stale_queue(
proof_follow_up_state=proof_follow_up_state,
proof_age_trend=proof_age_trend,
proof_follow_up_age_band=proof_follow_up_age_band,
)
proof_resolution_history = _digest_history_proof_resolution_history(
kind=kind,
proof_follow_up_state=proof_follow_up_state,
proof_resolved_state=proof_resolved_state,
refresh_confirmation_trail=refresh_confirmation_trail,
)
groups.append(
{
"kind": kind,
@@ -1995,6 +2143,10 @@ def _build_decision_assistant_digest_history_groups(history: list[dict], *, reco
proof_follow_up_state=proof_follow_up_state,
),
"proof_resolved_state": proof_resolved_state,
"proof_follow_up_age_seconds": age.get("age_seconds"),
"proof_follow_up_age_band": proof_follow_up_age_band,
"proof_follow_up_stale_queue": proof_follow_up_stale_queue,
"proof_resolution_history": proof_resolution_history,
"refresh_confirmation_trail": refresh_confirmation_trail,
"latest_confirmation_trail_item": refresh_confirmation_trail[0] if refresh_confirmation_trail else None,
"items": items[:3],
@@ -2226,6 +2378,60 @@ def _digest_history_proof_resolved_state(*, proof_escalation_level: str, freshne
return "unresolved"
def _digest_history_proof_follow_up_age_band(*, age: dict, proof_follow_up_state: str) -> str:
age_band = str(age.get("age_band") or "unknown_age")
if proof_follow_up_state == "resolved":
return "resolved"
if age_band in {"untimed", "unknown_age"}:
return "missing_age_proof"
if age_band == "stale":
return "stale_follow_up"
if age_band == "same_session":
return "aging_follow_up"
if age_band in {"fresh_now", "monitor_window"}:
return "current_follow_up"
return "unknown_follow_up_age"
def _digest_history_proof_follow_up_stale_queue(
*,
proof_follow_up_state: str,
proof_age_trend: str,
proof_follow_up_age_band: str,
) -> bool:
return (
proof_follow_up_state == "follow_up_required"
and (
proof_age_trend in {"stale", "missing"}
or proof_follow_up_age_band in {"stale_follow_up", "missing_age_proof"}
)
)
def _digest_history_proof_resolution_history(
*,
kind: str,
proof_follow_up_state: str,
proof_resolved_state: str,
refresh_confirmation_trail: list[dict],
) -> list[dict]:
history = []
for index, item in enumerate(refresh_confirmation_trail[:3], start=1):
history.append(
{
"kind": kind,
"event": "proof_resolution",
"state": proof_resolved_state,
"follow_up_state": proof_follow_up_state,
"proof_label": item.get("proof_label"),
"ts": item.get("ts"),
"evidence_anchor": item.get("evidence_anchor"),
"history_index": index,
}
)
return history
def _digest_history_refresh_evidence_anchor(*, kind: str, items: list[dict], latest_ts) -> dict:
latest = dict(items[0] or {}) if items else {}
title = str(latest.get("title") or kind or "history")
@@ -2540,6 +2746,16 @@ def _build_decision_assistant_scan_handoff_summary(cards: list[dict]) -> dict:
"lifecycle_state": _scan_handoff_lifecycle_state(str(item.get("handoff_status") or "")),
"resume_queue_rank": _scan_handoff_resume_rank(str(item.get("handoff_status") or "")),
"resume_action_label": _scan_handoff_resume_action_label(str(item.get("handoff_status") or "")),
"resume_persistence_key": _scan_handoff_resume_persistence_key(
persistence_key=str(item.get("handoff_persistence_key") or ""),
handoff_marker=str(item.get("handoff_marker") or ""),
),
"resume_persistence_status": _scan_handoff_resume_persistence_status(str(item.get("handoff_status") or "")),
"resolved_lane_state": _scan_handoff_resolved_lane_state(str(item.get("handoff_status") or "")),
"resolved_lane_prompt": _scan_handoff_resolved_lane_prompt(
title=str(item.get("title") or item.get("target_section") or "handoff"),
handoff_status=str(item.get("handoff_status") or ""),
),
}
for item in cards
if item.get("handoff_marker")
@@ -2552,6 +2768,15 @@ def _build_decision_assistant_scan_handoff_summary(cards: list[dict]) -> dict:
]
next_cleanup_marker = dict(cleanup_markers[0] or {}) if cleanup_markers else {}
operator_resume_queue = _build_scan_handoff_resume_queue(markers)
resolved_lane_state_counts = _build_scan_handoff_resolved_lane_state_counts(markers)
next_unresolved_marker = next(
(
item
for item in operator_resume_queue
if item.get("resolved_lane_state") not in {"resolved_ready", "resolved_carry_forward"}
),
{},
)
stale_after_values = [
int(item.get("stale_after_seconds") or 0)
for item in markers
@@ -2581,6 +2806,17 @@ def _build_decision_assistant_scan_handoff_summary(cards: list[dict]) -> dict:
"next_resume_marker": operator_resume_queue[0].get("handoff_marker") if operator_resume_queue else None,
"next_resume_action_label": operator_resume_queue[0].get("resume_action_label") if operator_resume_queue else None,
"next_resume_prompt": operator_resume_queue[0].get("handoff_prompt") if operator_resume_queue else None,
"resume_queue_persistence_status": _scan_handoff_resume_queue_persistence_status(operator_resume_queue),
"resume_queue_persistence_key": operator_resume_queue[0].get("resume_persistence_key") if operator_resume_queue else None,
"resolved_lane_state_counts": resolved_lane_state_counts,
"resolved_lane_count": int(resolved_lane_state_counts.get("resolved_ready") or 0)
+ int(resolved_lane_state_counts.get("resolved_carry_forward") or 0),
"unresolved_lane_count": sum(
int(count or 0)
for state, count in resolved_lane_state_counts.items()
if state not in {"resolved_ready", "resolved_carry_forward"}
),
"next_resolved_lane_prompt": next_unresolved_marker.get("resolved_lane_prompt"),
"handoff_stale_policy": "Persisted handoffs older than their stale window should be reopened and refreshed before reuse.",
"handoff_cleanup_recommendation": _scan_handoff_cleanup_recommendation(markers),
"snooze_options": [
@@ -2649,6 +2885,61 @@ def _scan_handoff_resume_action_label(handoff_status: str) -> str:
return "Resume Ready"
def _scan_handoff_resume_persistence_key(*, persistence_key: str, handoff_marker: str) -> str:
base = str(persistence_key or handoff_marker or "handoff").strip() or "handoff"
return f"{base}:resume_state"
def _scan_handoff_resume_persistence_status(handoff_status: str) -> str:
if handoff_status == "next_handoff":
return "persist_next_resume"
if handoff_status in {"blocked_handoff", "refresh_handoff", "ready_handoff"}:
return "persist_for_resolution"
return "no_persistence_needed"
def _scan_handoff_resume_queue_persistence_status(operator_resume_queue: list[dict]) -> str:
if not operator_resume_queue:
return "idle"
if any(item.get("resume_persistence_status") == "persist_next_resume" for item in operator_resume_queue):
return "persist_next_resume"
if any(item.get("resume_persistence_status") == "persist_for_resolution" for item in operator_resume_queue):
return "persist_for_resolution"
return "no_persistence_needed"
def _scan_handoff_resolved_lane_state(handoff_status: str) -> str:
if handoff_status == "context_ready":
return "resolved_carry_forward"
if handoff_status == "ready_handoff":
return "resolved_ready"
if handoff_status == "blocked_handoff":
return "pending_blocker_resolution"
if handoff_status == "refresh_handoff":
return "pending_refresh_resolution"
return "pending_resume"
def _scan_handoff_resolved_lane_prompt(*, title: str, handoff_status: str) -> str:
if handoff_status == "context_ready":
return f"{title} is carried forward; keep the saved context available for the next cockpit pass."
if handoff_status == "ready_handoff":
return f"{title} is resolved enough to resume when the operator returns to this lane."
if handoff_status == "blocked_handoff":
return f"Resolve the blocker for {title} before the lane is treated as complete."
if handoff_status == "refresh_handoff":
return f"Refresh {title} before reusing the persisted handoff."
return f"Resume {title} from the persisted handoff before opening a new lane."
def _build_scan_handoff_resolved_lane_state_counts(markers: list[dict]) -> dict:
counts: dict[str, int] = {}
for item in markers:
state = str(item.get("resolved_lane_state") or "pending_resume")
counts[state] = int(counts.get(state) or 0) + 1
return counts
def _build_scan_handoff_resume_queue(markers: list[dict]) -> list[dict]:
return sorted(
[
@@ -2660,6 +2951,11 @@ def _build_scan_handoff_resume_queue(markers: list[dict]) -> list[dict]:
"resume_action_label": item.get("resume_action_label"),
"handoff_prompt": item.get("handoff_prompt"),
"resume_queue_rank": item.get("resume_queue_rank"),
"persistence_key": item.get("persistence_key"),
"resume_persistence_key": item.get("resume_persistence_key"),
"resume_persistence_status": item.get("resume_persistence_status"),
"resolved_lane_state": item.get("resolved_lane_state"),
"resolved_lane_prompt": item.get("resolved_lane_prompt"),
}
for item in markers
if item.get("lifecycle_state") != "carry_forward"
@@ -3005,6 +3301,19 @@ def _build_setup_replay_broader_baseline_summary(
"promotion_queue_rank": 5,
"promotion_next_action": "Collect more replay samples before promotion review.",
"promotion_queue_item": None,
"promotion_decision_history": [
{
"event": "promotion_queue_decision",
"setup_family": setup_family,
"status": "collect_samples",
"promotion_ready": False,
"reason": "insufficient_samples",
}
],
"promotion_decision_history_count": 1,
"review_lane_age_status": "aging_collect_samples",
"review_lane_age_score": 30,
"review_lane_aging_hint": f"{setup_family} review lane is aging until enough replay samples exist.",
"demotion_history": [],
"demotion_history_count": 0,
"focus_recommendation": f"Collect samples before changing focus for {setup_family}.",
@@ -3213,6 +3522,19 @@ def _build_setup_family_trend_delta(
"promotion_queue_rank": 5,
"promotion_next_action": "Collect more replay samples before promotion review.",
"promotion_queue_item": None,
"promotion_decision_history": [
{
"event": "promotion_queue_decision",
"setup_family": setup_family,
"status": "collect_samples",
"promotion_ready": False,
"reason": "insufficient_samples",
}
],
"promotion_decision_history_count": 1,
"review_lane_age_status": "aging_collect_samples",
"review_lane_age_score": 30,
"review_lane_aging_hint": f"{setup_family} review lane is aging until enough replay samples exist.",
"demotion_history": [],
"demotion_history_count": 0,
"focus_recommendation": f"Collect samples before changing focus for {setup_family}.",
@@ -3299,6 +3621,35 @@ def _build_setup_family_trend_delta(
focus_reasons=focus_reasons,
trend_status=trend_status,
)
promotion_queue_rank = _setup_family_promotion_queue_rank(promotion_queue_status=promotion_queue_status)
promotion_next_action = _setup_family_promotion_next_action(
setup_family=setup_family,
promotion_queue_status=promotion_queue_status,
)
promotion_queue_item = _setup_family_promotion_queue_item(
setup_family=setup_family,
promotion_queue_status=promotion_queue_status,
promotion_ready=promotion_ready,
watchlist_priority_score=watchlist_priority_score,
)
promotion_decision_history = _setup_family_promotion_decision_history(
setup_family=setup_family,
promotion_queue_status=promotion_queue_status,
promotion_blockers=promotion_blockers,
demotion_history=demotion_history,
promotion_ready=promotion_ready,
)
review_lane_age_status = _setup_family_review_lane_age_status(
promotion_queue_status=promotion_queue_status,
sample_gap=sample_gap,
drift_alert=drift_alert,
trend_status=trend_status,
)
review_lane_age_score = _setup_family_review_lane_age_score(
watchlist_priority_score=watchlist_priority_score,
sample_gap=sample_gap,
drift_alert=drift_alert,
)
return {
"status": "active",
"trend_status": trend_status,
@@ -3326,16 +3677,16 @@ def _build_setup_family_trend_delta(
),
"promotion_blockers": promotion_blockers,
"promotion_queue_status": promotion_queue_status,
"promotion_queue_rank": _setup_family_promotion_queue_rank(promotion_queue_status=promotion_queue_status),
"promotion_next_action": _setup_family_promotion_next_action(
"promotion_queue_rank": promotion_queue_rank,
"promotion_next_action": promotion_next_action,
"promotion_queue_item": promotion_queue_item,
"promotion_decision_history": promotion_decision_history[:6],
"promotion_decision_history_count": len(promotion_decision_history),
"review_lane_age_status": review_lane_age_status,
"review_lane_age_score": review_lane_age_score,
"review_lane_aging_hint": _setup_family_review_lane_aging_hint(
setup_family=setup_family,
promotion_queue_status=promotion_queue_status,
),
"promotion_queue_item": _setup_family_promotion_queue_item(
setup_family=setup_family,
promotion_queue_status=promotion_queue_status,
promotion_ready=promotion_ready,
watchlist_priority_score=watchlist_priority_score,
review_lane_age_status=review_lane_age_status,
),
"demotion_history": demotion_history[:5],
"demotion_history_count": len(demotion_history),
@@ -3477,6 +3828,81 @@ def _setup_family_demotion_history(
return history
def _setup_family_promotion_decision_history(
*,
setup_family: str,
promotion_queue_status: str,
promotion_blockers: list[str],
demotion_history: list[dict],
promotion_ready: bool,
) -> list[dict]:
history = [
{
"event": "promotion_queue_decision",
"setup_family": setup_family,
"status": promotion_queue_status,
"promotion_ready": promotion_ready,
"reason": "operator_review_ready" if promotion_ready else (promotion_blockers[0] if promotion_blockers else "watch_lane"),
}
]
for blocker in promotion_blockers[:3]:
history.append(
{
"event": "promotion_blocker_reviewed",
"setup_family": setup_family,
"status": promotion_queue_status,
"reason": blocker,
}
)
for item in demotion_history[:3]:
history.append(
{
"event": item.get("event") or "demotion_history",
"setup_family": setup_family,
"status": promotion_queue_status,
"reason": item.get("reason") or item.get("severity") or "review_lane_history",
}
)
return history
def _setup_family_review_lane_age_status(
*,
promotion_queue_status: str,
sample_gap: int,
drift_alert: bool,
trend_status: str,
) -> str:
if drift_alert or promotion_queue_status == "demotion_review":
return "stale_demotion_review"
if sample_gap > 0:
return "aging_collect_samples"
if promotion_queue_status == "ready_for_operator_review":
return "ready_now"
if trend_status == "mixed_vs_family_peers":
return "aging_watch"
return "watching"
def _setup_family_review_lane_age_score(*, watchlist_priority_score: int, sample_gap: int, drift_alert: bool) -> int:
score = int(watchlist_priority_score or 0) + min(20, int(sample_gap or 0) * 4)
if drift_alert:
score += 10
return max(0, min(100, score))
def _setup_family_review_lane_aging_hint(*, setup_family: str, review_lane_age_status: str) -> str:
if review_lane_age_status == "ready_now":
return f"{setup_family} is ready for analysis-lane promotion review; execution permissions stay unchanged."
if review_lane_age_status == "stale_demotion_review":
return f"{setup_family} needs demotion review before it remains in the primary analysis lane."
if review_lane_age_status == "aging_collect_samples":
return f"{setup_family} review lane is aging until enough replay samples exist."
if review_lane_age_status == "aging_watch":
return f"{setup_family} remains in an aging watch lane until mixed deltas resolve."
return f"{setup_family} remains in watch mode while review-lane evidence accumulates."
def _rank_session_composite(rows: list[dict], *, session_code: str) -> int | None:
if not session_code:
return None
+25
View File
@@ -576,6 +576,12 @@ function renderDecisionAssistant(summary) {
['handoff lifecycle', formatJsonInline(scanHandoffSummary.handoff_lifecycle_summary?.state_counts || {})],
['resume queue', String(scanHandoffSummary.resume_queue_count ?? 0)],
['resume queue items', formatList((scanHandoffSummary.operator_resume_queue || []).map((item) => `${item.resume_action_label || 'Resume'}: ${item.title || item.target_section}`))],
['resume persistence', scanHandoffSummary.resume_queue_persistence_status || 'n/a'],
['resume persistence key', scanHandoffSummary.resume_queue_persistence_key || 'n/a'],
['resolved lane states', formatJsonInline(scanHandoffSummary.resolved_lane_state_counts || {})],
['resolved lanes', String(scanHandoffSummary.resolved_lane_count ?? 0)],
['unresolved lanes', String(scanHandoffSummary.unresolved_lane_count ?? 0)],
['next resolved lane prompt', scanHandoffSummary.next_resolved_lane_prompt || 'n/a'],
['next resume action', scanHandoffSummary.next_resume_action_label || 'n/a'],
['next resume prompt', scanHandoffSummary.next_resume_prompt || 'n/a'],
['handoff cleanup recommendation', scanHandoffSummary.handoff_cleanup_recommendation || 'n/a'],
@@ -705,8 +711,13 @@ function renderDecisionAssistant(summary) {
['next stale proof kind', digestSurface.history_summary?.next_stale_proof_kind || 'n/a'],
['next stale proof prompt', digestSurface.history_summary?.next_stale_proof_prompt || 'n/a'],
['proof follow-up queue', String(digestSurface.history_summary?.proof_follow_up_queue_count ?? 0)],
['stale proof follow-ups', String(digestSurface.history_summary?.stale_proof_follow_up_count ?? 0)],
['proof follow-up age', formatJsonInline(digestSurface.history_summary?.proof_follow_up_age_counts || {})],
['oldest proof follow-up', digestSurface.history_summary?.oldest_proof_follow_up_kind || 'n/a'],
['oldest proof age', formatDurationSeconds(digestSurface.history_summary?.oldest_proof_follow_up_age_seconds)],
['resolved proofs', String(digestSurface.history_summary?.resolved_proof_count ?? 0)],
['proof resolution states', formatJsonInline(digestSurface.history_summary?.proof_resolution_state_counts || {})],
['proof resolution history', formatList((digestSurface.history_summary?.proof_resolution_history || []).map((item) => `${item.kind}:${item.state}`))],
['next proof follow-up', digestSurface.history_summary?.next_proof_follow_up_prompt || 'n/a'],
['confirmation trail items', String(digestSurface.history_summary?.refresh_confirmation_trail_count ?? 0)],
['confirmation trail', formatList((digestSurface.history_summary?.refresh_confirmation_trail || []).map((item) => `${item.kind}:${item.proof_label}`))],
@@ -798,6 +809,9 @@ function renderDecisionAssistant(summary) {
['proof follow-up', item.proof_follow_up_state || 'n/a'],
['proof follow-up action', item.proof_follow_up_action_label || 'n/a'],
['proof resolved state', item.proof_resolved_state || 'n/a'],
['proof follow-up age', item.proof_follow_up_age_band || 'n/a'],
['proof stale queue', String(Boolean(item.proof_follow_up_stale_queue))],
['proof resolution history', formatList((item.proof_resolution_history || []).map((row) => `${row.proof_label || row.state}: ${formatTimestamp(row.ts)}`))],
['latest proof trail', item.latest_confirmation_trail_item?.proof_label || 'n/a'],
['proof trail items', formatList((item.refresh_confirmation_trail || []).map((row) => `${row.title || row.kind}: ${row.proof_label}`))],
['refresh evidence', item.refresh_evidence_anchor || 'n/a'],
@@ -857,6 +871,9 @@ function renderDecisionAssistant(summary) {
['negative guardrails', String(reviewEntrypoints.template_reuse_metrics?.template_feedback_aggregation?.negative_feedback_guardrail_count ?? 0)],
['guardrail templates', formatList(reviewEntrypoints.template_reuse_metrics?.template_feedback_aggregation?.negative_feedback_guardrail_template_ids || [])],
['guardrail resolution queue', String(reviewEntrypoints.template_reuse_metrics?.template_feedback_aggregation?.guardrail_resolution_count ?? 0)],
['preference recovery queue', String(reviewEntrypoints.template_reuse_metrics?.template_feedback_aggregation?.preference_recovery_count ?? 0)],
['next preference recovery', reviewEntrypoints.template_reuse_metrics?.template_feedback_aggregation?.next_preference_recovery_template_id || 'n/a'],
['guardrail audit trail', String(reviewEntrypoints.template_reuse_metrics?.template_feedback_aggregation?.guardrail_audit_trail_count ?? 0)],
['preferred template decay', reviewEntrypoints.template_reuse_metrics?.template_feedback_aggregation?.preferred_template_decay_status || 'n/a'],
['preferred decay templates', formatList(reviewEntrypoints.template_reuse_metrics?.template_feedback_aggregation?.preferred_template_decay_template_ids || [])],
['max preference decay', formatPercent(reviewEntrypoints.template_reuse_metrics?.template_feedback_aggregation?.preferred_template_decay_max)],
@@ -919,6 +936,9 @@ function renderDecisionAssistant(summary) {
['guardrail resolution prompt', item.guardrail_resolution_prompt || 'n/a'],
['preference decay', formatPercent(item.preferred_template_decay)],
['decay reason', item.preferred_template_decay_reason || 'n/a'],
['preference recovery', item.preference_recovery_status || 'n/a'],
['recovery prompt', item.preference_recovery_prompt || 'n/a'],
['guardrail audit trail', formatList((item.guardrail_audit_trail || []).map((row) => `${row.label || row.signal}: ${row.resolution_status}`))],
['feedback trend history', formatList((item.feedback_trend_history || []).map((row) => `${row.label || row.signal}: ${formatTimestamp(row.created_ts)}`))],
],
[
@@ -1221,6 +1241,11 @@ function renderDecisionAssistant(summary) {
['promotion queue status', setupLearningRollup.replay_statistics?.broader_baseline_summary?.setup_family_trend_delta?.promotion_queue_status || 'n/a'],
['promotion queue rank', String(setupLearningRollup.replay_statistics?.broader_baseline_summary?.setup_family_trend_delta?.promotion_queue_rank ?? 'n/a')],
['promotion next action', setupLearningRollup.replay_statistics?.broader_baseline_summary?.setup_family_trend_delta?.promotion_next_action || 'n/a'],
['promotion decision history', String(setupLearningRollup.replay_statistics?.broader_baseline_summary?.setup_family_trend_delta?.promotion_decision_history_count ?? 0)],
['promotion decision reasons', formatList((setupLearningRollup.replay_statistics?.broader_baseline_summary?.setup_family_trend_delta?.promotion_decision_history || []).map((item) => item.reason || item.event))],
['review lane age', setupLearningRollup.replay_statistics?.broader_baseline_summary?.setup_family_trend_delta?.review_lane_age_status || 'n/a'],
['review lane age score', String(setupLearningRollup.replay_statistics?.broader_baseline_summary?.setup_family_trend_delta?.review_lane_age_score ?? 'n/a')],
['review lane aging hint', setupLearningRollup.replay_statistics?.broader_baseline_summary?.setup_family_trend_delta?.review_lane_aging_hint || 'n/a'],
['demotion history', String(setupLearningRollup.replay_statistics?.broader_baseline_summary?.setup_family_trend_delta?.demotion_history_count ?? 0)],
['demotion reasons', formatList((setupLearningRollup.replay_statistics?.broader_baseline_summary?.setup_family_trend_delta?.demotion_history || []).map((item) => item.reason || item.event))],
['family sample threshold', String(setupLearningRollup.replay_statistics?.broader_baseline_summary?.setup_family_trend_delta?.sample_threshold ?? 'n/a')],
@@ -203,6 +203,11 @@ class OperatorWorkspaceServiceTests(unittest.TestCase):
self.assertIn("guardrail_resolution_count", summary["review_entrypoints"]["template_reuse_metrics"]["template_feedback_aggregation"])
self.assertIn("preferred_template_decay_status", summary["review_entrypoints"]["template_reuse_metrics"]["template_feedback_aggregation"])
self.assertIn("preferred_template_decay_template_ids", summary["review_entrypoints"]["template_reuse_metrics"]["template_feedback_aggregation"])
self.assertIn("preference_recovery_queue", summary["review_entrypoints"]["template_reuse_metrics"]["template_feedback_aggregation"])
self.assertIn("preference_recovery_count", summary["review_entrypoints"]["template_reuse_metrics"]["template_feedback_aggregation"])
self.assertIn("next_preference_recovery_template_id", summary["review_entrypoints"]["template_reuse_metrics"]["template_feedback_aggregation"])
self.assertIn("guardrail_audit_trail", summary["review_entrypoints"]["template_reuse_metrics"]["template_feedback_aggregation"])
self.assertIn("guardrail_audit_trail_count", summary["review_entrypoints"]["template_reuse_metrics"]["template_feedback_aggregation"])
self.assertEqual(summary["review_entrypoints"]["note_templates"][0]["reuse_count"], 1)
self.assertEqual(summary["review_entrypoints"]["note_templates"][0]["reuse_status"], "saved_reuse")
self.assertEqual(summary["review_entrypoints"]["note_templates"][0]["outcome_status"], "useful_signal")
@@ -216,6 +221,10 @@ class OperatorWorkspaceServiceTests(unittest.TestCase):
self.assertIn("guardrail_resolution_prompt", summary["review_entrypoints"]["note_templates"][0])
self.assertIn("preferred_template_decay", summary["review_entrypoints"]["note_templates"][0])
self.assertIn("preferred_template_decay_reason", summary["review_entrypoints"]["note_templates"][0])
self.assertIn("preference_recovery_status", summary["review_entrypoints"]["note_templates"][0])
self.assertIn("preference_recovery_prompt", summary["review_entrypoints"]["note_templates"][0])
self.assertIn("guardrail_audit_trail", summary["review_entrypoints"]["note_templates"][0])
self.assertIn("guardrail_audit_trail_count", summary["review_entrypoints"]["note_templates"][0])
self.assertTrue(any(item["template_id"] == "next_review" for item in summary["review_entrypoints"]["note_templates"]))
self.assertTrue(any(item["template_id"] == "queue_follow_up" for item in summary["review_entrypoints"]["note_templates"]))
self.assertTrue(any(item["reuse_hint"] for item in summary["review_entrypoints"]["note_templates"]))
@@ -240,6 +249,12 @@ class OperatorWorkspaceServiceTests(unittest.TestCase):
self.assertIn("resolved_proof_count", summary["digest_surface"]["history_summary"])
self.assertIn("proof_resolution_state_counts", summary["digest_surface"]["history_summary"])
self.assertIn("next_proof_follow_up_prompt", summary["digest_surface"]["history_summary"])
self.assertIn("stale_proof_follow_up_count", summary["digest_surface"]["history_summary"])
self.assertIn("proof_follow_up_age_counts", summary["digest_surface"]["history_summary"])
self.assertIn("proof_resolution_history", summary["digest_surface"]["history_summary"])
self.assertIn("proof_resolution_history_count", summary["digest_surface"]["history_summary"])
self.assertIn("oldest_proof_follow_up_kind", summary["digest_surface"]["history_summary"])
self.assertIn("oldest_proof_follow_up_age_seconds", summary["digest_surface"]["history_summary"])
self.assertIn("refresh_prompts", summary["digest_surface"]["history_summary"])
self.assertIn("digest_history_groups", summary)
self.assertIn("group_status", summary["digest_history_groups"][0])
@@ -261,6 +276,10 @@ class OperatorWorkspaceServiceTests(unittest.TestCase):
self.assertIn("proof_follow_up_state", summary["digest_history_groups"][0])
self.assertIn("proof_follow_up_action_label", summary["digest_history_groups"][0])
self.assertIn("proof_resolved_state", summary["digest_history_groups"][0])
self.assertIn("proof_follow_up_age_seconds", summary["digest_history_groups"][0])
self.assertIn("proof_follow_up_age_band", summary["digest_history_groups"][0])
self.assertIn("proof_follow_up_stale_queue", summary["digest_history_groups"][0])
self.assertIn("proof_resolution_history", summary["digest_history_groups"][0])
self.assertIn("refresh_confirmation_trail", summary["digest_history_groups"][0])
self.assertIn("latest_confirmation_trail_item", summary["digest_history_groups"][0])
self.assertTrue(summary["digest_history_preview"])
@@ -304,6 +323,11 @@ class OperatorWorkspaceServiceTests(unittest.TestCase):
self.assertIn("promotion_queue_rank", summary["setup_learning_rollup"]["replay_statistics"]["broader_baseline_summary"]["setup_family_trend_delta"])
self.assertIn("promotion_next_action", summary["setup_learning_rollup"]["replay_statistics"]["broader_baseline_summary"]["setup_family_trend_delta"])
self.assertIn("promotion_queue_item", summary["setup_learning_rollup"]["replay_statistics"]["broader_baseline_summary"]["setup_family_trend_delta"])
self.assertIn("promotion_decision_history", summary["setup_learning_rollup"]["replay_statistics"]["broader_baseline_summary"]["setup_family_trend_delta"])
self.assertIn("promotion_decision_history_count", summary["setup_learning_rollup"]["replay_statistics"]["broader_baseline_summary"]["setup_family_trend_delta"])
self.assertIn("review_lane_age_status", summary["setup_learning_rollup"]["replay_statistics"]["broader_baseline_summary"]["setup_family_trend_delta"])
self.assertIn("review_lane_age_score", summary["setup_learning_rollup"]["replay_statistics"]["broader_baseline_summary"]["setup_family_trend_delta"])
self.assertIn("review_lane_aging_hint", summary["setup_learning_rollup"]["replay_statistics"]["broader_baseline_summary"]["setup_family_trend_delta"])
self.assertIn("demotion_history", summary["setup_learning_rollup"]["replay_statistics"]["broader_baseline_summary"]["setup_family_trend_delta"])
self.assertIn("demotion_history_count", summary["setup_learning_rollup"]["replay_statistics"]["broader_baseline_summary"]["setup_family_trend_delta"])
self.assertIn("sample_threshold", summary["setup_learning_rollup"]["replay_statistics"]["broader_baseline_summary"]["setup_family_trend_delta"])
@@ -343,6 +367,12 @@ class OperatorWorkspaceServiceTests(unittest.TestCase):
self.assertIn("resume_queue_count", summary["scan_handoff_summary"])
self.assertIn("next_resume_marker", summary["scan_handoff_summary"])
self.assertIn("next_resume_prompt", summary["scan_handoff_summary"])
self.assertIn("resume_queue_persistence_status", summary["scan_handoff_summary"])
self.assertIn("resume_queue_persistence_key", summary["scan_handoff_summary"])
self.assertIn("resolved_lane_state_counts", summary["scan_handoff_summary"])
self.assertIn("resolved_lane_count", summary["scan_handoff_summary"])
self.assertIn("unresolved_lane_count", summary["scan_handoff_summary"])
self.assertIn("next_resolved_lane_prompt", summary["scan_handoff_summary"])
self.assertIn("handoff_cleanup_recommendation", summary["scan_handoff_summary"])
self.assertTrue(summary["scan_handoff_summary"]["snooze_options"])
self.assertIn("snooze_seconds", summary["scan_handoff_summary"]["handoff_markers"][0])
@@ -352,6 +382,10 @@ class OperatorWorkspaceServiceTests(unittest.TestCase):
self.assertIn("lifecycle_state", summary["scan_handoff_summary"]["handoff_markers"][0])
self.assertIn("resume_queue_rank", summary["scan_handoff_summary"]["handoff_markers"][0])
self.assertIn("resume_action_label", summary["scan_handoff_summary"]["handoff_markers"][0])
self.assertIn("resume_persistence_key", summary["scan_handoff_summary"]["handoff_markers"][0])
self.assertIn("resume_persistence_status", summary["scan_handoff_summary"]["handoff_markers"][0])
self.assertIn("resolved_lane_state", summary["scan_handoff_summary"]["handoff_markers"][0])
self.assertIn("resolved_lane_prompt", summary["scan_handoff_summary"]["handoff_markers"][0])
self.assertTrue(summary["external_context_summary"]["operator_questions"])
self.assertTrue(summary["external_context_summary"]["source_cards"])
self.assertEqual(summary["external_context_summary"]["exchange_status"]["status"], "active")