From 4e63fffec7ebd0d649dfcfa321a44c8b02ec3cb2 Mon Sep 17 00:00:00 2001 From: huangrt01 Date: Sun, 28 Jun 2026 02:21:48 +0800 Subject: [PATCH 1/4] Guard unscoped side-agent refresh state --- .../refresh-state-agent-lane-scope-smoke.py | 71 +++++++- loopx/state_refresh.py | 155 ++++++++++++++++-- 2 files changed, 209 insertions(+), 17 deletions(-) diff --git a/examples/refresh-state-agent-lane-scope-smoke.py b/examples/refresh-state-agent-lane-scope-smoke.py index bd33376a..5006a632 100644 --- a/examples/refresh-state-agent-lane-scope-smoke.py +++ b/examples/refresh-state-agent-lane-scope-smoke.py @@ -119,6 +119,71 @@ def main() -> None: assert side_payload["progress_scope"] == "agent_lane", side_payload assert side_payload["agent_id"] == "codex-side-bypass", side_payload + unscoped_side_action = f"Continue todo_side: {SIDE_ACTION}" + blocked_next_action_error = None + try: + state_refresh.refresh_state_run( + registry_path=registry_path, + runtime_root_override=str(runtime), + goal_id=GOAL_ID, + project=project, + state_file=None, + classification="frontstage_side_lane_next_action_write", + recommended_action=unscoped_side_action, + next_action=unscoped_side_action, + delivery_batch_scale="single_surface", + delivery_outcome="outcome_progress", + dry_run=True, + sync_global=False, + ) + except ValueError as exc: + blocked_next_action_error = str(exc) + assert blocked_next_action_error and ( + "inferred non-primary agent-lane scope" in blocked_next_action_error + ) + + state_refresh.now_local = lambda: "2026-06-20T00:02:00+00:00" + unscoped_side_payload = state_refresh.refresh_state_run( + registry_path=registry_path, + runtime_root_override=str(runtime), + goal_id=GOAL_ID, + project=project, + state_file=None, + classification="frontstage_side_lane_next_unscoped", + recommended_action=unscoped_side_action, + delivery_batch_scale="single_surface", + delivery_outcome="outcome_progress", + dry_run=False, + sync_global=False, + ) + assert unscoped_side_payload["progress_scope"] == "agent_lane", unscoped_side_payload + assert unscoped_side_payload["agent_id"] == "codex-side-bypass", unscoped_side_payload + assert unscoped_side_payload["agent_lane_scope_inference"]["todo_id"] == "todo_side" + + primary_review_handoff = ( + "Primary review todo_primary should inspect the refactor before deeper splits; " + "codex-side-bypass should switch to another eligible product todo." + ) + state_refresh.now_local = lambda: "2026-06-20T00:03:00+00:00" + handoff_payload = state_refresh.refresh_state_run( + registry_path=registry_path, + runtime_root_override=str(runtime), + goal_id=GOAL_ID, + project=project, + state_file=None, + classification="side_lane_review_handoff", + recommended_action=primary_review_handoff, + delivery_batch_scale="single_surface", + delivery_outcome="outcome_progress", + dry_run=False, + sync_global=False, + ) + assert handoff_payload["progress_scope"] == "agent_lane", handoff_payload + assert handoff_payload["agent_id"] == "codex-side-bypass", handoff_payload + assert handoff_payload["agent_lane_scope_inference"]["source"] == ( + "referenced_registered_non_primary_agent" + ) + history = collect_history( registry_path=registry_path, runtime_root=runtime, @@ -126,7 +191,7 @@ def main() -> None: limit=5, ) goal = history["goals"][0] - assert goal["latest_runs"][0]["classification"] == "frontstage_side_lane_next", goal + assert goal["latest_runs"][0]["classification"] == "side_lane_review_handoff", goal assert goal["latest_status_run"]["classification"] == "terminal_bench_primary_ready", goal status = collect_status( @@ -142,8 +207,8 @@ def main() -> None: lane = item["agent_lane_recommendation"] assert lane["progress_scope"] == "agent_lane", lane assert lane["agent_id"] == "codex-side-bypass", lane - assert lane["agent_lane"] == "productization_frontstage", lane - assert lane["recommended_action"] == SIDE_ACTION, lane + assert lane["agent_lane"] == "codex-side-bypass", lane + assert lane["recommended_action"] == primary_review_handoff, lane assert item["project_asset"]["agent_lane_recommendation"] == lane, item finally: state_refresh.now_local = original_now_local diff --git a/loopx/state_refresh.py b/loopx/state_refresh.py index b79bc6ae..e6890bfb 100644 --- a/loopx/state_refresh.py +++ b/loopx/state_refresh.py @@ -22,6 +22,9 @@ TODO_TASK_CLASS_BLOCKER, TODO_TASK_CLASS_MONITOR, TODO_TASK_CLASS_USER_GATE, + normalize_todo_claimed_by, + normalize_todo_id, + parse_todo_metadata_line, ) @@ -38,6 +41,7 @@ ACTIVE_STATE_NEXT_ACTION_UPDATE_SCHEMA_VERSION = "active_state_next_action_update_v0" REPAIR_DELTA_CONTRACT_SCHEMA_VERSION = "repair_delta_contract_v0" REPAIR_NOOP_SCHEMA_VERSION = "repair_noop_v0" +TODO_ID_REFERENCE_RE = re.compile(r"\btodo_[a-z0-9_-]{3,64}\b", flags=re.IGNORECASE) REPAIR_DELTA_KIND_CHOICES = ( "effective_action", "interaction_contract", @@ -141,6 +145,117 @@ def normalize_repair_delta_kinds(values: list[str] | None) -> list[str]: return normalized +def registered_agents_for_goal(registry_goal: dict[str, Any] | None) -> list[str]: + coordination = ( + registry_goal.get("coordination") + if registry_goal and isinstance(registry_goal.get("coordination"), dict) + else {} + ) + registered_raw = coordination.get("registered_agents") if isinstance(coordination, dict) else [] + registered_values = registered_raw if isinstance(registered_raw, list) else [] + registered_agents: list[str] = [] + for value in registered_values: + candidate = value.get("id") if isinstance(value, dict) else value + normalized = normalize_todo_claimed_by(candidate) + if normalized: + registered_agents.append(normalized) + return registered_agents + + +def primary_agent_for_goal(registry_goal: dict[str, Any] | None) -> str | None: + coordination = ( + registry_goal.get("coordination") + if registry_goal and isinstance(registry_goal.get("coordination"), dict) + else {} + ) + return normalize_todo_claimed_by(coordination.get("primary_agent") if coordination else None) + + +def todo_claims_by_id(state_text: str) -> dict[str, str]: + claims: dict[str, str] = {} + for line in state_text.splitlines(): + metadata = parse_todo_metadata_line(line) + if not metadata: + continue + todo_id = normalize_todo_id(metadata.get("todo_id")) + claimed_by = normalize_todo_claimed_by(metadata.get("claimed_by")) + if todo_id and claimed_by: + claims[todo_id] = claimed_by + return claims + + +def referenced_todo_ids(*values: str | None) -> list[str]: + todo_ids: list[str] = [] + seen: set[str] = set() + for value in values: + for match in TODO_ID_REFERENCE_RE.findall(str(value or "")): + todo_id = normalize_todo_id(match) + if todo_id and todo_id not in seen: + seen.add(todo_id) + todo_ids.append(todo_id) + return todo_ids + + +def referenced_non_primary_agent( + *, + registered_agents: list[str], + primary_agent: str, + values: list[str | None], +) -> str | None: + for value in values: + text = str(value or "") + for agent_id in registered_agents: + if agent_id == primary_agent: + continue + pattern = rf"(? dict[str, Any] | None: + registered_agents = registered_agents_for_goal(registry_goal) + primary_agent = primary_agent_for_goal(registry_goal) + if not registered_agents or not primary_agent: + return None + claims_by_id = todo_claims_by_id(state_text) + for todo_id in referenced_todo_ids(recommended_action, next_action): + claimed_by = claims_by_id.get(todo_id) + if not claimed_by or claimed_by == primary_agent or claimed_by not in registered_agents: + continue + return { + "schema_version": "refresh_state_agent_lane_scope_inference_v0", + "inferred": True, + "agent_id": claimed_by, + "agent_lane": claimed_by, + "todo_id": todo_id, + "primary_agent": primary_agent, + "source": "referenced_claimed_todo", + } + agent_id = referenced_non_primary_agent( + registered_agents=registered_agents, + primary_agent=primary_agent, + values=[recommended_action, next_action], + ) + if agent_id: + return { + "schema_version": "refresh_state_agent_lane_scope_inference_v0", + "inferred": True, + "agent_id": agent_id, + "agent_lane": agent_id, + "todo_id": None, + "primary_agent": primary_agent, + "source": "referenced_registered_non_primary_agent", + } + return None + + def _noop_classification_for(classification: str) -> str: normalized = str(classification or "").strip().lower() if "repair" in normalized and "replan" not in normalized: @@ -606,25 +721,35 @@ def refresh_state_run( project_override=project, state_file_override=state_file, ) + if not resolved_state_file.exists(): + raise FileNotFoundError(f"state file does not exist: {resolved_state_file}") + state_text = resolved_state_file.read_text(encoding="utf-8") + normalized_next_action = normalize_next_action_text(next_action) if next_action else None + agent_lane_scope_inference: dict[str, Any] | None = None + if not normalized_agent_id: + inferred_scope = infer_agent_lane_scope( + registry_goal=registry_goal, + state_text=state_text, + recommended_action=recommended_action, + next_action=normalized_next_action, + ) + if inferred_scope: + if normalized_next_action: + raise ValueError( + "unscoped refresh-state inferred non-primary agent-lane scope " + f"from todo_id={inferred_scope.get('todo_id')!r}; rerun with " + f"--agent-id {inferred_scope.get('agent_id')} without --next-action, " + "or have the primary agent update the durable active-state Next Action" + ) + normalized_agent_id = str(inferred_scope["agent_id"]) + normalized_agent_lane = str(inferred_scope["agent_lane"]) + agent_lane_scope_inference = inferred_scope if normalized_agent_id and registry_goal: - coordination = registry_goal.get("coordination") if isinstance(registry_goal.get("coordination"), dict) else {} - registered_raw = coordination.get("registered_agents") if isinstance(coordination, dict) else [] - registered_agents = [] - registered_values = registered_raw if isinstance(registered_raw, list) else [] - for value in registered_values: - if isinstance(value, dict): - registered_agents.append(str(value.get("id") or "")) - else: - registered_agents.append(str(value or "")) - registered_agents = [value for value in registered_agents if value] + registered_agents = registered_agents_for_goal(registry_goal) if registered_agents and normalized_agent_id not in registered_agents: raise ValueError( f"agent_id {normalized_agent_id!r} is not registered for goal {safe_goal_id!r}" ) - if not resolved_state_file.exists(): - raise FileNotFoundError(f"state file does not exist: {resolved_state_file}") - state_text = resolved_state_file.read_text(encoding="utf-8") - normalized_next_action = normalize_next_action_text(next_action) if next_action else None generated_at = now_local() active_state_next_action_update: dict[str, Any] | None = None if normalized_next_action: @@ -684,6 +809,8 @@ def refresh_state_run( autonomous_replan_recorded=effective_autonomous_replan_recorded, repair_delta_contract=repair_delta_contract, ) + if agent_lane_scope_inference: + record["agent_lane_scope_inference"] = agent_lane_scope_inference if autonomous_replan_recorded: if "autonomous_replan_ack" not in record: record["autonomous_replan_ack"] = { From 92d2ed4c9ecb304710be49faf2564114c2c891ff Mon Sep 17 00:00:00 2001 From: huangrt01 Date: Sun, 28 Jun 2026 02:43:39 +0800 Subject: [PATCH 2/4] Tighten side-agent refresh-state next-action guard --- .../refresh-state-agent-lane-scope-smoke.py | 49 +++++++++++++++++-- loopx/state_refresh.py | 14 ++++++ 2 files changed, 60 insertions(+), 3 deletions(-) diff --git a/examples/refresh-state-agent-lane-scope-smoke.py b/examples/refresh-state-agent-lane-scope-smoke.py index 5006a632..cf3cf4b7 100644 --- a/examples/refresh-state-agent-lane-scope-smoke.py +++ b/examples/refresh-state-agent-lane-scope-smoke.py @@ -120,6 +120,29 @@ def main() -> None: assert side_payload["agent_id"] == "codex-side-bypass", side_payload unscoped_side_action = f"Continue todo_side: {SIDE_ACTION}" + side_next_action_error = None + try: + state_refresh.refresh_state_run( + registry_path=registry_path, + runtime_root_override=str(runtime), + goal_id=GOAL_ID, + project=project, + state_file=None, + classification="frontstage_side_lane_explicit_next_action_write", + recommended_action=unscoped_side_action, + next_action=unscoped_side_action, + delivery_batch_scale="single_surface", + delivery_outcome="outcome_progress", + agent_id="codex-side-bypass", + dry_run=True, + sync_global=False, + ) + except ValueError as exc: + side_next_action_error = str(exc) + assert side_next_action_error and ( + "cannot update the durable active-state Next Action" in side_next_action_error + ) + blocked_next_action_error = None try: state_refresh.refresh_state_run( @@ -164,6 +187,27 @@ def main() -> None: "Primary review todo_primary should inspect the refactor before deeper splits; " "codex-side-bypass should switch to another eligible product todo." ) + ambiguous_agent_mention_error = None + try: + state_refresh.refresh_state_run( + registry_path=registry_path, + runtime_root_override=str(runtime), + goal_id=GOAL_ID, + project=project, + state_file=None, + classification="side_lane_review_handoff_ambiguous", + recommended_action=primary_review_handoff, + delivery_batch_scale="single_surface", + delivery_outcome="outcome_progress", + dry_run=True, + sync_global=False, + ) + except ValueError as exc: + ambiguous_agent_mention_error = str(exc) + assert ambiguous_agent_mention_error and ( + "text-only agent references are ambiguous" in ambiguous_agent_mention_error + ) + state_refresh.now_local = lambda: "2026-06-20T00:03:00+00:00" handoff_payload = state_refresh.refresh_state_run( registry_path=registry_path, @@ -175,14 +219,13 @@ def main() -> None: recommended_action=primary_review_handoff, delivery_batch_scale="single_surface", delivery_outcome="outcome_progress", + agent_id="codex-side-bypass", dry_run=False, sync_global=False, ) assert handoff_payload["progress_scope"] == "agent_lane", handoff_payload assert handoff_payload["agent_id"] == "codex-side-bypass", handoff_payload - assert handoff_payload["agent_lane_scope_inference"]["source"] == ( - "referenced_registered_non_primary_agent" - ) + assert "agent_lane_scope_inference" not in handoff_payload, handoff_payload history = collect_history( registry_path=registry_path, diff --git a/loopx/state_refresh.py b/loopx/state_refresh.py index e6890bfb..cccbf5b9 100644 --- a/loopx/state_refresh.py +++ b/loopx/state_refresh.py @@ -741,6 +741,13 @@ def refresh_state_run( f"--agent-id {inferred_scope.get('agent_id')} without --next-action, " "or have the primary agent update the durable active-state Next Action" ) + if inferred_scope.get("source") == "referenced_registered_non_primary_agent": + raise ValueError( + "unscoped refresh-state mentions registered non-primary agent " + f"{inferred_scope.get('agent_id')!r}; rerun with explicit " + f"--agent-id {inferred_scope.get('agent_id')} because text-only " + "agent references are ambiguous" + ) normalized_agent_id = str(inferred_scope["agent_id"]) normalized_agent_lane = str(inferred_scope["agent_lane"]) agent_lane_scope_inference = inferred_scope @@ -750,6 +757,13 @@ def refresh_state_run( raise ValueError( f"agent_id {normalized_agent_id!r} is not registered for goal {safe_goal_id!r}" ) + primary_agent = primary_agent_for_goal(registry_goal) + if normalized_next_action and primary_agent and normalized_agent_id != primary_agent: + raise ValueError( + "non-primary agent " + f"{normalized_agent_id!r} cannot update the durable active-state Next Action; " + "rerun without --next-action or have the primary agent update the global Next Action" + ) generated_at = now_local() active_state_next_action_update: dict[str, Any] | None = None if normalized_next_action: From 8bf1cf19632a14a0d84fac78508c7094d67ade3b Mon Sep 17 00:00:00 2001 From: huangrt01 Date: Sun, 28 Jun 2026 03:32:10 +0800 Subject: [PATCH 3/4] Infer side-agent refresh scope from claimed todo text --- .../refresh-state-agent-lane-scope-smoke.py | 36 ++++++++++ loopx/state_refresh.py | 68 ++++++++++++++++++- 2 files changed, 103 insertions(+), 1 deletion(-) diff --git a/examples/refresh-state-agent-lane-scope-smoke.py b/examples/refresh-state-agent-lane-scope-smoke.py index cf3cf4b7..d66801e2 100644 --- a/examples/refresh-state-agent-lane-scope-smoke.py +++ b/examples/refresh-state-agent-lane-scope-smoke.py @@ -18,6 +18,10 @@ GOAL_ID = "refresh-state-agent-lane-goal" PRIMARY_ACTION = "Run the primary benchmark bootstrap hardening slice." SIDE_ACTION = "Polish the hosted frontstage showcase for external developers." +AUTO_RESEARCH_ACTION = ( + "[P0-auto-research] Use rollout-backed research_evidence_graph_v0 to generate " + "live promotion and retirement candidates." +) def write_fixture(root: Path) -> tuple[Path, Path, Path]: @@ -43,6 +47,9 @@ def write_fixture(root: Path) -> tuple[Path, Path, Path]: f"- [ ] [P1] {SIDE_ACTION}\n" " \n\n" + f"- [ ] {AUTO_RESEARCH_ACTION}\n" + " \n\n" "## Next Action\n\n" f"- {PRIMARY_ACTION}\n", encoding="utf-8", @@ -182,6 +189,35 @@ def main() -> None: assert unscoped_side_payload["progress_scope"] == "agent_lane", unscoped_side_payload assert unscoped_side_payload["agent_id"] == "codex-side-bypass", unscoped_side_payload assert unscoped_side_payload["agent_lane_scope_inference"]["todo_id"] == "todo_side" + assert ( + unscoped_side_payload["agent_lane_scope_inference"]["source"] + == "referenced_claimed_todo" + ) + + state_refresh.now_local = lambda: "2026-06-20T00:02:30+00:00" + unscoped_text_payload = state_refresh.refresh_state_run( + registry_path=registry_path, + runtime_root_override=str(runtime), + goal_id=GOAL_ID, + project=project, + state_file=None, + classification="auto_research_rollout_read_path_merged", + recommended_action=AUTO_RESEARCH_ACTION, + delivery_batch_scale="single_surface", + delivery_outcome="outcome_progress", + dry_run=False, + sync_global=False, + ) + assert unscoped_text_payload["progress_scope"] == "agent_lane", unscoped_text_payload + assert unscoped_text_payload["agent_id"] == "codex-side-bypass", unscoped_text_payload + assert ( + unscoped_text_payload["agent_lane_scope_inference"]["source"] + == "matched_claimed_todo_text" + ) + assert ( + unscoped_text_payload["agent_lane_scope_inference"]["todo_id"] + == "todo_auto_research" + ) primary_review_handoff = ( "Primary review todo_primary should inspect the refactor before deeper splits; " diff --git a/loopx/state_refresh.py b/loopx/state_refresh.py index cccbf5b9..f497722a 100644 --- a/loopx/state_refresh.py +++ b/loopx/state_refresh.py @@ -184,6 +184,52 @@ def todo_claims_by_id(state_text: str) -> dict[str, str]: return claims +def normalize_action_match_text(value: str | None) -> str: + text = " ".join(str(value or "").strip().lower().split()) + text = BULLET_PREFIX_RE.sub("", text).strip() + checkbox = CHECKBOX_PREFIX_RE.match(text) + if checkbox: + text = text[checkbox.end() :].strip() + return text + + +def todo_claims_by_action_text(state_text: str) -> list[dict[str, str]]: + claims: list[dict[str, str]] = [] + previous_todo_text: str | None = None + for line in state_text.splitlines(): + stripped = line.strip() + if BULLET_PREFIX_RE.match(stripped): + bullet_text = BULLET_PREFIX_RE.sub("", stripped).strip() + if CHECKBOX_PREFIX_RE.match(bullet_text): + previous_todo_text = CHECKBOX_PREFIX_RE.sub("", bullet_text).strip() + else: + previous_todo_text = None + metadata = parse_todo_metadata_line(line) + if not metadata: + continue + todo_id = normalize_todo_id(metadata.get("todo_id")) + claimed_by = normalize_todo_claimed_by(metadata.get("claimed_by")) + status = str(metadata.get("status") or "").strip().lower() + if ( + not todo_id + or not claimed_by + or not previous_todo_text + or status in {"done", "deferred", "blocked"} + ): + continue + normalized_text = normalize_action_match_text(previous_todo_text) + if len(normalized_text) < 32: + continue + claims.append( + { + "todo_id": todo_id, + "claimed_by": claimed_by, + "action_text": normalized_text, + } + ) + return claims + + def referenced_todo_ids(*values: str | None) -> list[str]: todo_ids: list[str] = [] seen: set[str] = set() @@ -238,10 +284,30 @@ def infer_agent_lane_scope( "primary_agent": primary_agent, "source": "referenced_claimed_todo", } + values = [recommended_action, next_action] + normalized_values = [normalize_action_match_text(value) for value in values if value] + for claim in todo_claims_by_action_text(state_text): + claimed_by = claim["claimed_by"] + if claimed_by == primary_agent or claimed_by not in registered_agents: + continue + todo_text = claim["action_text"] + for value in normalized_values: + if not value or len(value) < 32: + continue + if value == todo_text or value in todo_text or todo_text in value: + return { + "schema_version": "refresh_state_agent_lane_scope_inference_v0", + "inferred": True, + "agent_id": claimed_by, + "agent_lane": claimed_by, + "todo_id": claim["todo_id"], + "primary_agent": primary_agent, + "source": "matched_claimed_todo_text", + } agent_id = referenced_non_primary_agent( registered_agents=registered_agents, primary_agent=primary_agent, - values=[recommended_action, next_action], + values=values, ) if agent_id: return { From c7d92043226d83f8e2b394c5ae0317730484b23e Mon Sep 17 00:00:00 2001 From: huangrt01 Date: Sun, 28 Jun 2026 03:45:36 +0800 Subject: [PATCH 4/4] Suppress stale warning after agent-lane snapshots --- .../refresh-state-agent-lane-scope-smoke.py | 14 +++++++++++++ loopx/state_refresh.py | 12 +++++++++++ loopx/status.py | 21 +++++++++++++++++++ 3 files changed, 47 insertions(+) diff --git a/examples/refresh-state-agent-lane-scope-smoke.py b/examples/refresh-state-agent-lane-scope-smoke.py index d66801e2..0f8919f6 100644 --- a/examples/refresh-state-agent-lane-scope-smoke.py +++ b/examples/refresh-state-agent-lane-scope-smoke.py @@ -106,6 +106,15 @@ def main() -> None: dry_run=False, sync_global=False, ) + state_path = project / f".codex/goals/{GOAL_ID}/ACTIVE_GOAL_STATE.md" + state_path.write_text( + state_path.read_text(encoding="utf-8").replace( + "updated_at: 2026-06-20T00:00:00+00:00", + "updated_at: 2026-06-20T00:00:30+00:00", + 1, + ), + encoding="utf-8", + ) state_refresh.now_local = lambda: "2026-06-20T00:01:00+00:00" side_payload = state_refresh.refresh_state_run( @@ -271,6 +280,9 @@ def main() -> None: ) goal = history["goals"][0] assert goal["latest_runs"][0]["classification"] == "side_lane_review_handoff", goal + latest_run_state = goal["latest_runs"][0]["state"] + assert latest_run_state["sha256_16"], latest_run_state + assert latest_run_state["frontmatter"]["updated_at"] == "2026-06-20T00:00:30+00:00" assert goal["latest_status_run"]["classification"] == "terminal_bench_primary_ready", goal status = collect_status( @@ -283,6 +295,8 @@ def main() -> None: item = next(item for item in items if item["goal_id"] == GOAL_ID) assert item["status"] == "terminal_bench_primary_ready", item assert item["recommended_action"] == PRIMARY_ACTION, item + assert "stale_latest_run_warning" not in item, item + assert "stale_latest_run_warning" not in item["project_asset"], item lane = item["agent_lane_recommendation"] assert lane["progress_scope"] == "agent_lane", lane assert lane["agent_id"] == "codex-side-bypass", lane diff --git a/loopx/state_refresh.py b/loopx/state_refresh.py index f497722a..9f7453e1 100644 --- a/loopx/state_refresh.py +++ b/loopx/state_refresh.py @@ -924,6 +924,18 @@ def refresh_state_run( "json_path": str(json_path), "markdown_path": str(markdown_path), } + record_state = record.get("state") if isinstance(record.get("state"), dict) else {} + record_frontmatter = ( + record_state.get("frontmatter") + if isinstance(record_state.get("frontmatter"), dict) + else {} + ) + index_record["state"] = { + "sha256_16": record_state.get("sha256_16"), + "frontmatter": { + "updated_at": record_frontmatter.get("updated_at"), + }, + } if normalized_delivery_batch_scale: index_record["delivery_batch_scale"] = normalized_delivery_batch_scale if normalized_delivery_outcome: diff --git a/loopx/status.py b/loopx/status.py index bc386997..4af5a877 100644 --- a/loopx/status.py +++ b/loopx/status.py @@ -6613,6 +6613,27 @@ def active_state_projection_warning(goal: dict[str, Any], current_run: dict[str, digest_mismatch = bool(run_state_digest and active_digest != run_state_digest) if not (active_newer_than_run_state or active_newer_than_run or digest_mismatch): return None + for run in goal.get("latest_runs") if isinstance(goal.get("latest_runs"), list) else []: + if not isinstance(run, dict): + continue + if str(run.get("progress_scope") or "") != AGENT_LANE_PROGRESS_SCOPE: + continue + agent_run_state = run.get("state") if isinstance(run.get("state"), dict) else {} + agent_run_frontmatter = ( + agent_run_state.get("frontmatter") + if isinstance(agent_run_state.get("frontmatter"), dict) + else {} + ) + agent_run_digest = str(agent_run_state.get("sha256_16") or "") + agent_run_updated_at = agent_run_frontmatter.get("updated_at") + agent_run_state_dt = parse_timestamp(agent_run_updated_at) + agent_run_generated_dt = parse_timestamp(str(run.get("generated_at") or "")) + if agent_run_digest and agent_run_digest == active_digest: + return None + if active_dt and agent_run_state_dt and active_dt <= agent_run_state_dt: + return None + if active_dt and not agent_run_state_dt and agent_run_generated_dt and active_dt <= agent_run_generated_dt: + return None reasons: list[str] = [] if active_newer_than_run: