Skip to content

Commit e79d8d4

Browse files
authored
fix(quota): bind watch ACKs to causal monitor frontier (#3037)
1 parent 7ed136d commit e79d8d4

8 files changed

Lines changed: 387 additions & 8 deletions

File tree

loopx/control_plane/goals/goal_frontier/__init__.py

Lines changed: 61 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,10 @@
1818
MONITOR_NO_CHANGE_STREAK_THRESHOLD,
1919
build_autonomous_replan_obligation_payload,
2020
)
21-
from ...work_items.repair_delta import repair_delta_kinds_have_frontier_delta
21+
from ...work_items.repair_delta import (
22+
repair_delta_kinds_have_frontier_delta,
23+
validate_repair_delta_claims,
24+
)
2225
from ..goal_vision_policy import goal_vision_repeats_advancement_until_closed
2326
from ..goal_vision_state import (
2427
goal_vision_state_is_closed,
@@ -243,6 +246,8 @@ def _watch_lane_ack_covers_dead_monitor_repeat(
243246
*,
244247
replan_obligation: dict[str, Any] | None,
245248
acceptance_gaps: list[dict[str, Any]],
249+
agent_todo_summary: dict[str, Any] | None,
250+
agent_id: str | None,
246251
) -> bool:
247252
"""Keep an as-needed watch ACK valid across unchanged heartbeat receipts."""
248253

@@ -262,15 +267,66 @@ def _watch_lane_ack_covers_dead_monitor_repeat(
262267
delta_kind="watch_lane_continuation",
263268
):
264269
return False
270+
if not isinstance(ack, dict):
271+
return False
265272
if not acceptance_gaps or any(
266273
gap.get("kind") != "vision_acceptance_gap"
267274
for gap in acceptance_gaps
268275
):
269276
return False
270-
return not any(
277+
if any(
271278
goal_vision_repeats_advancement_until_closed(gap.get("advancement_policy"))
272279
for gap in acceptance_gaps
280+
):
281+
return False
282+
frontier_identity = str(
283+
replan_obligation.get("frontier_identity")
284+
if isinstance(replan_obligation, dict)
285+
else ""
286+
).strip()
287+
if (
288+
not frontier_identity
289+
or str(ack.get("frontier_identity") or "").strip() != frontier_identity
290+
):
291+
return False
292+
delta_contract = ack.get("delta_contract")
293+
evidence_todo_ids = {
294+
str(todo_id).strip()
295+
for evidence in (
296+
delta_contract.get("auto_evidence") or []
297+
if isinstance(delta_contract, dict)
298+
else []
299+
)
300+
if isinstance(evidence, dict)
301+
and evidence.get("kind") == "watch_lane_continuation"
302+
for todo_id in (evidence.get("todo_ids") or [])
303+
if str(todo_id or "").strip()
304+
}
305+
if not evidence_todo_ids:
306+
return False
307+
summary = agent_todo_summary if isinstance(agent_todo_summary, dict) else {}
308+
exact_watch_items_by_id: dict[str, dict[str, Any]] = {}
309+
for lane in (
310+
"items",
311+
"current_agent_claimed_monitor_items",
312+
"claimed_monitor_open_items",
313+
"monitor_open_items",
314+
):
315+
for item in summary.get(lane) or []:
316+
if not isinstance(item, dict):
317+
continue
318+
todo_id = str(item.get("todo_id") or "").strip()
319+
if todo_id in evidence_todo_ids:
320+
exact_watch_items_by_id[todo_id] = item
321+
accepted, _, _ = validate_repair_delta_claims(
322+
["watch_lane_continuation"],
323+
agent_todo_summary={"items": list(exact_watch_items_by_id.values())},
324+
agent_id=agent_id,
325+
advancement_policy="as_needed",
326+
next_action_changed=False,
327+
vision_patch_written=False,
273328
)
329+
return "watch_lane_continuation" in accepted
274330

275331

276332
def _watch_lane_ack_covers_blocked_successor_repeat(
@@ -1670,6 +1726,8 @@ def build_goal_frontier_projection_context_from_status(
16701726
effective_replan_ack,
16711727
replan_obligation=replan_obligation,
16721728
acceptance_gaps=acceptance_gaps,
1729+
agent_todo_summary=agent_todo_summary,
1730+
agent_id=agent_id,
16731731
)
16741732
)
16751733
watch_lane_ack_covers_blocked_successor_repeat = (
@@ -1692,6 +1750,7 @@ def build_goal_frontier_projection_context_from_status(
16921750
replan_obligation,
16931751
)
16941752
or watch_lane_ack_covers_blocked_successor_repeat
1753+
or watch_lane_ack_covers_dead_monitor_repeat
16951754
)
16961755
and (
16971756
not acceptance_gaps

loopx/control_plane/goals/goal_frontier/outcome_continuity.py

Lines changed: 37 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,10 @@
55

66
from ...agents.agent_scope import agent_scope_item_claimed_by
77
from ...work_items.repair_delta import repair_delta_kinds_have_frontier_delta
8-
from ..goal_vision_policy import COMPLETED_TODO_CHAIN_REPLAN_THRESHOLD
8+
from ..goal_vision_policy import (
9+
COMPLETED_TODO_CHAIN_REPLAN_THRESHOLD,
10+
goal_vision_repeats_advancement_until_closed,
11+
)
912
from ..goal_vision_state import goal_vision_state_is_closed
1013

1114
VISION_OUTCOME_CHECKPOINT_REQUIRED_TRIGGER = "vision_outcome_checkpoint_required"
@@ -360,8 +363,40 @@ def _checkpoint_covers_completed_todo(
360363
for value in (checkpoint.get("repair_delta_kinds") or [])
361364
if str(value or "").strip()
362365
}
366+
if repair_delta_kinds & set(REPEAT_VISION_REPLAN_SATISFYING_DELTA_KINDS):
367+
return True
368+
qualification_vision_value = checkpoint.get("qualification_agent_vision")
369+
qualification_vision = (
370+
qualification_vision_value
371+
if isinstance(qualification_vision_value, dict)
372+
else agent_vision
373+
)
374+
qualification_patch = _dict_field(qualification_vision, "vision_patch")
375+
if goal_vision_repeats_advancement_until_closed(
376+
qualification_patch.get("advancement_policy")
377+
):
378+
return False
379+
path_delta = _dict_field(qualification_vision, "path_delta")
380+
evidence_refs = [
381+
value
382+
for value in (path_delta.get("evidence_refs") or [])
383+
if _compact_text(value, limit=140)
384+
]
385+
fresh_vision_patch = bool(
386+
checkpoint.get("decision") == "patched"
387+
and checkpoint.get("generated_at")
388+
and checkpoint.get("generated_at")
389+
== qualification_vision.get("generated_at")
390+
)
363391
return bool(
364-
repair_delta_kinds & set(REPEAT_VISION_REPLAN_SATISFYING_DELTA_KINDS)
392+
fresh_vision_patch
393+
and _compact_text(qualification_patch.get("acceptance_summary"), limit=420)
394+
and _compact_text(path_delta.get("outcome"), limit=32) == "replan"
395+
and evidence_refs
396+
and {
397+
"goal_vision_patch",
398+
"watch_lane_continuation",
399+
}.issubset(repair_delta_kinds)
365400
)
366401

367402

loopx/control_plane/work_items/autonomous_replan_ack.py

Lines changed: 56 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,10 +22,12 @@ def compact_autonomous_replan_ack(run: dict[str, Any] | None) -> dict[str, Any]
2222
if not isinstance(run, dict) or not autonomous_replan_ack_recorded(run):
2323
return None
2424
ack = run.get("autonomous_replan_ack")
25+
if not isinstance(ack, dict):
26+
return None
2527
delta_contract = ack.get("delta_contract") if isinstance(ack, dict) else {}
2628
if not isinstance(delta_contract, dict):
2729
return None
28-
compact_delta = {
30+
compact_delta: dict[str, Any] = {
2931
"schema_version": delta_contract.get("schema_version"),
3032
"delta_present": bool(delta_contract.get("delta_present")),
3133
"delta_kinds": [
@@ -34,6 +36,24 @@ def compact_autonomous_replan_ack(run: dict[str, Any] | None) -> dict[str, Any]
3436
if str(item or "").strip()
3537
],
3638
}
39+
watch_evidence: list[dict[str, Any]] = []
40+
for item in delta_contract.get("auto_evidence") or []:
41+
if not isinstance(item, dict) or item.get("kind") != "watch_lane_continuation":
42+
continue
43+
todo_ids = [
44+
str(todo_id).strip()
45+
for todo_id in (item.get("todo_ids") or [])
46+
if str(todo_id or "").strip()
47+
][:4]
48+
if todo_ids:
49+
watch_evidence.append(
50+
{
51+
"kind": "watch_lane_continuation",
52+
"todo_ids": todo_ids,
53+
}
54+
)
55+
if watch_evidence:
56+
compact_delta["auto_evidence"] = watch_evidence
3757
result = {
3858
"schema_version": ack.get("schema_version"),
3959
"recorded": True,
@@ -56,6 +76,33 @@ def latest_blocked_successor_frontier_identity(
5676
latest_runs: list[dict[str, Any]] | None,
5777
*,
5878
agent_id: str | None = None,
79+
) -> str | None:
80+
return _latest_monitor_replan_frontier_identity(
81+
latest_runs,
82+
agent_id=agent_id,
83+
include_generic_watch=False,
84+
)
85+
86+
87+
def latest_monitor_replan_frontier_identity(
88+
latest_runs: list[dict[str, Any]] | None,
89+
*,
90+
agent_id: str | None = None,
91+
) -> str | None:
92+
"""Return the latest monitor identity that a durable replan ACK must bind."""
93+
94+
return _latest_monitor_replan_frontier_identity(
95+
latest_runs,
96+
agent_id=agent_id,
97+
include_generic_watch=True,
98+
)
99+
100+
101+
def _latest_monitor_replan_frontier_identity(
102+
latest_runs: list[dict[str, Any]] | None,
103+
*,
104+
agent_id: str | None,
105+
include_generic_watch: bool,
59106
) -> str | None:
60107
normalized_agent_id = str(agent_id or "").strip()
61108
for run in latest_runs or []:
@@ -84,7 +131,14 @@ def latest_blocked_successor_frontier_identity(
84131
if target.get("monitor_mode") != (
85132
"blocked_successor_wait_without_material_transition"
86133
):
87-
continue
134+
if not (
135+
include_generic_watch
136+
and target.get("monitor_mode")
137+
== "monitor_quiet_until_material_transition"
138+
):
139+
continue
140+
target_identity = str(target.get("target_id") or "").strip()
141+
return target_identity or None
88142
frontier_identity = str(target.get("frontier_identity") or "").strip()
89143
return frontier_identity or None
90144
return None

loopx/control_plane/work_items/autonomous_replan_obligation.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -604,6 +604,10 @@ def build_autonomous_replan_obligation(
604604
extra_fields["frontier_identity"] = blocked_successor_evidence.get(
605605
"frontier_identity"
606606
)
607+
elif dead_monitor_evidence and dead_monitor_evidence.get("monitor_target_id"):
608+
extra_fields["frontier_identity"] = dead_monitor_evidence.get(
609+
"monitor_target_id"
610+
)
607611

608612
result = build_autonomous_replan_obligation_payload(
609613
schema_version=autonomous_replan_schema_version,

loopx/state_refresh.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@
3232
validate_repair_delta_claims,
3333
)
3434
from .control_plane.work_items.autonomous_replan_ack import (
35-
latest_blocked_successor_frontier_identity,
35+
latest_monitor_replan_frontier_identity,
3636
)
3737
from .control_plane.runtime.shared_runtime_refresh_projection import (
3838
build_shared_runtime_projection,
@@ -1163,7 +1163,7 @@ def refresh_state_run(
11631163
)
11641164
if autonomous_replan_recorded:
11651165
autonomous_replan_frontier_identity = (
1166-
latest_blocked_successor_frontier_identity(
1166+
latest_monitor_replan_frontier_identity(
11671167
newest_first_runs,
11681168
agent_id=normalized_agent_id,
11691169
)

skills/loopx-self-repair/references/repair-patterns.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,7 @@ teaches a reusable control-plane lesson.
6363
| `stale_active_next_priority_override` | A lower-priority todo named by durable `Next Action` becomes `agent_lane_next_action` even though the same peer has a higher-priority capability-runnable todo, commonly after a deferred P0 resumes or another peer refreshes shared state. | scoped quota payload, `capability_gate.runnable_candidates`, `agent_lane_next_action`, active-state `Next Action`, todo priority/resume/claim metadata, recent peer writes. | The active-next hint was ranked before todo priority, so stale steering text acted as a global override instead of a same-priority tie-breaker. | Preserve peer claim/profile routing, then rank todo priority before active-next preference; keep active-next as a same-priority tie-breaker. Cover a resume-ready P0 plus lower-priority active-next and concurrent other-peer work. |
6464
| `status_projection_history_neutral_gap` | `quota should-run` reports no user action or quiet monitor behavior, but `status` / `diagnose --limit N` misreports a stale controller/user gate or older connected-without-run state after recent monitor/spend/readiness events. | quota payload, diagnose/status payload, recent history classifications, neutral-run classification sets, UI `--limit`, latest meaningful state run. | Status/history/quota disagree on which runs are status-neutral, or a short display limit is reused as the control-plane reasoning window. | Share the neutral classification contract across history/status/quota and reason over a wider internal state window before trimming displayed runs; add a regression with consecutive neutral runs before a meaningful state transition. |
6565
| `monitor_replan_noop_loop` | Recent runs are monitor-only, replan, or self-repair-adjacent, but the same monitor/action recommendation keeps returning and no runnable todo, blocker, successor, supersede, user gate, capability change, or monitor-target change appears. A sharper form is `goal_frontier_projection` showing a monitor-only lane with zero current, unclaimed, or other-agent advancement frontier. | quota payload, `goal_frontier_projection`, interaction contract, todo summaries, recent history classifications, latest replan ACK, monitor target, active state todos. | Self-repair/replan was recorded as activity but did not change the machine-visible work frontier, or generic vision/no-follow-up deltas silently converted an empty active-goal frontier into a monitor wait. | Require a repair delta contract: classify no-delta repairs as no-ops, then add a blocker, supersede stale monitor work, create a successor runnable todo, or record an explicit `watch_lane_continuation` with expiry. Generic vision, no-follow-up, and next-action deltas must not suppress empty-frontier replan by themselves. |
66+
| `watch_lane_ack_causality_gap` | An accepted, expiring `watch_lane_continuation` clears one replan, but two later quiet heartbeat receipts reopen the same dead-monitor obligation before the watch is due; a stale ACK may also suppress a changed or expired monitor. | dead-monitor target id, compact ACK frontier identity and watch Todo ids, current scoped monitor target/cadence/due/expiry, agent vision advancement policy, and completed-Todo outcome checkpoint. | Dead-monitor obligations had no causal frontier identity, compact ACKs dropped their exact watch evidence, or a completed-Todo checkpoint treated every watch-only replan as invalid regardless of an as-needed vision. | Bind the obligation and ACK to the same monitor target, retain bounded exact watch Todo ids, revalidate that watch and expiry on every suppression, and allow an evidence-linked `as_needed` watch replan to close the completed-Todo checkpoint. Missing ACK/evidence, target drift, expiry, and `repeat_until_closed` remain replan triggers. |
6667
| `vision_replan_writeback_gap` | The user or agent identifies a missing outcome, acceptance condition, or product bottleneck, but later `quota should-run` shows no `goal_frontier_projection.acceptance_gaps[]`, no per-agent `agent_vision` / `vision_checkpoint_v0`, and no replan obligation; the agent keeps following older runnable todos or monitor waits. | recent user correction, `loopx history --goal-id`, latest runs' `agent_vision` and `vision_checkpoint`, quota `goal_frontier_projection.acceptance_gaps`, active-state todos, bad-case report or incident note. | The insight stayed in chat, prose, or a report instead of being converted into a bounded `goal_vision_replan_contract_v0` packet, per-agent checkpoint decision, or concrete successor todo. | Write `loopx refresh-state --agent-id <agent-id> --vision-summary ... --vision-replan-trigger ...` or `--agent-vision-json <packet>` with a compact `replan_trigger_summary`; use `--vision-unchanged-reason` when the current per-agent vision still applies; and add/link a successor todo when the next executable step is known. Then rerun quota so the vision gap or `vision_checkpoint_missing` can become `autonomous_replan_required` before local quiet/wait states. |
6768
| `active_goal_closed_vision_succession_gap` | A bounded stage is correctly marked `vision_closed`, but the registry goal remains active; quota shows no acceptance gap or exposes one without requiring replan, then continues ordinary or resume-ready deferred work or monitor quiet without establishing the next vision. | latest per-agent `agent_vision`, registry goal status, quota `goal_frontier_projection`, remaining open/deferred advancement todos, terminal lane rationale. | Closed vision was treated as terminal, or a generic ready-deferred shortcut returned before the successor-vision gap was evaluated, so closing one accepted stage erased or demoted the long-horizon vision frontier. | Treat `vision_closed` as stage closure: for an active registry goal, project `vision_successor_required` and run replan even when ordinary or resume-ready deferred advancement already exists. Explicit user/blocking handoff gates still take precedence. Only `retired`, `superseded`, or `no_followup` may terminate the lane without another vision. Cover active/inactive goal and runnable/deferred/empty frontier cases. |
6869
| `scheduler_liveness_backoff_gap` | `quota should-run` says `should_run=false`, `quiet_noop_allowed=true`, no spend, or `automation_action=keep_active_quiet`, but the host keeps polling at the bootstrap cadence for hours; Codex CLI TUI or Claude Code `/loop` also keeps repeating unchanged checks. | quota payload `automation_liveness`, `interaction_contract`, `heartbeat_recommendation`, host automation cadence, CLI/Claude loop logs or statusline, recent no-spend poll history. | LoopX separated execution permission from user notification, but did not expose a machine-readable next-wakeup/backoff/final-check/exit policy for host runtimes. | Add or repair `quota should-run.scheduler_hint`; host runtimes must apply Codex App cadence backoff and CLI/Claude unchanged-poll final quota/replan check before self-stop, without quota spend. Prompt/docs should treat fixed 3-minute cadence as bootstrap only. |

0 commit comments

Comments
 (0)