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}")