Skip to content

Commit 07ad5d2

Browse files
committed
fix(global-manager): respect agent-scoped gate state
1 parent 9299df9 commit 07ad5d2

2 files changed

Lines changed: 89 additions & 2 deletions

File tree

loopx/summary_all.py

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -264,6 +264,7 @@ def _global_gate_from_item(
264264
item: dict[str, Any],
265265
*,
266266
quota_payload: dict[str, Any],
267+
agent_id: str | None,
267268
) -> dict[str, Any] | None:
268269
interaction = _as_dict(quota_payload.get("interaction_contract"))
269270
user_channel = _as_dict(interaction.get("user_channel"))
@@ -272,7 +273,9 @@ def _global_gate_from_item(
272273
formal_gate = formal_gates[0] if formal_gates else None
273274
action_required = user_channel.get("action_required") is True
274275
waiting_on = str(item.get("waiting_on") or "").strip()
275-
controller_routed = waiting_on in {"controller", "user_or_controller"}
276+
controller_routed = waiting_on in {"controller", "user_or_controller"} and (
277+
not agent_id or quota_payload.get("state") == "operator_gate"
278+
)
276279
if not action_required and not formal_gate and not controller_routed:
277280
return None
278281

@@ -334,7 +337,11 @@ def _collect_global_gate_state(
334337
)
335338
if not _quota_matches_agent_scope(quota_payload, agent_id=agent_id):
336339
continue
337-
gate = _global_gate_from_item(item, quota_payload=quota_payload)
340+
gate = _global_gate_from_item(
341+
item,
342+
quota_payload=quota_payload,
343+
agent_id=agent_id,
344+
)
338345
if not gate:
339346
continue
340347
gates.append(gate)

tests/test_summary_all.py

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -330,6 +330,86 @@ def test_global_gates_preserves_shared_route_with_supported_owner(monkeypatch, t
330330
assert gate["owner"] != "user_or_controller"
331331

332332

333+
def test_global_gates_agent_scope_drops_goal_controller_route_when_quota_is_runnable(
334+
monkeypatch, tmp_path
335+
) -> None:
336+
agent_id = "codex-runnable"
337+
status_payload = {
338+
"ok": True,
339+
"attention_queue": {
340+
"items": [
341+
{
342+
"goal_id": "runnable-goal",
343+
"waiting_on": "controller",
344+
"operator_question": "Resolve another lane's gate?",
345+
}
346+
]
347+
},
348+
}
349+
quota_payload = {
350+
"ok": True,
351+
"goal_id": "runnable-goal",
352+
"state": "eligible",
353+
"agent_identity": {"agent_id": agent_id, "registered": True},
354+
"selected_todo": {"todo_id": "runnable-todo", "status": "open"},
355+
"agent_lane_next_action": {
356+
"todo_id": "runnable-todo",
357+
"status": "open",
358+
},
359+
"user_todo_summary": {"open_count": 0, "gate_open_items": []},
360+
"interaction_contract": {"user_channel": {"action_required": False}},
361+
}
362+
patch_manager_reads(
363+
monkeypatch,
364+
status_payload=status_payload,
365+
quota_payloads={"runnable-goal": quota_payload},
366+
)
367+
368+
payload = build_global_gates(tmp_path, agent_id=agent_id)
369+
370+
assert payload["gates"] == []
371+
assert payload["lanes"] == []
372+
assert payload["groups"]["blocked_work"] == []
373+
assert payload["summary"]["open_gate_count"] == 0
374+
assert payload["summary"]["blocked_lane_count"] == 0
375+
376+
377+
def test_global_gates_agent_scope_preserves_scoped_controller_gate(
378+
monkeypatch, tmp_path
379+
) -> None:
380+
agent_id = "codex-controller-gated"
381+
status_payload = {
382+
"ok": True,
383+
"attention_queue": {
384+
"items": [
385+
{
386+
"goal_id": "controller-gated-goal",
387+
"waiting_on": "controller",
388+
"operator_question": "Resolve this scoped controller gate?",
389+
}
390+
]
391+
},
392+
}
393+
patch_manager_reads(
394+
monkeypatch,
395+
status_payload=status_payload,
396+
quota_payloads={
397+
"controller-gated-goal": controller_quota(
398+
"controller-gated-goal", agent_id=agent_id
399+
)
400+
},
401+
)
402+
403+
payload = build_global_gates(tmp_path, agent_id=agent_id)
404+
405+
assert [gate["goal_id"] for gate in payload["gates"]] == [
406+
"controller-gated-goal"
407+
]
408+
assert payload["gates"][0]["owner"] == "controller"
409+
assert payload["lanes"][0]["status"] == "operator_gate"
410+
assert payload["groups"]["blocked_work"] == payload["lanes"]
411+
412+
333413
def test_global_gates_filters_every_group_to_requested_agent(monkeypatch, tmp_path) -> None:
334414
status_payload = {
335415
"ok": True,

0 commit comments

Comments
 (0)