From 6b0e5aed24ba9a1b85260ee09568d19ee5b1dae4 Mon Sep 17 00:00:00 2001
From: firestar5683 <168790843+firestar5683@users.noreply.github.com>
Date: Tue, 16 Jun 2026 21:19:57 -0500
Subject: [PATCH] I did the backstroke in college
---
opendbc_repo/opendbc/car/gm/carcontroller.py | 4 +-
.../car/gm/tests/test_carcontroller.py | 10 +++
.../test_conditional_experimental_mode.py | 39 ++++++++-
.../lib/conditional_experimental_mode.py | 4 +-
.../the_pond/assets/components/home/home.css | 6 +-
.../the_pond/assets/components/home/home.js | 39 +++++----
.../the_pond/tests/test_dashboard_stats.py | 74 +++++++++++++++++
starpilot/system/the_pond/utilities.py | 80 ++++++++++++++++---
8 files changed, 227 insertions(+), 29 deletions(-)
diff --git a/opendbc_repo/opendbc/car/gm/carcontroller.py b/opendbc_repo/opendbc/car/gm/carcontroller.py
index 153b54a78..9289f1cca 100644
--- a/opendbc_repo/opendbc/car/gm/carcontroller.py
+++ b/opendbc_repo/opendbc/car/gm/carcontroller.py
@@ -28,11 +28,11 @@ AUTO_HOLD_VOLT_CARS = {
CAR.CHEVROLET_VOLT_ASCM,
CAR.CHEVROLET_VOLT_CAMERA,
}
-AUTO_HOLD_DRIVE_GEARS = {
+AUTO_HOLD_DRIVE_GEARS = (
GearShifter.drive,
GearShifter.low,
GearShifter.manumatic,
-}
+)
AUTO_HOLD_MIN_BRAKE = 80
AUTO_HOLD_MAX_BRAKE = 240
AUTO_HOLD_MIN_DRIVE_TIME_S = 3.0
diff --git a/opendbc_repo/opendbc/car/gm/tests/test_carcontroller.py b/opendbc_repo/opendbc/car/gm/tests/test_carcontroller.py
index 35f1a3d1c..88c3accc5 100644
--- a/opendbc_repo/opendbc/car/gm/tests/test_carcontroller.py
+++ b/opendbc_repo/opendbc/car/gm/tests/test_carcontroller.py
@@ -2,6 +2,8 @@ import sys
import types
from types import SimpleNamespace
+from opendbc.car import structs
+
fake_interfaces = types.ModuleType("opendbc.car.interfaces")
@@ -33,6 +35,7 @@ fake_testing_grounds.testing_ground = SimpleNamespace(use_1=False)
sys.modules.setdefault("openpilot.starpilot.common.testing_grounds", fake_testing_grounds)
from opendbc.car.gm.carcontroller import (
+ AUTO_HOLD_DRIVE_GEARS,
CarController,
estimate_auto_hold_brake,
get_adas_keepalive_step,
@@ -220,6 +223,13 @@ def test_auto_hold_brake_estimate_uses_driver_or_op_brake_and_clamps():
assert estimate_auto_hold_brake(100.0, 400.0) == 240
+def test_auto_hold_drive_gears_accept_capnp_dynamic_enum_membership():
+ msg = structs.CarState.new_message()
+ msg.gearShifter = structs.CarState.GearShifter.drive
+
+ assert msg.gearShifter in AUTO_HOLD_DRIVE_GEARS
+
+
def test_auto_hold_activation_allows_direct_entry_from_stopped_brake_press():
assert should_activate_auto_hold(
True,
diff --git a/selfdrive/controls/tests/test_conditional_experimental_mode.py b/selfdrive/controls/tests/test_conditional_experimental_mode.py
index 58643db4b..81030502c 100644
--- a/selfdrive/controls/tests/test_conditional_experimental_mode.py
+++ b/selfdrive/controls/tests/test_conditional_experimental_mode.py
@@ -35,10 +35,18 @@ def make_cem(*, model_length: float, model_stopped: bool = False, tracking_lead:
model_length=model_length,
model_stopped=model_stopped,
tracking_lead=tracking_lead,
- starpilot_vcruise=SimpleNamespace(stop_sign_confirmed=stop_sign_confirmed, forcing_stop=forcing_stop),
+ starpilot_vcruise=SimpleNamespace(
+ stop_sign_confirmed=stop_sign_confirmed,
+ forcing_stop=forcing_stop,
+ slc=SimpleNamespace(experimental_mode=False),
+ ),
starpilot_following=SimpleNamespace(slower_lead=False, following_lead=False),
lead_one=SimpleNamespace(status=lead_status, dRel=lead_d_rel, vLead=lead_v_lead,
modelProb=lead_model_prob, radar=lead_radar),
+ road_curvature_detected=False,
+ driving_in_curve=False,
+ lane_width_left=0.0,
+ lane_width_right=0.0,
)
return ConditionalExperimentalMode(planner)
@@ -368,6 +376,35 @@ def test_standstill_update_can_activate_exp_from_red_light_detection(monkeypatch
assert cem.status_value == conditional_experimental_mode_module.CEStatus["STOP_LIGHT"]
+def test_first_post_standstill_pullaway_frame_does_not_blip_into_exp(monkeypatch):
+ cem = make_cem(
+ model_length=70.0,
+ tracking_lead=True,
+ lead_status=True,
+ lead_d_rel=8.4,
+ lead_v_lead=2.7,
+ lead_model_prob=0.999,
+ )
+ toggles = make_update_toggles()
+ toggles.conditional_lead = True
+ toggles.conditional_slower_lead = True
+ toggles.conditional_stopped_lead = True
+ standstill_sm = make_update_sm(standstill=True)
+ moving_sm = make_update_sm(standstill=False)
+
+ now = [100.0]
+ monkeypatch.setattr(conditional_experimental_mode_module.time, "monotonic", lambda: now[0])
+
+ cem.update(0.0, standstill_sm, toggles)
+ assert not cem.experimental_mode
+
+ now[0] = 100.1
+ cem.update(0.12, moving_sm, toggles)
+
+ assert not cem.experimental_mode
+ assert cem.status_value == conditional_experimental_mode_module.CEStatus["OFF"]
+
+
def test_standstill_update_can_activate_exp_from_dashboard_stop_sign(monkeypatch):
cem = make_cem(model_length=80.0, model_stopped=False)
toggles = make_update_toggles()
diff --git a/starpilot/controls/lib/conditional_experimental_mode.py b/starpilot/controls/lib/conditional_experimental_mode.py
index b16327501..1bbbef871 100644
--- a/starpilot/controls/lib/conditional_experimental_mode.py
+++ b/starpilot/controls/lib/conditional_experimental_mode.py
@@ -120,8 +120,10 @@ class ConditionalExperimentalMode:
if triggered:
self.mode_hold_until = now + self.CEM_TRANSITION_GUARD_TIME
self.mode_false_since = 0.0
- elif self.mode_false_since == 0.0:
+ elif self.prev_experimental_mode and self.mode_false_since == 0.0:
self.mode_false_since = now
+ elif not self.prev_experimental_mode:
+ self.mode_false_since = 0.0
hold_active = now < self.mode_hold_until
transition_buffer_active = self.mode_false_since != 0.0 and (now - self.mode_false_since) < self.CEM_TRANSITION_BUFFER_TIME
diff --git a/starpilot/system/the_pond/assets/components/home/home.css b/starpilot/system/the_pond/assets/components/home/home.css
index d612cb76d..7dbe9fc55 100644
--- a/starpilot/system/the_pond/assets/components/home/home.css
+++ b/starpilot/system/the_pond/assets/components/home/home.css
@@ -403,7 +403,7 @@
border-top: 1px solid rgba(255, 255, 255, 0.08);
display: grid;
gap: 1rem;
- grid-template-columns: minmax(170px, 1.3fr) minmax(230px, 1fr) minmax(210px, 1fr) minmax(180px, 0.9fr);
+ grid-template-columns: minmax(170px, 1.35fr) minmax(170px, 0.8fr) minmax(210px, 1fr) minmax(180px, 0.9fr);
min-width: 0;
padding: 0.9rem 0;
}
@@ -412,6 +412,10 @@
border-top: 0;
}
+.dashboard-drive-row.is-pending {
+ color: var(--dashboard-muted);
+}
+
.dashboard-drive-main,
.dashboard-drive-details,
.dashboard-attention,
diff --git a/starpilot/system/the_pond/assets/components/home/home.js b/starpilot/system/the_pond/assets/components/home/home.js
index 382847fb6..9b0d4c1f8 100644
--- a/starpilot/system/the_pond/assets/components/home/home.js
+++ b/starpilot/system/the_pond/assets/components/home/home.js
@@ -110,6 +110,7 @@ function fallbackDashboard(data, unit) {
segmentCount: 0,
distractedMoments: 0,
unresponsiveMoments: 0,
+ attentionKnown: true,
distanceUnit: unit,
speedUnit: unit === "kilometers" ? "kph" : "mph",
},
@@ -144,21 +145,28 @@ function fallbackDashboard(data, unit) {
};
}
+function driveStatsReady(drive) {
+ return drive?.attentionKnown !== false;
+}
+
function renderLastDrive(drive) {
+ const ready = driveStatsReady(drive);
return `
Last drive
${escapeHtml(formatDate(drive.date))}
-
${formatOneDecimal(drive.distance)}${escapeHtml(drive.distanceUnit || "miles")}
+
${ready ? formatOneDecimal(drive.distance) : "..."}${ready ? escapeHtml(drive.distanceUnit || "miles") : "analyzing"}
${formatDuration(drive.duration)}duration
-
${formatInt(drive.avgSpeed)}${escapeHtml(drive.speedUnit || "mph")} avg
-
${formatPercent(drive.engagedPercent)}engaged
+
${ready ? formatInt(drive.avgSpeed) : "..."}${ready ? `${escapeHtml(drive.speedUnit || "mph")} avg` : "speed"}
+
${ready ? formatPercent(drive.engagedPercent) : "..."}engaged
`;
@@ -236,27 +244,30 @@ function renderRecentDrives(drives) {
`;
}
- const rows = drives.map(drive => `
-
+ const rows = drives.map(drive => {
+ const ready = driveStatsReady(drive);
+ return `
+
${escapeHtml(formatDate(drive.date))}
${escapeHtml(drive.model || "Unknown model")}
- ${formatOneDecimal(drive.distance)} ${escapeHtml(drive.distanceUnit || "miles")}
+ ${ready ? `${formatOneDecimal(drive.distance)} ${escapeHtml(drive.distanceUnit || "miles")}` : "Analyzing stats"}
${formatDuration(drive.duration)}
- ${formatInt(drive.segmentCount)} segments
- ${formatInt(drive.distractedMoments)} distracted
- ${formatInt(drive.unresponsiveMoments)} unresponsive
+ ${ready
+ ? `${formatInt(drive.distractedMoments)} distracted${formatInt(drive.unresponsiveMoments)} unresponsive`
+ : `Waiting for full route analysis`}
-
-
${formatPercent(drive.engagedPercent)} engaged
+
+
${ready ? `${formatPercent(drive.engagedPercent)} engaged` : "Pending"}
- `).join("");
+ `;
+ }).join("");
return `
diff --git a/starpilot/system/the_pond/tests/test_dashboard_stats.py b/starpilot/system/the_pond/tests/test_dashboard_stats.py
index 0c6a9ee76..0b2b57a18 100644
--- a/starpilot/system/the_pond/tests/test_dashboard_stats.py
+++ b/starpilot/system/the_pond/tests/test_dashboard_stats.py
@@ -207,6 +207,11 @@ class FakeParams:
self.values[key] = value
+class FailingPutParams(FakeParams):
+ def put(self, key, value):
+ raise RuntimeError("unknown key")
+
+
class FakeMessage:
def __init__(self, kind, log_mono_time, payload):
self._kind = kind
@@ -331,6 +336,20 @@ def test_route_sampling_bounds_analyzed_segments():
assert [segment["num"] for segment in sampled["segments"]] == [0, 4, 9]
+def test_route_listing_prefers_segment_candidates_with_logs(tmp_path):
+ hd_root = tmp_path / "realdata_HD"
+ standard_root = tmp_path / "realdata"
+ hd_segment = hd_root / "00000001--abcdef1234--0"
+ standard_segment = standard_root / "00000001--abcdef1234--0"
+ hd_segment.mkdir(parents=True)
+ standard_segment.mkdir(parents=True)
+ (standard_segment / "qlog.zst").write_bytes(b"")
+
+ routes = utilities._list_dashboard_routes([hd_root, standard_root])
+
+ assert routes[0]["segments"][0]["path"] == standard_segment
+
+
def test_top_models_are_ranked_from_persisted_usage_not_favorites():
params = FakeParams({
"AvailableModels": "orion,vega,atlas,nova",
@@ -439,6 +458,29 @@ def test_persistent_loader_accepts_decoded_param_dict():
assert stats["routes"]["route-1"]["attentionKnown"] is False
+def test_dashboard_persistent_stats_fallback_to_file_when_param_put_fails(tmp_path, monkeypatch):
+ monkeypatch.setattr(utilities, "DASHBOARD_PARAMS_DIR", tmp_path)
+ params = FailingPutParams()
+ drive = {
+ "name": "route-1",
+ "date": "2026-06-15T08:00:00",
+ "distanceMeters": 1000.0,
+ "duration": 60,
+ "engagedSeconds": 30.0,
+ "model": "Orion",
+ "routeModifiedAt": 100,
+ "attentionKnown": True,
+ "analysisComplete": True,
+ }
+
+ utilities._update_dashboard_persistent_stats(params, [drive], wall_now=1000)
+ stats = utilities._load_dashboard_persistent_stats(params)
+
+ assert (tmp_path / utilities.DASHBOARD_PERSISTENT_STATS_PARAM).is_file()
+ assert stats["routes"]["route-1"]["distanceMeters"] == 1000
+ assert stats["routes"]["route-1"]["analysisComplete"] is True
+
+
def test_lightweight_routes_surface_recent_drives_without_log_analysis(monkeypatch):
utilities._invalidate_dashboard_cache()
route_infos = [
@@ -513,6 +555,38 @@ def test_unknown_attention_rows_do_not_reset_persisted_clean_records():
assert records["cleanDriveStreak"]["value"] == "1 drive"
+def test_lightweight_route_update_preserves_parsed_duration():
+ params = FakeParams()
+ parsed_drive = {
+ "name": "route-1",
+ "date": "2026-06-15T08:00:00",
+ "distanceMeters": 10000.0,
+ "duration": 58,
+ "engagedSeconds": 30.0,
+ "model": "Orion",
+ "routeModifiedAt": 100,
+ "attentionKnown": True,
+ "analysisComplete": True,
+ }
+ shell_drive = {
+ "name": "route-1",
+ "date": "2026-06-15T08:00:00",
+ "distanceMeters": 0.0,
+ "duration": 60,
+ "engagedSeconds": 0.0,
+ "model": "Orion",
+ "routeModifiedAt": 100,
+ "attentionKnown": False,
+ "analysisComplete": False,
+ }
+
+ utilities._update_dashboard_persistent_stats(params, [parsed_drive], wall_now=1000)
+ stats = utilities._update_dashboard_persistent_stats(params, [shell_drive], wall_now=1000)
+
+ assert stats["routes"]["route-1"]["duration"] == 58
+ assert stats["routes"]["route-1"]["analysisComplete"] is True
+
+
def test_github_urls_accept_owner_repo_origin():
assert utilities.get_github_changelog_url("owner/repo", "main") == "https://github.com/owner/repo/commits/main/"
assert utilities.get_github_changelog_url("github.com/owner/repo", "main") == "https://github.com/owner/repo/commits/main/"
diff --git a/starpilot/system/the_pond/utilities.py b/starpilot/system/the_pond/utilities.py
index 2f8e45f51..5af834c8b 100644
--- a/starpilot/system/the_pond/utilities.py
+++ b/starpilot/system/the_pond/utilities.py
@@ -61,12 +61,13 @@ DASHBOARD_CACHE_TTL_SECONDS = 5.0
DASHBOARD_ROUTE_SCAN_LIMIT = 24
DASHBOARD_ROUTE_ANALYSIS_LIMIT = 0
DASHBOARD_ANALYSIS_TIME_BUDGET_SECONDS = 0.0
-DASHBOARD_BACKGROUND_ROUTE_ANALYSIS_LIMIT = 24
+DASHBOARD_BACKGROUND_ROUTE_ANALYSIS_LIMIT = 5
DASHBOARD_RECENT_DRIVE_LIMIT = 5
DASHBOARD_ROUTE_SEGMENT_SAMPLE_LIMIT = 2
DASHBOARD_PERSISTED_ROUTE_LIMIT = 5000
DASHBOARD_PERSIST_MIN_ROUTE_AGE_SECONDS = 120
DASHBOARD_PERSISTENT_STATS_PARAM = "GalaxyDashboardStats"
+DASHBOARD_PARAMS_DIR = Path("/data/params/d")
DASHBOARD_ANALYZER_LOG_PATH = "/tmp/galaxy_dashboard_analyzer.log"
DASHBOARD_TOP_MODEL_LIMIT = 3
DASHBOARD_EVENT_DISTRACTED = "promptDriverDistracted"
@@ -633,14 +634,44 @@ def _params_get_bool(params_obj, key):
return value.strip().lower() in ("1", "true", "yes", "on")
+def _dashboard_param_file_path(key):
+ if key != DASHBOARD_PERSISTENT_STATS_PARAM:
+ return None
+ return DASHBOARD_PARAMS_DIR / key
+
+
+def _read_dashboard_param_file(key):
+ path = _dashboard_param_file_path(key)
+ if path is None or not path.is_file():
+ return None
+ try:
+ return path.read_text(encoding="utf-8")
+ except Exception:
+ return None
+
+
+def _write_dashboard_param_file(key, value):
+ path = _dashboard_param_file_path(key)
+ if path is None:
+ return False
+ try:
+ path.parent.mkdir(parents=True, exist_ok=True)
+ tmp_path = path.with_name(f".{path.name}.{os.getpid()}.tmp")
+ tmp_path.write_text(str(value), encoding="utf-8")
+ os.replace(tmp_path, path)
+ return True
+ except Exception:
+ return False
+
+
def _params_put_text(params_obj, key, value):
if params_obj is None:
- return False
+ return _write_dashboard_param_file(key, value)
try:
params_obj.put(key, value)
return True
except Exception:
- return False
+ return _write_dashboard_param_file(key, value)
def _split_csv(value):
@@ -691,6 +722,19 @@ def _segment_mtime(segment_path):
return 0.0
+def _segment_has_dashboard_log(segment_path):
+ path = Path(segment_path)
+ if not path.is_dir():
+ return False
+ return any((path / candidate).is_file() for candidate in ROUTE_TIME_LOG_CANDIDATES)
+
+
+def _select_dashboard_segment_candidate(candidates):
+ if not candidates:
+ return None
+ return next((candidate for candidate in candidates if _segment_has_dashboard_log(candidate)), candidates[0])
+
+
def _list_dashboard_routes(footage_paths, limit=DASHBOARD_ROUTE_SCAN_LIMIT):
routes = {}
@@ -726,7 +770,9 @@ def _list_dashboard_routes(footage_paths, limit=DASHBOARD_ROUTE_SCAN_LIMIT):
for route in routes.values():
segments = []
for segment_num, candidates in sorted(route["segments_by_num"].items()):
- segments.append({"num": segment_num, "path": candidates[0]})
+ selected = _select_dashboard_segment_candidate(candidates)
+ if selected is not None:
+ segments.append({"num": segment_num, "path": selected})
if not segments:
continue
@@ -993,6 +1039,7 @@ def _analyze_route_messages(messages, route_info, model_names, is_metric, deadli
"unresponsiveMoments": unresponsive_moments,
"routeModifiedAt": _safe_float(route_info.get("modifiedAt", 0.0), 0.0),
"attentionKnown": True,
+ "analysisComplete": analysis_segment_count >= segment_count,
}
@@ -1030,6 +1077,8 @@ def _empty_drive(is_metric):
"segmentCount": 0,
"distractedMoments": 0,
"unresponsiveMoments": 0,
+ "attentionKnown": True,
+ "analysisComplete": False,
"distanceUnit": "kilometers" if is_metric else "miles",
"speedUnit": "kph" if is_metric else "mph",
}
@@ -1076,6 +1125,7 @@ def _route_shell_drive(route_info, params_obj, model_names, is_metric):
"unresponsiveMoments": 0,
"routeModifiedAt": _safe_float(route_info.get("modifiedAt", 0.0), 0.0),
"attentionKnown": False,
+ "analysisComplete": False,
}
@@ -1100,6 +1150,7 @@ def _drive_from_persistent_route(route_name, entry, is_metric):
"unresponsiveMoments": max(0, _safe_int(entry.get("unresponsiveMoments", 0), 0)),
"routeModifiedAt": _safe_float(entry.get("modifiedAt", 0.0), 0.0),
"attentionKnown": bool(entry.get("attentionKnown", True)),
+ "analysisComplete": bool(entry.get("analysisComplete", False)),
}
@@ -1147,7 +1198,7 @@ def _analysis_candidates(route_infos, persistent_stats):
return True
if _safe_float(entry.get("modifiedAt", 0.0), 0.0) < _safe_float(route_info.get("modifiedAt", 0.0), 0.0):
return True
- return not bool(entry.get("attentionKnown", True))
+ return not bool(entry.get("attentionKnown", True)) or not bool(entry.get("analysisComplete", False))
missing = [route_info for route_info in route_infos if needs_analysis(route_info)]
return missing
@@ -1180,9 +1231,10 @@ def warm_dashboard_stats(footage_paths=None):
persistent_stats = _load_dashboard_persistent_stats(params_obj)
candidates = _analysis_candidates(route_infos, persistent_stats)[:DASHBOARD_BACKGROUND_ROUTE_ANALYSIS_LIMIT]
for route_info in candidates:
- sampled_route_info = _sample_route_info(route_info)
- messages = _iter_route_log_messages(sampled_route_info)
- drive = _analyze_route_messages(messages, sampled_route_info, model_names, is_metric)
+ full_route_info = dict(route_info)
+ full_route_info["analysisSegmentCount"] = max(0, _safe_int(route_info.get("segmentCount", 0), 0))
+ messages = _iter_route_log_messages(full_route_info)
+ drive = _analyze_route_messages(messages, full_route_info, model_names, is_metric)
_update_dashboard_persistent_stats(params_obj, [drive], time.time())
@@ -1541,12 +1593,16 @@ def _normalize_persistent_routes(raw_routes):
"segmentCount": max(0, _safe_int(entry.get("segmentCount", 0), 0)),
"modifiedAt": _safe_float(entry.get("modifiedAt", 0.0), 0.0),
"attentionKnown": bool(entry.get("attentionKnown", True)),
+ "analysisComplete": bool(entry.get("analysisComplete", False)),
}
return routes
def _load_dashboard_persistent_stats(params_obj):
- data = _decode_json_param(_params_get_value(params_obj, DASHBOARD_PERSISTENT_STATS_PARAM, None), {})
+ raw_data = _read_dashboard_param_file(DASHBOARD_PERSISTENT_STATS_PARAM)
+ if raw_data is None:
+ raw_data = _params_get_value(params_obj, DASHBOARD_PERSISTENT_STATS_PARAM, None)
+ data = _decode_json_param(raw_data, {})
if not isinstance(data, dict):
data = {}
data["version"] = 1
@@ -1771,6 +1827,7 @@ def _update_dashboard_persistent_stats(params_obj, drives, wall_now):
"segmentCount": max(0, _safe_int(drive.get("segmentCount", 0), 0)),
"modifiedAt": _safe_float(drive.get("routeModifiedAt", 0.0), 0.0),
"attentionKnown": attention_known,
+ "analysisComplete": bool(drive.get("analysisComplete", False)),
}
existing_entry = routes.get(route_name)
if isinstance(existing_entry, dict):
@@ -1782,12 +1839,15 @@ def _update_dashboard_persistent_stats(params_obj, drives, wall_now):
next_entry["clean"] = bool(existing_entry.get("clean", False))
next_entry["undistracted"] = bool(existing_entry.get("undistracted", existing_entry.get("clean", False)))
next_entry["attentionKnown"] = True
+ next_entry["analysisComplete"] = bool(existing_entry.get("analysisComplete", False))
if not attention_known and existing_current and existing_distance >= next_distance:
next_entry["distanceMeters"] = existing_distance
- next_entry["duration"] = max(_safe_int(existing_entry.get("duration", 0), 0), next_entry["duration"])
+ existing_duration = _safe_int(existing_entry.get("duration", 0), 0)
+ next_entry["duration"] = existing_duration if existing_attention_known else max(existing_duration, next_entry["duration"])
next_entry["engagedSeconds"] = max(0.0, _safe_float(existing_entry.get("engagedSeconds", 0.0), 0.0))
next_entry["distractedMoments"] = max(0, _safe_int(existing_entry.get("distractedMoments", 0), 0))
next_entry["unresponsiveMoments"] = max(0, _safe_int(existing_entry.get("unresponsiveMoments", 0), 0))
+ next_entry["analysisComplete"] = bool(existing_entry.get("analysisComplete", False))
if (not model_name or model_name == "Unknown model") and _clean_model_label(existing_entry.get("model", "")):
next_entry["model"] = _clean_model_label(existing_entry.get("model", ""))
next_entry["modelKey"] = canonical_model_key(existing_entry.get("modelKey", "")) or _model_usage_key(next_entry["model"])