Skip to content

Commit 7b5058a

Browse files
authored
Merge pull request #4883 from loopx-project/codex/repair-archived-bound-todo-closeout-clean
fix(quota): close archived Todo-bound host turns
2 parents c4314d9 + 2bed3b4 commit 7b5058a

5 files changed

Lines changed: 250 additions & 19 deletions

File tree

‎docs/heartbeat-automation-prompt.md‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -298,7 +298,9 @@ whose capabilities are known when the automation is installed.
298298
ordinary work selection. The host must repair the prior closeout, rerun the
299299
same current Turn, and then continue an eligible successor. Recovery is
300300
idempotent and no-spend; receipts created before this explicit flag are not
301-
retroactively treated as unsettled;
301+
retroactively treated as unsettled. Exact-ID lifecycle reads include retained
302+
`Completed Work Archive` rows, while ordinary Todo lists remain active-only;
303+
an archived terminal fact closes recovery without rewriting archive history;
302304
- use `user_gate` only for an exact authority boundary such as approval to merge
303305
an aggregate branch into `main`, release, launch a benchmark, or perform a
304306
protected action;

‎loopx/control_plane/coordination/local_authority.py‎

Lines changed: 34 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -303,23 +303,7 @@ def canonical_todo_summary_fields(
303303
for item in todos
304304
if item.get("archive_state") == "archive"
305305
}
306-
# Native provider records have no Markdown address. Allocate display
307-
# positions from stable provider order; never read legacy Markdown here.
308-
todos = [
309-
{
310-
**item,
311-
"schema_version": TODO_ITEM_SCHEMA_VERSION,
312-
"source_section": (
313-
"Completed Work Archive"
314-
if item["archive_state"] == "archive"
315-
else TODO_SECTION_HEADINGS[item["role"]]
316-
),
317-
"index": index,
318-
}
319-
if item.get("schema_version") == TODO_DOMAIN_ITEM_SCHEMA_VERSION
320-
else item
321-
for index, item in enumerate(todos, 1)
322-
]
306+
todos = canonical_todo_items(todos)
323307
# These are native authority decisions, not persisted Todo fields. Keep the
324308
# records visible while every summary/selection uses the same work guard.
325309
if goal_acceptance_contract and goal_acceptance_contract.get("enabled") is True:
@@ -366,3 +350,36 @@ def canonical_todo_summary_fields(
366350
if standing_authority:
367351
fields["standing_decision_authority"] = standing_authority
368352
return fields
353+
354+
355+
def canonical_todo_items(todos: list[dict[str, Any]]) -> list[dict[str, Any]]:
356+
"""Adapt every canonical Todo, including retained archive history."""
357+
358+
from ..todos.active_state_editing import TODO_SECTION_HEADINGS
359+
360+
# Native provider records have no Markdown address. Allocate display
361+
# positions from stable provider order; never read legacy Markdown here.
362+
return [
363+
{
364+
**item,
365+
**(
366+
{"schema_version": TODO_ITEM_SCHEMA_VERSION}
367+
if item.get("schema_version") == TODO_DOMAIN_ITEM_SCHEMA_VERSION
368+
else {}
369+
),
370+
"source_section": "Completed Work Archive",
371+
"index": index,
372+
}
373+
if item.get("archive_state") == "archive"
374+
else (
375+
{
376+
**item,
377+
"schema_version": TODO_ITEM_SCHEMA_VERSION,
378+
"source_section": TODO_SECTION_HEADINGS[item["role"]],
379+
"index": index,
380+
}
381+
if item.get("schema_version") == TODO_DOMAIN_ITEM_SCHEMA_VERSION
382+
else item
383+
)
384+
for index, item in enumerate(todos, 1)
385+
]

‎loopx/control_plane/todos/goal_todo_projection.py‎

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -316,6 +316,60 @@ def todo_summaries_from_fields(
316316
uncapped_todo_count=uncapped_todo_count,
317317
)
318318

319+
320+
def exact_archived_todo_summaries(
321+
*,
322+
archived_items: list[dict[str, Any]],
323+
source: str,
324+
projection_fields: dict[str, Any] | None,
325+
projection_overlay: dict[str, Any] | None,
326+
rollout_events: list[dict[str, Any]],
327+
roles: list[str],
328+
status: str | None,
329+
todo_id: str,
330+
agent_id: str | None,
331+
limit: int | None,
332+
) -> GoalTodoSummaries | None:
333+
"""Project one exact retained Todo without widening normal active lists."""
334+
335+
item = next(
336+
(
337+
dict(candidate)
338+
for candidate in archived_items
339+
if normalize_todo_id(candidate.get("todo_id")) == todo_id
340+
and candidate.get("archive_state") == "archive"
341+
),
342+
None,
343+
)
344+
if item is None:
345+
return None
346+
item_role = item.get("role")
347+
if item_role not in {"user", "agent"} or item_role not in roles:
348+
return None
349+
summary = compact_todo_group(
350+
[item],
351+
source_section=str(item.get("source_section") or "Completed Work Archive"),
352+
role=item_role,
353+
include_empty_source=True,
354+
resume_source_items=archived_items,
355+
rollout_events=rollout_events,
356+
item_limit=None,
357+
)
358+
if summary is None:
359+
return None
360+
return todo_summaries_from_fields(
361+
fields={f"{item_role}_todos": summary},
362+
source=source,
363+
projection_fields=projection_fields,
364+
projection_overlay=projection_overlay,
365+
rollout_events=rollout_events,
366+
roles=roles,
367+
status=status,
368+
todo_id=todo_id,
369+
agent_id=agent_id,
370+
limit=limit,
371+
)
372+
319373
def project_goal_todo_items(
320374
goal: dict[str, Any] | None,
321375
*,
@@ -345,6 +399,7 @@ def project_goal_todo_items(
345399
__all__ = [
346400
"GoalTodoSummaries",
347401
"empty_todo_summary",
402+
"exact_archived_todo_summaries",
348403
"filtered_todo_summary",
349404
"goal_todo_summaries",
350405
"merge_todo_projection_fields",

‎loopx/todos.py‎

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -89,9 +89,11 @@
8989
todo_list_projection_contract,
9090
)
9191
from .control_plane.todos.goal_todo_projection import (
92+
exact_archived_todo_summaries,
9293
goal_todo_summaries,
9394
todo_summaries_from_fields,
9495
)
96+
from .control_plane.todos.active_state_todo_parser import parse_todo_source
9597
from .control_plane.todos import monitor_metadata as todo_monitor_metadata
9698
from .control_plane.todos.mutation_authority import authorize_todo_lifecycle_mutation, todo_update_authority_action
9799
from .control_plane.todos.succession_warning import build_open_parent_successor_advisory
@@ -117,6 +119,7 @@
117119
)
118120
from .control_plane.coordination.legacy_writer_fence import legacy_todo_write_transaction
119121
from .control_plane.coordination.local_authority import (
122+
canonical_todo_items,
120123
canonical_todo_summary_fields,
121124
claim_canonical_todo_if_promoted,
122125
local_authority_is_promoted,
@@ -234,9 +237,10 @@ def list_goal_todos(
234237
else:
235238
if not resolved_state_file.exists():
236239
raise ValueError(f"active state file does not exist: {resolved_state_file}")
240+
state_text = resolved_state_file.read_text(encoding="utf-8")
237241
projected = goal_todo_summaries(
238242
goal,
239-
state_text=resolved_state_file.read_text(encoding="utf-8"),
243+
state_text=state_text,
240244
state_path=resolved_state_file,
241245
rollout_events=rollout_events,
242246
roles=roles,
@@ -245,6 +249,33 @@ def list_goal_todos(
245249
agent_id=normalized_agent_id,
246250
limit=limit,
247251
)
252+
if normalized_todo_id and not projected.todos:
253+
if canonical_read is not None:
254+
archived_items = [
255+
item
256+
for item in canonical_todo_items(canonical_read["todos"])
257+
if item.get("archive_state") == "archive"
258+
]
259+
else:
260+
_active_items, archived_items, _source_sections = parse_todo_source(
261+
state_text,
262+
goal=goal,
263+
state_path=resolved_state_file,
264+
)
265+
archived_projection = exact_archived_todo_summaries(
266+
archived_items=archived_items,
267+
source=projected.source,
268+
projection_fields=projected.projection_fields,
269+
projection_overlay=projected.projection_overlay,
270+
rollout_events=rollout_events,
271+
roles=roles,
272+
status=status,
273+
todo_id=normalized_todo_id,
274+
agent_id=normalized_agent_id,
275+
limit=limit,
276+
)
277+
if archived_projection is not None:
278+
projected = archived_projection
248279
source = projected.source
249280
projection_fields = projected.projection_fields
250281
projection_overlay = projected.projection_overlay

‎tests/control_plane/test_quota_settlement_cli.py‎

Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1601,6 +1601,132 @@ def test_prior_host_closeout_survives_hidden_todo_lifecycle(
16011601
assert resumed["quota"]["spent_slots"] == prior["quota"]["spent_slots"]
16021602

16031603

1604+
@pytest.mark.parametrize("provider", ["legacy", "file", "sqlite"])
1605+
def test_prior_host_closeout_reads_archived_exact_todo(
1606+
tmp_path: Path,
1607+
provider: str,
1608+
monkeypatch: pytest.MonkeyPatch,
1609+
) -> None:
1610+
from canonical_authority_fixture import (
1611+
initialize_canonical_authority,
1612+
isolate_sqlite_runtime,
1613+
)
1614+
from loopx.control_plane.coordination.runtime_shadow import (
1615+
build_todo_runtime_shadow_projection,
1616+
)
1617+
1618+
if provider == "sqlite":
1619+
isolate_sqlite_runtime(tmp_path, monkeypatch)
1620+
project, runtime, registry_path = _write_fixture(tmp_path)
1621+
guard = (
1622+
"quota",
1623+
"should-run",
1624+
"--codex-app",
1625+
"--goal-id",
1626+
GOAL_ID,
1627+
"--agent-id",
1628+
AGENT_ID,
1629+
"--scan-path",
1630+
str(project),
1631+
)
1632+
prior_turn_id = "turn-archived-closeout-prior"
1633+
rc, prior = _run_cli(
1634+
registry_path,
1635+
runtime,
1636+
*guard,
1637+
"--turn-instance-id",
1638+
prior_turn_id,
1639+
"--todo-id",
1640+
TODO_ID,
1641+
)
1642+
assert rc == 0, prior
1643+
assert prior["heartbeat_receipt"]["closeout_required"] is True
1644+
1645+
state = project / ".codex" / "goals" / GOAL_ID / "ACTIVE_GOAL_STATE.md"
1646+
if provider != "legacy":
1647+
rc, listed = _run_cli(
1648+
registry_path, runtime, "todo", "list", "--goal-id", GOAL_ID
1649+
)
1650+
assert rc == 0, listed
1651+
projection = build_todo_runtime_shadow_projection(
1652+
goal_id=GOAL_ID,
1653+
handoff_mode="soft_claim",
1654+
todos=listed["todos"],
1655+
)
1656+
initialize_canonical_authority(
1657+
runtime, GOAL_ID, projection, state_path=state, provider=provider
1658+
)
1659+
1660+
rc, complete = _run_cli(
1661+
registry_path,
1662+
runtime,
1663+
"todo",
1664+
"complete",
1665+
"--goal-id",
1666+
GOAL_ID,
1667+
"--agent-id",
1668+
AGENT_ID,
1669+
"--todo-id",
1670+
TODO_ID,
1671+
"--turn-instance-id",
1672+
prior_turn_id,
1673+
"--claimed-by",
1674+
AGENT_ID,
1675+
"--evidence",
1676+
"archived closeout lifecycle validated",
1677+
"--next-agent-todo",
1678+
"Continue after archived closeout validation.",
1679+
"--next-claimed-by",
1680+
AGENT_ID,
1681+
"--next-action-kind",
1682+
"implement",
1683+
)
1684+
assert rc == 0, complete
1685+
rc, archived = _run_cli(
1686+
registry_path,
1687+
runtime,
1688+
"todo",
1689+
"archive-completed",
1690+
"--goal-id",
1691+
GOAL_ID,
1692+
"--max-active-done",
1693+
"0",
1694+
"--execute",
1695+
)
1696+
assert rc == 0, archived
1697+
assert archived["moved_count"] == 1
1698+
1699+
rc, active = _run_cli(
1700+
registry_path, runtime, "todo", "list", "--goal-id", GOAL_ID
1701+
)
1702+
assert rc == 0, active
1703+
assert TODO_ID not in {item["todo_id"] for item in active["todos"]}
1704+
rc, exact = _run_cli(
1705+
registry_path,
1706+
runtime,
1707+
"todo",
1708+
"list",
1709+
"--goal-id",
1710+
GOAL_ID,
1711+
"--role",
1712+
"agent",
1713+
"--todo-id",
1714+
TODO_ID,
1715+
)
1716+
assert rc == 0, exact
1717+
assert exact["todo"]["todo_id"] == TODO_ID
1718+
assert exact["todo"]["status"] == "done"
1719+
assert exact["todo"]["archive_state"] == "archive"
1720+
assert exact["todo"]["source_section"] == "Completed Work Archive"
1721+
1722+
rc, observed = _run_cli(registry_path, runtime, *guard, "--begin-turn")
1723+
assert rc == 0, observed
1724+
assert observed["effective_action"] != "unsettled_host_turn_recovery", observed.get(
1725+
"unsettled_host_turn_recovery"
1726+
)
1727+
assert observed["quota"]["spent_slots"] == prior["quota"]["spent_slots"]
1728+
1729+
16041730
def test_standard_codex_app_settlement_is_receipted_and_idempotent(
16051731
tmp_path: Path,
16061732
) -> None:

0 commit comments

Comments
 (0)