From 0d88099e5f205af337767279328c2ba2e61a0b4a Mon Sep 17 00:00:00 2001 From: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> Date: Wed, 7 Oct 2026 17:14:41 +0800 Subject: [PATCH 1/4] refactor(todos): compose frontier index in the summary batch Signed-off-by: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> --- .../control_plane/todos/frontier_revision.py | 7 --- .../control_plane/todos/frontier_revision.ts | 31 +++++++++- .../control_plane/todos/summary_projection.ts | 11 +++- loopx/control_plane/todos/todo_summary.py | 10 +++- .../test_long_history_todo_reads.py | 4 +- .../control_plane/test_todo_consumer_scope.py | 2 +- .../test_todo_summary_projection.py | 59 +++++++++++++++++++ .../todo_summary_projection.test.ts | 51 ++++++++++++++++ 8 files changed, 159 insertions(+), 16 deletions(-) diff --git a/loopx/control_plane/todos/frontier_revision.py b/loopx/control_plane/todos/frontier_revision.py index e6689ed81a..19cb612b6f 100644 --- a/loopx/control_plane/todos/frontier_revision.py +++ b/loopx/control_plane/todos/frontier_revision.py @@ -103,10 +103,3 @@ def build_advancement_frontier_revision_index( if not isinstance(index, dict): raise TypeError("typed frontier revision response index must be an object") return index - - -def attach_advancement_frontier_revision_index( - summary: dict[str, Any], source_items: list[dict[str, Any]], *, role: str | None, -) -> None: - if role == "agent": - summary["advancement_frontier_revision_index"] = build_advancement_frontier_revision_index(source_items) diff --git a/loopx/control_plane/todos/frontier_revision.ts b/loopx/control_plane/todos/frontier_revision.ts index 2f39f417e0..23a2f39a3d 100644 --- a/loopx/control_plane/todos/frontier_revision.ts +++ b/loopx/control_plane/todos/frontier_revision.ts @@ -232,15 +232,42 @@ export function projectAdvancementFrontier(value: unknown): JsonObject { const rows = decodeRows(request.rows); if (request.operation === "select") return {checkpoint: checkpoint(rows, agent)}; if (request.operation !== "index") throw new EffectRuntimeRequestError("unsupported frontier revision operation"); + return {index: frontierIndex(rows)}; +} + +function frontierIndex(rows: Row[] | null): JsonObject { // An excluded agent can have no claimed rows. It still needs its own lane; // falling back to the global unclaimed checkpoint would include excluded work. const agents = [...new Set((rows ?? []).filter(row => row.advancement) .flatMap(row => [...(row.claim ? [row.claim] : []), ...row.excluded]))].sort(); const counts = claimedAdvancementCounts(rows); - return {index: {schema_version: INDEX, ...(counts === null ? {} : {claimed_advancement_counts: counts}), + return {schema_version: INDEX, ...(counts === null ? {} : {claimed_advancement_counts: counts}), all: checkpoint(rows, null), unclaimed: checkpoint(rows, null, true), - by_agent: agents.map(agent_id => ({agent_id, ...checkpoint(rows, agent_id)}))}}; + by_agent: agents.map(agent_id => ({agent_id, ...checkpoint(rows, agent_id)}))}; +} + +/** Compose the existing index owner inside an already-required summary read. + * Bind the lossless legacy codec to the validated source before selecting; + * display limits must never decide replan identity or commitment counts. */ +export function projectSummaryFrontierIndex(value: unknown, source: readonly JsonObject[], + indices: readonly number[]): JsonObject { + const rows = decodeRows(value); + if (rows === null || rows.length !== source.length) { + throw new EffectRuntimeRequestError("summary frontier source cardinality mismatch"); + } + for (const [index, row] of rows.entries()) { + const fact = source[index]; + const actionable = fact.status === "open" && !fact.done && + (!fact.has_resume || fact.resume_ready === true) && !fact.acceptance_blocked; + if (row.id !== text(fact.todo_id) || row.claim !== (text(fact.claim) || null) || + JSON.stringify(row.excluded) !== JSON.stringify(fact.excluded) || + row.advancement !== (fact.task_class === "advancement_task") || + row.actionable !== actionable || row.updated !== text(fact.updated_at || fact.completed_at)) { + throw new EffectRuntimeRequestError("summary frontier facts disagree with the validated source"); + } + } + return frontierIndex(indices.map(index => rows[index])); } function classifyAck(observation: LongChainObservation, value: unknown): AckDecision { diff --git a/loopx/control_plane/todos/summary_projection.ts b/loopx/control_plane/todos/summary_projection.ts index 1855cdb3f1..ca7a094ca9 100644 --- a/loopx/control_plane/todos/summary_projection.ts +++ b/loopx/control_plane/todos/summary_projection.ts @@ -5,6 +5,7 @@ import {EffectRuntimeRequestError} from "../effect_runtime_errors.ts"; import {requireBoolean, requireJsonObject, requireStringLiteral} from "../runtime_decode.ts"; import {parseTodoTimestampMicros} from "../runtime_timestamp.ts"; import {projectTodoSummaryLanes, type TodoSummaryLane} from "./summary_lanes.ts"; +import {projectSummaryFrontierIndex} from "./frontier_revision.ts"; import {projectTodoClosure, projectTodoSuccession, SUCCESSION_FACT_COLUMNS, SUCCESSION_EVALUATION_COLUMNS} from "./succession.ts"; type Format = "raw" | "compact" | "active" | "recent" | "gap"; @@ -100,7 +101,8 @@ function claimedVisibility(indices: readonly number[], rows: readonly JsonObject export function projectTodoSummary(value: unknown): SummaryProjection { const request = requireJsonObject(value, "Todo summary request"); if (request.schema_version !== "todo_summary_projection_request_v1" && - request.schema_version !== "todo_summary_projection_request_v2") { + request.schema_version !== "todo_summary_projection_request_v2" && + request.schema_version !== "todo_summary_projection_request_v3") { throw new EffectRuntimeRequestError("Todo summary request schema mismatch"); } const role = request.role === null ? null : requireStringLiteral(request.role, ["user", "agent"], "role"); @@ -142,6 +144,13 @@ export function projectTodoSummary(value: unknown): SummaryProjection { monitor_schedule_gap_count: selected.monitor_schedule_gap_items.length, }; const lanes: Record = {}; + if (request.schema_version === "todo_summary_projection_request_v3") { + if (role === "agent") { + fields.advancement_frontier_revision_index = projectSummaryFrontierIndex(request.frontier_rows, rows, source); + } else if (request.frontier_rows !== null) { + throw new EffectRuntimeRequestError("non-Agent summary must omit frontier source facts"); + } + } const lane = (name: string, indices: readonly number[], cap: number | null = null, format: Format = "compact") => { lanes[name] = {indices: cap === null ? [...indices] : indices.slice(0, cap), format}; }; diff --git a/loopx/control_plane/todos/todo_summary.py b/loopx/control_plane/todos/todo_summary.py index 86e35adff0..0ee6071ba4 100644 --- a/loopx/control_plane/todos/todo_summary.py +++ b/loopx/control_plane/todos/todo_summary.py @@ -38,7 +38,7 @@ todo_done_for_status, ) from .completion_validation_projection import project_completion_validation_authority -from .frontier_revision import attach_advancement_frontier_revision_index +from .frontier_revision import frontier_source_facts, TODO_FRONTIER_REVISION_INDEX_SCHEMA_VERSION from .handoff_gate import build_todo_handoff_gate_states from .handoff_note import attach_todo_handoff_note from .todo_semantics import ( @@ -84,7 +84,7 @@ # facts: repeating every key name per Todo pushed a long-history request past the # effect-runtime request budget. The typed owner decodes the declared columns # back into row objects before validating them, so no cell changes meaning. -SUMMARY_PROJECTION_REQUEST_SCHEMA_VERSION = "todo_summary_projection_request_v2" +SUMMARY_PROJECTION_REQUEST_SCHEMA_VERSION = "todo_summary_projection_request_v3" SUMMARY_PROJECTION_COLUMNS = ( "status", "done", "task_class", "has_resume", "resume_ready", "resume_evaluated", "acceptance_blocked", "claimed", "preferred", "watch_only", "due_at", "expires_at", @@ -874,6 +874,7 @@ def _project_summary(items: list[dict[str, Any]], preferred_todo_ids: set[str] | "columns": list(SUMMARY_PROJECTION_COLUMNS), "rows": [[row[name] for name in SUMMARY_PROJECTION_COLUMNS] for row in rows], "succession": succession_request(items, reuse=True), + "frontier_rows": frontier_source_facts(items) if role == "agent" else None, "observed_at": now_utc().timestamp(), "selection": selection, "role": role, "source_section": source_section, "item_limit": item_limit, "full_selection": full_selection, @@ -906,6 +907,10 @@ def valid_ordinals(value: Any) -> TypeGuard[list[int]]: summary = result.get("fields") if not isinstance(summary, dict) or summary.get("schema_version") != "todo_summary_v0": raise ValueError("invalid typed Todo summary fields") + if role == "agent": + index = summary.get("advancement_frontier_revision_index") + if not isinstance(index, dict) or index.get("schema_version") != TODO_FRONTIER_REVISION_INDEX_SCHEMA_VERSION: + raise ValueError("invalid typed Todo summary frontier index") warning = summary.get("todo_succession_warning") warning_action = warning.get("recommended_action") if isinstance(warning, dict) else None if summary.get("completed_without_successor_count") and not isinstance(warning_action, str): @@ -1012,7 +1017,6 @@ def compact_evaluated_todo_group( return None summary: dict[str, Any] = projected["summary"] handoff_gates = build_todo_handoff_gate_states(items, evaluations=projected["succession"]) - attach_advancement_frontier_revision_index(summary, items, role=role) attach_active_vision_waits( summary, vision_runs, role=role, items=items, lineage_items=lineage_items, diff --git a/tests/control_plane/test_long_history_todo_reads.py b/tests/control_plane/test_long_history_todo_reads.py index 06bbd0d4d9..5ac1103796 100644 --- a/tests/control_plane/test_long_history_todo_reads.py +++ b/tests/control_plane/test_long_history_todo_reads.py @@ -42,7 +42,7 @@ def test_public_list_preserves_completed_dependency_after_archive(tmp_path: Path code, result = run_json_cli_result("todo", "list", "--goal-id", "goal-a", "--role", "agent", "--todo-id", "todo_successor", "--agent-id", "agent-a", registry_path=registry, runtime_root=runtime) assert code == 0, result - assert result["todos"][0]["resume_ready"] is True + assert result["todo"]["resume_ready"] is True assert state.read_bytes() == after @@ -85,7 +85,7 @@ def test_capture_preserves_archived_dependency_for_real_canonical_cli_read(tmp_p code, result = run_json_cli_result("todo", "list", "--goal-id", "goal-a", "--role", "agent", "--todo-id", "todo_waiting", registry_path=registry, runtime_root=runtime) assert code == 0, result - assert result["todos"][0]["resume_ready"] is True + assert result["todo"]["resume_ready"] is True code, completed = run_json_cli_result("todo", "list", "--goal-id", "goal-a", "--role", "agent", "--status", "done", registry_path=registry, runtime_root=runtime) assert code == 0, completed diff --git a/tests/control_plane/test_todo_consumer_scope.py b/tests/control_plane/test_todo_consumer_scope.py index 9ccd9a8e93..5f3566af72 100644 --- a/tests/control_plane/test_todo_consumer_scope.py +++ b/tests/control_plane/test_todo_consumer_scope.py @@ -103,7 +103,7 @@ def track(method, request, **kwargs): monkeypatch.setattr(effect_runtime, "effect_runtime_result", track) assert filtered_todo_summary(source, role="user", agent_id="agent-a")["total_count"] == 1 assert len(requests) == 1 - assert requests[0]["schema_version"] == "todo_summary_projection_request_v2" + assert requests[0]["schema_version"] == "todo_summary_projection_request_v3" # One whole-source batch stays columnar, so adding fields cannot silently # push a long-history request past the runtime request budget. assert requests[0]["columns"][:3] == ["status", "done", "task_class"] diff --git a/tests/control_plane/test_todo_summary_projection.py b/tests/control_plane/test_todo_summary_projection.py index 41ba1d5a78..8e989e38d7 100644 --- a/tests/control_plane/test_todo_summary_projection.py +++ b/tests/control_plane/test_todo_summary_projection.py @@ -154,3 +154,62 @@ def track(method, request, **kwargs): import pytest with pytest.raises(Exception, match="matching full-source"): filtered_todo_summary(source, role="agent", todo_id="todo_summary_1") + + +def test_frontier_identity_precedes_display_caps_and_respects_selection(): + from loopx.control_plane.todos.goal_todo_projection import filtered_todo_summary + + items = [row(i, claimed_by="agent-a" if i < 24 else "agent-b", + updated_at="2026-01-01T00:00:00.000001Z") for i in range(32)] + full = summarize(items, item_limit=None) + capped = summarize(items, item_limit=1) + assert capped["advancement_frontier_revision_index"] == full["advancement_frontier_revision_index"] + index = full["advancement_frontier_revision_index"] + assert index["claimed_advancement_counts"] == {"agent-a": 24, "agent-b": 8} + selected = filtered_todo_summary(full, role="agent", agent_id="agent-a", item_limit=1) + assert selected["total_count"] == 24 and len(selected["items"]) == 1 + assert selected["advancement_frontier_revision_index"]["claimed_advancement_counts"] == {"agent-a": 24} + assert selected["advancement_frontier_revision_index"]["all"]["frontier_revision"] == index["by_agent"][0]["frontier_revision"] + empty = filtered_todo_summary(full, role="agent", todo_id="todo_absent") + assert empty["total_count"] == 0 and "advancement_frontier_revision_index" not in empty + + +def test_selected_frontier_uses_the_summary_crossing_after_python_attachment_retirement(monkeypatch): + from loopx.control_plane import effect_runtime + from loopx.control_plane.todos import frontier_revision + from loopx.control_plane.todos.goal_todo_projection import filtered_todo_summary + + source = summarize([row(i, claimed_by="agent-a", updated_at="2026-01-01T00:00:00Z") + for i in range(24)], item_limit=None) + original = effect_runtime.effect_runtime_result + calls = [] + + def track(method, request, **kwargs): + calls.append(method) + return original(method, request, **kwargs) + + monkeypatch.setattr(effect_runtime, "effect_runtime_result", track) + monkeypatch.setattr(frontier_revision, "effect_runtime_result", track) + selected = filtered_todo_summary(source, role="agent", agent_id="agent-a", item_limit=1) + assert selected["advancement_frontier_revision_index"]["claimed_advancement_counts"] == {"agent-a": 24} + assert calls == ["todo.summary.project"] + assert not hasattr(frontier_revision, "attach_advancement_frontier_revision_index") + + +def test_missing_batched_frontier_never_falls_back_to_another_owner_call(monkeypatch): + import pytest + from loopx.control_plane import effect_runtime + from loopx.control_plane.todos.goal_todo_projection import filtered_todo_summary + + source = summarize([row(1)], item_limit=None) + original = effect_runtime.effect_runtime_result + + def drop_index(method, request, **kwargs): + assert method == "todo.summary.project" + result = original(method, request, **kwargs) + result["fields"].pop("advancement_frontier_revision_index") + return result + + monkeypatch.setattr(effect_runtime, "effect_runtime_result", drop_index) + with pytest.raises(ValueError, match="summary frontier index"): + filtered_todo_summary(source, role="agent") diff --git a/tests/control_plane_ts/todo_summary_projection.test.ts b/tests/control_plane_ts/todo_summary_projection.test.ts index 1539531ce3..12c3b35ab0 100644 --- a/tests/control_plane_ts/todo_summary_projection.test.ts +++ b/tests/control_plane_ts/todo_summary_projection.test.ts @@ -2,6 +2,7 @@ import assert from "node:assert/strict"; import test from "node:test"; import type {JsonObject} from "../../loopx/control_plane/effect_program.ts"; import {TODO_SUMMARY_PROJECTION_COLUMNS, projectTodoSummary} from "../../loopx/control_plane/todos/summary_projection.ts"; +import {projectAdvancementFrontier} from "../../loopx/control_plane/todos/frontier_revision.ts"; import {productionScaleCoordinationFixture} from "./production_scale_coordination_fixture.ts"; import {evaluateTodoSuccession, SUCCESSION_FACT_COLUMNS, SUCCESSION_EVALUATION_COLUMNS} from "../../loopx/control_plane/todos/succession.ts"; @@ -30,6 +31,56 @@ function fused(rows: JsonObject[], facts: JsonObject[], evaluations = evaluateTo }}); } +function withFrontier(rows: JsonObject[]): JsonObject { + const facts = rows.map(row => ({todo_id: row.todo_id, status: row.status, active: true, + advancement: row.task_class === "advancement_task", no_followup: row.no_followup, + successors: [], superseded_by: null, unblocks: null, resumes: null, handoff: false, + done: row.done, route_flag: null, legacy_route_label: "", context_fields: []})); + return {...fused(rows, facts), schema_version: "todo_summary_projection_request_v3", + frontier_rows: rows.map(row => ({id: row.todo_id, claim: row.claim, excluded: row.excluded, + advancement: row.task_class === "advancement_task", actionable: row.status === "open", + updated: row.updated_at, serialized: JSON.stringify({todo_id: row.todo_id, status: row.status})}))}; +} + +test("summary frontier uses selected full-source rows before caps and retains the existing index codec", () => { + const rows = Array.from({length: 32}, (_, index) => row({todo_id: `todo_batch_${index}`, + claimed: true, claim: index < 24 ? "a" : "b", updated_at: "2026-01-01T00:00:00.000001Z"})); + const carrier = withFrontier(rows), before = structuredClone(carrier); + const original = projectAdvancementFrontier({schema_version: "todo_frontier_revision_request_v0", + operation: "index", rows: carrier.frontier_rows}).index; + for (const limit of [null, 0, 1, 12]) { + const result = projectTodoSummary({...carrier, item_limit: limit}); + assert.deepEqual(result.fields.advancement_frontier_revision_index, original); + const selected = projectTodoSummary({...carrier, item_limit: limit, + selection: {role: "agent", status: null, todo_id: null, agent_id: "a"}}); + const index = selected.fields.advancement_frontier_revision_index as JsonObject; + assert.deepEqual(index.claimed_advancement_counts, {a: 24}); + assert.equal(selected.fields.total_count, 24); + } + assert.deepEqual(carrier, before); + assert.equal(projectTodoSummary(fused([], [])).fields.advancement_frontier_revision_index, undefined); + assert.deepEqual((projectTodoSummary(withFrontier([])).fields.advancement_frontier_revision_index as JsonObject).all, + {complete: false}); +}); + +test("summary frontier rejects absent, reordered and conflicting facts instead of mixing snapshots", () => { + const carrier = withFrontier([row({todo_id: "todo_first", claimed: true, claim: "a", updated_at: "2026-01-01T00:00:00Z"}), + row({todo_id: "todo_second", updated_at: "2026-01-01T00:00:00Z"})]); + for (const mutate of [ + (value: JsonObject) => { value.frontier_rows = null; }, + (value: JsonObject) => { value.frontier_rows = []; }, + (value: JsonObject) => { (value.frontier_rows as JsonObject[]).reverse(); }, + ...["claim", "excluded", "advancement", "actionable", "updated"].map(key => (value: JsonObject) => { + (value.frontier_rows as JsonObject[])[0][key] = {claim: "b", excluded: ["b"], advancement: false, + actionable: false, updated: "2026-02-01T00:00:00Z"}[key]; + }), + ]) { + const changed = structuredClone(carrier); mutate(changed); + assert.throws(() => projectTodoSummary(changed), /summary frontier/); + } + assert.throws(() => projectTodoSummary({...carrier, role: "user"}), /non-Agent summary/); +}); + test("fused summary preserves legacy decisions and full-source inferred edges through filtering", () => { const facts = [{todo_id: "todo_source", status: "done", active: true, advancement: true, no_followup: false, successors: [], superseded_by: null, unblocks: null, resumes: null, From 24a62ab4bf602063c486bf94af7b1c530f86602f Mon Sep 17 00:00:00 2001 From: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> Date: Wed, 7 Oct 2026 17:14:41 +0800 Subject: [PATCH 2/4] docs(todos): record frontier read retirement boundary Signed-off-by: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> --- docs/reference/todo-work-counts.md | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/docs/reference/todo-work-counts.md b/docs/reference/todo-work-counts.md index 29e804e922..1291d2c1dd 100644 --- a/docs/reference/todo-work-counts.md +++ b/docs/reference/todo-work-counts.md @@ -156,6 +156,29 @@ evaluation still precedes filtering, and returned positions refer to the origina array, not a newly numbered subset. Python retains public field allowlists, warning text, privacy redaction and Markdown parsing/rendering. +Agent summaries compute their existing `advancement_frontier_revision_index` +inside this same projection, after source selection and before display caps. +The shared frontier owner retains the v0 serialization/hash, excluded-agent +lanes, owned-work ACK identity and commitment counts. Missing or contradictory +codec facts fail the read; no separate index RPC repairs the result. The +single-caller Python attachment is retired. Direct frontier calls used by replan +and successor causality remain, as do Markdown writers for supported unmigrated +Goals, backup/receipt recovery and Host IO. Internal summary request v3 composes +the existing owners; v1/v2 callers and public summary/index schemas remain +compatible. This is a source-read retirement slice, not SQLite-default or full +writer-retirement qualification. Roll back the matching Python/TS distribution +together; no persisted data migration is involved. + +Agent summary 在同一 TS 批次中生成既有 `advancement_frontier_revision_index`:先筛选 +完整来源,再生成索引,最后应用展示上限。沿用 frontier owner 的 v0 序列化/哈希、 +排除 Agent 的 lane、已拥有工作对应的 ACK 身份和承诺计数。缺失或矛盾的 codec 事实会 +明确拒绝读取,不会补调另一个索引 RPC。删除仅有一个调用方的 Python attachment; +replan 和 successor 因果核验仍使用的直接 frontier 入口、未迁移 Goal 的 Markdown +writer、备份/原回执恢复及 Host IO 继续保留。内部 summary 请求升级为 v3,保留 +v1/v2 调用兼容和公共 summary/index schema。本批只完成来源读取边界的退役,不代表 +SQLite 默认或全部 writer 退役已验收;回退时一起回退匹配的 Python/TS 分发,无需迁移 +持久化数据。 + **Intentional read behavior changes:** recent completions are ordered by the actual `completed_at` instant, preserving timezone offsets and microseconds. Later `updated_at` edits no longer make an old completion recent. Missing or From 42ca2aaf21f56b1db29c8a4b347cb26ae34e7f99 Mon Sep 17 00:00:00 2001 From: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> Date: Wed, 7 Oct 2026 18:45:26 +0800 Subject: [PATCH 3/4] test(todos): preserve lifecycle checks through exact detail reads Signed-off-by: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> --- .../test_canonical_claim_execution_proof.py | 2 +- .../test_canonical_claim_transfer.py | 6 +-- .../test_canonical_lease_acquire.py | 2 +- .../test_canonical_todo_writer_isolation.py | 5 ++- .../test_causal_blocked_closeout_cli.py | 4 +- .../test_shadow_registry_source.py | 2 +- .../test_succession_provider_readback.py | 41 +++++++++++++++++-- .../test_todo_projection_recovery.py | 12 ++++-- 8 files changed, 56 insertions(+), 18 deletions(-) diff --git a/tests/control_plane/test_canonical_claim_execution_proof.py b/tests/control_plane/test_canonical_claim_execution_proof.py index 83151b7285..5f9bac937a 100644 --- a/tests/control_plane/test_canonical_claim_execution_proof.py +++ b/tests/control_plane/test_canonical_claim_execution_proof.py @@ -72,7 +72,7 @@ def cli(command, *args, expected_exit=0): assert not state.exists(), "rejected recovery cannot revive a legacy writer" readback = cli(["todo", "list"]) assert first["source_authority"] == provider + "_v0" - assert readback["todos"][0]["claimed_by"] == ("agent-b" if retirement == "transfer" else "agent-a") + assert readback["todo"]["claimed_by"] == ("agent-b" if retirement == "transfer" else "agent-a") assert not (runtime / "goals" / goal / "task-leases" / f"{target}.json").exists() finally: subprocess.run([sys.executable, "-c", "from loopx.control_plane.effect_runtime import effect_runtime_result; effect_runtime_result('runtime.shutdown',{},retry_safe=False)"], diff --git a/tests/control_plane/test_canonical_claim_transfer.py b/tests/control_plane/test_canonical_claim_transfer.py index e21bce3248..0d830beb13 100644 --- a/tests/control_plane/test_canonical_claim_transfer.py +++ b/tests/control_plane/test_canonical_claim_transfer.py @@ -59,7 +59,7 @@ def cli(command, *args, expected_exit=0): assert result["source_authority"] == provider + "_v0" assert result["projection_delivery"] == "pending" assert result["projection_outbox"]["retry_business_mutation"] is False - assert cli(["todo", "list"])["todos"][0]["claimed_by"] == "agent-b" + assert cli(["todo", "list"])["todo"]["claimed_by"] == "agent-b" state.rmdir() state.write_text(original_text) replay = cli(["task-lease", "transfer"], *transfer, "--transfer-claim") @@ -82,13 +82,13 @@ def cli(command, *args, expected_exit=0): "--task-lease-expected-version", "2", "--evidence", "validation://claim-transfer", "--no-follow-up") assert complete["ok"] current = cli(["todo", "list"]) - assert current["todos"][0]["status"] == "done" + assert current["todo"]["status"] == "done" current_lease = cli(["task-lease", "inspect"]) assert current_lease["lease"]["status"] == "released" and not current_lease["active"] historical = cli(["task-lease", "transfer"], *transfer, "--transfer-claim") assert historical["original_receipt"] == result["original_receipt"] assert historical["lease"]["status"] == "active" - assert cli(["todo", "list"])["todos"][0]["status"] == "done" + assert cli(["todo", "list"])["todo"]["status"] == "done" assert cli(["task-lease", "inspect"])["lease"] == current_lease["lease"] assert not (runtime / "goals" / goal / "task-leases" / f"{target}.json").exists() finally: diff --git a/tests/control_plane/test_canonical_lease_acquire.py b/tests/control_plane/test_canonical_lease_acquire.py index 9fe9b3ee68..feae5aebcc 100644 --- a/tests/control_plane/test_canonical_lease_acquire.py +++ b/tests/control_plane/test_canonical_lease_acquire.py @@ -75,7 +75,7 @@ def cli(command, *args, expected_exit=0): assert completed["ok"] assert state.exists() readback = cli(["todo", "list"]) - assert readback["todos"][0]["status"] == "done" + assert readback["todo"]["status"] == "done" terminal = cli(["task-lease", "inspect"]) assert terminal["lease"]["status"] == "released" and not terminal["active"] assert (cli(["task-lease", "acquire"], *next_proof, "--ttl-seconds", "600", expected_exit=1))["error_code"] == "todo_not_open" diff --git a/tests/control_plane/test_canonical_todo_writer_isolation.py b/tests/control_plane/test_canonical_todo_writer_isolation.py index 4a481f0dfc..63855b5dca 100644 --- a/tests/control_plane/test_canonical_todo_writer_isolation.py +++ b/tests/control_plane/test_canonical_todo_writer_isolation.py @@ -72,8 +72,9 @@ def add(role, text, operation): "--note", "Retain source identity", "--update-operation-id", "update", "--update-expected-provider-revision", listing["authority_read"]["provider_revision"]) assert updated["todo_id"] == original - updated_records = cli("list", "--goal-id", "goal-a", "--todo-id", original)["todos"] - assert updated_records[0]["note"] == "Retain source identity" + updated_record = cli("list", "--goal-id", "goal-a", "--todo-id", original)["todo"] + assert updated_record["todo_id"] == original + assert updated_record["note"] == "Retain source identity" completed = cli("complete", "--goal-id", "goal-a", "--todo-id", user, "--no-follow-up", "--note", "Independent user action accepted") assert completed["completed"] and completed["todo_id"] == user diff --git a/tests/control_plane/test_causal_blocked_closeout_cli.py b/tests/control_plane/test_causal_blocked_closeout_cli.py index 47476571da..6751a8e633 100644 --- a/tests/control_plane/test_causal_blocked_closeout_cli.py +++ b/tests/control_plane/test_causal_blocked_closeout_cli.py @@ -79,7 +79,7 @@ def cli(*args: str, cwd: Path = project) -> tuple[int, dict]: assert rc == 0 and guard["should_run"] is True, guard rc, original = cli("todo", "list", "--goal-id", GOAL_ID, "--todo-id", TODO_ID) assert rc == 0, original - digest = original["todos"][0]["completion_validation_sha256"] + digest = original["todo"]["completion_validation_sha256"] rc, wait = cli("todo", "update", "--goal-id", GOAL_ID, "--todo-id", TODO_ID, "--agent-id", AGENT_ID, "--resume-when", f"{kind}:{MONITOR_ID}", @@ -121,7 +121,7 @@ def cli(*args: str, cwd: Path = project) -> tuple[int, dict]: assert _spend_run_count(runtime) == 0 rc, after = cli("todo", "list", "--goal-id", GOAL_ID, "--todo-id", TODO_ID) assert rc == 0, after - todo = after["todos"][0] + todo = after["todo"] assert todo["status"] == ("deferred" if defer else "open") and todo["resume_ready"] is False assert todo["resume_when"] == f"{kind}:{MONITOR_ID}" assert todo["completion_validation_sha256"] == digest diff --git a/tests/control_plane/test_shadow_registry_source.py b/tests/control_plane/test_shadow_registry_source.py index f5b2ed5b4c..78bd4040bc 100644 --- a/tests/control_plane/test_shadow_registry_source.py +++ b/tests/control_plane/test_shadow_registry_source.py @@ -171,7 +171,7 @@ def test_terminal_lease_survives_public_cutover_and_original_recovery(tmp_path: assert recovered["promotion"]["status"] == "replayed", recovered assert ws.cli("task-lease", "inspect", "--todo-id", todo_id)["lease"] == expected retained = ws.cli("todo", "list", "--todo-id", later["todo_id"]) - assert retained["todos"][0]["text"] == "Later canonical work must survive recovery" + assert retained["todo"]["text"] == "Later canonical work must survive recovery" assert lease.read_bytes() == original assert not ws.state.exists() diff --git a/tests/control_plane/test_succession_provider_readback.py b/tests/control_plane/test_succession_provider_readback.py index ab6adfcb6c..ed4e0e9fe2 100644 --- a/tests/control_plane/test_succession_provider_readback.py +++ b/tests/control_plane/test_succession_provider_readback.py @@ -10,7 +10,12 @@ from canonical_authority_fixture import initialize_canonical_authority, isolate_sqlite_runtime from loopx.chat_manager_details import read_manager_goal_details -from loopx.control_plane.coordination.local_authority import read_canonical_todos_if_promoted +from loopx.control_plane.coordination.local_authority import ( + canonical_todo_summary_fields, read_canonical_todos_if_promoted, +) +from loopx.control_plane.todos.goal_todo_projection import ( + goal_todo_summaries, todo_summaries_from_fields, +) from loopx.control_plane.todos.machine_section_projection import render_canonical_todo_sections from loopx.control_plane.coordination.runtime_shadow import build_todo_runtime_shadow_projection from loopx.todos import list_goal_todos @@ -31,6 +36,26 @@ def cli(registry: Path, *args: str): return json.loads(child.stdout) +def selected_summary(registry: Path, runtime: Path, state: Path, todo_id: str, *, limit: int | None = None): + # Exact CLI detail owns the full task body. Relationship/closure judgments + # belong to the existing summary owner over the complete provider graph, + # before presentation selection; never reconstruct them from detail. + options = dict(rollout_events=[], roles=['agent'], status=None, + todo_id=todo_id, agent_id=None, limit=limit) + canonical = read_canonical_todos_if_promoted(runtime_root=runtime, goal_id='goal-a') + if canonical is not None: + projected = todo_summaries_from_fields( + fields=canonical_todo_summary_fields(canonical['todos']), + source='file_authority', **options, + ) + else: + projected = goal_todo_summaries( + json.loads(registry.read_text())['goals'][0], + state_text=state.read_text(), state_path=state, **options, + ) + return projected.summaries['agent_todos'] + + @pytest.mark.parametrize('provider', ['legacy', 'file', 'sqlite']) def test_real_cli_historical_route_advisory_preserves_old_source_and_provider(tmp_path, monkeypatch, provider): isolate_sqlite_runtime(tmp_path, monkeypatch) @@ -55,7 +80,9 @@ def test_real_cli_historical_route_advisory_preserves_old_source_and_provider(tm before = read_canonical_todos_if_promoted(runtime_root=runtime, goal_id='goal-a') for todo_id, expected in [('todo_legacy', True), ('todo_ordinary', False), ('todo_closed', False)]: result = cli(registry, '--todo-id', todo_id) - summary = result['agent_todos'] + assert result['todo']['todo_id'] == todo_id + assert all(key not in result for key in ('todos', 'agent_todos', 'user_todos')) + summary = selected_summary(registry, runtime, state, todo_id) handoff = summary['handoff_gates'][0] assert (handoff.get('route_continuation_replan_required') is True) is expected assert handoff['gate_state'] == ('cleared_no_followup' if todo_id == 'todo_closed' else 'blocking') @@ -88,7 +115,9 @@ def test_real_cli_graph_selection_history_and_read_only_manager(tmp_path, monkey before = read_canonical_todos_if_promoted(runtime_root=runtime, goal_id='goal-a') for name, gap in [('inferred_source', 0), ('missing_source', 1), ('self_source', 1), ('handoff_source', 0)]: result = cli(registry, '--todo-id', cases[name], '--limit', '1') - summary = result['agent_todos'] + assert result['todo']['todo_id'] == cases[name] + assert all(key not in result for key in ('todos', 'agent_todos', 'user_todos')) + summary = selected_summary(registry, runtime, state, cases[name], limit=1) assert summary.get('completed_without_successor_count', 0) == gap assert 'terminal_closure_proof' not in summary if name == 'handoff_source': @@ -134,7 +163,11 @@ def test_large_goal_reuses_complete_succession_without_exceeding_rpc_budget(tmp_ assert summary['open_count'] == 1 assert summary.get('completed_without_successor_count', 0) == 0 assert 'terminal_closure_proof' not in summary - selected = cli(registry, '--todo-id', 'todo_capacity_00000', '--limit', '1')['agent_todos'] + detail = cli(registry, '--todo-id', 'todo_capacity_00000', '--limit', '1') + assert detail['todo']['todo_id'] == 'todo_capacity_00000' + assert detail['todo']['note'] == 'Retained metadata 完整🙂' + assert all(key not in detail for key in ('todos', 'agent_todos', 'user_todos')) + selected = selected_summary(registry, runtime, state, 'todo_capacity_00000', limit=1) assert selected.get('completed_without_successor_count', 0) == 0 assert 'terminal_closure_proof' not in selected after = read_canonical_todos_if_promoted(runtime_root=runtime, goal_id='goal-a') diff --git a/tests/control_plane/test_todo_projection_recovery.py b/tests/control_plane/test_todo_projection_recovery.py index e95ce9fc1e..d8abd1c030 100644 --- a/tests/control_plane/test_todo_projection_recovery.py +++ b/tests/control_plane/test_todo_projection_recovery.py @@ -128,10 +128,14 @@ def test_long_committed_todo_rebuilds_from_the_fresh_head_without_a_second_creat assert code == 0 and listed["todo"]["text"] == text, listed assert listed["todo"]["title"] == todo_priority_parts(summary_text)[1] assert listed["authority_read"]["provider_revision"] == before["provider_revision"] - code, thin = _cli(registry, "list", "--goal-id", "goal-a", "--todo-id", record["todo_id"], "--thin") - assert code == 0 and thin["todo"]["todo_id"] == record["todo_id"], thin - assert len(thin["todo"]["text"]) <= 500 - assert "retain the final obligation" not in thin["todo"]["text"] + code, rejected = _cli(registry, "list", "--goal-id", "goal-a", "--todo-id", record["todo_id"], "--thin") + assert code == 1 and rejected["ok"] is False, rejected + assert "Exact Todo reads return full requirements" in rejected["error"] + assert _read(runtime) == before and state.read_text() == rendered + code, thin = _cli(registry, "list", "--goal-id", "goal-a", "--thin") + assert code == 0 and thin["todos"][0]["todo_id"] == record["todo_id"], thin + assert len(thin["todos"][0]["text"]) <= 500 + assert "retain the final obligation" not in thin["todos"][0]["text"] code, hot = _cli(registry, "list", "--goal-id", "goal-a") assert code == 0 and hot["todos"][0]["text"] == summary_text, hot assert _read(runtime) == before From cb0f080942029eb8ba02cbb014f86dae8831abc1 Mon Sep 17 00:00:00 2001 From: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> Date: Wed, 7 Oct 2026 18:51:19 +0800 Subject: [PATCH 4/4] test(todos): verify the installed SQLite cutover entrypoint Signed-off-by: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> --- tests/control_plane/test_shadow_registry_source.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/tests/control_plane/test_shadow_registry_source.py b/tests/control_plane/test_shadow_registry_source.py index 78bd4040bc..437eabf148 100644 --- a/tests/control_plane/test_shadow_registry_source.py +++ b/tests/control_plane/test_shadow_registry_source.py @@ -149,11 +149,14 @@ def test_terminal_lease_survives_public_cutover_and_original_recovery(tmp_path: original = json.dumps(expected, indent=2).encode() lease.write_bytes(original) if provider == "sqlite": - subprocess.run([os.environ.get("LOOPX_CONTROL_PLANE_NODE", "node"), + selection = subprocess.run([os.environ.get("LOOPX_CONTROL_PLANE_NODE", "node"), "--no-warnings", "--experimental-strip-types", "--experimental-sqlite", - "loopx/control_plane/coordination/local_authority_provider.ts", "--runtime-root", + str((REPO / "loopx/control_plane/coordination/local_authority_provider.ts").resolve()), "--runtime-root", str(ws.runtime), "--goal-id", ws.goal, "--execute"], cwd=REPO, capture_output=True, text=True, timeout=45, check=True) + assert json.loads(selection.stdout) == { + "ok": True, "provider": "sqlite", "changed": True, "executed": True, + } assert ws.cli("coordination-shadow", "bootstrap", "--execute")["bootstrap"]["status"] == "applied" for index in range(3): ws.add(f"Independent migration work {index}")