From be046dd29ad619536fe8edd532d1cff9e87fd4b5 Mon Sep 17 00:00:00 2001 From: Wes Huber Date: Sun, 14 Jun 2026 23:07:03 -0700 Subject: [PATCH] fix(cookbook): preserve state during lifecycle tick Log malformed cookbook state and re-read fresh state before writing scheduled-stop mutations so concurrent UI changes are preserved. --- src/cookbook_serve_lifecycle.py | 25 ++++++++++++++++++++++--- 1 file changed, 22 insertions(+), 3 deletions(-) diff --git a/src/cookbook_serve_lifecycle.py b/src/cookbook_serve_lifecycle.py index e30ddfd09..fcdacbe7a 100644 --- a/src/cookbook_serve_lifecycle.py +++ b/src/cookbook_serve_lifecycle.py @@ -136,7 +136,8 @@ async def _tick() -> None: return try: state = json.loads(state_path.read_text(encoding="utf-8")) - except Exception: + except Exception as e: + logger.warning("cookbook_serve_lifecycle: state file unreadable (%s), skipping tick", e) return tasks = state.get("tasks") or [] now_ms = int(time.time() * 1000) @@ -178,8 +179,26 @@ async def _tick() -> None: if stopped_any: try: from core.atomic_io import atomic_write_json - state["tasks"] = tasks - atomic_write_json(state_path, state) + # Re-read the state file so concurrent UI writes (task adds, + # status flips, config edits) are not silently overwritten. + # Apply only our stop mutations to the fresh snapshot. + try: + fresh = json.loads(state_path.read_text(encoding="utf-8")) + fresh_tasks = fresh.get("tasks") or [] + except Exception: + fresh = state + fresh_tasks = tasks + stopped_sids = {sid for sid, _, _ in to_stop} + for ft in fresh_tasks: + if not isinstance(ft, dict): + continue + ft_sid = ft.get("sessionId") or ft.get("id") + if ft_sid in stopped_sids: + ft["status"] = "stopped" + ft["_scheduledStopAtMs"] = None + ft["_lastStatusFlipAt"] = now_ms + fresh["tasks"] = fresh_tasks + atomic_write_json(state_path, fresh) except Exception as e: logger.warning(f"cookbook_serve_lifecycle: state write failed: {e}")