diff --git a/docs/architecture/rfcs/typescript-control-plane-migration-v0.md b/docs/architecture/rfcs/typescript-control-plane-migration-v0.md
index 44368ff77e..4ab3bba3d5 100644
--- a/docs/architecture/rfcs/typescript-control-plane-migration-v0.md
+++ b/docs/architecture/rfcs/typescript-control-plane-migration-v0.md
@@ -84,6 +84,44 @@ Each page reuses the existing executable-revision request scope; operation
records remain live reads, and the next page resolves the current code again.
+### Todo read transaction source checkpoint
+
+`todo.read` now composes canonical provider reads, full-source resume and
+succession evidence, standing decisions, both role summaries, selection and the
+public result through `todos/read_projection.ts` and `todos/read_transaction.ts`.
+Ordinary canonical queries use one request. Large result continuations retain
+one observation and result digest; changed results refuse rather than combining
+pages. Dependency and closure decisions still use the complete source.
+Python retains registry, document/material IO, independent Vision-wait consumers
+and compatibility exports. Its replaced summary, filter and result coordination
+is retired. Persisted completion spellings use shared token data and compatibility codecs
+without scalar RPCs;
+`todo.summary.project`, `todo.completion_state.normalize` and
+`todo.completion_state.require_metadata` are retired after their production
+callers exit. Existing semantic owners execute in process inside the transaction.
+
+CLI query results carry a query-scoped `projection_envelope`. A read grants no
+execution, acceptance, lease or mutation authority. The shared wire and private
+snapshot budgets are unchanged. Oversized Python facts use an owned temporary
+source, not another durable store; complete results use digest-bound pieces.
+Envelope coverage counts the complete matching source before display limits.
+Hot, limited and thin views report their final displayed count separately;
+display truncation alone does not create a source omission or alert. Exact
+detail reads count their returned Todo rather than the removed list view.
+Remaining historical/source adapters still name their actual IO and crossings.
+Python source codecs and transport now share the existing
+`todos/goal_todo_projection.py` module. The separate Python `read_projection.py`
+is removed; the independent py/ts twin budget stays at 43. Current host oracle
+observations remain with the existing acceptance owner.
+The unchanged 18-row Todo output matrix adds 783–854 JSON characters for source
+freshness and omitted-work coverage; Markdown is unchanged. Only the first
+envelope schema transition receives a 896-character allowance on the three
+measured Todo query views. Existing absolute ceilings and later growth limits
+remain in force. These are controlled-transport output measurements.
+Source/controlled-IPC evidence does not qualify normal installed CLI/Host use,
+full-command performance, R3 adoption or D2/D3/default delivery.
+
+
## Current delivery frontier (post-merge, 2026-10-16)
diff --git a/docs/architecture/rfcs/typescript-control-plane-migration-v0.zh-CN.md b/docs/architecture/rfcs/typescript-control-plane-migration-v0.zh-CN.md
index 3dee7c57d9..b1bc9e2699 100644
--- a/docs/architecture/rfcs/typescript-control-plane-migration-v0.zh-CN.md
+++ b/docs/architecture/rfcs/typescript-control-plane-migration-v0.zh-CN.md
@@ -71,6 +71,36 @@ Python 保留有界地址扫描、请求者注册检查和实时 `Delegations.re
每页复用既有代码版本请求作用域;操作记录仍实时读取,下一页重新解析当前代码。
+### Todo 读取事务源码检查点
+
+`todo.read` 现由 `todos/read_projection.ts` 与 `todos/read_transaction.ts`
+组合 canonical provider 读取、完整源上的恢复与后继证据、standing decisions、
+两个角色的摘要、筛选与公共结果。普通 canonical 查询使用一次请求。
+大结果的连续页保留同一次观察和结果摘要;结果变化即拒绝,不拼接新旧页。
+依赖与闭合判断仍使用完整源。
+Python 保留 registry、文档/材料 IO、独立的 Vision-wait 消费者及兼容导出。
+被替代的摘要、筛选与结果协调已退役。持久化 completion 拼写使用共享 token 数据与兼容 codec,
+不再逐标量调用 RPC;实际生产调用者退出后,退役 `todo.summary.project`、
+`todo.completion_state.normalize` 与 `todo.completion_state.require_metadata`。
+事务在进程内执行既有语义 owner。
+
+CLI 查询结果携带查询范围内的 `projection_envelope`。读取不授予执行、验收、
+租约或修改权限。共享 wire 与私有快照预算不变。超大 Python 事实使用私有临时
+源,不增加持久化存储;完整结果通过摘要绑定的片段传输。
+信封覆盖计数使用显示限制前的完整匹配源。hot、限量及 thin 视图分别报告最终
+显示数量;显示截断本身不产生来源遗漏或告警。精确详情按返回的 Todo 计数,
+不按已移除的列表视图计数。
+仍保留的历史/源适配明确记录实际 IO 与跨运行时调用。
+Python 源 codec 与传输现在共用既有 `todos/goal_todo_projection.py` 模块,
+删除独立的 Python `read_projection.py`,独立维护的 py/ts 配对预算仍为 43。
+当前宿主 oracle 观测仍归既有 acceptance owner。
+同一 18 行 Todo 输出矩阵为来源新鲜度和遗漏工作覆盖增加 783–854 个 JSON 字符,
+Markdown 不变。仅三个已测 Todo 查询视图的首次信封 schema 迁移获得 896 字符
+余量;既有绝对上限与后续增长限制继续生效。这些是受控传输下的输出测量。
+源码/受控 IPC 证据不代表正常安装的 CLI/Host
+使用、完整命令性能、R3 采用或 D2/D3/默认交付资格。
+
+
## 当前交付边界(合入后,2026-10-16)
diff --git a/docs/reference/todo-work-counts.md b/docs/reference/todo-work-counts.md
index 31dfe11363..fbc673c118 100644
--- a/docs/reference/todo-work-counts.md
+++ b/docs/reference/todo-work-counts.md
@@ -18,6 +18,11 @@ new provider setting: legacy inputs and promoted File/SQLite inputs share the
same typed summary owner. PostgreSQL uses the same provider-neutral records;
service deployment and whole-Goal promotion remain separately qualified.
+The list's `projection_envelope` also separates source coverage from display.
+Its `included_count` covers all query matches; `shown_count` counts the final
+ordinary, hot, limited, thin or exact view. Display truncation alone does not
+raise `incomplete_coverage`; see the [envelope contract](contracts/projection-envelope-contract.md).
+
| Field | Meaning |
| --- | --- |
| `open` | Nonterminal source rows, including blocked work; it is not executable work |
@@ -51,8 +56,9 @@ are lower bounds while `complete=false`. Current capability re-evaluation
replaces prior readiness for observed rows and cannot certify unseen rows.
The TypeScript `todos/summary_lanes.ts` owner returns indexes into the one input
-array instead of repeating full Todo bodies for each lane. The Python adapter
-normalizes legacy fields/timestamps and validates the returned ordinal bounds.
+array instead of repeating full Todo bodies for each lane. The native read owner
+normalizes canonical fields/timestamps and materializes the selected records.
+Python retains the legacy source codecs.
One observation time governs Monitor due and expiry classification within the
batch. Source order, completed/deferred conventions and claimant visibility
remain compatible; `done_count` still includes deferred rows as required by its
@@ -92,11 +98,14 @@ read and continue to show the whole Goal; no new configuration editor is needed.
Resume and succession are evaluated on the complete source before selection.
The typed batch filters rows without renumbering their original source indexes,
then builds lanes/counts, and only then applies display limits. Status/identity
-filters do not recompute dependencies from their smaller view. One internal `todo.summary.project` batch now composes selection, counts,
-visibility allocation and closure. Its transient request replaces the separate
-lane and closure RPC calls; it does not change persisted Todo or public summary
-schemas. Python decodes legacy input, validates source ordinals and materializes
-public fields, with no independent summary count, cap or claimant-allocation rule.
+filters do not recompute dependencies from their smaller view. The `todo.read` transaction composes source evaluation, selection, counts,
+frontier identity, handoff context and result construction through the existing
+TypeScript owners. Canonical provider IO runs in that transaction; Markdown
+source codecs and material IO remain explicit Python adapters. Filtering reuses
+validated full-source evidence, never dependencies inferred from a small view.
+The old `todo.summary.project` transport and Python ordinal materializer are
+retired. Large source/result transport preserves complete decisions and refuses
+changed continuations; display limits still disclose shown and available counts.
Route-continuation visibility also uses the existing typed quota planning batch.
It shares the normal claim/exclusion rule; eligibility, first-identity deduplication,
diff --git a/loopx/capabilities/periodic_report/post_writeback_hook.py b/loopx/capabilities/periodic_report/post_writeback_hook.py
index 6b03f7a1c9..a10e6158e6 100644
--- a/loopx/capabilities/periodic_report/post_writeback_hook.py
+++ b/loopx/capabilities/periodic_report/post_writeback_hook.py
@@ -16,10 +16,6 @@
build_goal_frontier_projection_from_summaries,
)
from .todo_source import read_report_source_history, read_report_todo_source
-from ...control_plane.effect_runtime import (
- CANONICAL_AUTHORITY_READ_TIMEOUT_SECONDS,
- effect_runtime_result,
-)
from ...control_plane.todos.quota_summary import summarize_user_todos_for_quota
from ...control_plane.todos.todo_index import MAX_TODO_INDEX_ROLLOUT_EVENTS_PER_GOAL
from ...history import collect_history, load_index_snapshot, load_registry
@@ -202,18 +198,18 @@ def build_periodic_report_post_writeback_projection(
if available_capabilities is None and isinstance(payload.get("turn"), Mapping):
available_capabilities = payload["turn"].get("available_capabilities")
if source_todos is None and "todo_source" in payload:
- source_todos = effect_runtime_result(
- "coordination.local_authority.todo_source",
- {
- "schema_version": "loopx_local_coordination_todo_source_request_v0",
- "runtime_root": str(runtime_root), "goal_id": goal_id,
- "source": payload["todo_source"],
- },
- timeout=CANONICAL_AUTHORITY_READ_TIMEOUT_SECONDS,
- large_local_snapshot=True,
- )
+ from ...control_plane.todos.goal_todo_projection import project_todo_read, restore_todo_field_evaluations
+ from ...control_plane.runtime.time import now_utc
+
+ now = now_utc()
+ readback = project_todo_read({"kind": "historical_fields",
+ "source": {"runtime_root": str(runtime_root), "goal_id": goal_id, "original_source": payload["todo_source"]},
+ "rollout_events": [], "available_capabilities": available_capabilities,
+ "observed_at": now.timestamp(), "evaluated_at": now.isoformat()})
+ source_todos = readback["canonical"]
if source_todos.get("status") != "loaded":
raise ValueError("original canonical Todo source is unavailable")
+ source_todos["_todo_fields"] = restore_todo_field_evaluations(readback)
source_runs = read_report_source_history(
runtime_root=runtime_root, goal_id=goal_id, run=payload,
)
@@ -228,7 +224,7 @@ def build_periodic_report_post_writeback_projection(
state.get("path") if isinstance(state, Mapping) else None
) or payload.get("state_file")
if source_todos is not None:
- fields = canonical_todo_summary_fields(
+ fields = source_todos.get("_todo_fields") or canonical_todo_summary_fields(
source_todos["todos"],
rollout_events=events,
available_capabilities=available_capabilities,
diff --git a/loopx/capabilities/periodic_report/todo_source.py b/loopx/capabilities/periodic_report/todo_source.py
index b925285b29..b6ef0d6350 100644
--- a/loopx/capabilities/periodic_report/todo_source.py
+++ b/loopx/capabilities/periodic_report/todo_source.py
@@ -11,7 +11,6 @@
from typing import Any
from ...control_plane.coordination.local_authority import (
- canonical_todo_summary_fields,
read_canonical_todos_if_promoted,
)
from ...control_plane.todos.active_state_todo_parser import parse_active_state_todos
@@ -62,16 +61,11 @@ def read_report_todo_source(
registry, None, registry_path=registry_path
)
canonical = read_canonical_todos_if_promoted(
- runtime_root=runtime_root, goal_id=goal_id
+ runtime_root=runtime_root, goal_id=goal_id, include_summary=True,
+ rollout_events=rollout_events, available_capabilities=available_capabilities
)
if canonical is not None:
- fields = canonical_todo_summary_fields(
- canonical["todos"],
- rollout_events=rollout_events,
- available_capabilities=available_capabilities,
- goal_acceptance_contract=canonical.get("goal_acceptance_contract"),
- goal_acceptance_work_guards=canonical.get("goal_acceptance_work_guards"),
- )
+ fields = canonical["_todo_fields"]
return fields, [row for row in canonical["todos"] if row.get("role") == "user"]
state_path = state_path or resolve_state_file(
Path(str(goal.get("repo") or "")).expanduser(),
diff --git a/loopx/cli_commands/todo_list.py b/loopx/cli_commands/todo_list.py
index 335f80feb2..3f4a26b16d 100644
--- a/loopx/cli_commands/todo_list.py
+++ b/loopx/cli_commands/todo_list.py
@@ -42,6 +42,7 @@ def handle_todo_list_command(
project=Path(args.project).expanduser() if args.project else None,
state_file=Path(args.state_file).expanduser() if args.state_file else None,
runtime_root_arg=runtime_root_arg,
+ json_references=(format_name or str(getattr(args, "format", None) or "markdown")) == "json",
)
except Exception as exc:
from ..usage_ping import capture_failure
@@ -56,6 +57,7 @@ def handle_todo_list_command(
)
output_format = format_name or str(getattr(args, "format", None) or "markdown")
if (payload.get("ok") and not args.todo_id and not args.todo_thin
+ and "todo_list_record_references" not in payload
and output_format == "json"):
try:
payload = effect_runtime_result(
diff --git a/loopx/control_plane/collaboration/conversation_binding.ts b/loopx/control_plane/collaboration/conversation_binding.ts
index 466fb6c52d..cf3068d553 100644
--- a/loopx/control_plane/collaboration/conversation_binding.ts
+++ b/loopx/control_plane/collaboration/conversation_binding.ts
@@ -496,7 +496,7 @@ function planContextRecovery(params: JsonObject, row: JsonObject, current: JsonO
|| !["context_window_exceeded", "host_gate", "upstream_transport_failure", "server_overloaded"].includes(String(failed.error_code))
|| typeof failed.upstream_turn_id !== "string" || !failed.upstream_turn_id) return no;
const previous = row.context_recovery == null ? null : requireJsonObject(row.context_recovery, "reserved context recovery");
- if (recovery.admitted_turn != null && previous?.failed_turn_id === failed.turn_id
+ if (recovery.admitted_turn != null && previous !== null && previous.failed_turn_id === failed.turn_id
&& Number.isSafeInteger(previous.routing_attempt) && Number(previous.routing_attempt) >= 1
&& Number(previous.routing_attempt) <= 3 && previous.version === version) {
// A crash after queue admission must adopt that exact canonical Turn,
@@ -560,7 +560,7 @@ function planRoutingRecovery(recovery: JsonObject, row: JsonObject, current: Jso
failed: JsonObject, previous: JsonObject | null, version: number): JsonObject | null {
if (!["host_gate", "upstream_transport_failure"].includes(String(failed.error_code))) return null;
// An unfinished older reservation must finish under its original identity.
- if (previous?.failed_turn_id === failed.turn_id && previous.routing_attempt === undefined) return null;
+ if (previous !== null && previous.failed_turn_id === failed.turn_id && previous.routing_attempt === undefined) return null;
const observed = recovery.native_observation == null ? null
: requireJsonObject(recovery.native_observation, "native failure readback");
if (observed && observed.pre_execution_routing_timeout !== true) return null;
diff --git a/loopx/control_plane/coordination/local_authority.py b/loopx/control_plane/coordination/local_authority.py
index f6d3c6c6bd..98f45aa9e5 100644
--- a/loopx/control_plane/coordination/local_authority.py
+++ b/loopx/control_plane/coordination/local_authority.py
@@ -7,8 +7,6 @@
from __future__ import annotations
-from ..goals.acceptance_readback import configured_checks
-
from collections.abc import Mapping
from copy import deepcopy
from pathlib import Path
@@ -247,6 +245,7 @@ def claim_canonical_todo_if_promoted(
def read_canonical_todos_if_promoted(
*, runtime_root: Path, goal_id: str, include_leases: bool = False,
projection_readback: Mapping[str, Any] | None = None,
+ include_summary: bool = False, rollout_events=None, available_capabilities=None,
) -> dict[str, Any] | None:
"""Return canonical Todos after cutover, or ``None`` before cutover.
@@ -258,13 +257,28 @@ def read_canonical_todos_if_promoted(
if not local_authority_is_promoted(runtime_root=runtime_root, goal_id=goal_id):
return None
- result = read_canonical_snapshot(
- rpc=effect_runtime_result,
- runtime_root=str(runtime_root.expanduser().resolve(strict=False)),
- goal_id=goal_id, include_leases=include_leases,
- projection_readback=projection_readback,
- timeout=LOCAL_COORDINATION_TODO_LIST_TIMEOUT_SECONDS,
- )
+ projected = None
+ if include_summary:
+ from ..todos.goal_todo_projection import project_todo_read
+ from ..runtime.time import now_utc
+
+ now = now_utc()
+ projected = project_todo_read({"kind": "canonical_fields",
+ "source": {"runtime_root": str(runtime_root.expanduser().resolve(strict=False)), "goal_id": goal_id,
+ "include_leases": include_leases, "projection_readback": dict(projection_readback) if projection_readback is not None else None},
+ "rollout_events": list(rollout_events or []),
+ "rollout_event_source": getattr(rollout_events, "source", None),
+ "available_capabilities": sorted({str(value).strip().lower() for value in available_capabilities if str(value).strip()}) if available_capabilities is not None else None,
+ "observed_at": now.timestamp(), "evaluated_at": now.isoformat()})
+ result = projected["canonical"]
+ else:
+ result = read_canonical_snapshot(
+ rpc=effect_runtime_result,
+ runtime_root=str(runtime_root.expanduser().resolve(strict=False)),
+ goal_id=goal_id, include_leases=include_leases,
+ projection_readback=projection_readback,
+ timeout=LOCAL_COORDINATION_TODO_LIST_TIMEOUT_SECONDS,
+ )
if not isinstance(result, Mapping):
raise LocalCoordinationAuthorityUnavailable(
"local coordination authority returned an invalid Todo list",
@@ -318,6 +332,9 @@ def read_canonical_todos_if_promoted(
"canonical projection confirmation is missing or invalid",
code="local_authority_projection_confirmation_invalid", payload=payload,
)
+ if projected is not None and "fields" in projected:
+ from ..todos.goal_todo_projection import restore_todo_field_evaluations
+ payload["_todo_fields"] = restore_todo_field_evaluations(projected)
return payload
@@ -340,8 +357,10 @@ def read(self, *, runtime_root: Path, goal_id: str) -> dict[str, Any] | None:
key = (runtime_root.expanduser().resolve(strict=False), goal_id)
if key not in self._reads:
try:
+ from ..todos.resume_condition import TodoResumeRolloutEvents
self._reads[key] = read_canonical_todos_if_promoted(
- runtime_root=key[0], goal_id=goal_id,
+ runtime_root=key[0], goal_id=goal_id, include_summary=True,
+ rollout_events=TodoResumeRolloutEvents([], runtime_root=key[0], goal_id=goal_id),
)
except LocalCoordinationAuthorityUnavailable as error:
# A failed first read cannot become a successful attention view
@@ -369,13 +388,10 @@ def read_canonical_todo_fields_if_promoted(
read neither repairs Markdown nor grants mutation/promotion authority.
Callers pass the same fields to all decisions in one planning operation.
"""
- canonical = read_canonical_todos_if_promoted(runtime_root=runtime_root, goal_id=goal_id)
- return (
- canonical_todo_summary_fields(canonical["todos"], rollout_events=rollout_events,
- goal_acceptance_contract=canonical.get("goal_acceptance_contract"),
- goal_acceptance_work_guards=canonical.get("goal_acceptance_work_guards"))
- if canonical is not None else None
- )
+ canonical = read_canonical_todos_if_promoted(runtime_root=runtime_root, goal_id=goal_id,
+ include_summary=True, rollout_events=rollout_events)
+ return canonical["_todo_fields"] if canonical is not None else None
+
def canonical_todo_summary_fields(
@@ -386,69 +402,12 @@ def canonical_todo_summary_fields(
goal_acceptance_contract: dict[str, Any] | None = None,
goal_acceptance_work_guards: dict[str, Any] | None = None,
) -> dict[str, Any]:
- """Adapt canonical records into the existing Todo summary read model."""
+ from ..todos.goal_todo_projection import canonical_todo_fields
- from ..todos.active_state_editing import TODO_SECTION_HEADINGS
- from ..todos.standing_decision import build_standing_decision_authority
- from ..todos.todo_summary import compact_todo_group, count_advancement_todos
-
- # Read standing decisions before assigning presentation-only indexes, and
- # include retained history so archiving a revocation cannot revive approval.
- standing_authority = build_standing_decision_authority(
- [item for item in todos if item.get("role") == "user"], canonical_records=True,
- )
- archived_ids = {
- item["todo_id"]
- for item in todos
- if item.get("archive_state") == "archive"
- }
- todos = canonical_todo_items(todos)
- # These are native authority decisions, not persisted Todo fields. Keep the
- # records visible while every summary/selection uses the same work guard.
- if goal_acceptance_contract and configured_checks(goal_acceptance_contract) is not None:
- guards = goal_acceptance_work_guards or {}
- todos = [{**item, "goal_acceptance_guard": guards[item["todo_id"]]}
- if item.get("todo_id") in guards else item for item in todos]
- fields: dict[str, Any] = {}
- for role in ("user", "agent"):
- items = [
- item
- for item in todos
- if ("user" if item.get("role") == "user" else "agent") == role
- and item.get("todo_id") not in archived_ids
- ]
- summary = compact_todo_group(
- items,
- source_section=TODO_SECTION_HEADINGS[role],
- role=role,
- include_empty_source=True,
- resume_source_items=todos,
- rollout_events=rollout_events,
- available_capabilities=available_capabilities,
- item_limit=None,
- )
- if summary:
- if role == "agent":
- if goal_acceptance_contract and (configured_checks(goal_acceptance_contract) is not None or goal_acceptance_contract.get("outcome_requirements")):
- summary["goal_acceptance_contract"] = goal_acceptance_contract
- archived_done = count_advancement_todos(
- [
- item
- for item in todos
- if item.get("todo_id") in archived_ids
- and item.get("done") is True
- ]
- )
- if archived_done:
- summary["archived_advancement_done_count"] = archived_done
- summary["advancement_done_count"] = (
- int(summary.get("advancement_done_count") or 0) + archived_done
- )
- fields[f"{role}_todos"] = summary
- if role == "user":
- if standing_authority:
- fields["standing_decision_authority"] = standing_authority
- return fields
+ return canonical_todo_fields(todos, rollout_events=rollout_events,
+ available_capabilities=available_capabilities,
+ goal_acceptance_contract=goal_acceptance_contract,
+ goal_acceptance_work_guards=goal_acceptance_work_guards)
def canonical_todo_items(todos: list[dict[str, Any]]) -> list[dict[str, Any]]:
diff --git a/loopx/control_plane/effect_runtime.py b/loopx/control_plane/effect_runtime.py
index 8f4e59e89b..41f865d165 100644
--- a/loopx/control_plane/effect_runtime.py
+++ b/loopx/control_plane/effect_runtime.py
@@ -42,6 +42,7 @@
MAX_REQUEST_BYTES = 2 * 1024 * 1024
MAX_LOCAL_SNAPSHOT_BYTES = 64 * 1024 * 1024
LOCAL_SNAPSHOT_METHODS = frozenset({
+ "todo.read",
"collaboration.inbox.read",
"collaboration.delegation.inventory_page",
"benchmark.trajectory.codex-usage",
diff --git a/loopx/control_plane/effect_runtime_handlers.ts b/loopx/control_plane/effect_runtime_handlers.ts
index 7f39a1b37a..b7476c528c 100644
--- a/loopx/control_plane/effect_runtime_handlers.ts
+++ b/loopx/control_plane/effect_runtime_handlers.ts
@@ -268,13 +268,11 @@ export function createEffectRuntimeHandlers(
["turn_journal.find_settlement", lazyHandler(() => import("./turn_driver/turn_journal_query.ts"), ({findTurnJournalBySettlement}) => findTurnJournalBySettlement)],
["turn_journal.observed_capabilities", lazyHandler(() => import("./turn_driver/turn_journal_query.ts"), ({readTurnJournalCapabilities}) => readTurnJournalCapabilities)],
["todo.completion_fence.evaluate", lazyHandler(() => import("./todos/completion_fence.ts"), ({evaluateTodoCompletionFence}) => evaluateTodoCompletionFence)],
- ["todo.completion_state.normalize", lazyHandler(() => import("./todos/completion_state.ts"), ({normalizeTodoCompletionValue}) => normalizeTodoCompletionValue)],
- ["todo.completion_state.require_metadata", lazyHandler(() => import("./todos/completion_state.ts"), ({requireTodoCompletionMetadataValue}) => requireTodoCompletionMetadataValue)],
["todo.field_update.plan", lazyHandler(() => import("./todos/field_update.ts"), ({planTodoFieldUpdate}) => planTodoFieldUpdate)],
["todo.priority.plan", lazyHandler(() => import("./todos/priority.ts"), ({evaluateTodoPriority}) => evaluateTodoPriority)],
["todo.public_update.plan", lazyHandler(() => import("./todos/public_update.ts"), ({planPublicTodoUpdate}) => planPublicTodoUpdate)],
["todo.standing_decision.project", lazyHandler(() => import("./todos/standing_decision.ts"), ({evaluateStandingDecisionProjection}) => evaluateStandingDecisionProjection)],
- ["todo.summary.project", lazyHandler(() => import("./todos/summary_projection.ts"), ({projectTodoSummary}) => projectTodoSummary)],
+ ["todo.read", lazyHandler(() => import("./todos/read_transport.ts"), ({readTodoRuntime}) => readTodoRuntime)],
["capabilities.periodic_report.progress.select", lazyHandler(() => import("./capabilities/periodic_report_progress.ts"), ({selectPeriodicReportProgress}) => selectPeriodicReportProgress)],
["capabilities.periodic_report.approval_retry.select", lazyHandler(() => import("./capabilities/periodic_report_progress.ts"), ({selectPeriodicReportApprovalRetry}) => selectPeriodicReportApprovalRetry)],
["todo.succession.project", lazyHandler(() => import("./todos/succession.ts"), ({projectTodoSuccession}) => projectTodoSuccession)],
diff --git a/loopx/control_plane/effect_runtime_server.ts b/loopx/control_plane/effect_runtime_server.ts
index 6a06e308c7..54f2e767fa 100644
--- a/loopx/control_plane/effect_runtime_server.ts
+++ b/loopx/control_plane/effect_runtime_server.ts
@@ -28,6 +28,7 @@ const MAX_REQUEST_BYTES = 2 * 1024 * 1024;
const MAX_INLINE_RESPONSE_BYTES = 2 * 1024 * 1024;
// Explicit opt-in: ordinary effects retain the 2 MiB request/response wire.
const LOCAL_SNAPSHOT_METHODS = new Set([
+ "todo.read",
"collaboration.inbox.read",
"collaboration.delegation.inventory_page",
"benchmark.trajectory.codex-usage",
diff --git a/loopx/control_plane/effect_runtime_snapshot.ts b/loopx/control_plane/effect_runtime_snapshot.ts
index 94f493657b..b3bd40438a 100644
--- a/loopx/control_plane/effect_runtime_snapshot.ts
+++ b/loopx/control_plane/effect_runtime_snapshot.ts
@@ -60,6 +60,12 @@ export async function readPrivateJsonSnapshot(value: unknown): Promise
}
}
+/** A domain-owned source can exceed a wire frame; it still uses the same
+ * private-file admission. The domain verifies its full source identity. */
+export async function openPrivateSnapshotSource(path: unknown): Promise {
+ return privateFile(requireNonEmptyString(path, "source snapshot path"), constants.O_RDONLY);
+}
+
export async function openPrivateResponseSink(path: unknown): Promise {
try {
return await privateFile(requireNonEmptyString(path, "response sink"), constants.O_WRONLY);
diff --git a/loopx/control_plane/testing/cli_output_budget.py b/loopx/control_plane/testing/cli_output_budget.py
index 122c3f9f0e..8cb7837323 100644
--- a/loopx/control_plane/testing/cli_output_budget.py
+++ b/loopx/control_plane/testing/cli_output_budget.py
@@ -327,7 +327,7 @@ class CliOutputCommandClassification:
"role/status filters, full per-role sections without --limit, and "
"direct todo-id lifecycle commands"
),
- semantic_json_keys=("todos", "agent_todos", "filter_semantics"),
+ semantic_json_keys=("todos", "agent_todos", "filter_semantics", "projection_envelope"),
markdown_anchor="# LoopX Todo List",
max_chars={
"small": {"json": 7_500, "markdown": 1_000},
@@ -573,6 +573,7 @@ class CliOutputCommandClassification:
"agent_todos",
"explicit_limit",
"todo_list_projection",
+ "projection_envelope",
),
markdown_anchor="# LoopX Todo List",
max_chars={"json": 40_000, "markdown": 1_200},
@@ -591,6 +592,7 @@ class CliOutputCommandClassification:
"returned_todo_count",
"omitted_todo_count",
"todo_list_field_projection",
+ "projection_envelope",
),
markdown_anchor="# LoopX Todo List",
max_chars={"json": 20_000, "markdown": 6_000},
diff --git a/loopx/control_plane/testing/cli_output_differential.py b/loopx/control_plane/testing/cli_output_differential.py
index 80e51e1648..742af46b1e 100644
--- a/loopx/control_plane/testing/cli_output_differential.py
+++ b/loopx/control_plane/testing/cli_output_differential.py
@@ -685,6 +685,11 @@ def _schema_migration_growth_allowance(
json={"chars": 3_000, "utf8_bytes": 3_000, "lines": 110, "compact_payload_chars": 2_800},
markdown={"chars": 192, "utf8_bytes": 224, "lines": 3, "compact_payload_chars": 0},
)
+# The unchanged 18-row Todo matrix adds 783–854 JSON chars and no lines.
+# Source freshness and omitted-work coverage are useful; Markdown is unchanged.
+_TODO_PROJECTION_ENVELOPE_V0_MIGRATION_ALLOWANCE: dict[Metric, int] = {
+ "chars": 896, "utf8_bytes": 896, "lines": 0, "compact_payload_chars": 896,
+}
def _projection_envelope_migration(
@@ -696,6 +701,13 @@ def _projection_envelope_migration(
if before == after:
return {}, [], []
surface_id = str(base.get("surface_id") or "")
+ row_surface = str(base.get("row_id") or "").partition("/")[2].partition("/")[0]
+ if (surface_id == "todo_list" and row_surface in {
+ "todo_list", "todo_list_limited", "todo_list_thin"}
+ and output_format == "json" and before == ()
+ and after == ("loopx_projection_envelope_v0",)):
+ return _TODO_PROJECTION_ENVELOPE_V0_MIGRATION_ALLOWANCE, [], [
+ "Todo projection envelope schema migrated: none -> loopx_projection_envelope_v0"]
if (surface_id in {"status", "quota_should_run"}
and before == () and after == ("loopx_projection_envelope_v0",)):
migration_allowance = (
diff --git a/loopx/control_plane/todos/active_state_todo_parser.py b/loopx/control_plane/todos/active_state_todo_parser.py
index ab43f928b0..8847859565 100644
--- a/loopx/control_plane/todos/active_state_todo_parser.py
+++ b/loopx/control_plane/todos/active_state_todo_parser.py
@@ -9,14 +9,10 @@
normalize_todo_id,
TODO_TASK_PATTERN,
)
-from .standing_decision import build_standing_decision_authority
from .machine_region import find_todo_source_regions, visible_markdown_lines
from .todo_block_codec import decode_todo_blocks
from .todo_summary import (
MAX_STATUS_TODOS_PER_ROLE,
- compact_todo_group,
- count_advancement_todos,
- normalize_todo_text,
)
from ..runtime.time import now_utc_iso
@@ -77,61 +73,20 @@ def parse_active_state_todos(
and int(orchestration.get("max_children") or 0) > 0
)
items, archive_items, source_sections = parse_todo_source(state_text, goal=goal, state_path=state_path)
- result: dict[str, Any] = {}
- archived_resume_source_items = [
- item for item in archive_items if normalize_todo_id(item.get("todo_id"))
- ]
- resume_source_items = [*items["user"], *items["agent"], *archived_resume_source_items]
- user = compact_todo_group(
- items["user"],
- source_section=source_sections["user"],
- role="user",
- include_empty_source=source_sections["user"] is not None,
- preferred_todo_ids=preferred_todo_ids,
- resume_source_items=resume_source_items,
- rollout_events=rollout_events,
- available_capabilities=available_capabilities,
- item_limit=item_limit,
- include_task_orchestration_authority=include_task_orchestration_authority,
- evaluated_at=resume_evaluated_at,
- )
- agent = compact_todo_group(
- items["agent"],
- source_section=source_sections["agent"],
- role="agent",
- include_empty_source=source_sections["agent"] is not None,
- preferred_todo_ids=preferred_todo_ids,
- resume_source_items=resume_source_items,
- rollout_events=rollout_events,
- available_capabilities=available_capabilities,
- item_limit=item_limit,
- include_task_orchestration_authority=include_task_orchestration_authority,
- vision_runs=(goal or {}).get("latest_runs"),
- evaluated_at=resume_evaluated_at,
- )
- archived_advancement_done_count = count_advancement_todos(
- # Lossless source text must not widen legacy attention classification.
- [
- {**item, "text": normalize_todo_text(str(item.get("text") or ""))}
- for item in archive_items if item.get("done") is True
- ]
- )
- if agent and archived_advancement_done_count:
- agent["archived_advancement_done_count"] = archived_advancement_done_count
- agent["advancement_done_count"] = (
- int(agent.get("advancement_done_count") or 0)
- + archived_advancement_done_count
- )
- if user:
- result["user_todos"] = user
- if agent:
- result["agent_todos"] = agent
+ from .goal_todo_projection import source_todo_fields
+ from .standing_decision import standing_decision_source_items
+
+ lineage = [*items["user"], *items["agent"],
+ *[item for item in archive_items if normalize_todo_id(item.get("todo_id"))]]
archived_decisions = [item for item in archive_items if item.get("role") == "user"]
- standing_authority = build_standing_decision_authority(
- [*items["user"], *archived_decisions],
- # Separate sections are not a single chronological append log.
+ result = source_todo_fields([
+ {"role": role, "source_section": source_sections[role], "items": items[role],
+ "include_empty_source": source_sections[role] is not None} for role in ("user", "agent")],
+ lineage=lineage, rollout_events=rollout_events, available_capabilities=available_capabilities,
+ preferred_todo_ids=preferred_todo_ids, item_limit=item_limit, evaluated_at=resume_evaluated_at,
+ archived_items=archive_items,
+ standing_items=standing_decision_source_items([*items["user"], *archived_decisions]),
legacy_source_order=not archived_decisions,
- )
- if standing_authority:
- result["standing_decision_authority"] = standing_authority
+ include_task_orchestration_authority=include_task_orchestration_authority,
+ vision_runs=(goal or {}).get("latest_runs"))
return result
diff --git a/loopx/control_plane/todos/active_state_todos.py b/loopx/control_plane/todos/active_state_todos.py
index 5f92a1990e..f561cf44a2 100644
--- a/loopx/control_plane/todos/active_state_todos.py
+++ b/loopx/control_plane/todos/active_state_todos.py
@@ -6,7 +6,6 @@
from ..coordination.local_authority import (
CanonicalTodoSnapshot,
- canonical_todo_summary_fields,
read_canonical_todos_if_promoted,
)
from ..goals.active_state_metadata import parse_state_frontmatter
@@ -91,11 +90,11 @@ def active_state_todo_fields(
goal = {**goal, **source_goal}
# Inspect authority before the display file. A missing/stale projection is
# not an empty Todo collection, and an unavailable provider must fail closed.
- canonical_reader = todo_snapshot.read if todo_snapshot is not None else read_canonical_todos_if_promoted
- canonical = (
- canonical_reader(runtime_root=runtime_root, goal_id=goal_id)
- if runtime_root is not None and goal_id else None
- )
+ canonical = (None if runtime_root is None or not goal_id else
+ todo_snapshot.read(runtime_root=runtime_root, goal_id=goal_id) if todo_snapshot is not None else
+ read_canonical_todos_if_promoted(runtime_root=runtime_root, goal_id=goal_id,
+ include_summary=True, rollout_events=TodoResumeRolloutEvents(
+ rollout_events or [], runtime_root=runtime_root, goal_id=goal_id)))
state_path = resolve_goal_local_path(goal.get("state_file"), goal, fallback_base=Path.cwd())
if canonical is None and state_path is not None:
from ..goals.legacy_event_source import require_no_legacy_todo_events
@@ -128,17 +127,9 @@ def active_state_todo_fields(
if runtime_root is not None and goal_id:
events = TodoResumeRolloutEvents(events, runtime_root=runtime_root, goal_id=goal_id)
if canonical is not None:
- from ..goals.acceptance import observe_canonical_goal_acceptance
- acceptance = observe_canonical_goal_acceptance(canonical,
+ from .goal_todo_projection import observe_todo_acceptance
+ fields = observe_todo_acceptance(canonical["_todo_fields"], canonical,
registry_path=source_registry, goal_id=goal_id)
- fields = canonical_todo_summary_fields(
- canonical["todos"],
- rollout_events=events,
- goal_acceptance_contract=acceptance,
- goal_acceptance_work_guards=canonical.get(
- "goal_acceptance_work_guards"
- ),
- )
# Canonical observation/successor transactions now support current
# lease proof. Scheduling exposes due work; mutation admission still
# validates the caller's proof and never falls back to the old writer.
diff --git a/loopx/control_plane/todos/completion_state.py b/loopx/control_plane/todos/completion_state.py
index 9337087b4d..57b65d918a 100644
--- a/loopx/control_plane/todos/completion_state.py
+++ b/loopx/control_plane/todos/completion_state.py
@@ -4,13 +4,15 @@
from enum import Enum
from functools import lru_cache
-from typing import Any, Mapping
+import json
+from pathlib import Path
+from typing import Any
-from ..effect_runtime import EffectRuntimeRejected, effect_runtime_result
TODO_COMPLETION_STATE_REQUEST_SCHEMA = "loopx_todo_completion_state_request_v0"
TODO_COMPLETION_STATE_RESULT_SCHEMA = "loopx_todo_completion_state_result_v0"
+_VALUE_CODEC = json.loads(Path(__file__).with_name("completion_value_codec.json").read_text(encoding="utf-8"))
class TodoCompletionContinuation(str, Enum):
@@ -36,37 +38,22 @@ def _no_followup_value(value: Any) -> bool | str:
return value if isinstance(value, bool) else _string_value(value)
-def _result(method: str, params: dict[str, Any]) -> Mapping[str, Any]:
- try:
- result = effect_runtime_result(method, params)
- except EffectRuntimeRejected as exc:
- raise ValueError(str(exc)) from None
- if not isinstance(result, Mapping):
- raise RuntimeError("TypeScript Todo completion state result must be an object")
- if result.get("schema_version") != TODO_COMPLETION_STATE_RESULT_SCHEMA:
- raise RuntimeError("TypeScript Todo completion state result shape mismatch")
- return result
@lru_cache(maxsize=64)
def _normalize_cached(kind: str, value: bool | str) -> bool | str | None:
- result = _result(
- "todo.completion_state.normalize",
- {
- "schema_version": TODO_COMPLETION_STATE_REQUEST_SCHEMA,
- "kind": kind,
- "value": value,
- },
- )
- normalized = result.get("value")
if kind == "no_followup":
- if normalized is not None and not isinstance(normalized, bool):
- raise RuntimeError("TypeScript no_followup normalization shape mismatch")
- elif normalized is not None and not isinstance(normalized, str):
- raise RuntimeError(
- "TypeScript completion metadata normalization shape mismatch"
- )
- return normalized
+ if isinstance(value, bool):
+ return value
+ candidate = value.strip(_VALUE_CODEC["ecmascript_trim"]).lower()
+ if candidate in _VALUE_CODEC["true_tokens"]:
+ return True
+ if candidate in _VALUE_CODEC["false_tokens"]:
+ return False
+ return None
+ candidate = str(value).strip(_VALUE_CODEC["ecmascript_trim"]).lower()
+ vocabulary = TodoCompletionContinuation if kind == "continuation" else TodoCompletionRecovery
+ return candidate if candidate in {item.value for item in vocabulary} else None
def normalize_todo_no_followup(value: Any) -> bool | None:
@@ -85,15 +72,13 @@ def normalize_todo_completion_recovery(value: Any) -> str | None:
def require_todo_completion_metadata(key: str, value: Any) -> str | None:
- result = _result(
- "todo.completion_state.require_metadata",
- {
- "schema_version": TODO_COMPLETION_STATE_REQUEST_SCHEMA,
- "key": key,
- "value": _string_value(value),
- },
- )
- normalized = result.get("value")
- if normalized is not None and not isinstance(normalized, str):
- raise RuntimeError("TypeScript completion metadata result shape mismatch")
+ raw = _string_value(value)
+ if key == "completion_continuation":
+ normalized = _normalize_cached("continuation", raw)
+ if normalized is None:
+ raise ValueError("completion_continuation must be one of: active_goal, successor, no_followup")
+ else:
+ normalized = _normalize_cached("recovery", raw)
+ if normalized is None and raw != "":
+ raise ValueError("completion_recovery must be same_turn_terminal_closeout or lifecycle_reentry_terminal_closeout")
return normalized
diff --git a/loopx/control_plane/todos/completion_state.ts b/loopx/control_plane/todos/completion_state.ts
index e2271ddc15..663902c0f1 100644
--- a/loopx/control_plane/todos/completion_state.ts
+++ b/loopx/control_plane/todos/completion_state.ts
@@ -1,4 +1,5 @@
import type { JsonObject } from "../effect_program.ts";
+import valueCodec from "./completion_value_codec.json" with {type: "json"};
import { EffectRuntimeRequestError } from "../effect_runtime_errors.ts";
import {
requireBoolean as requiredBoolean,
@@ -64,10 +65,10 @@ export function normalizeTodoNoFollowup(value: unknown): boolean | null {
throw new EffectRuntimeRequestError("no_followup must be a boolean, string, or null");
}
const candidate = value.trim().toLowerCase();
- if (["1", "true", "yes", "y", "no_followup", "no-followup"].includes(candidate)) {
+ if (valueCodec.true_tokens.includes(candidate)) {
return true;
}
- if (["0", "false", "no", "n"].includes(candidate)) return false;
+ if (valueCodec.false_tokens.includes(candidate)) return false;
return null;
}
@@ -136,35 +137,6 @@ export function completionContinuationForWrite(
return "active_goal";
}
-export function normalizeTodoCompletionValue(value: unknown): JsonObject {
- const request = requestObject(value, "normalize");
- const kind = request.kind;
- if (kind !== "no_followup" && kind !== "continuation" && kind !== "recovery") {
- throw new EffectRuntimeRequestError("completion normalization kind is unsupported");
- }
- const raw = request.value;
- const normalized = kind === "no_followup"
- ? normalizeTodoNoFollowup(raw)
- : kind === "continuation"
- ? normalizeTodoCompletionContinuation(raw)
- : normalizeTodoCompletionRecovery(raw);
- return {
- schema_version: TODO_COMPLETION_STATE_RESULT_SCHEMA,
- kind,
- value: normalized,
- };
-}
-
-export function requireTodoCompletionMetadataValue(value: unknown): JsonObject {
- const request = requestObject(value, "require_metadata");
- const key = optionalString(request.key, "key");
- if (key === null) throw new EffectRuntimeRequestError("key must be a string");
- return {
- schema_version: TODO_COMPLETION_STATE_RESULT_SCHEMA,
- value: requireTodoCompletionMetadata(key, request.value),
- };
-}
-
export function selectTodoCompletionState(value: unknown): TodoCompletionStateResult {
const request = requestObject(value, "evaluate");
const todo = requiredObject(request.todo, "todo");
diff --git a/loopx/control_plane/todos/completion_value_codec.json b/loopx/control_plane/todos/completion_value_codec.json
new file mode 100644
index 0000000000..efdf9eb246
--- /dev/null
+++ b/loopx/control_plane/todos/completion_value_codec.json
@@ -0,0 +1,17 @@
+{
+ "true_tokens": [
+ "1",
+ "true",
+ "yes",
+ "y",
+ "no_followup",
+ "no-followup"
+ ],
+ "false_tokens": [
+ "0",
+ "false",
+ "no",
+ "n"
+ ],
+ "ecmascript_trim": "\t\n\u000b\f\r \u00a0\u1680\u2000\u2001\u2002\u2003\u2004\u2005\u2006\u2007\u2008\u2009\u200a\u2028\u2029\u202f\u205f\u3000\ufeff"
+}
diff --git a/loopx/control_plane/todos/context_projection.ts b/loopx/control_plane/todos/context_projection.ts
index fa9f97cdff..22beacb5e0 100644
--- a/loopx/control_plane/todos/context_projection.ts
+++ b/loopx/control_plane/todos/context_projection.ts
@@ -3,6 +3,7 @@ import type { JsonObject } from "../effect_program.ts";
import { EffectRuntimeRequestError } from "../effect_runtime_errors.ts";
import { requireBoolean, requireInteger, requireJsonObject } from "../runtime_decode.ts";
import { canonicalAuthorityJson } from "../coordination/authority_store_codec.ts";
+import {compactThinTodoRead, compactTodoReadSummary, compactThinTodoReadSummary} from "./read_presentation.ts";
// A read lens over canonical records, shared by global and Goal conversations.
// These are existing Todo facts, not dependency evaluation or execution grants.
@@ -15,6 +16,21 @@ const CONTEXT_FIELDS = [
/** Select before redaction; never convert nested scopes or booleans to prose. */
export function projectTodoContextPage(input: JsonObject): JsonObject {
+ if (input.thin_summary_payload !== undefined) {
+ const details = requireJsonObject(input.details, "thin summary details");
+ return compactThinTodoReadSummary(requireJsonObject(input.thin_summary_payload, "thin Todo summary"), {
+ role: details.role, items_matched: requireInteger(details.items_matched, "items matched"),
+ items_returned: requireInteger(details.items_returned, "items returned"),
+ item_limit_per_role: requireInteger(details.item_limit_per_role, "item limit per role")});
+ }
+ if (input.thin_payload !== undefined) {
+ if (Object.keys(input).some(key => key !== "thin_payload")) throw new EffectRuntimeRequestError("thin Todo presentation cannot be combined with other views");
+ return compactThinTodoRead(requireJsonObject(input.thin_payload, "thin Todo payload"));
+ }
+ if (input.summary_payload !== undefined) {
+ if (Object.keys(input).some(key => !["summary_payload", "role", "limit"].includes(key))) throw new EffectRuntimeRequestError("Todo summary presentation cannot be combined with other views");
+ return compactTodoReadSummary(requireJsonObject(input.summary_payload, "Todo summary payload"), String(input.role), input.limit == null ? null : Number(input.limit));
+ }
if (input.handoff_sources !== undefined) {
if (Object.keys(input).some(key => key !== "handoff_sources" && key !== "handoff_followups")) {
throw new EffectRuntimeRequestError("handoff context cannot be combined with list/detail/page inputs");
diff --git a/loopx/control_plane/todos/contract.py b/loopx/control_plane/todos/contract.py
index 24e9fd269a..f51e7c6dbc 100644
--- a/loopx/control_plane/todos/contract.py
+++ b/loopx/control_plane/todos/contract.py
@@ -1,6 +1,8 @@
from __future__ import annotations
import hashlib
+import json
+from pathlib import Path
import re
from dataclasses import dataclass
from enum import Enum
@@ -28,6 +30,8 @@
)
+_READ_LEGACY_CODEC = json.loads(Path(__file__).with_name("read_legacy_codec.json").read_text(encoding="utf-8"))
+
TODO_TASK_PATTERN = re.compile(r"^\s*[-*]\s+\[([ xX-])\]\s+(.+?)\s*$")
TODO_METADATA_PATTERN = re.compile(r"^\s*\s*$")
TODO_METADATA_TOKEN_PATTERN = re.compile(
@@ -138,32 +142,8 @@ def resolve_next_user_task_class(
TODO_TERMINAL_STATUS_VALUES = {TODO_STATUS_DONE, TODO_STATUS_DEFERRED}
TODO_LEGACY_TERMINAL_STATUS_VALUES = {"completed", "closed", "archived"}
-TODO_ACTION_KIND_ADVANCEMENT_VALUES = {
- "advance",
- "analyze",
- "benchmark_run",
- "codex_run",
- "compact_blocker_writeback",
- "compare",
- "execute",
- "fix",
- "implement",
- "rebuild",
- "rebuild_score",
- "repair",
- "run",
- "run_eval",
- "test",
- "validate",
- "writeback",
-}
-TODO_ACTION_KIND_MONITOR_VALUES = {
- "external_evidence",
- "monitor",
- "observe",
- "poll",
- "watch",
-}
+TODO_ACTION_KIND_ADVANCEMENT_VALUES = set(_READ_LEGACY_CODEC["advancement_actions"])
+TODO_ACTION_KIND_MONITOR_VALUES = set(_READ_LEGACY_CODEC["monitor_actions"])
class TodoContinuationPolicy(str, Enum):
@@ -180,44 +160,12 @@ class TodoContinuationPolicy(str, Enum):
}
-TODO_HARD_MONITOR_PATTERNS = (
- re.compile(r"(?i)\bdo not\b.*\b(?:launch|run|execute|start)\b.*\buntil\b"),
- re.compile(r"(?i)\b(?:only|just)\b.*\b(?:after|when|once)\b.*\b(?:owner|user|credential|approval|prerequisite|evidence)\b"),
- re.compile(r"(?i)\b(?:credential|gcp|gcs|gcp_project|gcp_sa_key|gs://)\b.*\b(?:missing|required|provide|proof|prerequisite|gate|gated)\b"),
- re.compile(r"(?i)\b(?:readiness|proof)\b.*\bbefore any formal\b.*\brun\b"),
- re.compile(r"(?i)\bremaining formal\b.*\bpath\b"),
- re.compile(r"(?i)\b(?:route|input)\b.*\babsent\b"),
- re.compile(r"(?i)\b0\b.*\b(?:candidate|candidates)\b"),
-)
+TODO_HARD_MONITOR_PATTERNS = tuple(re.compile(pattern) for pattern in _READ_LEGACY_CODEC["TODO_HARD_MONITOR_PATTERNS"])
-TODO_ADVANCEMENT_OVERRIDE_PATTERNS = (
- re.compile(
- r"(?i)(?:^|[::]\s*)(?:implement|add|make|fix|build|wire|define|compare|run|"
- r"execute|test|validate|rebuild|repair|archive|publish|merge|write|attribute|"
- r"collect|aggregate|generate|produce|materialize|rerun|repeat|eval|evaluate|score)\b"
- ),
- re.compile(
- r"(?i)\b(?:implementation slice|validation-backed patch|smoke fixture|"
- r"regression suite|readiness scan|source preflight|setup-readiness scan)\b"
- ),
-)
-TODO_BLOCKED_MONITOR_PATTERNS = (
- *TODO_HARD_MONITOR_PATTERNS,
- re.compile(r"(?i)\b(?:blocked|gated|waiting)\b.*\b(?:owner|user|credential|substrate|proof|prerequisite|evidence)\b"),
-)
-TODO_MONITOR_PATTERNS = (
- re.compile(r"(?i)\bdependency monitor\b"),
- re.compile(r"(?i)\bobservation lane\b"),
- re.compile(r"(?i)(?:^|[::]\s*)observe\b"),
- re.compile(r"(?i)(?:^|[::]\s*)poll\b"),
- re.compile(r"(?i)(?:^|[::]\s*)watch\b"),
- re.compile(r"(?i)\bmonitor-only\b"),
- *TODO_BLOCKED_MONITOR_PATTERNS,
-)
-TODO_ADVANCEMENT_PATTERNS = (
- *TODO_ADVANCEMENT_OVERRIDE_PATTERNS,
- re.compile(r"(?i)\b(?:task|validation hypothesis|validation step|bounded step|learning run)\b"),
-)
+TODO_ADVANCEMENT_OVERRIDE_PATTERNS = tuple(re.compile(pattern) for pattern in _READ_LEGACY_CODEC["TODO_ADVANCEMENT_OVERRIDE_PATTERNS"])
+TODO_BLOCKED_MONITOR_PATTERNS = tuple(re.compile(pattern) for pattern in _READ_LEGACY_CODEC["TODO_BLOCKED_MONITOR_PATTERNS"])
+TODO_MONITOR_PATTERNS = tuple(re.compile(pattern) for pattern in _READ_LEGACY_CODEC["TODO_MONITOR_PATTERNS"])
+TODO_ADVANCEMENT_PATTERNS = tuple(re.compile(pattern) for pattern in _READ_LEGACY_CODEC["TODO_ADVANCEMENT_PATTERNS"])
NEXT_ACTION_HARD_MONITOR_PATTERNS = (
re.compile(r"(?i)\bdo not\b.*\b(?:launch|run|execute|start)\b.*\buntil\b"),
diff --git a/loopx/control_plane/todos/frontier_revision.py b/loopx/control_plane/todos/frontier_revision.py
index ac3b8f061f..2b29d51485 100644
--- a/loopx/control_plane/todos/frontier_revision.py
+++ b/loopx/control_plane/todos/frontier_revision.py
@@ -5,6 +5,7 @@
import base64
import json
import zlib
+from pathlib import Path
from typing import Any
# Refs #4447: the todo contract owns this vocabulary; import it instead of
@@ -20,39 +21,7 @@
TODO_FRONTIER_REVISION_SCHEMA_VERSION = "todo_frontier_revision_v0"
TODO_FRONTIER_REVISION_INDEX_SCHEMA_VERSION = "todo_frontier_revision_index_v0"
-FRONTIER_REVISION_FIELDS = (
- "todo_id",
- "status",
- "done",
- "title",
- "text",
- "task_class",
- "claimed_by",
- "bound_agent",
- "blocks_agent",
- "excluded_agents",
- "priority",
- "action_kind",
- "task_domain",
- "task_repository",
- "capability_binding_ref",
- "required_capabilities",
- "target_capabilities",
- "target_key",
- "continuation_policy",
- "removed_continuation_policy",
- "decision_scope",
- "required_decision_scopes",
- "decision_outcome",
- "replan_obligation_id",
- "unblocks_todo_id",
- "depends_on_todo_id",
- "depends_on_todo_ids",
- "resume_when",
- "no_followup",
- "successor_todo_ids",
- "completion_continuation",
-)
+FRONTIER_REVISION_FIELDS = tuple(json.loads(Path(__file__).with_name("read_legacy_codec.json").read_text(encoding="utf-8"))["frontier_fields"])
def frontier_source_facts(
diff --git a/loopx/control_plane/todos/goal_todo_projection.py b/loopx/control_plane/todos/goal_todo_projection.py
index 531564b21f..5939cd8c7a 100644
--- a/loopx/control_plane/todos/goal_todo_projection.py
+++ b/loopx/control_plane/todos/goal_todo_projection.py
@@ -14,15 +14,12 @@
from ..goals.legacy_event_source import require_no_legacy_todo_events
from .active_state_editing import TODO_SECTION_HEADINGS
-from .active_state_todo_parser import parse_active_state_todos, parse_todo_source
-from .list_projection import compact_explicit_limit_todo_summary
-from .succession_warning import public_todo_summary
from .contract import (
normalize_todo_claimed_by,
normalize_todo_id,
normalize_todo_status,
)
-from .todo_summary import compact_evaluated_todo_group, compact_todo_group
+from .todo_summary import compact_evaluated_todo_group
def empty_todo_summary(*, role: str) -> dict[str, Any]:
@@ -69,6 +66,17 @@ def summary_items(fields: dict[str, Any], role: str) -> list[dict[str, Any]]:
return [item for item in summary.get("items") or [] if isinstance(item, dict)]
+def observe_todo_acceptance(fields, canonical, *, registry_path, goal_id):
+ from ..goals.acceptance import observe_canonical_goal_acceptance
+
+ acceptance = observe_canonical_goal_acceptance(
+ canonical, registry_path=registry_path, goal_id=goal_id)
+ summary = fields.get("agent_todos")
+ if isinstance(summary, dict) and "goal_acceptance_contract" in summary:
+ return {**fields, "agent_todos": {**summary, "goal_acceptance_contract": acceptance}}
+ return fields
+
+
class GoalTodoSummaries:
"""Role summaries and todo items projected from one active-state text."""
@@ -108,37 +116,19 @@ def goal_todo_summaries(
limit: int | None,
include_retained: bool = False,
) -> GoalTodoSummaries:
- """Project todo summaries from active-state text plus its event projection.
-
- The text is a parameter rather than a file read so a writer that still
- holds the state-file lock can project the exact bytes it is about to
- commit; ``list_goal_todos`` passes the on-disk text.
- """
+ from ..runtime.time import now_utc
require_no_legacy_todo_events(goal or {}, state_path=state_path)
- if include_retained:
- active, archived, _sections = parse_todo_source(state_text, goal=goal, state_path=state_path)
- markdown_fields = retained_todo_summary_fields(
- [*active["user"], *active["agent"], *archived], rollout_events=rollout_events,
- )
- else:
- markdown_fields = parse_active_state_todos(
- state_text,
- goal=goal,
- state_path=state_path,
- item_limit=None,
- rollout_events=rollout_events,
- )
- return todo_summaries_from_fields(
- fields=markdown_fields,
- source="markdown_active_state",
- rollout_events=rollout_events,
- roles=roles,
- status=status,
- todo_id=todo_id,
- agent_id=agent_id,
- limit=limit,
- )
+ facts = prepare_todo_source_text(goal, state_text, state_path, todo_id=todo_id, retained=include_retained)
+ now = now_utc()
+ projected = project_todo_read({"kind": "source_fields", **facts,
+ "rollout_events": list(rollout_events), "rollout_event_source": getattr(rollout_events, "source", None),
+ "observed_at": now.timestamp(), "evaluated_at": now.isoformat(),
+ "projection_query": {"roles": roles, "status": normalize_todo_status(status),
+ "todo_id": normalize_todo_id(todo_id) if todo_id else None,
+ "agent_id": normalize_todo_claimed_by(agent_id) if agent_id else None,
+ "limit": limit, "source_name": "markdown_active_state"}})["projection"]
+ return GoalTodoSummaries(**projected)
def todo_summaries_from_fields(
@@ -152,47 +142,18 @@ def todo_summaries_from_fields(
agent_id: str | None,
limit: int | None,
) -> GoalTodoSummaries:
- """Apply the shared Todo consumer semantics to an authority read model."""
-
- summaries: dict[str, dict[str, Any]] = {}
- todos: list[dict[str, Any]] = []
- unfiltered_count = 0
- uncapped_todo_count = 0
- for item_role in roles:
- key = f"{item_role}_todos"
- raw_summary = fields.get(key) if isinstance(fields, dict) else None
- unfiltered_count += len((raw_summary or {}).get("items") or [])
- summary = filtered_todo_summary(
- raw_summary,
- role=item_role,
- status=status,
- todo_id=todo_id,
- agent_id=agent_id,
- item_limit=limit,
- )
- # Rebuilding a display group must not discard its canonical acceptance
- # source. Receipt-bound quota reentry consumes this provider-first list;
- # work guards without their owner contract cannot reconstruct a hold.
- acceptance = (raw_summary or {}).get("goal_acceptance_contract")
- if isinstance(acceptance, dict):
- summary["goal_acceptance_contract"] = acceptance
- if limit is not None:
- summary = compact_explicit_limit_todo_summary(
- summary,
- role=item_role,
- item_limit=limit,
- )
- summary = public_todo_summary(summary)
- summaries[key] = summary
- todos.extend(summary.get("items") or [])
- uncapped_todo_count += int(summary.get("total_count") or 0)
- return GoalTodoSummaries(
- source=source,
- summaries=summaries,
- todos=todos,
- unfiltered_count=unfiltered_count,
- uncapped_todo_count=uncapped_todo_count,
- )
+ from ..runtime.time import now_utc
+
+ now = now_utc()
+ result = project_todo_read({"kind": "select", "fields": fields,
+ "field_evaluations": {f"{item_role}_todos": [getattr(item, "succession_evaluation", None)
+ for item in (fields.get(f"{item_role}_todos") or {}).get("items") or []] for item_role in roles},
+ "roles": roles, "status": normalize_todo_status(status),
+ "todo_id": normalize_todo_id(todo_id) if todo_id else None,
+ "agent_id": normalize_todo_claimed_by(agent_id) if agent_id else None,
+ "limit": limit, "source_name": source,
+ "observed_at": now.timestamp(), "evaluated_at": now.isoformat()})["projection"]
+ return GoalTodoSummaries(**result)
def retained_todo_summary_fields(
@@ -200,25 +161,12 @@ def retained_todo_summary_fields(
*,
rollout_events: list[dict[str, Any]],
) -> dict[str, Any]:
- """Feed retained source rows through the existing typed summary owner.
- History is an explicit cold read; active scheduler/attention summaries
- continue to exclude archives. Preserve full text, evidence and source ids.
- """
- return {
- f"{role}_todos": compact_todo_group(
- [item for item in items if item.get("role") == role],
- source_section=TODO_SECTION_HEADINGS[role],
- role=role,
- include_empty_source=True,
- resume_source_items=items,
- rollout_events=rollout_events,
- item_limit=None,
- text_limit=None,
- include_content_revision=False,
- )
- for role in ("user", "agent")
- }
+ return source_todo_fields([
+ {"role": role, "source_section": TODO_SECTION_HEADINGS[role],
+ "items": [item for item in items if item.get("role") == role],
+ "include_empty_source": True, "text_limit": None, "include_content_revision": False}
+ for role in ("user", "agent")], lineage=items, rollout_events=rollout_events)
def exact_archived_todo_summaries(
@@ -232,43 +180,20 @@ def exact_archived_todo_summaries(
agent_id: str | None,
limit: int | None,
) -> GoalTodoSummaries | None:
- """Project one exact retained Todo without widening normal active lists."""
-
- item = next(
- (
- dict(candidate)
- for candidate in archived_items
- if normalize_todo_id(candidate.get("todo_id")) == todo_id
- and candidate.get("archive_state") == "archive"
- ),
- None,
- )
- if item is None:
- return None
- item_role = item.get("role")
- if item_role not in {"user", "agent"} or item_role not in roles:
- return None
- summary = compact_todo_group(
- [item],
- source_section=str(item.get("source_section") or "Completed Work Archive"),
- role=item_role,
- include_empty_source=True,
- resume_source_items=archived_items,
- rollout_events=rollout_events,
- item_limit=None,
- )
- if summary is None:
- return None
- return todo_summaries_from_fields(
- fields={f"{item_role}_todos": summary},
- source=source,
- rollout_events=rollout_events,
- roles=roles,
- status=status,
- todo_id=todo_id,
- agent_id=agent_id,
- limit=limit,
- )
+ from ..runtime.time import now_utc
+
+ lineage, facts = legacy_lineage_facts(archived_items)
+ groups = [{"role": role, "source_section": "Completed Work Archive", "include_empty_source": True,
+ "items": [row for row in archived_items if row.get("role") == role]} for role in ("user", "agent")]
+ now = now_utc()
+ projected = project_todo_read({"kind": "source_fields", "groups": prepare_todo_source_groups(groups, lineage=archived_items),
+ "lineage": lineage, "lineage_facts": facts, "rollout_events": list(rollout_events),
+ "rollout_event_source": getattr(rollout_events, "source", None),
+ "observed_at": now.timestamp(), "evaluated_at": now.isoformat(),
+ "projection_query": {"exact_archive": True, "roles": roles, "status": normalize_todo_status(status),
+ "todo_id": normalize_todo_id(todo_id), "agent_id": normalize_todo_claimed_by(agent_id) if agent_id else None,
+ "limit": limit, "source_name": source}})["projection"]
+ return GoalTodoSummaries(**projected) if projected is not None else None
def project_goal_todo_items(
goal: dict[str, Any] | None,
@@ -306,3 +231,234 @@ def project_goal_todo_items(
"summary_items",
"todo_summaries_from_fields",
]
+
+
+
+def project_todo_read(params: dict[str, Any]) -> dict[str, Any]:
+ from ..effect_runtime import EffectRuntimeRejected, effect_runtime_result
+
+ import base64
+ import hashlib
+ import json
+ import os
+ import tempfile
+ from contextlib import ExitStack
+ from pathlib import Path
+ from ..effect_runtime import MAX_LOCAL_SNAPSHOT_BYTES
+
+ request = {"schema_version": "todo_read_request_v0", **params}
+ encoded = json.dumps(request, ensure_ascii=False, separators=(",", ":"), allow_nan=False).encode("utf-8")
+ chunks = []
+ identity = None
+ try:
+ with ExitStack() as stack:
+ if len(encoded) > MAX_LOCAL_SNAPSHOT_BYTES - 4096:
+ directory = Path(stack.enter_context(tempfile.TemporaryDirectory(prefix="loopx-effect-todo-")))
+ source = directory / "facts.json"
+ fd = os.open(source, os.O_WRONLY | os.O_CREAT | os.O_EXCL, 0o600)
+ with os.fdopen(fd, "wb") as file:
+ file.write(encoded)
+ request = {"schema_version": "todo_read_request_v0", "facts_ref": {
+ "path": str(source), "byte_count": len(encoded), "sha256": hashlib.sha256(encoded).hexdigest()}}
+ consumed = 0
+ while True:
+ result = effect_runtime_result("todo.read", request, large_local_snapshot=True,
+ timeout=15.0 if params.get("kind") in {"query", "canonical_fields", "historical_fields"} else 10.0)
+ if not isinstance(result, dict) or result.get("schema_version") != "todo_read_result_v0":
+ raise ValueError("invalid typed Todo read result")
+ piece = result.get("result_piece")
+ if piece is None:
+ if chunks and not (result.get("canonical") or result.get("rejection")):
+ raise ValueError("incomplete Todo read result")
+ break
+ if not isinstance(piece, dict) or piece.get("offset") != consumed:
+ raise ValueError("invalid Todo read result page")
+ current = (piece.get("sha256"), piece.get("byte_count"))
+ if identity is None:
+ identity = current
+ if identity != current or type(current[1]) is not int or current[1] <= consumed:
+ raise ValueError("Todo read result identity changed")
+ block = base64.b64decode(piece["data"], validate=True)
+ if not block:
+ raise ValueError("Todo read result page did not advance")
+ chunks.append(block)
+ consumed += len(block)
+ if consumed > current[1]:
+ raise ValueError("Todo read result exceeds its declared population")
+ after = piece.get("next")
+ if after is None:
+ complete = b"".join(chunks)
+ if consumed != current[1] or hashlib.sha256(complete).hexdigest() != current[0]:
+ raise ValueError("Todo read result digest mismatch")
+ result = json.loads(complete)
+ break
+ if not isinstance(after, dict) or after.get("offset") != consumed or after.get("sha256") != current[0]:
+ raise ValueError("invalid Todo read result continuation")
+ request = {**request, "result_page": after}
+ except EffectRuntimeRejected as exc:
+ raise ValueError(str(exc)) from exc
+ if not isinstance(result, dict) or result.get("schema_version") != "todo_read_result_v0":
+ raise ValueError("invalid typed Todo read result")
+ if result.get("rejection"):
+ raise EffectRuntimeRejected(result["rejection"]["message"])
+ if params.get("kind") == "group":
+ summary = result.get("summary")
+ if not isinstance(summary, dict) or summary.get("schema_version") != "todo_summary_v0":
+ raise ValueError("invalid typed Todo summary fields")
+ if params.get("role") == "agent":
+ frontier = summary.get("advancement_frontier_revision_index")
+ if not isinstance(frontier, dict) or frontier.get("schema_version") != "todo_frontier_revision_index_v0":
+ raise ValueError("invalid typed Todo summary frontier index")
+ return result
+
+
+def restore_todo_field_evaluations(result):
+ from .succession_warning import _EvaluatedTodo
+
+ fields = result["fields"]
+ for role, decisions in result["field_evaluations"].items():
+ fields[role]["items"] = [_EvaluatedTodo(item, decision)
+ for item, decision in zip(fields[role]["items"], decisions, strict=True)]
+ return fields
+
+
+def canonical_todo_fields(todos, *, rollout_events=None, **facts):
+ from ..runtime.time import now_utc
+
+ now = now_utc()
+ if facts.get("available_capabilities") is not None:
+ facts["available_capabilities"] = sorted({str(value).strip().lower()
+ for value in facts["available_capabilities"] if str(value).strip()})
+ return restore_todo_field_evaluations(project_todo_read({
+ "kind": "fields", "todos": todos, **facts,
+ "rollout_events": list(rollout_events or []),
+ "rollout_event_source": getattr(rollout_events, "source", None),
+ "observed_at": now.timestamp(), "evaluated_at": now.isoformat(),
+ }))
+
+
+def source_todo_fields(groups, *, lineage, rollout_events=None, available_capabilities=None,
+ preferred_todo_ids=None, item_limit=None, evaluated_at=None,
+ archived_items=None, standing_items=None, legacy_source_order=False,
+ vision_runs=None, include_task_orchestration_authority=False):
+ from ..runtime.time import now_utc
+
+ now = now_utc()
+ prepared = prepare_todo_source_groups(groups, lineage=lineage)
+ lineage_rows, lineage_facts = legacy_lineage_facts(lineage)
+ result = project_todo_read({"kind": "source_fields", "groups": prepared,
+ "lineage": lineage_rows, "lineage_facts": lineage_facts, "rollout_events": list(rollout_events or []),
+ "rollout_event_source": getattr(rollout_events, "source", None),
+ "available_capabilities": available_capabilities,
+ "preferred_todo_ids": sorted(preferred_todo_ids or []), "item_limit": item_limit,
+ "archived_items": archived_items, "standing_items": standing_items,
+ "legacy_source_order": legacy_source_order,
+ "include_full_source": bool(vision_runs),
+ "include_task_orchestration_authority": include_task_orchestration_authority,
+ "observed_at": now.timestamp(), "evaluated_at": evaluated_at or now.isoformat()})
+ fields = restore_todo_field_evaluations(result)
+ for role in ("user", "agent"):
+ summary = fields.get(f"{role}_todos")
+ if not summary:
+ continue
+ if include_task_orchestration_authority:
+ from .todo_summary import _task_orchestration_authority
+ summary["task_orchestration_authority"] = _task_orchestration_authority(
+ result["source_orchestration"][role], role=role)
+ if vision_runs:
+ from .succession_warning import _EvaluatedTodo
+ from ..goals.goal_vision_wait_projection import attach_active_vision_waits
+ source = [_EvaluatedTodo(item, decision) for item, decision in zip(
+ result["source_items"][role], result["source_evaluations"][role], strict=True)]
+ attach_active_vision_waits(summary, vision_runs, role=role, items=source,
+ lineage_items=lineage)
+ return fields
+
+
+def project_todo_read_group(items, *, source_section, role, preferred_todo_ids=None,
+ lineage=None, rollout_events=None, available_capabilities=None,
+ item_limit=None, evaluated_at=None, evaluated=False,
+ full_selection=True, selection=None, resume_lineage=None,
+ include_full_source=False, include_task_orchestration_authority=False):
+ from .succession_warning import _EvaluatedTodo
+ from ..runtime.time import now_utc
+
+ now = now_utc()
+ lineage_rows, lineage_facts = legacy_lineage_facts(lineage or [])
+ result = project_todo_read({
+ "kind": "group", "items": items, "lineage": lineage_rows, "lineage_facts": lineage_facts,
+ "resume_lineage": resume_lineage,
+ "evaluated": evaluated,
+ "evaluations": [item.succession_evaluation if isinstance(item, _EvaluatedTodo) else None
+ for item in items] if evaluated else None,
+ "source_section": source_section, "role": role,
+ "preferred_todo_ids": sorted(preferred_todo_ids or []),
+ "rollout_events": list(rollout_events or []),
+ "rollout_event_source": getattr(rollout_events, "source", None),
+ "available_capabilities": sorted(available_capabilities) if available_capabilities is not None else None,
+ "item_limit": item_limit, "evaluated_at": evaluated_at or now.isoformat(),
+ "observed_at": now.timestamp(), "full_selection": full_selection,
+ "selection": selection,
+ "include_full_source": include_full_source,
+ "include_task_orchestration_authority": include_task_orchestration_authority,
+ })
+ # The native owner validates this ephemeral evidence again on filtered reads.
+ if "items" in result:
+ result["items"] = [_EvaluatedTodo(item, decision)
+ for item, decision in zip(result["items"], result["evaluations"], strict=True)]
+ result["summary"]["items"] = [
+ _EvaluatedTodo(item, decision) for item, decision in zip(
+ result["summary"].get("items", []), result["visible_evaluations"], strict=True)
+ ]
+ return result
+
+
+def prepare_todo_source_groups(groups, *, lineage):
+ from .todo_summary import _structured_todo_group_items, _structured_resume_source_items
+ from .resume_condition import build_todo_resume_evaluation_request
+
+ prepared = []
+ for group in groups:
+ items = _structured_todo_group_items(group["items"], source_section=group["source_section"],
+ role=group["role"], text_limit=group.get("text_limit", 500),
+ include_content_revision=group.get("include_content_revision", True))
+ prepared.append({**group, "items": items,
+ "resume_lineage": build_todo_resume_evaluation_request([], source_items=_structured_resume_source_items(
+ lineage, source_section=group["source_section"]))["source_items"]
+ if any(item.get("resume_when") for item in items) else None})
+ return prepared
+
+
+def legacy_lineage_facts(items):
+ from .succession_warning import succession_facts
+
+ return ([{"todo_id": item.get("todo_id"), "role": item.get("role"), "source_section": item.get("source_section"),
+ "archive_state": item.get("archive_state") or "active"} for item in items],
+ [succession_facts(item) for item in items])
+
+
+def prepare_todo_source_text(goal, state_text, state_path, *, todo_id=None, retained=False):
+ from .active_state_todo_parser import parse_todo_source
+ from .todo_summary import _structured_resume_source_items
+ from .standing_decision import standing_decision_source_items
+ from .active_state_editing import TODO_SECTION_HEADINGS
+ from .contract import normalize_todo_id
+
+ active, archived, sections = parse_todo_source(state_text, goal=goal, state_path=state_path)
+ records = [*active["user"], *active["agent"], *archived]
+ groups = [{"role": role, "source_section": TODO_SECTION_HEADINGS[role] if retained else sections[role],
+ "items": [item for item in records if item.get("role") == role] if retained else active[role],
+ "include_empty_source": retained or sections[role] is not None,
+ "text_limit": None if retained else 500, "include_content_revision": not retained}
+ for role in ("user", "agent")]
+ archived_decisions = [item for item in archived if item.get("role") == "user"]
+ archived_query = _structured_resume_source_items(archived, source_section="Completed Work Archive") if todo_id else []
+ lineage = records if retained else [*active["user"], *active["agent"], *[item for item in archived if normalize_todo_id(item.get("todo_id"))]]
+ lineage_rows, lineage_facts = legacy_lineage_facts(lineage)
+ return {"groups": prepare_todo_source_groups(groups, lineage=records), "lineage": lineage_rows, "lineage_facts": lineage_facts,
+ "archived_items": None if retained else archived,
+ "standing_items": None if retained else standing_decision_source_items([*active["user"], *archived_decisions]),
+ "legacy_source_order": not archived_decisions,
+ "archived_items_for_query": archived_query,
+ "full_bodies": [item for item in records if item.get("todo_id") == todo_id] if todo_id else [],
+ "source_count": len(records)}
diff --git a/loopx/control_plane/todos/list_projection.py b/loopx/control_plane/todos/list_projection.py
index 57df51e17e..fa6c9fe40b 100644
--- a/loopx/control_plane/todos/list_projection.py
+++ b/loopx/control_plane/todos/list_projection.py
@@ -1,493 +1,59 @@
+"""Compatibility exports for complete native Todo presentation lenses."""
from __future__ import annotations
+import json
+from pathlib import Path
from typing import Any, Literal
+from ..effect_runtime import effect_runtime_result
-from .todo_semantics import todo_blocker_reason
+_READ_CODEC = json.loads(Path(__file__).with_name("read_legacy_codec.json").read_text(encoding="utf-8"))
+_READ_LIST_LAYOUT = _READ_CODEC["list_constants"]
+AGENT_LANE_TODO_LIST_PROJECTION_SCHEMA_VERSION = _READ_LIST_LAYOUT["AGENT_LANE_TODO_LIST_PROJECTION_SCHEMA_VERSION"]
+AGENT_LANE_TODO_LIST_SUMMARY_SCHEMA_VERSION = _READ_LIST_LAYOUT["AGENT_LANE_TODO_LIST_SUMMARY_SCHEMA_VERSION"]
+EXPLICIT_LIMIT_TODO_LIST_SUMMARY_SCHEMA_VERSION = _READ_LIST_LAYOUT["EXPLICIT_LIMIT_TODO_LIST_SUMMARY_SCHEMA_VERSION"]
+THIN_TODO_LIST_PROJECTION_SCHEMA_VERSION = _READ_LIST_LAYOUT["THIN_TODO_LIST_PROJECTION_SCHEMA_VERSION"]
+THIN_TODO_LIST_SUMMARY_SCHEMA_VERSION = _READ_LIST_LAYOUT["THIN_TODO_LIST_SUMMARY_SCHEMA_VERSION"]
+AGENT_LANE_TODO_LIST_ITEM_LIMIT = _READ_LIST_LAYOUT["AGENT_LANE_TODO_LIST_ITEM_LIMIT"]
+AGENT_LANE_TODO_LIST_TEXT_LIMIT = _READ_LIST_LAYOUT["AGENT_LANE_TODO_LIST_TEXT_LIMIT"]
+THIN_TODO_LIST_ITEM_LIMIT_PER_ROLE = _READ_LIST_LAYOUT["THIN_TODO_LIST_ITEM_LIMIT_PER_ROLE"]
+THIN_TODO_LIST_NESTED_ITEM_LIMIT = _READ_LIST_LAYOUT["THIN_TODO_LIST_NESTED_ITEM_LIMIT"]
+THIN_TODO_LIST_NESTED_DICT_FIELD_LIMIT = _READ_LIST_LAYOUT["THIN_TODO_LIST_NESTED_DICT_FIELD_LIMIT"]
+THIN_TODO_LIST_ITEM_FIELDS = tuple(_READ_LIST_LAYOUT["THIN_TODO_LIST_ITEM_FIELDS"])
+AGENT_LANE_HOT_PATH_VIEW = "agent_lane_hot_path"
+EXPLICIT_LIMIT_COLD_PATH_VIEW = "explicit_limit_cold_path"
+THIN_EXPLICIT_VIEW = "thin_explicit_view"
+TodoListProjectionView = Literal["agent_lane_hot_path", "explicit_limit_cold_path"]
-AGENT_LANE_TODO_LIST_PROJECTION_SCHEMA_VERSION = "agent_lane_todo_list_projection_v0"
-AGENT_LANE_TODO_LIST_SUMMARY_SCHEMA_VERSION = (
- "agent_lane_todo_list_summary_compaction_v0"
-)
-EXPLICIT_LIMIT_TODO_LIST_SUMMARY_SCHEMA_VERSION = (
- "explicit_limit_todo_list_summary_compaction_v0"
-)
-THIN_TODO_LIST_PROJECTION_SCHEMA_VERSION = "todo_list_thin_projection_v0"
-THIN_TODO_LIST_SUMMARY_SCHEMA_VERSION = "todo_list_thin_summary_compaction_v0"
-AGENT_LANE_TODO_LIST_ITEM_LIMIT = 12
-AGENT_LANE_TODO_LIST_TEXT_LIMIT = 180
-# Two items per role keeps the maximally populated retained field shape inside
-# the registered 20k/600-line JSON budget; the regression owns that invariant.
-THIN_TODO_LIST_ITEM_LIMIT_PER_ROLE = 2
-THIN_TODO_LIST_NESTED_ITEM_LIMIT = 3
-THIN_TODO_LIST_NESTED_DICT_FIELD_LIMIT = 8
-AGENT_LANE_HOT_PATH_VIEW: Literal["agent_lane_hot_path"] = "agent_lane_hot_path"
-EXPLICIT_LIMIT_COLD_PATH_VIEW: Literal["explicit_limit_cold_path"] = (
- "explicit_limit_cold_path"
-)
-THIN_EXPLICIT_VIEW: Literal["thin_explicit_view"] = "thin_explicit_view"
-TodoListProjectionView = Literal[
- "agent_lane_hot_path",
- "explicit_limit_cold_path",
-]
-_RETAINED_LANE_LIMITS = {
- "items": AGENT_LANE_TODO_LIST_ITEM_LIMIT,
- "first_open_items": 3,
- "first_executable_items": 3,
- "deferred_items": 3,
- "monitor_due_items": 2,
- "monitor_schedule_gap_items": 2,
- "blocker_items": 2,
- "resume_blocked_items": 2,
- "active_next_action_items": 3,
- "active_next_action_executable_items": 3,
-}
-_RETAINED_DICTS = {
- "work_counts",
- "deferred_work_counts",
- "monitor_writeback",
- "source_proof",
- "terminal_closure_proof",
-}
-_ITEM_FIELDS = (
- "schema_version",
- "gate_state",
- "successor_count",
- "excluded_agents",
- "index",
- "todo_id",
- "role",
- "status",
- "priority",
- "text",
- "title",
- "note",
- "task_class",
- "action_kind",
- "task_domain",
- "capability_binding_ref",
- "task_repository",
- "continuation_policy",
- "required_capabilities",
- "target_capabilities",
- "claimed_by",
- "bound_agent",
- "goal_bound",
- "blocks_agent",
- "global_gate",
- "unblocks_todo_id",
- "successor_todo_ids",
- "completion_continuation",
- "completion_recovery",
- "resume_when",
- "resume_monitor_generation",
- "resume_ready",
- "target_key",
- "cadence",
- "next_due_at",
- "expires_at",
- "watch_only",
-)
+def compact_thin_todo_list_payload(payload):
+ return effect_runtime_result("todo.context.page", {"thin_payload": payload}, large_local_snapshot=True)
-THIN_TODO_LIST_ITEM_FIELDS = (
- "todo_id",
- "role",
- "status",
- "priority",
- "text",
- "task_class",
- "action_kind",
- "claimed_by",
- "bound_agent",
- "goal_bound",
- "blocks_agent",
- "global_gate",
- "unblocks_todo_id",
- "decision_scope",
- "required_decision_scopes",
- "resume_when",
- "resume_ready",
- "target_key",
- "cadence",
- "next_due_at",
- "expires_at",
- "watch_only",
-)
-_COMPACT_ITEM_TEXT_FIELDS = frozenset({"text", "title", "note"})
-_THIN_ITEM_TEXT_FIELDS = frozenset({"text"})
+def compact_agent_lane_todo_summary(summary, *, role):
+ return effect_runtime_result("todo.context.page", {"summary_payload": summary, "role": role, "limit": None}, large_local_snapshot=True)
-def _compact_text(value: Any) -> Any:
- if not isinstance(value, str):
- return value
- text = value.strip()
- if len(text) <= AGENT_LANE_TODO_LIST_TEXT_LIMIT:
- return text
- return text[: AGENT_LANE_TODO_LIST_TEXT_LIMIT - 3].rstrip() + "..."
+def compact_explicit_limit_todo_summary(summary, *, role, item_limit):
+ return effect_runtime_result("todo.context.page", {"summary_payload": summary, "role": role, "limit": item_limit}, large_local_snapshot=True)
-def _project_item_fields(
- value: Any,
- *,
- fields: tuple[str, ...],
- text_fields: frozenset[str],
-) -> Any:
- if not isinstance(value, dict):
- return value
- compact: dict[str, Any] = {}
- for key in fields:
- child = value.get(key)
- if child is None:
- continue
- compact[key] = (
- _compact_text(child) if key in text_fields else child
- )
- return compact
-
-
-def _compact_item(value: Any) -> Any:
- compact = _project_item_fields(
- value,
- fields=_ITEM_FIELDS,
- text_fields=_COMPACT_ITEM_TEXT_FIELDS,
- )
- if isinstance(value, dict) and isinstance(compact, dict):
- reason = todo_blocker_reason(value)
- if reason:
- compact["reason"] = reason
- return compact
-
-
-def _compact_thin_item(value: Any) -> Any:
- projected = _project_item_fields(
- value,
- fields=THIN_TODO_LIST_ITEM_FIELDS,
- text_fields=_THIN_ITEM_TEXT_FIELDS,
- )
- if not isinstance(projected, dict):
- return projected
- return {key: _compact_thin_value(child) for key, child in projected.items()}
-
-
-def _compact_thin_value(value: Any) -> Any:
- """Bound every retained thin field, including nested scope metadata."""
-
- if isinstance(value, str):
- return _compact_text(value)
- if isinstance(value, list):
- return [
- _compact_thin_value(child)
- for child in value[:THIN_TODO_LIST_NESTED_ITEM_LIMIT]
- ]
- if isinstance(value, dict):
- return {
- key: _compact_thin_value(child)
- for key, child in list(value.items())[
- :THIN_TODO_LIST_NESTED_DICT_FIELD_LIMIT
- ]
- }
- return value
-
-
-def _summary_source_view(value: Any) -> str:
- if not isinstance(value, dict):
- return "full_detail"
- view = value.get("view")
- if isinstance(view, str) and view:
- return view
- if value.get("schema_version") == AGENT_LANE_TODO_LIST_SUMMARY_SCHEMA_VERSION:
- return AGENT_LANE_HOT_PATH_VIEW
- return "full_detail"
-
-
-def compact_thin_todo_summary(
- summary: dict[str, Any],
- *,
- role: str,
- items_matched: int,
- items_returned: int,
- item_limit_per_role: int,
-) -> dict[str, Any]:
- """Project one role summary to a bounded field-only Todo list view."""
-
- compact: dict[str, Any] = {}
- omitted_nonempty_lane_count = 0
- omitted_nonempty_dict_count = 0
- source_view = "full_detail"
- for key, value in summary.items():
- if isinstance(value, list):
- if key != "items":
- omitted_nonempty_lane_count += bool(value)
- continue
- if isinstance(value, dict):
- if key in {"work_counts", "deferred_work_counts"}:
- compact[key] = value
- elif key == "payload_compaction":
- source_view = _summary_source_view(value)
- else:
- omitted_nonempty_dict_count += bool(value)
- continue
- compact[key] = value
- items_omitted = max(0, items_matched - items_returned)
- compacted_lanes = {}
- if items_omitted:
- compacted_lanes["items"] = {
- "shown": items_returned,
- "total": items_matched,
- }
- compact["payload_compaction"] = {
- "schema_version": THIN_TODO_LIST_SUMMARY_SCHEMA_VERSION,
- "view": THIN_EXPLICIT_VIEW,
- "source_view": source_view,
- "role": role,
- "items_projected_to": "todos",
- "item_limit_per_role": item_limit_per_role,
- "item_fields": list(THIN_TODO_LIST_ITEM_FIELDS),
- "item_text_limit": AGENT_LANE_TODO_LIST_TEXT_LIMIT,
- "nested_item_limit": THIN_TODO_LIST_NESTED_ITEM_LIMIT,
- "items_matched": items_matched,
- "items_returned": items_returned,
- "items_omitted": items_omitted,
- "compacted_lanes": compacted_lanes,
- "omitted_nonempty_lane_count": omitted_nonempty_lane_count,
- "omitted_nonempty_dict_count": omitted_nonempty_dict_count,
- "full_detail_cold_path": "todo list without --thin or active state",
- }
- return compact
-
-
-def thin_todo_list_field_projection_contract(
- *,
- matched_todo_count: int,
- returned_todo_count: int,
- item_limit_per_role: int,
- source_view: str,
-) -> dict[str, Any]:
- return {
- "schema_version": THIN_TODO_LIST_PROJECTION_SCHEMA_VERSION,
- "view": THIN_EXPLICIT_VIEW,
- "source_view": source_view,
- "item_container": "todos",
- "matched_todo_count": matched_todo_count,
- "returned_todo_count": returned_todo_count,
- "omitted_todo_count": max(
- 0,
- matched_todo_count - returned_todo_count,
- ),
- "item_limit_per_role": item_limit_per_role,
- "counts_cover_full_match": True,
- "item_fields": list(THIN_TODO_LIST_ITEM_FIELDS),
- "item_text_limit": AGENT_LANE_TODO_LIST_TEXT_LIMIT,
- "nested_item_limit": THIN_TODO_LIST_NESTED_ITEM_LIMIT,
- "nested_dict_field_limit": THIN_TODO_LIST_NESTED_DICT_FIELD_LIMIT,
- "full_detail_cold_paths": [
- "todo list without --thin",
- "todo list --todo-id without --thin",
- "active state",
- ],
- }
-
-
-def compact_thin_todo_list_payload(payload: dict[str, Any]) -> dict[str, Any]:
- """Return the opt-in Todo list field and cardinality projection."""
-
- compact = dict(payload)
- compact.pop("state_file", None)
- compact.pop("project", None)
- compact.pop("filter_semantics", None)
-
- explicit_limit = compact.get("explicit_limit")
- item_limit_per_role = THIN_TODO_LIST_ITEM_LIMIT_PER_ROLE
- if isinstance(explicit_limit, int):
- item_limit_per_role = min(item_limit_per_role, explicit_limit)
-
- source_projection = compact.get("todo_list_projection")
- source_projection = source_projection if isinstance(source_projection, dict) else {}
- source_view = str(source_projection.get("view") or "full_detail")
- matched_todo_count = int(
- source_projection.get("matched_todo_count") or compact.get("todo_count") or 0
- )
-
- projected_items: list[dict[str, Any]] = []
- for key, role in (("user_todos", "user"), ("agent_todos", "agent")):
- summary = compact.get(key)
- if not isinstance(summary, dict):
- continue
- source_items = [
- item for item in summary.get("items") or [] if isinstance(item, dict)
- ]
- retained_items = source_items[:item_limit_per_role]
- projected_role_items = [_compact_thin_item(item) for item in retained_items]
- projected_items.extend(projected_role_items)
- items_matched = int(summary.get("total_count") or len(source_items))
- projected = compact_thin_todo_summary(
- summary,
- role=role,
- items_matched=items_matched,
- items_returned=len(projected_role_items),
- item_limit_per_role=item_limit_per_role,
- )
- compact[key] = projected
- compact["todos"] = projected_items
- returned_todo_count = len(projected_items)
- omitted_todo_count = max(0, matched_todo_count - returned_todo_count)
- compact["matched_todo_count"] = matched_todo_count
- compact["returned_todo_count"] = returned_todo_count
- compact["omitted_todo_count"] = omitted_todo_count
- todo_id_filter = compact.get("todo_id_filter")
- if isinstance(todo_id_filter, str):
- matched = next(
- (
- item
- for item in projected_items
- if item.get("todo_id") == todo_id_filter
- ),
- None,
- )
- compact["todo"] = matched
- compact["relations"] = todo_item_relations(matched) if matched else {}
-
- compact["thin"] = True
- compact["todo_list_field_projection"] = thin_todo_list_field_projection_contract(
- matched_todo_count=matched_todo_count,
- returned_todo_count=returned_todo_count,
- item_limit_per_role=item_limit_per_role,
- source_view=source_view,
- )
- return compact
+def compact_thin_todo_summary(summary, *, role, items_matched, items_returned, item_limit_per_role):
+ return effect_runtime_result("todo.context.page", {"thin_summary_payload": summary, "details": {
+ "role": role, "items_matched": items_matched, "items_returned": items_returned,
+ "item_limit_per_role": item_limit_per_role}}, large_local_snapshot=True)
def todo_item_relations(item: dict[str, Any]) -> dict[str, Any]:
"""Project the stable relationship fields for one todo list item."""
relations: dict[str, Any] = {}
- for key in (
- "claimed_by",
- "bound_agent",
- "goal_bound",
- "blocks_agent",
- "excluded_agents",
- "global_gate",
- "unblocks_todo_id",
- "successor_todo_ids",
- "completion_continuation",
- "completion_recovery",
- "superseded_by",
- "resume_when",
- "resume_monitor_generation",
- "resume_condition",
- "resume_ready",
- "decision_scope",
- "required_decision_scopes",
- "required_write_scopes",
- "required_capabilities",
- "target_capabilities",
- "task_class",
- "action_kind",
- "continuation_policy",
- "target_key",
- "cadence",
- "next_due_at",
- "expires_at",
- ):
+ for key in _READ_CODEC["relation_fields"]:
value = item.get(key)
if value is not None and value != []:
relations[key] = value
return relations
-def compact_agent_lane_todo_summary(
- summary: dict[str, Any],
- *,
- role: str,
-) -> dict[str, Any]:
- """Bound one agent-lane list summary while retaining actionable identities."""
- compact: dict[str, Any] = {}
- compacted_lanes: dict[str, dict[str, int]] = {}
- omitted_nonempty_lane_count = 0
- omitted_nonempty_dict_count = 0
- for key, value in summary.items():
- if isinstance(value, list):
- limit = _RETAINED_LANE_LIMITS.get(key)
- if limit is None:
- omitted_nonempty_lane_count += bool(value)
- continue
- if key == "items":
- retained = [
- item
- for item in value
- if not isinstance(item, dict) or item.get("status") != "done"
- ]
- else:
- retained = value
- compact[key] = [_compact_item(item) for item in retained[:limit]]
- if len(retained) > limit:
- compacted_lanes[key] = {"shown": limit, "total": len(retained)}
- continue
- if isinstance(value, dict):
- if key in _RETAINED_DICTS:
- compact[key] = value
- else:
- omitted_nonempty_dict_count += bool(value)
- continue
- compact[key] = value
- compact["payload_compaction"] = {
- "schema_version": AGENT_LANE_TODO_LIST_SUMMARY_SCHEMA_VERSION,
- "role": role,
- "item_limit": AGENT_LANE_TODO_LIST_ITEM_LIMIT,
- "item_text_limit": AGENT_LANE_TODO_LIST_TEXT_LIMIT,
- "terminal_items_in_hot_path": False,
- "compacted_lanes": compacted_lanes,
- "omitted_nonempty_lane_count": omitted_nonempty_lane_count,
- "omitted_nonempty_dict_count": omitted_nonempty_dict_count,
- "full_detail_cold_path": (
- "todo list with --role, --status, or --todo-id; "
- "todo list without --agent-id; or active state"
- ),
- }
- return compact
-
-
-def compact_explicit_limit_todo_summary(
- summary: dict[str, Any],
- *,
- role: str,
- item_limit: int,
-) -> dict[str, Any]:
- """Bound every item-bearing lane for the explicit-limit cold path."""
- compact: dict[str, Any] = {}
- compacted_lanes: dict[str, dict[str, int]] = {}
- omitted_nonempty_dict_count = 0
- for key, value in summary.items():
- if isinstance(value, list):
- compact[key] = [_compact_item(item) for item in value[:item_limit]]
- if len(value) > item_limit:
- compacted_lanes[key] = {
- "shown": item_limit,
- "total": len(value),
- }
- continue
- if isinstance(value, dict):
- if key in _RETAINED_DICTS:
- compact[key] = value
- else:
- omitted_nonempty_dict_count += bool(value)
- continue
- compact[key] = value
- compact["payload_compaction"] = {
- "schema_version": EXPLICIT_LIMIT_TODO_LIST_SUMMARY_SCHEMA_VERSION,
- "view": EXPLICIT_LIMIT_COLD_PATH_VIEW,
- "role": role,
- "item_limit": item_limit,
- "item_text_limit": AGENT_LANE_TODO_LIST_TEXT_LIMIT,
- "compacted_lanes": compacted_lanes,
- "omitted_nonempty_dict_count": omitted_nonempty_dict_count,
- "full_detail_cold_path": "todo list without --limit or active state",
- }
- return compact
-
-
def todo_list_projection_contract(
*,
matched_todo_count: int,
diff --git a/loopx/control_plane/todos/list_readback.py b/loopx/control_plane/todos/list_readback.py
index 332588ea6d..b9403a37d7 100644
--- a/loopx/control_plane/todos/list_readback.py
+++ b/loopx/control_plane/todos/list_readback.py
@@ -11,43 +11,12 @@
from ...history import load_registry
from ...paths import resolve_runtime_root
from ...rollout_event_log import load_rollout_events, rollout_event_log_path
-from ..effect_runtime import effect_runtime_result
from ..goals.state_resolution import resolve_goal_state
-from ..coordination.local_authority import (
- canonical_todo_items,
- canonical_todo_summary_fields,
- read_canonical_todos_if_promoted,
-)
-from .active_state_todo_parser import parse_todo_source
from .contract import normalize_todo_claimed_by, normalize_todo_id, normalize_todo_status
-from .goal_todo_projection import (
- exact_archived_todo_summaries,
- retained_todo_summary_fields,
- goal_todo_summaries,
- todo_summaries_from_fields,
-)
-from .list_projection import (
- compact_agent_lane_todo_summary,
- compact_thin_todo_list_payload,
- todo_item_relations,
- todo_list_projection_contract,
-)
from .todo_index import MAX_TODO_INDEX_ROLLOUT_EVENTS_PER_GOAL
from .resume_condition import TodoResumeRolloutEvents
-def _restore_full_source_body(
- detail: dict[str, Any], todo_id: str, source_items: list[dict[str, Any]],
-) -> None:
- """Restore source text only after the shared reader has scoped the record."""
- matches = [item for item in source_items
- if item.get("todo_id") == todo_id
- and item.get("role") == detail.get("role")
- and item.get("archive_state", "active") == detail.get("archive_state", "active")]
- if len(matches) == 1:
- detail["text"] = str(matches[0].get("text") or "")
- else:
- raise ValueError("Todo detail cannot recover an unambiguous full source body")
def list_goal_todos(
@@ -64,6 +33,7 @@ def list_goal_todos(
limit: int | None = None,
thin: bool = False,
read_scope: Literal["active", "completed_history"] = "active",
+ json_references: bool = False,
) -> dict[str, Any]:
if read_scope not in {"active", "completed_history"}:
raise ValueError("Todo read_scope must be active or completed_history")
@@ -101,173 +71,47 @@ def list_goal_todos(
rollout_events = TodoResumeRolloutEvents(rollout_events, runtime_root=runtime_root, goal_id=goal_id)
- roles = [role] if role else ["user", "agent"]
- canonical_read = read_canonical_todos_if_promoted(
- runtime_root=runtime_root,
- goal_id=goal_id,
- )
- if canonical_read is not None:
- projected = todo_summaries_from_fields(
- fields=(retained_todo_summary_fields(
- canonical_todo_items(canonical_read["todos"]), rollout_events=rollout_events,
- ) if read_scope == "completed_history" else canonical_todo_summary_fields(
- canonical_read["todos"],
- rollout_events=rollout_events,
- goal_acceptance_contract=canonical_read.get("goal_acceptance_contract"),
- goal_acceptance_work_guards=canonical_read.get("goal_acceptance_work_guards"),
- )),
- source="file_authority",
- rollout_events=rollout_events,
- roles=roles,
- status=status,
- todo_id=normalized_todo_id,
- agent_id=normalized_agent_id,
- limit=limit,
- )
- else:
- if not resolved_state_file.exists():
- raise ValueError(f"active state file does not exist: {resolved_state_file}")
- state_text = resolved_state_file.read_text(encoding="utf-8")
- projected = goal_todo_summaries(
- goal,
- state_text=state_text,
- state_path=resolved_state_file,
- rollout_events=rollout_events,
- roles=roles,
- status=status,
- todo_id=normalized_todo_id,
- agent_id=normalized_agent_id,
- limit=limit,
- include_retained=read_scope == "completed_history",
- )
- if read_scope == "active" and normalized_todo_id and not projected.todos:
- if canonical_read is not None:
- archived_items = [
- item
- for item in canonical_todo_items(canonical_read["todos"])
- if item.get("archive_state") == "archive"
- ]
- else:
- _active_items, archived_items, _source_sections = parse_todo_source(
- state_text,
- goal=goal,
- state_path=resolved_state_file,
- )
- archived_projection = exact_archived_todo_summaries(
- archived_items=archived_items,
- source=projected.source,
- rollout_events=rollout_events,
- roles=roles,
- status=status,
- todo_id=normalized_todo_id,
- agent_id=normalized_agent_id,
- limit=limit,
- )
- if archived_projection is not None:
- projected = archived_projection
- source = projected.source
- summaries = projected.summaries
- todos = projected.todos
- # Exact cold reads restore source bytes after the shared summary owner has
- # evaluated identity/status/guards. List and thin projections stay bounded;
- # a hot summary is never treated as the original request.
- if normalized_todo_id and not thin and len(todos) == 1:
- if canonical_read is not None:
- source_items = canonical_read["todos"]
- else:
- active, archived, _sections = parse_todo_source(
- state_text, goal=goal, state_path=resolved_state_file,
- )
- source_items = [*active["user"], *active["agent"], *archived]
- _restore_full_source_body(todos[0], normalized_todo_id, source_items)
- unfiltered_count = projected.unfiltered_count
- uncapped_todo_count = projected.uncapped_todo_count
+ from ..coordination.local_authority import local_authority_is_promoted, LocalCoordinationAuthorityUnavailable
+ from .goal_todo_projection import project_todo_read
+ from ..runtime.time import now_utc
- matched_todo_count = len(todos)
- agent_lane_hot_path = bool(
- read_scope == "active" and normalized_agent_id and limit is None
- and role is None
- and status is None
- and normalized_todo_id is None
- )
- if agent_lane_hot_path:
- summaries = {
- key: compact_agent_lane_todo_summary(
- summary,
- role=key.removesuffix("_todos"),
- )
- for key, summary in summaries.items()
- }
- todos = [
- item
- for key in ("user_todos", "agent_todos")
- for item in summaries.get(key, {}).get("items") or []
- if isinstance(item, dict)
- ]
+ now = now_utc()
+ params = {"kind": "query",
+ "source": {"runtime_root": str(runtime_root.expanduser().resolve(strict=False)), "goal_id": goal_id},
+ "rollout_events": list(rollout_events), "rollout_event_source": rollout_events.source,
+ "observed_at": now.timestamp(), "evaluated_at": now.isoformat(),
+ "query": {"goal_id": goal_id, "role": role, "status": normalize_todo_status(status),
+ "todo_id": normalized_todo_id, "agent_id": normalized_agent_id, "limit": limit,
+ "read_scope": read_scope, "json_references": json_references, "thin": thin,
+ "state_file": str(resolved_state_file), "project": str(resolved_project) if resolved_project else None}}
+ if not local_authority_is_promoted(runtime_root=runtime_root, goal_id=goal_id):
+ params["legacy_source"] = _legacy_todo_read_source(goal, resolved_state_file,
+ todo_id=normalized_todo_id, retained=read_scope == "completed_history")
+ result = project_todo_read(params)
+ if result.get("source_request") == "legacy_todo_source":
+ params["legacy_source"] = _legacy_todo_read_source(goal, resolved_state_file,
+ todo_id=normalized_todo_id, retained=read_scope == "completed_history")
+ result = project_todo_read(params)
+ if "payload" not in result:
+ failure = result["canonical"]
+ code = str(failure.get("reason_code") or "local_authority_todo_list_unavailable")
+ payload = {"source_authority": "unknown_fail_closed"} if code == "local_authority_mode_read_failed" else failure
+ raise LocalCoordinationAuthorityUnavailable(str(failure.get("reason") or "canonical Todo authority is unavailable"),
+ code=code, payload=payload)
+ payload = result["payload"]
+ if "canonical_acceptance" in result:
+ from .goal_todo_projection import observe_todo_acceptance
+ payload = observe_todo_acceptance(payload, result["canonical_acceptance"],
+ registry_path=registry_path, goal_id=goal_id)
+ return payload
+
+
+def _legacy_todo_read_source(goal, state_file, *, todo_id, retained):
+ from ..goals.legacy_event_source import require_no_legacy_todo_events
- matched_todo = todos[0] if len(todos) == 1 else None
- payload: dict[str, Any] = {
- "ok": True,
- "dry_run": True,
- "read_only": True,
- "command": "list",
- "goal_id": goal_id,
- "role": role or "all",
- "status_filter": normalize_todo_status(status) if status else None,
- "source": source,
- "todo_count": matched_todo_count,
- "todos": todos,
- "state_file": str(resolved_state_file),
- "project": str(resolved_project) if resolved_project else None,
- }
- if canonical_read is not None:
- payload["authority_read"] = {
- "source_authority": canonical_read["source_authority"],
- "provider_revision": canonical_read.get("provider_revision"),
- "cursor": canonical_read.get("cursor"),
- "todo_read_model": canonical_read.get("todo_read_model"),
- "decision_read_from_provider": True,
- "legacy_fallback_used": False,
- }
- if normalized_agent_id:
- payload["agent_id_filter"] = normalized_agent_id
- payload["unfiltered_todo_count"] = unfiltered_count
- payload["filter_semantics"] = (
- "agent todos include unclaimed items plus claimed_by=; "
- "User gates use global_gate, then blocks_agent, then legacy claimed_by scope; "
- "User actions use bound_agent, then legacy claimed_by scope; unscoped items remain visible"
- )
- if agent_lane_hot_path:
- payload["returned_todo_count"] = len(todos)
- payload["todo_list_projection"] = todo_list_projection_contract(
- matched_todo_count=matched_todo_count,
- returned_todo_count=len(todos),
- )
- if limit is not None:
- payload["explicit_limit"] = limit
- payload["unfiltered_todo_count"] = unfiltered_count
- payload["returned_todo_count"] = len(todos)
- payload["todo_list_projection"] = todo_list_projection_contract(
- matched_todo_count=uncapped_todo_count,
- returned_todo_count=len(todos),
- view="explicit_limit_cold_path",
- item_limit_per_role=limit,
- full_detail_cold_paths=(
- "todo list without --limit",
- "active state",
- ),
- )
- if normalized_todo_id:
- payload["todo_id_filter"] = normalized_todo_id
- payload["matched"] = bool(todos)
- payload["relations"] = todo_item_relations(matched_todo) if matched_todo else {}
- if len(todos) > 1:
- payload["ambiguous"] = True
- if not todos:
- payload["not_found"] = True
- if normalized_todo_id:
- return effect_runtime_result(
- "todo.context.page", {"detail_payload": payload}, large_local_snapshot=True,
- )
- payload.update(summaries)
- return compact_thin_todo_list_payload(payload) if thin else payload
+ if not state_file.exists():
+ raise ValueError(f"active state file does not exist: {state_file}")
+ state_text = state_file.read_text(encoding="utf-8")
+ require_no_legacy_todo_events(goal, state_path=state_file)
+ from .goal_todo_projection import prepare_todo_source_text
+ return prepare_todo_source_text(goal, state_text, state_file, todo_id=todo_id, retained=retained)
diff --git a/loopx/control_plane/todos/read_codec.ts b/loopx/control_plane/todos/read_codec.ts
new file mode 100644
index 0000000000..11da7fbe83
--- /dev/null
+++ b/loopx/control_plane/todos/read_codec.ts
@@ -0,0 +1,254 @@
+/** Compatibility codecs for canonical and historical Todo read records. */
+import {createHash} from "node:crypto";
+import type {JsonObject} from "../effect_program.ts";
+import {canonicalAuthorityJson, canonicalAuthoritySha256, authorityUnicodeCompare, AuthorityStoreProtocolError} from "../coordination/authority_store_codec.ts";
+import {COORDINATION_STATE_CONTRACT} from "../coordination/coordination_state_contract.generated.ts";
+import {compactPythonWhitespace, stripPythonWhitespace, normalizeTodoAgent} from "../coordination/todo_agents.ts";
+import {legacyTodoPriority} from "./priority.ts";
+import {normalizeTodoNoFollowup} from "./completion_state.ts";
+import {normalizeTodoResumeWhen, TODO_RESUME_NORMALIZE_REQUEST_SCHEMA_VERSION} from "./resume_condition.ts";
+import {normalizeTodoDecisionScope} from "./decision_metadata.ts";
+import {normalizeTodoCapabilities, normalizeTodoRepository} from "./work_requirements.ts";
+import {EffectRuntimeRequestError} from "../effect_runtime_errors.ts";
+import codec from "./read_legacy_codec.json" with {type: "json"};
+
+export const recordFields = COORDINATION_STATE_CONTRACT.todo_read_record.fields;
+const classes = ["advancement_task", "continuous_monitor", "user_gate", "user_action", "blocker"];
+export const id = (value: unknown): string | null => {
+ const candidate = typeof value === "string" ? stripPythonWhitespace(value).toLowerCase() : "";
+ return /^todo_[a-z0-9_-]{3,64}$/.test(candidate) ? candidate : null;
+};
+export const object = (value: unknown): JsonObject => value !== null && typeof value === "object" && !Array.isArray(value) ? value as JsonObject : {};
+export const text = (value: unknown): string => value == null ? "" : String(value);
+export function ids(value: unknown): string[] {
+ const values = Array.isArray(value) ? value : [value];
+ const found = values.flatMap(raw => (typeof raw === "object" && raw !== null ? JSON.stringify(raw) : text(raw || ""))
+ .match(new RegExp(boundary + "todo_[A-Za-z0-9_-]+" + boundary, "gu")) ?? []);
+ return [...new Set(found.map(id).filter((v): v is string => v !== null))];
+}
+const hash = (value: string): string => createHash("sha256").update(value).digest("hex");
+export const withoutNull = (value: JsonObject): JsonObject => Object.fromEntries(Object.entries(value).filter(([, v]) => v != null));
+
+export function compactTodoReadText(value: string, limit: number | null = 500): string {
+ const full = compactPythonWhitespace(value), points = Array.from(full);
+ return limit === null || points.length <= limit ? full : stripPythonWhitespace(points.slice(0, limit - 1).join("")) + "…";
+}
+export function pythonJson(value: unknown): string {
+ return JSON.stringify(canonicalAuthorityJson(value)).replace(/[\u007f-\uffff]/g,
+ ch => "\\u" + ch.charCodeAt(0).toString(16).padStart(4, "0"));
+}
+const boundary = "(?:(?<=[\\p{L}\\p{N}_])(?![\\p{L}\\p{N}_])|(? values.map(pattern => new RegExp(pattern.replace("(?i)", "").replaceAll("\\b", boundary).replaceAll("\\s", " "), "iu"));
+const classification = [
+ [patterns(codec.TODO_HARD_MONITOR_PATTERNS), "continuous_monitor"],
+ [patterns(codec.TODO_ADVANCEMENT_OVERRIDE_PATTERNS), "advancement_task"],
+ [patterns(codec.TODO_BLOCKED_MONITOR_PATTERNS), "continuous_monitor"],
+ [patterns(codec.TODO_MONITOR_PATTERNS), "continuous_monitor"],
+] as const;
+
+export function taskClass(item: JsonObject): string {
+ const explicit = text(item.task_class).trim().toLowerCase();
+ if (classes.includes(explicit)) return explicit;
+ const action = text(item.action_kind).trim().toLowerCase();
+ if (codec.advancement_actions.includes(action)) return "advancement_task";
+ if (codec.monitor_actions.includes(action)) return "continuous_monitor";
+ const label = compactPythonWhitespace(text(item.text)).replace(/[\u0130\u0131]/gu, "i");
+ for (const [rules, kind] of classification) if (rules.some(rule => rule.test(label))) return kind;
+ return "advancement_task";
+}
+
+export function readAgent(value: unknown): string | null {
+ try {return normalizeTodoAgent(typeof value === "boolean" ? value ? "True" : "" : value, "agent");}
+ catch (error) {if (error instanceof AuthorityStoreProtocolError) return null; throw error;}
+}
+export function readExcludedAgents(value: unknown): string[] {
+ const values = typeof value === "string" ? value.split(",") : Array.isArray(value) ? value : [value];
+ return [...new Set(values.map(readAgent).filter((v): v is string => v !== null))].sort(authorityUnicodeCompare);
+}
+export const readNoFollowup = (value: unknown): boolean | null => normalizeTodoNoFollowup(typeof value === "boolean" ? value : text(value || ""));
+export function readResume(value: unknown): string | null {
+ const candidate = compactPythonWhitespace(text(value || ""));
+ const known = normalizeTodoResumeWhen({schema_version: TODO_RESUME_NORMALIZE_REQUEST_SCHEMA_VERSION, resume_when: candidate});
+ if (known) return known;
+ const lowered = candidate.toLowerCase();
+ return !lowered.startsWith("resume_at:") && /^[a-z][a-z0-9_-]{0,31}(?::[a-z0-9_.:@-]{1,96})?$/.test(lowered) ? lowered : null;
+}
+
+export function readScope(value: unknown): JsonObject | null {
+ const raw = typeof value === "string" ? value : object(value);
+ const selected = typeof raw === "string" ? raw : {kind: raw.kind, granularity: raw.granularity, scope_key: raw.scope_key,
+ ...(id(raw.decision_id) ? {decision_id: id(raw.decision_id)} : {})};
+ try {return normalizeTodoDecisionScope(selected);}
+ catch (error) {if (error instanceof EffectRuntimeRequestError) return null; throw error;}
+}
+function readScopes(value: unknown): JsonObject[] {
+ const values = Array.isArray(value) ? value : text(value || "").split(/[,;|]/u), result: JsonObject[] = [];
+ for (const raw of values) {
+ const scope = readScope(raw);
+ if (scope && !result.some(row => row.kind === scope.kind && row.granularity === scope.granularity && row.scope_key === scope.scope_key)) result.push(scope);
+ }
+ return result;
+}
+
+function listValues(value: unknown): unknown[] {
+ return Array.isArray(value) ? value : text(value || "").split(/[,;|]/u);
+}
+function readCapabilities(value: unknown): string[] {
+ const result: string[] = [];
+ for (const raw of listValues(value)) {
+ try {
+ for (const token of normalizeTodoCapabilities([scalarText(raw)], "capability")) if (!result.includes(token)) result.push(token);
+ } catch (error) {if (!(error instanceof EffectRuntimeRequestError)) throw error;}
+ }
+ return result;
+}
+function writeScopes(value: unknown): string[] {
+ const scopes = listValues(value).map(raw => compactPythonWhitespace(scalarText(raw))).filter(scope => scope &&
+ Array.from(scope).length <= 160 && !scope.startsWith("/") && !scope.startsWith("~") && !scope.split("/").includes("..") && !/[ <>]/u.test(scope));
+ return [...new Set(scopes)];
+}
+
+const truthy = (v: unknown): boolean => Array.isArray(v) ? v.length > 0 : v !== null && typeof v === "object" ? Object.keys(v).length > 0 : Boolean(v);
+function repr(value: unknown): string {
+ if (value == null) return "None";
+ if (typeof value === "boolean") return value ? "True" : "False";
+ if (Array.isArray(value)) return "[" + value.map(repr).join(", ") + "]";
+ if (typeof value === "object") return "{" + Object.entries(value).map(([k, v]) => `${repr(k)}: ${repr(v)}`).join(", ") + "}";
+ if (typeof value !== "string") return String(value);
+ const quote = value.includes("'") && !value.includes('"') ? '"' : "'";
+ return quote + Array.from(value).map(point => {
+ if (point === quote || point === "\\") return "\\" + point;
+ if (point === "\n") return "\\n";
+ if (point === "\r") return "\\r";
+ if (point === "\t") return "\\t";
+ if (point !== " " && /[\p{C}\p{Z}]/u.test(point)) {
+ const code = point.codePointAt(0)!;
+ return code <= 255 ? "\\x" + code.toString(16).padStart(2, "0") : code <= 65535 ?
+ "\\u" + code.toString(16).padStart(4, "0") : "\\U" + code.toString(16).padStart(8, "0");
+ }
+ return point;
+ }).join("") + quote;
+}
+const scalarText = (value: unknown): string => !truthy(value) ? "" : typeof value === "string" ? value : repr(value);
+
+export function handoffSource(item: JsonObject): JsonObject {
+ const refs = (value: unknown): JsonObject => ({...(truthy(value) ? {truthy: true} : {}),
+ ...(Array.isArray(value) && value.length ? {values: value.map(scalarText)} : {})});
+ const nestedMetadata: JsonObject = {}, nested: JsonObject = {};
+ for (const key of ["handoff", "handoff_note"]) {
+ const value = item[key], row = object(value);
+ if (truthy(value)) nested[key] = {truthy: true, fields: value !== null && typeof value === "object" && !Array.isArray(value)
+ ? Object.fromEntries(Object.entries(row).map(([k, v]) => [k, scalarText(v)])) : null};
+ nestedMetadata[key] = {from_agent: readAgent(truthy(row.from_agent) ? row.from_agent : item.agent_id),
+ to_agent: readAgent(row.to_agent), evidence_refs: refs(row.evidence_refs)};
+ }
+ const fields = ["continuation_hint", "suggested_next_action", "note", "reason", "summary", "intent", "blocked_on", "evidence", "title", "text", "task_class", "goal_id", "source", "latest_event_kind"];
+ const action = compactPythonWhitespace(scalarText(item.action_kind)).toLowerCase();
+ const legacy = "(" + [item.goal_id, item.role, item.index, truthy(item.text) ? item.text : item.title].map(repr).join(", ") + ")";
+ return {...nested, nested_metadata: nestedMetadata,
+ texts: Object.fromEntries(fields.filter(key => truthy(item[key])).map(key => [key, scalarText(item[key])])),
+ metadata: {todo_id: id(item.todo_id), claimed_by: readAgent(item.claimed_by), excluded_agents: readExcludedAgents(item.excluded_agents),
+ successor_todo_ids: ids(item.successor_todo_ids), unblocks_todo_id: id(item.unblocks_todo_id), superseded_by: id(item.superseded_by),
+ action_kind: /^[a-z][a-z0-9_-]{0,63}$/.test(action) ? action : null, resume_when: readResume(item.resume_when),
+ required_decision_scopes: readScopes(item.required_decision_scopes), decision_scope: readScope(item.decision_scope)},
+ evidence_refs: refs(item.evidence_refs), has_evidence: truthy(item.evidence), has_note: truthy(item.note),
+ goal_id: "", source: "", legacy_id: id(item.todo_id) ? null : "handoff_" + createHash("sha1").update(legacy).digest("hex").slice(0, 12)};
+}
+
+/** Provider records are already decoded. This changes presentation, never authority. */
+export function canonicalTodoReadItems(values: JsonObject[]): JsonObject[] {
+ return values.map((value, offset) => {
+ const archived = value.archive_state === "archive", native = value.schema_version === "todo_domain_record_v0";
+ const row = {...value, ...(archived ? {source_section: "Completed Work Archive", index: offset + 1} : {}),
+ ...(native ? {schema_version: "todo_item_v0", source_section: archived ? "Completed Work Archive" :
+ value.role === "agent" ? "Agent Todo" : "User Todo / Owner Review Reading Queue", index: offset + 1} : {})};
+ return row;
+ });
+}
+
+export function structureCanonicalTodo(item: JsonObject, role: unknown, section: unknown,
+ limit: number | null = 500, revision = true): JsonObject {
+ const full = compactPythonWhitespace(text(item.text)), body = compactTodoReadText(full, limit);
+ const normalizedStatus = stripPythonWhitespace(text(item.status)).toLowerCase();
+ const status = ["open", "blocked", "done", "deferred"].includes(normalizedStatus) ? normalizedStatus : item.done ? "done" : "open";
+ const parsed = legacyTodoPriority(body);
+ return {...readMetadata(item), schema_version: "todo_item_v0", role, source_section: section,
+ archive_state: item.archive_state ?? "active", status, done: status === "done" || status === "deferred",
+ text: body, task_class: taskClass({...item, text: body}),
+ ...(revision && full ? {content_revision: "sha256:" + hash(full)} : {}),
+ ...(parsed.priority ? {priority: parsed.priority, title: compactTodoReadText(parsed.title, limit)} : {})};
+}
+
+function readMetadata(item: JsonObject): JsonObject {
+ const result = {...item};
+ for (const key of ["created_by", "claimed_by", "bound_agent", "blocks_agent"]) {
+ const normalized = readAgent(item[key]);
+ if (normalized !== null) result[key] = normalized;
+ }
+ for (const key of ["global_gate", "goal_bound"]) {
+ const value = item[key];
+ if (typeof value === "boolean") continue;
+ const token = compactPythonWhitespace(text(value || "")).toLowerCase();
+ if (["1", "true", "yes", "y", "global_gate", "global-gate"].includes(token)) result[key] = true;
+ else if (["0", "false", "no", "n"].includes(token)) result[key] = false;
+ }
+ const excluded = readExcludedAgents(item.excluded_agents);
+ if (excluded.length) result.excluded_agents = excluded;
+ const resume = readResume(item.resume_when);
+ if (resume !== null) result.resume_when = resume;
+ for (const key of ["required_capabilities", "target_capabilities"]) {
+ const values = readCapabilities(item[key]);
+ if (values.length) result[key] = values;
+ }
+ const scopes = writeScopes(item.required_write_scopes);
+ if (scopes.length) result.required_write_scopes = scopes;
+ const decision = readScope(item.decision_scope), required = readScopes(item.required_decision_scopes);
+ if (decision) result.decision_scope = decision;
+ if (required.length) result.required_decision_scopes = required;
+ const outcome = compactPythonWhitespace(scalarText(item.decision_outcome)).toLowerCase();
+ if (["approve", "reject", "cancel"].includes(outcome)) result.decision_outcome = outcome;
+ const binding = stripPythonWhitespace(scalarText(item.capability_binding_ref)).toLowerCase();
+ if (/^[a-z][a-z0-9_.-]{0,31}:[a-z][a-z0-9_.:-]{2,95}$/u.test(binding)) result.capability_binding_ref = binding;
+ if (item.task_repository) {
+ try {const repository = normalizeTodoRepository(item.task_repository); if (repository) result.task_repository = repository;}
+ catch (error) {if (!(error instanceof EffectRuntimeRequestError)) throw error;}
+ }
+ for (const [key, values] of [["continuation_policy", ["independent_handoff", "same_agent_non_delivery"]],
+ ["removed_continuation_policy", ["primary_review", "review_handoff"]]] as const) {
+ const value = stripPythonWhitespace(scalarText(item[key])).toLowerCase();
+ if ((values as readonly string[]).includes(value)) result[key] = value;
+ }
+ for (const key of ["resume_monitor_generation", "material_change_generation"]) {
+ const value = item[key];
+ if (value != null && value !== "" && (typeof value === "number" || typeof value === "boolean" || typeof value === "string" && /^[+-]?\d+$/.test(value.trim()))) {
+ const number = Math.trunc(Number(value));
+ if (Number.isSafeInteger(number) && number >= 0) result[key] = number;
+ }
+ }
+ for (const key of ["action_kind", "task_domain"]) {
+ const value = compactPythonWhitespace(text(item[key])).toLowerCase();
+ if (new RegExp(key === "action_kind" ? "^[a-z][a-z0-9_-]{0,63}$" : "^[a-z][a-z0-9_.-]{0,63}$").test(value)) result[key] = value;
+ }
+ const noFollowup = readNoFollowup(item.no_followup);
+ if (noFollowup !== null) result.no_followup = noFollowup;
+ const declarationKeys = ["validation_command", "validation_command_argv", "validation_label", "validation_timeout_seconds"];
+ if (declarationKeys.some(key => item[key] != null && item[key] !== "")) {
+ const command = typeof item.validation_command === "string" ? stripPythonWhitespace(item.validation_command) : item.validation_command;
+ let argv = item.validation_command_argv ?? null;
+ if (typeof argv === "string") {
+ argv = stripPythonWhitespace(argv);
+ if (!argv) argv = null;
+ else {try {argv = JSON.parse(argv as string);} catch (error) {if (!(error instanceof SyntaxError)) throw error;}}
+ }
+ const timeout = typeof item.validation_timeout_seconds === "string" && /^\d+$/.test(item.validation_timeout_seconds.trim())
+ ? Number(item.validation_timeout_seconds.trim()) : item.validation_timeout_seconds === "" ? null : item.validation_timeout_seconds ?? null;
+ result.completion_validation_required = true;
+ result.completion_validation_sha256 = canonicalAuthoritySha256({validation_command: command === "" ? null : command ?? null,
+ validation_command_argv: argv, validation_label: item.validation_label === "" ? null : item.validation_label ?? null,
+ validation_timeout_seconds: timeout});
+ if (!Object.hasOwn(result, "completion_validation_revision")) result.completion_validation_revision = 0;
+ if (!Object.hasOwn(result, "completion_validation_revision_history")) result.completion_validation_revision_history = [];
+ }
+ for (const key of declarationKeys) delete result[key];
+ return result;
+}
diff --git a/loopx/control_plane/todos/read_legacy_codec.json b/loopx/control_plane/todos/read_legacy_codec.json
new file mode 100644
index 0000000000..3bea730d27
--- /dev/null
+++ b/loopx/control_plane/todos/read_legacy_codec.json
@@ -0,0 +1,236 @@
+{
+ "TODO_HARD_MONITOR_PATTERNS": [
+ "(?i)\\bdo not\\b.*\\b(?:launch|run|execute|start)\\b.*\\buntil\\b",
+ "(?i)\\b(?:only|just)\\b.*\\b(?:after|when|once)\\b.*\\b(?:owner|user|credential|approval|prerequisite|evidence)\\b",
+ "(?i)\\b(?:credential|gcp|gcs|gcp_project|gcp_sa_key|gs://)\\b.*\\b(?:missing|required|provide|proof|prerequisite|gate|gated)\\b",
+ "(?i)\\b(?:readiness|proof)\\b.*\\bbefore any formal\\b.*\\brun\\b",
+ "(?i)\\bremaining formal\\b.*\\bpath\\b",
+ "(?i)\\b(?:route|input)\\b.*\\babsent\\b",
+ "(?i)\\b0\\b.*\\b(?:candidate|candidates)\\b"
+ ],
+ "TODO_ADVANCEMENT_OVERRIDE_PATTERNS": [
+ "(?i)(?:^|[::]\\s*)(?:implement|add|make|fix|build|wire|define|compare|run|execute|test|validate|rebuild|repair|archive|publish|merge|write|attribute|collect|aggregate|generate|produce|materialize|rerun|repeat|eval|evaluate|score)\\b",
+ "(?i)\\b(?:implementation slice|validation-backed patch|smoke fixture|regression suite|readiness scan|source preflight|setup-readiness scan)\\b"
+ ],
+ "TODO_BLOCKED_MONITOR_PATTERNS": [
+ "(?i)\\bdo not\\b.*\\b(?:launch|run|execute|start)\\b.*\\buntil\\b",
+ "(?i)\\b(?:only|just)\\b.*\\b(?:after|when|once)\\b.*\\b(?:owner|user|credential|approval|prerequisite|evidence)\\b",
+ "(?i)\\b(?:credential|gcp|gcs|gcp_project|gcp_sa_key|gs://)\\b.*\\b(?:missing|required|provide|proof|prerequisite|gate|gated)\\b",
+ "(?i)\\b(?:readiness|proof)\\b.*\\bbefore any formal\\b.*\\brun\\b",
+ "(?i)\\bremaining formal\\b.*\\bpath\\b",
+ "(?i)\\b(?:route|input)\\b.*\\babsent\\b",
+ "(?i)\\b0\\b.*\\b(?:candidate|candidates)\\b",
+ "(?i)\\b(?:blocked|gated|waiting)\\b.*\\b(?:owner|user|credential|substrate|proof|prerequisite|evidence)\\b"
+ ],
+ "TODO_MONITOR_PATTERNS": [
+ "(?i)\\bdependency monitor\\b",
+ "(?i)\\bobservation lane\\b",
+ "(?i)(?:^|[::]\\s*)observe\\b",
+ "(?i)(?:^|[::]\\s*)poll\\b",
+ "(?i)(?:^|[::]\\s*)watch\\b",
+ "(?i)\\bmonitor-only\\b",
+ "(?i)\\bdo not\\b.*\\b(?:launch|run|execute|start)\\b.*\\buntil\\b",
+ "(?i)\\b(?:only|just)\\b.*\\b(?:after|when|once)\\b.*\\b(?:owner|user|credential|approval|prerequisite|evidence)\\b",
+ "(?i)\\b(?:credential|gcp|gcs|gcp_project|gcp_sa_key|gs://)\\b.*\\b(?:missing|required|provide|proof|prerequisite|gate|gated)\\b",
+ "(?i)\\b(?:readiness|proof)\\b.*\\bbefore any formal\\b.*\\brun\\b",
+ "(?i)\\bremaining formal\\b.*\\bpath\\b",
+ "(?i)\\b(?:route|input)\\b.*\\babsent\\b",
+ "(?i)\\b0\\b.*\\b(?:candidate|candidates)\\b",
+ "(?i)\\b(?:blocked|gated|waiting)\\b.*\\b(?:owner|user|credential|substrate|proof|prerequisite|evidence)\\b"
+ ],
+ "TODO_ADVANCEMENT_PATTERNS": [
+ "(?i)(?:^|[::]\\s*)(?:implement|add|make|fix|build|wire|define|compare|run|execute|test|validate|rebuild|repair|archive|publish|merge|write|attribute|collect|aggregate|generate|produce|materialize|rerun|repeat|eval|evaluate|score)\\b",
+ "(?i)\\b(?:implementation slice|validation-backed patch|smoke fixture|regression suite|readiness scan|source preflight|setup-readiness scan)\\b",
+ "(?i)\\b(?:task|validation hypothesis|validation step|bounded step|learning run)\\b"
+ ],
+ "advancement_actions": [
+ "advance",
+ "analyze",
+ "benchmark_run",
+ "codex_run",
+ "compact_blocker_writeback",
+ "compare",
+ "execute",
+ "fix",
+ "implement",
+ "rebuild",
+ "rebuild_score",
+ "repair",
+ "run",
+ "run_eval",
+ "test",
+ "validate",
+ "writeback"
+ ],
+ "monitor_actions": [
+ "external_evidence",
+ "monitor",
+ "observe",
+ "poll",
+ "watch"
+ ],
+ "frontier_fields": [
+ "todo_id",
+ "status",
+ "done",
+ "title",
+ "text",
+ "task_class",
+ "claimed_by",
+ "bound_agent",
+ "blocks_agent",
+ "excluded_agents",
+ "priority",
+ "action_kind",
+ "task_domain",
+ "task_repository",
+ "capability_binding_ref",
+ "required_capabilities",
+ "target_capabilities",
+ "target_key",
+ "continuation_policy",
+ "removed_continuation_policy",
+ "decision_scope",
+ "required_decision_scopes",
+ "decision_outcome",
+ "replan_obligation_id",
+ "unblocks_todo_id",
+ "depends_on_todo_id",
+ "depends_on_todo_ids",
+ "resume_when",
+ "no_followup",
+ "successor_todo_ids",
+ "completion_continuation"
+ ],
+ "list_constants": {
+ "_ITEM_FIELDS": [
+ "schema_version",
+ "gate_state",
+ "successor_count",
+ "excluded_agents",
+ "index",
+ "todo_id",
+ "role",
+ "status",
+ "priority",
+ "text",
+ "title",
+ "note",
+ "task_class",
+ "action_kind",
+ "task_domain",
+ "capability_binding_ref",
+ "task_repository",
+ "continuation_policy",
+ "required_capabilities",
+ "target_capabilities",
+ "claimed_by",
+ "bound_agent",
+ "goal_bound",
+ "blocks_agent",
+ "global_gate",
+ "unblocks_todo_id",
+ "successor_todo_ids",
+ "completion_continuation",
+ "completion_recovery",
+ "resume_when",
+ "resume_monitor_generation",
+ "resume_ready",
+ "target_key",
+ "cadence",
+ "next_due_at",
+ "expires_at",
+ "watch_only"
+ ],
+ "_RETAINED_LANE_LIMITS": {
+ "items": 12,
+ "first_open_items": 3,
+ "first_executable_items": 3,
+ "deferred_items": 3,
+ "monitor_due_items": 2,
+ "monitor_schedule_gap_items": 2,
+ "blocker_items": 2,
+ "resume_blocked_items": 2,
+ "active_next_action_items": 3,
+ "active_next_action_executable_items": 3
+ },
+ "_RETAINED_DICTS": [
+ "deferred_work_counts",
+ "monitor_writeback",
+ "source_proof",
+ "terminal_closure_proof",
+ "work_counts"
+ ],
+ "THIN_TODO_LIST_ITEM_FIELDS": [
+ "todo_id",
+ "role",
+ "status",
+ "priority",
+ "text",
+ "task_class",
+ "action_kind",
+ "claimed_by",
+ "bound_agent",
+ "goal_bound",
+ "blocks_agent",
+ "global_gate",
+ "unblocks_todo_id",
+ "decision_scope",
+ "required_decision_scopes",
+ "resume_when",
+ "resume_ready",
+ "target_key",
+ "cadence",
+ "next_due_at",
+ "expires_at",
+ "watch_only"
+ ],
+ "_COMPACT_ITEM_TEXT_FIELDS": [
+ "note",
+ "text",
+ "title"
+ ],
+ "_THIN_ITEM_TEXT_FIELDS": [
+ "text"
+ ],
+ "AGENT_LANE_TODO_LIST_ITEM_LIMIT": 12,
+ "AGENT_LANE_TODO_LIST_TEXT_LIMIT": 180,
+ "THIN_TODO_LIST_ITEM_LIMIT_PER_ROLE": 2,
+ "THIN_TODO_LIST_NESTED_ITEM_LIMIT": 3,
+ "THIN_TODO_LIST_NESTED_DICT_FIELD_LIMIT": 8,
+ "AGENT_LANE_TODO_LIST_PROJECTION_SCHEMA_VERSION": "agent_lane_todo_list_projection_v0",
+ "AGENT_LANE_TODO_LIST_SUMMARY_SCHEMA_VERSION": "agent_lane_todo_list_summary_compaction_v0",
+ "EXPLICIT_LIMIT_TODO_LIST_SUMMARY_SCHEMA_VERSION": "explicit_limit_todo_list_summary_compaction_v0",
+ "THIN_TODO_LIST_PROJECTION_SCHEMA_VERSION": "todo_list_thin_projection_v0",
+ "THIN_TODO_LIST_SUMMARY_SCHEMA_VERSION": "todo_list_thin_summary_compaction_v0"
+ },
+ "relation_fields": [
+ "claimed_by",
+ "bound_agent",
+ "goal_bound",
+ "blocks_agent",
+ "excluded_agents",
+ "global_gate",
+ "unblocks_todo_id",
+ "successor_todo_ids",
+ "completion_continuation",
+ "completion_recovery",
+ "superseded_by",
+ "resume_when",
+ "resume_monitor_generation",
+ "resume_condition",
+ "resume_ready",
+ "decision_scope",
+ "required_decision_scopes",
+ "required_write_scopes",
+ "required_capabilities",
+ "target_capabilities",
+ "task_class",
+ "action_kind",
+ "continuation_policy",
+ "target_key",
+ "cadence",
+ "next_due_at",
+ "expires_at"
+ ]
+}
diff --git a/loopx/control_plane/todos/read_presentation.ts b/loopx/control_plane/todos/read_presentation.ts
new file mode 100644
index 0000000000..489e72756d
--- /dev/null
+++ b/loopx/control_plane/todos/read_presentation.ts
@@ -0,0 +1,116 @@
+/** Existing Todo list response codecs, after full-source decisions. */
+import type {JsonObject} from "../effect_program.ts";
+import {stripPythonWhitespace} from "../coordination/todo_agents.ts";
+import {INTERNAL_STATE_SHAPE_PATTERNS} from "../goals/vision_checkpoint.ts";
+import {compactTodoReadText} from "./read_codec.ts";
+import codec from "./read_legacy_codec.json" with {type: "json"};
+
+const layout = codec.list_constants;
+const dict = (v: unknown): v is JsonObject => v !== null && typeof v === "object" && !Array.isArray(v);
+const nonempty = (v: unknown): boolean => Array.isArray(v) ? v.length > 0 : dict(v) ? Object.keys(v).length > 0 : Boolean(v);
+function compactText(v: unknown): unknown {
+ if (typeof v !== "string") return v;
+ const full = stripPythonWhitespace(v), points = Array.from(full);
+ return points.length <= 180 ? full : stripPythonWhitespace(points.slice(0, 177).join("")) + "...";
+}
+function compactItem(value: unknown, thin = false): unknown {
+ if (!dict(value)) return value;
+ const fields = thin ? layout.THIN_TODO_LIST_ITEM_FIELDS : layout._ITEM_FIELDS;
+ const result: JsonObject = {};
+ for (const key of fields) if (value[key] != null) result[key] = ["text", ...(thin ? [] : ["title", "note"])].includes(key) ? compactText(value[key]) : value[key];
+ if (!thin && (value.task_class === "blocker" || value.status === "blocked")) {
+ const reason = compactTodoReadText(String(value.reason || ""), 220);
+ if (reason && !INTERNAL_STATE_SHAPE_PATTERNS.some(rule => rule.test(reason))) result.reason = reason;
+ }
+ return thin ? Object.fromEntries(Object.entries(result).map(([key, v]) => [key, compactThinValue(v)])) : result;
+}
+function compactThinValue(value: unknown): unknown {
+ if (typeof value === "string") return compactText(value);
+ if (Array.isArray(value)) return value.slice(0, 3).map(compactThinValue);
+ if (dict(value)) return Object.fromEntries(Object.entries(value).slice(0, 8).map(([k, v]) => [k, compactThinValue(v)]));
+ return value;
+}
+
+export function compactTodoReadSummary(summary: JsonObject, role: string, limit: number | null): JsonObject {
+ const result: JsonObject = {}, lanes: JsonObject = {};
+ let omittedLanes = 0, omittedDicts = 0;
+ for (const [key, value] of Object.entries(summary)) {
+ if (Array.isArray(value)) {
+ const cap = limit ?? (layout._RETAINED_LANE_LIMITS as Record)[key];
+ if (cap === undefined) {omittedLanes += Number(nonempty(value)); continue;}
+ const retained = limit === null && key === "items" ? value.filter(row => !dict(row) || row.status !== "done") : value;
+ result[key] = retained.slice(0, cap).map(row => compactItem(row));
+ if (retained.length > cap) lanes[key] = {shown: cap, total: retained.length};
+ } else if (dict(value)) {
+ if (layout._RETAINED_DICTS.includes(key)) result[key] = value;
+ else omittedDicts += Number(nonempty(value));
+ } else result[key] = value;
+ }
+ result.payload_compaction = limit === null ? {
+ schema_version: layout.AGENT_LANE_TODO_LIST_SUMMARY_SCHEMA_VERSION, role, item_limit: 12, item_text_limit: 180,
+ terminal_items_in_hot_path: false, compacted_lanes: lanes, omitted_nonempty_lane_count: omittedLanes,
+ omitted_nonempty_dict_count: omittedDicts,
+ full_detail_cold_path: "todo list with --role, --status, or --todo-id; todo list without --agent-id; or active state",
+ } : {
+ schema_version: layout.EXPLICIT_LIMIT_TODO_LIST_SUMMARY_SCHEMA_VERSION, view: "explicit_limit_cold_path", role,
+ item_limit: limit, item_text_limit: 180, compacted_lanes: lanes, omitted_nonempty_dict_count: omittedDicts,
+ full_detail_cold_path: "todo list without --limit or active state",
+ };
+ return result;
+}
+
+export function todoReadProjectionContract(matched: number, returned: number, limit: number | null): JsonObject {
+ return {schema_version: layout.AGENT_LANE_TODO_LIST_PROJECTION_SCHEMA_VERSION,
+ view: limit === null ? "agent_lane_hot_path" : "explicit_limit_cold_path", matched_todo_count: matched,
+ returned_todo_count: returned, item_limit_per_role: limit ?? 12, counts_cover_full_match: true,
+ full_detail_cold_paths: limit === null ? ["todo list with --role, --status, or --todo-id", "todo list without --agent-id", "active state"] :
+ ["todo list without --limit", "active state"]};
+}
+
+export function compactThinTodoRead(payload: JsonObject): JsonObject {
+ const result = {...payload};
+ for (const key of ["state_file", "project", "filter_semantics"]) delete result[key];
+ const limit = Math.min(2, typeof result.explicit_limit === "number" ? result.explicit_limit : 2);
+ const projection = dict(result.todo_list_projection) ? result.todo_list_projection : {};
+ const sourceView = String(projection.view || "full_detail"), matched = Number(projection.matched_todo_count || result.todo_count || 0);
+ const todos: unknown[] = [];
+ for (const role of ["user", "agent"]) {
+ const key = `${role}_todos`, summary = result[key];
+ if (!dict(summary)) continue;
+ const items = Array.isArray(summary.items) ? summary.items.filter(dict) : [];
+ const chosen = items.slice(0, limit).map(row => compactItem(row, true));
+ todos.push(...chosen);
+ result[key] = compactThinTodoReadSummary(summary, {role, items_matched: Number(summary.total_count || items.length),
+ items_returned: chosen.length, item_limit_per_role: limit});
+ }
+ return {...result, todos, matched_todo_count: matched, returned_todo_count: todos.length,
+ omitted_todo_count: Math.max(0, matched - todos.length), thin: true,
+ todo_list_field_projection: {schema_version: layout.THIN_TODO_LIST_PROJECTION_SCHEMA_VERSION, view: "thin_explicit_view",
+ source_view: sourceView, item_container: "todos", matched_todo_count: matched, returned_todo_count: todos.length,
+ omitted_todo_count: Math.max(0, matched - todos.length), item_limit_per_role: limit, counts_cover_full_match: true,
+ item_fields: layout.THIN_TODO_LIST_ITEM_FIELDS, item_text_limit: 180, nested_item_limit: 3, nested_dict_field_limit: 8,
+ full_detail_cold_paths: ["todo list without --thin", "todo list --todo-id without --thin", "active state"]}};
+}
+
+export function compactThinTodoReadSummary(summary: JsonObject, details: {
+ role: unknown; items_matched: number; items_returned: number; item_limit_per_role: number;
+}): JsonObject {
+ const fields: JsonObject = {};
+ let omittedLanes = 0, omittedDicts = 0, view = "full_detail";
+ for (const [name, value] of Object.entries(summary)) {
+ if (Array.isArray(value)) {if (name !== "items") omittedLanes += Number(nonempty(value));}
+ else if (dict(value)) {
+ if (["work_counts", "deferred_work_counts"].includes(name)) fields[name] = value;
+ else if (name === "payload_compaction") view = typeof value.view === "string" && value.view ? value.view :
+ value.schema_version === layout.AGENT_LANE_TODO_LIST_SUMMARY_SCHEMA_VERSION ? "agent_lane_hot_path" : "full_detail";
+ else omittedDicts += Number(nonempty(value));
+ } else fields[name] = value;
+ }
+ const total = details.items_matched, omitted = Math.max(0, total - details.items_returned);
+ fields.payload_compaction = {schema_version: layout.THIN_TODO_LIST_SUMMARY_SCHEMA_VERSION, view: "thin_explicit_view", source_view: view,
+ role: details.role, items_projected_to: "todos", item_limit_per_role: details.item_limit_per_role, item_fields: layout.THIN_TODO_LIST_ITEM_FIELDS, item_text_limit: 180,
+ nested_item_limit: 3, items_matched: total, items_returned: details.items_returned, items_omitted: omitted,
+ compacted_lanes: omitted ? {items: {shown: details.items_returned, total}} : {}, omitted_nonempty_lane_count: omittedLanes,
+ omitted_nonempty_dict_count: omittedDicts, full_detail_cold_path: "todo list without --thin or active state"};
+ return fields;
+}
diff --git a/loopx/control_plane/todos/read_projection.ts b/loopx/control_plane/todos/read_projection.ts
new file mode 100644
index 0000000000..f72cc23311
--- /dev/null
+++ b/loopx/control_plane/todos/read_projection.ts
@@ -0,0 +1,332 @@
+/** One full-source Todo read reduction; the existing owners decide every lane. */
+import {authorityUnicodeCompare} from "../coordination/authority_store_codec.ts";
+import {stripPythonWhitespace} from "../coordination/todo_agents.ts";
+import {compactTodoReadSummary} from "./read_presentation.ts";
+import type {JsonObject} from "../effect_program.ts";
+import {EffectRuntimeRequestError} from "../effect_runtime_errors.ts";
+import {requireJsonObject, requireBoolean} from "../runtime_decode.ts";
+import {schedulerTimestampMilliseconds} from "../scheduler/state_store.ts";
+import {evaluateTodoResumeConditionsFromSource} from "./resume_condition.ts";
+import {normalizeTodoNoFollowup} from "./completion_state.ts";
+import {readTodoPriority, todoPriorityRank} from "./priority.ts";
+import {evaluateTodoSuccession, validateTodoSuccession, SUCCESSION_FACT_COLUMNS,
+ SUCCESSION_EVALUATION_COLUMNS} from "./succession.ts";
+import {projectTodoSummary, TODO_SUMMARY_PROJECTION_COLUMNS} from "./summary_projection.ts";
+import {projectTodoHandoffContext} from "./handoff_projection.ts";
+import {projectStandingDecisions} from "./standing_decision.ts";
+import {configuredGoalChecks} from "../goals/acceptance_contract.ts";
+import codec from "./read_legacy_codec.json" with {type: "json"};
+
+export const TODO_READ_REQUEST = "todo_read_request_v0";
+export const TODO_READ_RESULT = "todo_read_result_v0";
+import {recordFields, id, object, text, ids, withoutNull, compactTodoReadText, pythonJson,
+ taskClass, canonicalTodoReadItems, structureCanonicalTodo, readAgent, readExcludedAgents, readNoFollowup, readResume, handoffSource} from "./read_codec.ts";
+
+function successionFacts(item: JsonObject): JsonObject {
+ const resume = readResume(item.resume_when) || "", handoff = readExcludedAgents(item.excluded_agents).length > 0 && id(item.unblocks_todo_id) !== null;
+ const status = stripPythonWhitespace(text(item.status)).toLowerCase();
+ return {todo_id: id(item.todo_id), status: ["open", "done", "blocked", "deferred"].includes(status) ? status : item.done ? "done" : "open",
+ active: (item.archive_state || "active") === "active", advancement: taskClass(item) === "advancement_task",
+ no_followup: readNoFollowup(item.no_followup) === true, successors: ids(item.successor_todo_ids),
+ superseded_by: id(item.superseded_by), unblocks: id(item.unblocks_todo_id),
+ resumes: resume.startsWith("todo_done:") ? id(resume.slice(10)) : null, handoff, done: item.done === true,
+ route_flag: typeof item.route_continuation_replan_required === "boolean" ? item.route_continuation_replan_required : null,
+ legacy_route_label: handoff && typeof item.route_continuation_replan_required !== "boolean"
+ ? [item.action_kind, item.title, item.text].filter(v => text(v).trim()).map(text).join(" ") : "",
+ context_fields: Object.keys(withoutNull(item)).filter(key => key !== "succession_evaluation").sort(authorityUnicodeCompare)};
+}
+function compactRecord(item: JsonObject): JsonObject {
+ const result: JsonObject = {index: item.index ?? null, done: Boolean(item.done), text: item.text ?? null};
+ for (const key of recordFields) if (!Object.hasOwn(result, key) && item[key] != null) result[key] = item[key]!;
+ if (item.content_revision) result.content_revision = item.content_revision;
+ if (item.goal_acceptance_guard && typeof item.goal_acceptance_guard === "object") result.goal_acceptance_guard = item.goal_acceptance_guard;
+ return result;
+}
+
+function summaryRow(item: JsonObject, evaluation: JsonObject, preferred: Set): JsonObject {
+ const resume = readResume(item.resume_when) || "", status = item.status || (item.done ? "done" : "open");
+ const index = typeof item.index === "number" && Number.isInteger(item.index) && item.index >= 0 ? item.index : 999999;
+ return {status, done: Boolean(item.done), task_class: taskClass(item), has_resume: Boolean(resume), resume_ready: item.resume_ready ?? null,
+ resume_evaluated: resumeEvaluated(item),
+ acceptance_blocked: object(item.goal_acceptance_guard).allowed === false, claimed: Boolean(item.claimed_by),
+ preferred: preferred.has(item.todo_id), watch_only: taskClass(item) === "continuous_monitor" && (item.watch_only === true || stripPythonWhitespace(text(item.watch_only || "")).toLowerCase() === "true"),
+ due_at: readScheduleTime(item.next_due_at), expires_at: readScheduleTime(item.expires_at),
+ sort: [todoPriorityRank(readTodoPriority(item)), index,
+ index === 999999 ? text(item.completed_at || item.updated_at) : "", index === 999999 ? text(item.todo_id) : ""],
+ completed_at: item.completed_at || null, updated_at: item.updated_at || null, completion_index: Number(item.index || 0),
+ linked_user_action: id(item.unblocks_todo_id) !== null, no_followup: readNoFollowup(item.no_followup) === true,
+ successor_gap: evaluation.successor_gap!, handoff_state: evaluation.handoff_state!, replan: evaluation.route_continuation_replan_required!,
+ todo_id: id(item.todo_id), claim: readAgent(item.claimed_by), bound: readAgent(item.bound_agent), blocks: readAgent(item.blocks_agent),
+ global: Boolean(item.global_gate), excluded: readExcludedAgents(item.excluded_agents)};
+}
+
+function resumeEvaluated(item: JsonObject): boolean {
+ const resume = readResume(item.resume_when), condition = object(item.resume_condition);
+ return !resume || condition.schema_version === "todo_resume_condition_v0" && condition.resume_when === resume &&
+ typeof condition.satisfied === "boolean" && item.resume_ready === condition.satisfied;
+}
+
+function readScheduleTime(value: unknown): number | null {
+ if (typeof value === "string") {
+ const date = /^([0-9]{4})-([0-9]{2})-([0-9]{2})/.exec(value.trim());
+ if (date) {
+ const year = Number(date[1]), month = Number(date[2]), day = Number(date[3]);
+ const check = new Date(0); check.setUTCFullYear(year, month - 1, day);
+ if (check.getUTCFullYear() !== year || check.getUTCMonth() !== month - 1 || check.getUTCDate() !== day) return null;
+ }
+ }
+ const milliseconds = schedulerTimestampMilliseconds(value);
+ return milliseconds === null ? null : milliseconds / 1000;
+}
+
+function frontierFacts(items: JsonObject[]): JsonObject[] {
+ return items.map(item => ({id: stripPythonWhitespace(text(item.todo_id)), claim: readAgent(item.claimed_by),
+ excluded: readExcludedAgents(item.excluded_agents), updated: stripPythonWhitespace(text(item.updated_at || item.completed_at)),
+ advancement: taskClass(item) === "advancement_task", actionable: item.done !== true && item.status === "open" && (!readResume(item.resume_when) || item.resume_ready === true) && object(item.goal_acceptance_guard).allowed !== false,
+ serialized: pythonJson(Object.fromEntries(codec.frontier_fields.filter(key => item[key] != null).map(key => [key, item[key]!])))}));
+}
+
+function handoffGates(items: JsonObject[], evaluations: JsonObject[]): JsonObject[] {
+ const fields = ["todo_id", "role", "task_class", "action_kind", "task_domain", "task_repository", "continuation_policy", "claimed_by", "unblocks_todo_id", "resume_when", "no_followup", "superseded_by", "route_continuation_replan_required", "route_continuation_reason", "route_id", "route_key", "next_due_at"];
+ return items.flatMap((item, i) => {
+ const decision = evaluations[i]!;
+ if (decision.handoff_state == null) return [];
+ const successors = decision.successor_todo_ids as string[];
+ return [withoutNull({schema_version: "todo_handoff_gate_v1", gate_state: decision.handoff_state,
+ index: item.index ?? null, done: item.done === true || item.status === "done" || item.status === "deferred", status: item.status!,
+ text: stripPythonWhitespace(text(item.text)), excluded_agents: item.excluded_agents ?? [], successor_count: successors.length,
+ ...Object.fromEntries(fields.filter(key => item[key] != null).map(key => [key, item[key]!])),
+ ...(decision.route_continuation_replan_required ? {route_continuation_replan_required: true,
+ route_continuation_reason: item.route_continuation_reason ?? "stale handoff closeout requires a bounded route continuation replan"} : {}),
+ ...(successors.length ? {successor_todo_ids: successors} : {})})];
+ }).sort((a, b) => Number(a.index || 999999) - Number(b.index || 999999) || authorityUnicodeCompare(text(a.todo_id), text(b.todo_id)));
+}
+
+export async function projectTodoReadGroup(input: JsonObject): Promise {
+ if (!Array.isArray(input.items) || !Array.isArray(input.lineage)) throw new EffectRuntimeRequestError("Todo read requires complete source arrays");
+ const items = input.items.map(value => ({...requireJsonObject(value, "Todo read item")}));
+ const lineage = input.lineage.map(value => requireJsonObject(value, "Todo read lineage"));
+ const lineageFacts = input.lineage_facts == null ? lineage.map(successionFacts) : input.lineage_facts;
+ if (!Array.isArray(lineageFacts) || lineageFacts.length !== lineage.length) throw new EffectRuntimeRequestError("Todo lineage fact cardinality mismatch");
+ for (const [index, value] of lineageFacts.entries()) {
+ const fact = requireJsonObject(value, "Todo lineage facts");
+ if (fact.todo_id !== id(lineage[index]!.todo_id) || fact.active !== ((lineage[index]!.archive_state || "active") === "active")) {
+ throw new EffectRuntimeRequestError("Todo lineage facts disagree with source identity");
+ }
+ }
+ const evaluated = requireBoolean(input.evaluated, "evaluated");
+ if (evaluated && items.some(item => !resumeEvaluated(item))) {
+ throw new EffectRuntimeRequestError("Todo display requires a matching full-source resume evaluation");
+ }
+ if (!evaluated) {
+ const waiting = items.filter(item => readResume(item.resume_when));
+ if (waiting.length) {
+ const result = await evaluateTodoResumeConditionsFromSource({schema_version: "todo_resume_evaluation_request_v0", items: waiting,
+ source_items: [...(Array.isArray(input.resume_lineage) ? input.resume_lineage : lineage), ...items], rollout_events: input.rollout_events ?? [], evaluated_at: input.evaluated_at!,
+ ...(input.rollout_event_source == null ? {} : {rollout_event_source: input.rollout_event_source}),
+ available_capabilities: input.available_capabilities ?? null});
+ const conditions = new Map((result.conditions as JsonObject[]).map(row => [row.todo_id, row.condition]));
+ for (const item of waiting) {
+ item.resume_condition = conditions.get(item.todo_id) ?? {schema_version: "todo_resume_condition_v0", resume_when: item.resume_when!, satisfied: false, unsupported: true};
+ item.resume_ready = object(item.resume_condition).satisfied === true;
+ }
+ }
+ }
+ const facts = items.map(successionFacts);
+ let decisions: JsonObject[];
+ if (items.length === 0) {
+ decisions = [];
+ } else if (evaluated) {
+ if (!Array.isArray(input.evaluations) || input.evaluations.length !== items.length || input.evaluations.some(value => value == null)) throw new EffectRuntimeRequestError("Todo display requires a matching full-source succession evaluation");
+ const evaluations = input.evaluations;
+ decisions = facts.map((row, i) => validateTodoSuccession(row, evaluations[i]));
+ } else {
+ const selected = new Map();
+ const key = (row: JsonObject) => JSON.stringify([id(row.todo_id), row.role ?? null, row.archive_state || "active"]);
+ for (const item of items) selected.set(key(item), (selected.get(key(item)) ?? 0) + 1);
+ const source: JsonObject[] = [];
+ lineage.forEach((row, index) => {
+ const count = selected.get(key(row)) ?? 0;
+ if (!count) source.push(requireJsonObject(lineageFacts[index], "Todo lineage facts"));
+ else selected.set(key(row), count - 1);
+ });
+ try {
+ decisions = evaluateTodoSuccession([...source, ...facts]).slice(source.length);
+ } catch (error) {
+ if (!(error instanceof EffectRuntimeRequestError)) throw error;
+ return {schema_version: TODO_READ_RESULT, rejection: {phase: "succession", message: error.message}};
+ }
+ }
+ const preferred = new Set(Array.isArray(input.preferred_todo_ids) ? input.preferred_todo_ids : []);
+ const rows = items.map((item, i) => summaryRow(item, decisions[i]!, preferred));
+ const contexts = facts.map(row => row.context_fields!);
+ const compacts = items.map(compactRecord);
+ const projected = projectTodoSummary({schema_version: "todo_summary_projection_request_v3", columns: [...TODO_SUMMARY_PROJECTION_COLUMNS],
+ rows: rows.map(row => TODO_SUMMARY_PROJECTION_COLUMNS.map(name => row[name]!)),
+ succession: {schema_version: "todo_succession_request_v1", row_columns: [...SUCCESSION_FACT_COLUMNS],
+ rows: facts.map((row, i) => SUCCESSION_FACT_COLUMNS.map(name => name === "context_fields" ? i : row[name]!)),
+ context_field_sets: contexts, evaluation_columns: [...SUCCESSION_EVALUATION_COLUMNS],
+ evaluations: decisions.map(row => SUCCESSION_EVALUATION_COLUMNS.map(name => row[name]!))},
+ frontier_rows: input.role === "agent" ? frontierFacts(items) : null,
+ observed_at: input.observed_at!, selection: input.selection ?? null, role: input.role ?? null,
+ source_section: input.source_section ?? null, item_limit: input.item_limit ?? null, full_selection: input.full_selection ?? true});
+ const visible = [...new Set([...Object.values(projected.lanes).flatMap(lane => lane.format === "raw" ? [] : lane.indices),
+ ...Object.values(projected.orchestration).flat()])].sort((a, b) => a - b);
+ const handoffs = projectTodoHandoffContext(visible.map(index => handoffSource(compacts[index]!)));
+ visible.forEach((index, i) => {if (handoffs[i]!.note) compacts[index]!.handoff_note = handoffs[i]!.note;});
+ const summary = projected.fields;
+ for (const [name, lane] of Object.entries(projected.lanes)) {
+ summary[name] = lane.indices.map(index => {
+ if (lane.format === "raw") return items[index]!;
+ const row = {...compacts[index]!};
+ const removed = lane.format === "active" ? ["note", "evidence", "reason", "completed_at", "updated_at", "superseded_by"] :
+ lane.format === "recent" || lane.format === "gap" ? ["note", "evidence", "reason"] : [];
+ for (const key of removed) delete row[key];
+ if (lane.format === "gap") Object.assign(row, {succession_tracked: true, recommended_action: object(summary.todo_succession_warning).recommended_action});
+ return row;
+ });
+ }
+ if (summary.todo_succession_warning) object(summary.todo_succession_warning).items = summary.completed_without_successor_items!;
+ const chosen = projected.source_indices.map(index => items[index]!);
+ const chosenDecisions = projected.source_indices.map(index => decisions[index]!);
+ const gates = handoffGates(chosen, chosenDecisions);
+ if (gates.length) summary.handoff_gates = gates;
+ return {schema_version: TODO_READ_RESULT, summary, items: chosen, evaluations: chosenDecisions,
+ visible_evaluations: projected.lanes.items!.indices.map(index => decisions[index]!),
+ orchestration: Object.fromEntries(Object.entries(projected.orchestration).map(([name, ordinals]) => [name, ordinals.map(i => compacts[i]!)]))};
+}
+
+export async function projectCanonicalTodoFields(input: JsonObject): Promise {
+ if (!Array.isArray(input.todos)) throw new EffectRuntimeRequestError("canonical Todo read source must be an array");
+ const original = input.todos.map(value => requireJsonObject(value, "canonical Todo source"));
+ const decisions = original.filter(row => row.role === "user");
+ const standing = decisions.length ? projectStandingDecisions(decisions, false) : null;
+ const all = input.presentation_ready === true ? original : canonicalTodoReadItems(original);
+ const archived = new Set(all.filter(row => row.archive_state === "archive").map(row => row.todo_id));
+ const acceptance = object(input.goal_acceptance_contract), guards = object(input.goal_acceptance_work_guards);
+ const checksConfigured = configuredGoalChecks(acceptance) !== null;
+ const fields: JsonObject = {}, evaluations: JsonObject = {};
+ for (const role of ["user", "agent"]) {
+ const section = role === "agent" ? "Agent Todo" : "User Todo / Owner Review Reading Queue";
+ const items = all.filter(row => (row.role === "user" ? "user" : "agent") === role && (input.retained === true || !archived.has(row.todo_id)))
+ .map(row => structureCanonicalTodo({...row,
+ ...(input.retained !== true && checksConfigured && guards[text(row.todo_id)] ? {goal_acceptance_guard: guards[text(row.todo_id)]} : {})}, role, section,
+ input.retained === true ? null : 500, input.retained !== true));
+ const lineage = Array.isArray(input.lineage) ? input.lineage.map(value => requireJsonObject(value, "Todo lineage")) : all;
+ const grouped = await projectTodoReadGroup({...input, items, lineage, evaluated: false, role, source_section: section,
+ item_limit: null, full_selection: true, resume_lineage: all.map(row => structureCanonicalTodo(row, row.role ?? null,
+ row.source_section ?? section))});
+ if (grouped.rejection) return grouped;
+ const summary = requireJsonObject(grouped.summary, "canonical Todo summary");
+ if (role === "agent" && input.retained !== true) {
+ if (checksConfigured || Object.keys(object(acceptance.outcome_requirements)).length) summary.goal_acceptance_contract = acceptance;
+ const done = all.filter(row => archived.has(row.todo_id) && row.done === true && taskClass(row) === "advancement_task").length;
+ if (done) {summary.archived_advancement_done_count = done; summary.advancement_done_count = Number(summary.advancement_done_count || 0) + done;}
+ }
+ fields[`${role}_todos`] = summary;
+ evaluations[`${role}_todos`] = grouped.visible_evaluations;
+ }
+ if (standing && input.retained !== true) fields.standing_decision_authority = standing;
+ return {schema_version: TODO_READ_RESULT, fields, field_evaluations: evaluations};
+}
+
+function emptySummary(role: string): JsonObject {
+ return {schema_version: "todo_summary_v0", role, source_section: role === "agent" ? "Agent Todo" : "User Todo / Owner Review Reading Queue",
+ total_count: 0, open_count: 0, done_count: 0, items: [], first_open_items: []};
+}
+
+export async function selectTodoReadFields(input: JsonObject): Promise {
+ const fields = requireJsonObject(input.fields, "Todo source fields"), evaluations = requireJsonObject(input.field_evaluations, "Todo source evaluations");
+ if (!Array.isArray(input.roles) || input.roles.some(role => role !== "user" && role !== "agent")) throw new EffectRuntimeRequestError("Todo read roles must be user or agent");
+ const summaries: JsonObject = {}, todos: unknown[] = [];
+ let unfiltered = 0, uncapped = 0;
+ for (const role of input.roles as string[]) {
+ const key = `${role}_todos`, raw = object(fields[key]), items = Array.isArray(raw.items) ? raw.items : [];
+ unfiltered += items.length;
+ let summary = emptySummary(role);
+ if (items.length) {
+ const projected = await projectTodoReadGroup({...input, items, lineage: [], lineage_facts: [], role,
+ source_section: raw.source_section || summary.source_section, evaluated: true,
+ evaluations: evaluations[key], item_limit: input.limit ?? null, full_selection: true,
+ selection: {role, status: input.status ?? null, todo_id: input.todo_id ?? null, agent_id: input.agent_id ?? null}});
+ if (projected.rejection) return projected;
+ if ((projected.items as unknown[]).length) summary = requireJsonObject(projected.summary, "selected Todo summary");
+ }
+ if (raw.goal_acceptance_contract && typeof raw.goal_acceptance_contract === "object") {
+ summary.goal_acceptance_contract = raw.goal_acceptance_contract;
+ }
+ if (input.limit != null) summary = compactTodoReadSummary(summary, role, Number(input.limit));
+ summaries[key] = summary;
+ todos.push(...summary.items as unknown[]);
+ uncapped += Number(summary.total_count || 0);
+ }
+ return {schema_version: TODO_READ_RESULT, projection: {source: input.source_name!, summaries, todos,
+ unfiltered_count: unfiltered, uncapped_todo_count: uncapped}};
+}
+
+export async function projectSourceTodoFields(input: JsonObject): Promise {
+ if (!Array.isArray(input.groups)) throw new EffectRuntimeRequestError("Todo source requires role groups");
+ let groups = input.groups;
+ const query = input.projection_query == null ? null : requireJsonObject(input.projection_query, "Todo projection query");
+ if (query?.exact_archive === true) {
+ const target = (input.lineage as JsonObject[]).find(row => id(row.todo_id) === query.todo_id && row.archive_state === "archive");
+ if (!target || !(query.roles as unknown[]).includes(target.role)) return {schema_version: TODO_READ_RESULT, projection: null};
+ const group = groups.map(value => requireJsonObject(value, "Todo archive group")).find(row => row.role === target.role)!;
+ const item = (group.items as JsonObject[]).find(row => id(row.todo_id) === query.todo_id)!;
+ groups = [{...group, items: [{...item, source_section: String(target.source_section || "Completed Work Archive")}],
+ source_section: String(target.source_section || "Completed Work Archive"), include_empty_source: true}];
+ }
+ const fields: JsonObject = {}, evaluations: JsonObject = {}, sources: JsonObject = {}, sourceEvaluations: JsonObject = {}, orchestration: JsonObject = {};
+ for (const value of groups) {
+ const group = requireJsonObject(value, "Todo source role group");
+ const projected = await projectTodoReadGroup({...input, ...group, evaluated: false});
+ if (projected.rejection) return projected;
+ const items = projected.items as unknown[];
+ if (!items.length && group.include_empty_source !== true) continue;
+ fields[`${String(group.role)}_todos`] = projected.summary;
+ evaluations[`${String(group.role)}_todos`] = projected.visible_evaluations;
+ if (input.include_full_source === true) {
+ sources[String(group.role)] = projected.items;
+ sourceEvaluations[String(group.role)] = projected.evaluations;
+ }
+ if (input.include_task_orchestration_authority === true) orchestration[String(group.role)] = projected.orchestration;
+ }
+ const agent = object(fields.agent_todos);
+ if (Object.keys(agent).length && Array.isArray(input.archived_items)) {
+ const count = input.archived_items.filter(value => {
+ const row = requireJsonObject(value, "archived Todo source");
+ return row.done === true && taskClass({...row, text: compactTodoReadText(text(row.text))}) === "advancement_task";
+ }).length;
+ if (count) {agent.archived_advancement_done_count = count; agent.advancement_done_count = Number(agent.advancement_done_count || 0) + count;}
+ }
+ if (Array.isArray(input.standing_items) && input.standing_items.length) {
+ const standing = projectStandingDecisions(input.standing_items.map(value => requireJsonObject(value, "standing decision source")), input.legacy_source_order === true);
+ if (standing) fields.standing_decision_authority = standing;
+ }
+ const result = {schema_version: TODO_READ_RESULT, fields, field_evaluations: evaluations,
+ ...(input.include_full_source === true ? {source_items: sources, source_evaluations: sourceEvaluations} : {}),
+ ...(input.include_task_orchestration_authority === true ? {source_orchestration: orchestration} : {})};
+ return query === null ? result : selectTodoReadFields({...input, ...query, fields, field_evaluations: evaluations});
+}
+
+export async function readTodo(value: unknown, observedAt = new Date().toISOString()): Promise {
+ const input = requireJsonObject(value, "Todo read request");
+ if (input.schema_version !== TODO_READ_REQUEST || !["group", "fields", "canonical_fields", "select", "source_fields", "query", "historical_fields"].includes(text(input.kind))) {
+ throw new EffectRuntimeRequestError("Todo read request schema mismatch");
+ }
+ if (input.kind === "canonical_fields") {
+ const {readCanonicalTodoProjection} = await import("./read_transaction.ts");
+ return readCanonicalTodoProjection({...input, read_observed_at: observedAt});
+ }
+ if (input.kind === "query") {
+ const {readTodoQuery} = await import("./read_transaction.ts");
+ return readTodoQuery({...input, read_observed_at: observedAt});
+ }
+ if (input.kind === "historical_fields") {
+ const {readHistoricalTodoProjection} = await import("./read_transaction.ts");
+ return readHistoricalTodoProjection(input);
+ }
+ if (input.kind === "select") return selectTodoReadFields(input);
+ if (input.kind === "source_fields") return projectSourceTodoFields(input);
+ return input.kind === "fields" ? projectCanonicalTodoFields(input) : projectTodoReadGroup(input);
+}
diff --git a/loopx/control_plane/todos/read_transaction.ts b/loopx/control_plane/todos/read_transaction.ts
new file mode 100644
index 0000000000..e94ee8999f
--- /dev/null
+++ b/loopx/control_plane/todos/read_transaction.ts
@@ -0,0 +1,171 @@
+/** Canonical provider IO and full-source summaries share one read transaction. */
+import type {JsonObject} from "../effect_program.ts";
+import {requireJsonObject} from "../runtime_decode.ts";
+import {stat} from "node:fs/promises";
+import {legacyCoordinationWriterFencePath} from "../coordination/legacy_writer_fence.ts";
+import {requireLocalAuthorityRuntimeRoot} from "../coordination/local_authority_provider.ts";
+import {requireAuthorityStoreId} from "../coordination/authority_store_codec.ts";
+import {readCanonicalSnapshot} from "../coordination/canonical_snapshot_page.ts";
+import {readLocalCoordinationTodoSource} from "../coordination/local_authority_read.ts";
+import {LOCAL_COORDINATION_TODO_SNAPSHOT_PAGE_REQUEST_SCHEMA,
+ LOCAL_COORDINATION_TODO_LIST_RESULT_SCHEMA} from "../coordination/coordination_state_contract.generated.ts";
+import {projectCanonicalTodoFields, selectTodoReadFields, projectSourceTodoFields, projectTodoReadGroup, TODO_READ_RESULT} from "./read_projection.ts";
+import {canonicalTodoReadItems, structureCanonicalTodo} from "./read_codec.ts";
+import {compactTodoReadSummary, compactThinTodoRead, todoReadProjectionContract} from "./read_presentation.ts";
+import {projectTodoContextPage} from "./context_projection.ts";
+import {sealProjectionEnvelope, PROJECTION_ENVELOPE_SEAL_REQUEST} from "../projection_envelope.ts";
+
+export async function readCanonicalTodoProjection(input: JsonObject): Promise {
+ const source = requireJsonObject(input.source, "canonical Todo read source");
+ const todos: unknown[] = [], leases: unknown[] = [], guards: JsonObject = {};
+ let after: unknown = null, first: JsonObject | null = null;
+ do {
+ const page = await readCanonicalSnapshot({schema_version: LOCAL_COORDINATION_TODO_SNAPSHOT_PAGE_REQUEST_SCHEMA,
+ runtime_root: source.runtime_root, goal_id: source.goal_id, include_leases: source.include_leases === true,
+ projection_readback: source.projection_readback ?? null, after});
+ if (page.status !== "page") return {schema_version: TODO_READ_RESULT, canonical: {
+ ...page, schema_version: LOCAL_COORDINATION_TODO_LIST_RESULT_SCHEMA}};
+ if (first === null) first = page;
+ todos.push(...page.todos as unknown[]);
+ if (Array.isArray(page.leases)) leases.push(...page.leases);
+ Object.assign(guards, page.goal_acceptance_work_guards ?? {});
+ after = page.next;
+ } while (after !== null);
+ const snapshot = requireJsonObject(first!.snapshot, "canonical snapshot identity");
+ const canonical: JsonObject = {schema_version: LOCAL_COORDINATION_TODO_LIST_RESULT_SCHEMA,
+ status: "loaded", ...requireJsonObject(first!.metadata, "canonical Todo metadata"),
+ todos, todo_ids: todos.map(value => requireJsonObject(value, "canonical Todo").todo_id),
+ store_identity: snapshot.store_identity,
+ ...(source.include_leases === true ? {leases} : {}),
+ source_authority: first!.source_authority, provider_revision: snapshot.provider_revision,
+ cursor: snapshot.cursor, decision_read_from_provider: true, legacy_fallback_used: false,
+ ...(Object.hasOwn(requireJsonObject(first!.metadata, "canonical metadata"), "goal_acceptance_contract") ? {goal_acceptance_work_guards: guards} : {})};
+ const projected = await projectCanonicalTodoFields({...input, todos, retained: requireJsonObject(input.query ?? {}, "Todo query").read_scope === "completed_history",
+ goal_acceptance_contract: canonical.goal_acceptance_contract ?? null,
+ goal_acceptance_work_guards: guards});
+ if (input.query == null || projected.rejection) return {...projected, canonical};
+ return todoListPayload(input, originalRecords(canonical.todos), projected, canonical);
+}
+
+const originalRecords = (value: unknown): JsonObject[] => Array.isArray(value) ? value.map(row => requireJsonObject(row, "Todo source row")) : [];
+
+export async function readHistoricalTodoProjection(input: JsonObject): Promise {
+ const source = requireJsonObject(input.source, "historical Todo read source");
+ const canonical = await readLocalCoordinationTodoSource({schema_version: "loopx_local_coordination_todo_source_request_v0",
+ runtime_root: source.runtime_root, goal_id: source.goal_id, source: source.original_source});
+ if (canonical.status !== "loaded") return {schema_version: TODO_READ_RESULT, canonical};
+ return {...await projectCanonicalTodoFields({...input, todos: canonical.todos,
+ goal_acceptance_contract: canonical.goal_acceptance_contract ?? null,
+ goal_acceptance_work_guards: canonical.goal_acceptance_work_guards ?? null}), canonical};
+}
+
+export async function readTodoQuery(input: JsonObject): Promise {
+ const source = requireJsonObject(input.source, "Todo read source");
+ const root = requireLocalAuthorityRuntimeRoot(source.runtime_root), goal = requireAuthorityStoreId(source.goal_id, "goal id");
+ let promoted = false;
+ try {
+ await stat(legacyCoordinationWriterFencePath(root, goal));
+ promoted = true;
+ } catch (error) {
+ if ((error as NodeJS.ErrnoException).code !== "ENOENT") return {schema_version: TODO_READ_RESULT, canonical: {
+ status: "failed", reason_code: "local_authority_mode_read_failed", reason: "local coordination authority mode cannot be inspected",
+ source_authority: "unknown_fail_closed"}};
+ }
+ if (promoted) return readCanonicalTodoProjection(input);
+ if (input.legacy_source == null) return {schema_version: TODO_READ_RESULT, source_request: "legacy_todo_source"};
+ const legacy = requireJsonObject(input.legacy_source, "legacy Todo source");
+ const projected = await projectSourceTodoFields({...input, ...legacy});
+ if (projected.rejection) return projected;
+ return todoListPayload(input, originalRecords(legacy.full_bodies), projected, null);
+}
+
+async function todoListPayload(input: JsonObject, original: JsonObject[], fields: JsonObject, canonical: JsonObject | null): Promise {
+ const query = requireJsonObject(input.query, "Todo list query");
+ const sourceName = canonical === null ? "markdown_active_state" : "file_authority";
+ const roles: unknown[] = query.role == null ? ["user", "agent"] : [query.role];
+ const select = async (source: JsonObject) => selectTodoReadFields({...input, fields: source.fields,
+ field_evaluations: source.field_evaluations, roles, status: query.status ?? null,
+ todo_id: query.todo_id ?? null, agent_id: query.agent_id ?? null, limit: query.limit ?? null, source_name: sourceName});
+ let selected = await select(fields);
+ if (selected.rejection) return selected;
+ let projection = requireJsonObject(selected.projection, "Todo read projection");
+ if (query.read_scope !== "completed_history" && query.todo_id && !(projection.todos as unknown[]).length) {
+ const legacy = requireJsonObject(input.legacy_source ?? {}, "legacy Todo source");
+ const archived = canonical === null ? originalRecords(legacy.archived_items_for_query) : canonicalTodoReadItems(original).filter(row => row.archive_state === "archive");
+ const target = archived.find(row => row.todo_id === query.todo_id);
+ if (target && roles.includes(target.role)) {
+ const section = target.source_section || "Completed Work Archive";
+ const item = canonical === null ? target : structureCanonicalTodo(target, target.role, section);
+ const grouped = await projectTodoReadGroup({...input, items: [item], role: item.role, source_section: section,
+ lineage: archived, resume_lineage: archived.map(row => canonical === null ? row : structureCanonicalTodo(row, row.role ?? null, row.source_section ?? section)),
+ evaluated: false, item_limit: null, full_selection: true,
+ ...(canonical === null && Array.isArray(legacy.archived_handoffs) ? {handoff_sources: [legacy.archived_handoffs[archived.indexOf(target)]]} : {})});
+ if (grouped.rejection) return grouped;
+ const key = `${String(target.role)}_todos`, exact = {fields: {[key]: grouped.summary}, field_evaluations: {[key]: grouped.visible_evaluations}};
+ selected = await select(exact);
+ if (selected.rejection) return selected;
+ projection = requireJsonObject(selected.projection, "archived Todo projection");
+ }
+ }
+ let todos = projection.todos as JsonObject[];
+ if (query.todo_id && todos.length === 1) {
+ const item = todos[0]!;
+ const matching = original.filter(row => row.todo_id === query.todo_id && row.role === item.role &&
+ (row.archive_state ?? "active") === (item.archive_state ?? "active"));
+ if (matching.length !== 1) throw new Error("Todo detail cannot recover an unambiguous full source body");
+ item.text = String(matching[0]!.text ?? "");
+ }
+ const matchedCount = todos.length;
+ const hot = query.read_scope !== "completed_history" && query.agent_id && query.limit == null &&
+ query.role == null && query.status == null && query.todo_id == null;
+ if (hot) {
+ const summaries = requireJsonObject(projection.summaries, "Todo summaries");
+ for (const [key, summary] of Object.entries(summaries)) summaries[key] = compactTodoReadSummary(
+ requireJsonObject(summary, "Todo summary"), key.replace(/_todos$/, ""), null);
+ todos = Object.values(summaries).flatMap(value => requireJsonObject(value, "Todo summary").items as JsonObject[]);
+ }
+ const payload: JsonObject = {ok: true, dry_run: true, read_only: true, command: "list", goal_id: query.goal_id,
+ role: query.role ?? "all", status_filter: query.status ?? null, source: sourceName, todo_count: matchedCount,
+ todos, state_file: query.state_file, project: query.project ?? null,
+ ...(canonical === null ? {} : {authority_read: {source_authority: canonical.source_authority, provider_revision: canonical.provider_revision,
+ cursor: canonical.cursor, todo_read_model: canonical.todo_read_model,
+ decision_read_from_provider: true, legacy_fallback_used: false}})};
+ if (query.agent_id) Object.assign(payload, {agent_id_filter: query.agent_id, unfiltered_todo_count: projection.unfiltered_count,
+ filter_semantics: "agent todos include unclaimed items plus claimed_by=; " +
+ "User gates use global_gate, then blocks_agent, then legacy claimed_by scope; " +
+ "User actions use bound_agent, then legacy claimed_by scope; unscoped items remain visible"});
+ if (hot) Object.assign(payload, {returned_todo_count: todos.length,
+ todo_list_projection: todoReadProjectionContract(matchedCount, todos.length, null)});
+ if (query.limit != null) Object.assign(payload, {explicit_limit: query.limit,
+ unfiltered_todo_count: projection.unfiltered_count, returned_todo_count: todos.length,
+ todo_list_projection: todoReadProjectionContract(Number(projection.uncapped_todo_count), todos.length, Number(query.limit))});
+ if (query.todo_id) {
+ const item = todos[0], relations: JsonObject = {};
+ const relationFields = ["claimed_by", "bound_agent", "goal_bound", "blocks_agent", "excluded_agents", "global_gate", "unblocks_todo_id",
+ "successor_todo_ids", "completion_continuation", "completion_recovery", "superseded_by", "resume_when", "resume_monitor_generation",
+ "resume_condition", "resume_ready", "decision_scope", "required_decision_scopes", "required_write_scopes", "required_capabilities",
+ "target_capabilities", "task_class", "action_kind", "continuation_policy", "target_key", "cadence", "next_due_at", "expires_at"];
+ if (item) for (const key of relationFields) if (item[key] != null && (!Array.isArray(item[key]) || (item[key] as unknown[]).length)) relations[key] = item[key];
+ Object.assign(payload, {todo_id_filter: query.todo_id, matched: todos.length > 0, relations,
+ ...(todos.length > 1 ? {ambiguous: true} : {}), ...(todos.length === 0 ? {not_found: true} : {})});
+ }
+ Object.assign(payload, projection.summaries);
+ // The existing formatter is composed here, so a public read never calls back
+ // across runtimes for fields of the result it just received.
+ const result = query.todo_id ? projectTodoContextPage({detail_payload: payload}) : query.thin ? compactThinTodoRead(payload) :
+ query.json_references ? projectTodoContextPage({list_payload: payload}) : payload;
+ const shownCount = query.todo_id ? Number(result.todo !== null) : (result.todos as JsonObject[]).length;
+ const observed = String(input.read_observed_at);
+ result.projection_envelope = sealProjectionEnvelope({schema_version: PROJECTION_ENVELOPE_SEAL_REQUEST,
+ projection: "goal_todo_read", observed_at: observed,
+ sources: [{source_id: sourceName, read_status: "read", last_read_at: observed,
+ item_count: canonical === null ? Number(requireJsonObject(input.legacy_source, "legacy source").source_count) : original.length}],
+ coverage: {scope: "goal_todo_query", expected_count: projection.uncapped_todo_count,
+ included_count: projection.uncapped_todo_count, shown_count: shownCount,
+ available_count: projection.uncapped_todo_count, omitted: []}});
+ return {schema_version: TODO_READ_RESULT, payload: result,
+ ...(canonical?.goal_acceptance_oracle_observation_basis ? {canonical_acceptance: {
+ goal_acceptance_contract: canonical.goal_acceptance_contract,
+ goal_acceptance_oracle_observation_basis: canonical.goal_acceptance_oracle_observation_basis,
+ }} : {})};
+}
diff --git a/loopx/control_plane/todos/read_transport.ts b/loopx/control_plane/todos/read_transport.ts
new file mode 100644
index 0000000000..1c8eff700c
--- /dev/null
+++ b/loopx/control_plane/todos/read_transport.ts
@@ -0,0 +1,65 @@
+/** Python transport only; native callers consume readTodo in process. */
+import {createHash} from "node:crypto";
+import {readTodo, TODO_READ_REQUEST, TODO_READ_RESULT} from "./read_projection.ts";
+import type {JsonObject} from "../effect_program.ts";
+import {MAX_LOCAL_SNAPSHOT_BYTES, openPrivateSnapshotSource} from "../effect_runtime_snapshot.ts";
+import {EffectRuntimeRequestError} from "../effect_runtime_errors.ts";
+import {requireJsonObject, requireNonEmptyString} from "../runtime_decode.ts";
+import {BARE_SHA256_PATTERN} from "../content_digest.ts";
+
+const completeBytes = MAX_LOCAL_SNAPSHOT_BYTES - 2 * 1024 * 1024;
+const pieceBytes = Math.floor(completeBytes / 4) * 3;
+
+async function sourceFacts(value: unknown): Promise {
+ const ref = requireJsonObject(value, "Todo fact source reference");
+ const digest = requireNonEmptyString(ref.sha256, "Todo fact source digest"), size = ref.byte_count;
+ if (!BARE_SHA256_PATTERN.test(digest) || typeof size !== "number" || !Number.isSafeInteger(size) || size <= 0) {
+ throw new EffectRuntimeRequestError("invalid complete Todo fact source");
+ }
+ const file = await openPrivateSnapshotSource(ref.path);
+ try {
+ const before = await file.stat();
+ if (before.size !== size) throw new Error("Todo source size changed");
+ const bytes = await file.readFile(), after = await file.stat();
+ if (bytes.length !== size || after.size !== size || before.ino !== after.ino || before.dev !== after.dev ||
+ createHash("sha256").update(bytes).digest("hex") !== digest) throw new Error("Todo source identity changed");
+ return requireJsonObject(JSON.parse(new TextDecoder("utf-8", {fatal: true, ignoreBOM: true}).decode(bytes)), "Todo source facts");
+ } finally {await file.close();}
+}
+
+export async function readTodoRuntime(value: unknown): Promise {
+ const request = requireJsonObject(value, "Todo read request");
+ if (request.schema_version !== TODO_READ_REQUEST) throw new EffectRuntimeRequestError("Todo read request schema mismatch");
+ let input = request;
+ if (request.facts_ref !== undefined) {
+ try {input = await sourceFacts(request.facts_ref);}
+ catch {throw new EffectRuntimeRequestError("complete Todo fact source unavailable or invalid; repeat the original read");}
+ if (input.facts_ref !== undefined || input.schema_version !== TODO_READ_REQUEST) throw new EffectRuntimeRequestError("invalid Todo fact source envelope");
+ }
+ const page = request.result_page == null ? null : requireJsonObject(request.result_page, "Todo result page");
+ const observed = page === null ? new Date().toISOString() : requireNonEmptyString(page.observed_at, "original read observation");
+ const clock = Date.parse(observed);
+ if (!Number.isFinite(clock) || clock > Date.now()) throw new EffectRuntimeRequestError("invalid original Todo read observation");
+ let result = await readTodo(input, observed);
+ if (result.rejection || result.source_request || (result.canonical && !result.fields && !result.payload)) return result;
+ if (input.kind === "group") {
+ const {items, evaluations, orchestration, ...projection} = result;
+ result = {...projection, selected_count: (items as unknown[]).length,
+ ...(input.include_full_source === true ? {items, evaluations} : {}),
+ ...(input.include_task_orchestration_authority === true ? {orchestration} : {})};
+ }
+ const bytes = Buffer.from(JSON.stringify(result), "utf8");
+ if (page === null && bytes.length <= completeBytes) return result;
+ const digest = createHash("sha256").update(bytes).digest("hex");
+ const offset = page === null ? 0 : page.offset;
+ if (typeof offset !== "number" || !Number.isSafeInteger(offset) || offset < 0 || offset >= bytes.length || offset % pieceBytes !== 0 ||
+ (page !== null && page.sha256 !== digest)) {
+ return {schema_version: TODO_READ_RESULT, canonical: {status: "failed", reason_code: "canonical_snapshot_changed",
+ reason: "Todo read result changed; restart the complete read, never append a newer page",
+ source_authority: "unknown_fail_closed", decision_read_from_provider: false, legacy_fallback_used: false}};
+ }
+ const end = Math.min(bytes.length, offset + pieceBytes);
+ return {schema_version: TODO_READ_RESULT, result_piece: {offset, byte_count: bytes.length, sha256: digest,
+ data: bytes.subarray(offset, end).toString("base64"),
+ next: end === bytes.length ? null : {offset: end, sha256: digest, observed_at: observed}}};
+}
diff --git a/loopx/control_plane/todos/standing_decision.py b/loopx/control_plane/todos/standing_decision.py
index efae270f74..4d163fb1c0 100644
--- a/loopx/control_plane/todos/standing_decision.py
+++ b/loopx/control_plane/todos/standing_decision.py
@@ -26,6 +26,26 @@ def build_standing_decision_authority(
Canonical IDs obey their provider contract, not the Markdown token codec.
Both sources use the same typed eligibility and chronology owner.
"""
+ items = standing_decision_source_items(user_items, canonical_records=canonical_records)
+ if not items:
+ return None
+ result = effect_runtime_result(
+ "todo.standing_decision.project",
+ {
+ "schema_version": "standing_decision_projection_request_v0",
+ "items": items,
+ "legacy_source_order": legacy_source_order and not canonical_records,
+ },
+ )
+ if result is not None and (
+ not isinstance(result, dict)
+ or result.get("schema_version") != "standing_decision_authority_v0"
+ ):
+ raise ValueError("invalid typed standing decision projection")
+ return result
+
+
+def standing_decision_source_items(user_items, *, canonical_records=False):
items = []
for item in user_items or []:
if not isinstance(item, dict):
@@ -85,19 +105,4 @@ def build_standing_decision_authority(
"unblocks_todo_id": item.get("unblocks_todo_id"),
}
)
- if not items:
- return None
- result = effect_runtime_result(
- "todo.standing_decision.project",
- {
- "schema_version": "standing_decision_projection_request_v0",
- "items": items,
- "legacy_source_order": legacy_source_order and not canonical_records,
- },
- )
- if result is not None and (
- not isinstance(result, dict)
- or result.get("schema_version") != "standing_decision_authority_v0"
- ):
- raise ValueError("invalid typed standing decision projection")
- return result
+ return items
diff --git a/loopx/control_plane/todos/summary_projection.ts b/loopx/control_plane/todos/summary_projection.ts
index c853825f06..033cf0f034 100644
--- a/loopx/control_plane/todos/summary_projection.ts
+++ b/loopx/control_plane/todos/summary_projection.ts
@@ -1,5 +1,5 @@
import {projectTodoHandoffContext} from "./handoff_projection.ts";
-/** Whole-source summary decisions. Python materializes these ordinals as public
+/** Whole-source summary decisions. The native read owner materializes public
* display records; counts and closure never depend on a presentation budget. */
import type {JsonObject} from "../effect_program.ts";
import {EffectRuntimeRequestError} from "../effect_runtime_errors.ts";
diff --git a/loopx/control_plane/todos/todo_summary.py b/loopx/control_plane/todos/todo_summary.py
index fe8acdd63b..648064a2e7 100644
--- a/loopx/control_plane/todos/todo_summary.py
+++ b/loopx/control_plane/todos/todo_summary.py
@@ -2,7 +2,7 @@
from datetime import datetime
import re
-from typing import Any, Callable, Optional, TypeGuard
+from typing import Any, Callable, Optional
from ..goals.goal_vision_wait_projection import attach_active_vision_waits
from .contract import (
@@ -39,14 +39,11 @@
)
from .summary_item import todo_text_content_revision
from .completion_validation_projection import project_completion_validation_authority
-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_notes, handoff_context_source, validate_handoff_context
+from .handoff_note import attach_todo_handoff_notes
from .todo_semantics import (
todo_item_is_actionable_open as projection_todo_item_is_actionable_open,
todo_item_is_deferred as projection_todo_item_is_deferred,
todo_item_is_due_monitor as projection_todo_item_is_due_monitor,
- todo_item_is_watch_only_monitor as projection_todo_item_is_watch_only_monitor,
todo_item_missing_monitor_schedule as projection_todo_item_missing_monitor_schedule,
todo_item_task_class as projection_todo_item_task_class,
todo_item_next_due_at as projection_todo_item_next_due_at,
@@ -56,8 +53,6 @@
todo_presentation_sort_key as projection_todo_presentation_sort_key,
todo_projection_sort_key as projection_todo_projection_sort_key,
)
-from .resume_condition import evaluate_todo_resume_conditions
-from ..runtime.time import now_utc, now_utc_iso
from ..work_items.project_asset import build_project_asset_todo_summary
from .user_gate import open_user_gate_todo_items
from ..coordination.coordination_state_contract import (
@@ -81,18 +76,6 @@
TASK_ORCHESTRATION_AUTHORITY_SCHEMA_VERSION = "task_orchestration_authority_v0"
TODO_ARCHIVE_STATE_ACTIVE = "active"
-# One internal batch carries the whole source, so the adapter sends columnar
-# 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_v3"
-SUMMARY_PROJECTION_COLUMNS = (
- "status", "done", "task_class", "has_resume", "resume_ready", "resume_evaluated",
- "acceptance_blocked", "claimed", "preferred", "watch_only", "due_at", "expires_at",
- "sort", "completed_at", "updated_at", "completion_index", "linked_user_action",
- "no_followup", "successor_gap", "handoff_state", "replan", "todo_id", "claim",
- "bound", "blocks", "global", "excluded",
-)
AttentionItemBuilder = Callable[..., dict[str, Any]]
GoalLifecycleFields = Callable[[dict[str, Any], Optional[dict[str, Any]]], dict[str, Any]]
PublicSafeText = Callable[..., Optional[str]]
@@ -736,52 +719,6 @@ def attach_dependency_blockers(
item["dependency_blockers"] = blockers
-def _apply_resume_conditions(
- items: list[dict[str, Any]],
- *,
- source_section: str | None,
- resume_source_items: list[dict[str, Any]] | None = None,
- rollout_events: list[dict[str, Any]] | None = None,
- available_capabilities: Any = None,
- evaluated_at: str | None = None,
-) -> None:
- resume_items = [
- item
- for item in items
- if normalize_todo_resume_when(item.get("resume_when"))
- ]
- if not resume_items:
- return
- # Only prepare history when at least one item needs the typed evaluator.
- # Succession still receives the complete original lineage independently.
- source_items = [
- *_structured_resume_source_items(
- resume_source_items, source_section=source_section,
- ),
- *items,
- ]
- conditions = evaluate_todo_resume_conditions(
- resume_items,
- source_items=source_items,
- rollout_events=rollout_events,
- available_capabilities=available_capabilities,
- evaluated_at=evaluated_at or now_utc_iso(),
- )
- for item in items:
- resume_when = normalize_todo_resume_when(item.get("resume_when"))
- if not resume_when:
- continue
- todo_id = normalize_todo_id(item.get("todo_id"))
- condition = conditions.get(todo_id or "")
- if condition is None:
- condition = {
- "schema_version": "todo_resume_condition_v0",
- "resume_when": resume_when,
- "satisfied": False,
- "unsupported": True,
- }
- item["resume_condition"] = condition
- item["resume_ready"] = bool(condition.get("satisfied"))
def active_next_action_todo_ids(value: Any) -> set[str]:
@@ -851,177 +788,10 @@ def _structured_resume_source_items(
]
-def _resume_condition_evaluated(item: dict[str, Any], resume: str | None) -> bool:
- """The source's own full-source resume evaluation for this condition."""
- condition = item.get("resume_condition")
- return (isinstance(condition, dict)
- and condition.get("schema_version") == "todo_resume_condition_v0"
- and condition.get("resume_when") == resume
- and isinstance(condition.get("satisfied"), bool)
- and item.get("resume_ready") is condition.get("satisfied"))
-
-def _materialize_summary_lanes(items: list[dict[str, Any]], compacts: list[dict[str, Any]],
- result: dict[str, Any], summary: dict[str, Any], lanes: dict[str, Any],
- orchestration: dict[str, list[int]], *, fused_context: bool,
-) -> None:
- """Materialize validated ordinals with the existing display field allowlist."""
- if fused_context:
- contexts = validate_handoff_context(result.get("handoff_context"), len(items))
- for compact, context in zip(compacts, contexts, strict=True):
- if context["note"]:
- compact["handoff_note"] = context["note"]
- else:
- # Counts/closure still use every source row in their existing batch.
- # The independent display lens only needs the ordinals that are used.
- visible = sorted({index for lane in lanes.values() if lane["format"] != "raw" for index in lane["indices"]}
- | {index for indices in orchestration.values() for index in indices})
- attach_todo_handoff_notes([compacts[index] for index in visible])
- 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):
- raise ValueError("invalid typed Todo succession warning")
- for name, lane in lanes.items():
- mode = lane.get("format")
- if mode not in {"raw", "active", "compact", "recent", "gap"}:
- raise ValueError("invalid Todo summary display format")
- formatted = []
- for index in lane["indices"]:
- item = items[index]
- if mode == "raw":
- compact = item
- elif mode == "active":
- compact = dict(compacts[index])
- for key in ("note", "evidence", "reason", "completed_at", "updated_at", "superseded_by"):
- compact.pop(key, None)
- elif mode in {"compact", "recent", "gap"}:
- compact = dict(compacts[index])
- if mode in {"recent", "gap"}:
- for key in ("note", "evidence", "reason"):
- compact.pop(key, None)
- if mode == "gap":
- compact.update(succession_tracked=True,
- recommended_action=warning_action)
- else:
- raise ValueError("invalid Todo summary display format")
- formatted.append(compact)
- summary[name] = formatted
- if isinstance(warning, dict):
- warning["items"] = summary["completed_without_successor_items"]
-
-
-def _project_summary(items: list[dict[str, Any]], preferred_todo_ids: set[str] | None,
- *, selection: dict[str, Any] | None, role: str | None, source_section: str | None,
- item_limit: int | None, full_selection: bool,
-) -> dict[str, Any]:
- """Adapt evaluated facts and materialize one typed summary decision."""
- import json
- from ..effect_runtime import MAX_REQUEST_BYTES, EffectRuntimeRejected, effect_runtime_result
- from .succession_warning import succession_evaluations, succession_request
- # Display may never invent the source's resume decision. Assert the
- # full-source precondition before any RPC that reuses the evaluation, so an
- # unevaluated source fails with its own diagnostic instead of a downstream
- # "succession evaluation must be an object" from a later owner.
- for item in items:
- resume = normalize_todo_resume_when(item.get("resume_when"))
- if resume and not _resume_condition_evaluated(item, resume):
- raise ValueError("Todo display requires a matching full-source resume evaluation")
- # Evidence is merely carried here. The shared TS summary owner validates
- # the same full-source hashes/semantics before using any summary fact,
- # avoiding a separate RPC for every role/filter (including empty roles).
- succession = succession_evaluations(items)
- rows = []
- for item, evaluation in zip(items, succession, strict=True):
- resume = normalize_todo_resume_when(item.get("resume_when"))
- evaluated = _resume_condition_evaluated(item, resume)
- due = projection_todo_item_next_due_at(item)
- expires = projection_todo_item_expires_at(item)
- guard = item.get("goal_acceptance_guard")
- rows.append({"status": item.get("status") or ("done" if item.get("done") else "open"),
- "done": bool(item.get("done")), "task_class": projection_todo_item_task_class(item),
- "has_resume": bool(resume), "resume_ready": item.get("resume_ready"),
- "resume_evaluated": evaluated, "acceptance_blocked": isinstance(guard, dict) and guard.get("allowed") is False,
- "claimed": bool(item.get("claimed_by")), "preferred": item.get("todo_id") in (preferred_todo_ids or set()),
- "watch_only": projection_todo_item_is_watch_only_monitor(item),
- "due_at": due.timestamp() if due else None, "expires_at": expires.timestamp() if expires else None,
- "sort": list(projection_todo_presentation_sort_key(item)),
- "completed_at": str(item.get("completed_at") or "") or None,
- "updated_at": str(item.get("updated_at") or "") or None,
- "completion_index": int(item.get("index") or 0),
- "linked_user_action": bool(normalize_todo_id(item.get("unblocks_todo_id"))),
- "no_followup": normalize_todo_no_followup(item.get("no_followup")) is True,
- "successor_gap": evaluation["successor_gap"], "handoff_state": evaluation["handoff_state"],
- "replan": evaluation["route_continuation_replan_required"],
- **{"todo_id": normalize_todo_id(item.get("todo_id")),
- "claim": normalize_todo_claimed_by(item.get("claimed_by")),
- "bound": normalize_todo_bound_agent(item.get("bound_agent")),
- "blocks": normalize_todo_blocks_agent(item.get("blocks_agent")),
- "global": bool(item.get("global_gate")),
- "excluded": normalize_todo_excluded_agents(item.get("excluded_agents"))}})
- compacts = [_compact_todo_item_record(item) for item in items]
- contexts = [handoff_context_source(item) for item in compacts]
- for context in contexts:
- for key in ("todo_id", "claimed_by", "excluded_agents"):
- context.get("metadata", {}).pop(key, None)
- context.get("texts", {}).pop("task_class", None)
- if not context.get("metadata"):
- context.pop("metadata", None)
- try:
- request = {
- "schema_version": SUMMARY_PROJECTION_REQUEST_SCHEMA_VERSION,
- "handoff_sources": contexts,
- "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,
- }
- fused_context = len(json.dumps(request, separators=(",", ":")).encode()) <= MAX_REQUEST_BYTES - 4096
- if not fused_context:
- request.pop("handoff_sources")
- result = effect_runtime_result("todo.summary.project", request)
- except EffectRuntimeRejected as error:
- raise ValueError(str(error)) from error
- if not isinstance(result, dict) or result.get("schema_version") != "todo_summary_projection_v0":
- raise ValueError("invalid typed Todo summary projection")
-
- def valid_ordinals(value: Any) -> TypeGuard[list[int]]:
- return (isinstance(value, list)
- and all(type(index) is int and 0 <= index < len(items) for index in value)
- and len(set(value)) == len(value))
-
- selected = result.get("source_indices")
- if not valid_ordinals(selected):
- raise ValueError("invalid typed Todo selection ordinals")
- if type(result.get("full_selection")) is not bool:
- raise ValueError("invalid typed Todo selection ordinals")
- selected_set = set(selected)
- lanes, orchestration = result.get("lanes"), result.get("orchestration")
- if not isinstance(lanes, dict) or not isinstance(orchestration, dict):
- raise ValueError("invalid typed Todo summary lanes")
- for indices in [*(lane.get("indices") if isinstance(lane, dict) else None for lane in lanes.values()),
- *orchestration.values()]:
- if not valid_ordinals(indices):
- raise ValueError("invalid Todo summary source ordinal")
- if not set(indices) <= selected_set:
- raise ValueError("Todo summary lane escaped the selected source")
- 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")
- _materialize_summary_lanes(items, compacts, result, summary, lanes, orchestration,
- fused_context=fused_context)
- return {"summary": summary, "items": [items[index] for index in selected],
- "succession": [succession[index] for index in selected],
- "orchestration": {name: [compacts[index] for index in indices] for name, indices in orchestration.items()}}
def compact_todo_group(
@@ -1041,32 +811,30 @@ def compact_todo_group(
vision_runs: list[dict[str, Any]] | None = None,
evaluated_at: str | None = None,
) -> dict[str, Any] | None:
+ from .goal_todo_projection import project_todo_read_group
+
if not items and not include_empty_source:
return None
- items = _structured_todo_group_items(
- items,
- source_section=source_section,
- role=role,
- text_limit=text_limit,
- include_content_revision=include_content_revision,
- )
- _apply_resume_conditions(
- items,
- source_section=source_section,
- resume_source_items=resume_source_items,
- rollout_events=rollout_events,
- available_capabilities=available_capabilities,
- evaluated_at=evaluated_at,
- )
- from .succession_warning import evaluate_succession
-
- evaluate_succession(items, resume_source_items)
- return compact_evaluated_todo_group(
- items, source_section=source_section, role=role,
- include_empty_source=include_empty_source, preferred_todo_ids=preferred_todo_ids,
- item_limit=item_limit, include_task_orchestration_authority=include_task_orchestration_authority,
- vision_runs=vision_runs, lineage_items=resume_source_items,
- )
+ items = _structured_todo_group_items(items, source_section=source_section, role=role,
+ text_limit=text_limit, include_content_revision=include_content_revision)
+ resume_lineage = (_structured_resume_source_items(resume_source_items, source_section=source_section)
+ if any(normalize_todo_resume_when(item.get("resume_when")) for item in items) else None)
+ projected = project_todo_read_group(items, source_section=source_section, role=role,
+ preferred_todo_ids=preferred_todo_ids, lineage=resume_source_items,
+ resume_lineage=resume_lineage, item_limit=item_limit,
+ rollout_events=rollout_events, available_capabilities=available_capabilities,
+ evaluated_at=evaluated_at, include_full_source=bool(vision_runs),
+ include_task_orchestration_authority=include_task_orchestration_authority)
+ items = projected.get("items", projected["summary"]["items"])
+ if not projected["selected_count"] and not include_empty_source:
+ return None
+ summary = projected["summary"]
+ attach_active_vision_waits(summary, vision_runs, role=role, items=items,
+ lineage_items=resume_source_items)
+ if include_task_orchestration_authority:
+ summary["task_orchestration_authority"] = _task_orchestration_authority(
+ projected["orchestration"], role=role)
+ return summary
@@ -1084,27 +852,22 @@ def compact_evaluated_todo_group(
full_selection: bool = True,
selection: dict[str, Any] | None = None,
) -> dict[str, Any] | None:
- """Filter/display an already evaluated snapshot, never re-evaluate topology.
+ from .goal_todo_projection import project_todo_read_group
- Callers must come from the full-source parser or canonical summary, not
- persisted derived fields. Missing or mismatched evaluations fail closed.
- """
if not items and not include_empty_source:
return None
- projected = _project_summary(items, preferred_todo_ids, selection=selection,
- role=role, source_section=source_section, item_limit=item_limit, full_selection=full_selection)
- items = projected["items"]
- if not items and not include_empty_source:
+ lineage = lineage_items
+ projected = project_todo_read_group(items, source_section=source_section, role=role,
+ preferred_todo_ids=preferred_todo_ids, lineage=lineage, item_limit=item_limit,
+ evaluated=True, selection=selection, full_selection=full_selection,
+ include_full_source=bool(vision_runs), include_task_orchestration_authority=include_task_orchestration_authority)
+ items = projected.get("items", projected["summary"]["items"])
+ if not projected["selected_count"] and not include_empty_source:
return None
- summary: dict[str, Any] = projected["summary"]
- handoff_gates = build_todo_handoff_gate_states(items, evaluations=projected["succession"])
- attach_active_vision_waits(
- summary, vision_runs, role=role, items=items,
- lineage_items=lineage_items,
- )
+ summary = projected["summary"]
+ attach_active_vision_waits(summary, vision_runs, role=role, items=items,
+ lineage_items=lineage_items)
if include_task_orchestration_authority:
summary["task_orchestration_authority"] = _task_orchestration_authority(
projected["orchestration"], role=role)
- if handoff_gates:
- summary["handoff_gates"] = handoff_gates
return summary
diff --git a/loopx/control_plane/work_items/refresh_recommendation.py b/loopx/control_plane/work_items/refresh_recommendation.py
index df8be91e66..df98b10e23 100644
--- a/loopx/control_plane/work_items/refresh_recommendation.py
+++ b/loopx/control_plane/work_items/refresh_recommendation.py
@@ -6,7 +6,7 @@
from typing import Any
from ..agents.agent_lane_recommendation import build_agent_lane_next_action
-from ..coordination.local_authority import read_canonical_todos_if_promoted, canonical_todo_summary_fields
+from ..coordination.local_authority import read_canonical_todos_if_promoted
from ..effect_runtime import EffectRuntimeRejected, effect_runtime_result
from ..todos.active_state_todo_parser import parse_active_state_todos
from ..todos.contract import normalize_todo_id
@@ -49,22 +49,20 @@ def load_refresh_planning_source(
that task snapshot. Provider failures propagate rather than using prose.
"""
events = load_rollout_events(rollout_event_log_path(runtime_root, goal_id))
- canonical = read_canonical_todos_if_promoted(runtime_root=runtime_root, goal_id=goal_id)
+ canonical = read_canonical_todos_if_promoted(runtime_root=runtime_root, goal_id=goal_id,
+ include_summary=True, rollout_events=events)
if canonical is not None:
- from ..goals.acceptance import observe_canonical_goal_acceptance
+ from ..todos.goal_todo_projection import observe_todo_acceptance
oracle_registry = registry_path
if canonical.get("goal_acceptance_oracle_observation_basis") is not None and oracle_registry is not None:
from .recommendation_source_io import load_recommendation_source_goal
# A shared roster's checkout is display data, including on refresh
# paths without --next-action. Observe the current source workspace.
oracle_registry, _ = load_recommendation_source_goal(oracle_registry, goal_id)
- acceptance = observe_canonical_goal_acceptance(canonical,
+ fields = observe_todo_acceptance(canonical["_todo_fields"], canonical,
registry_path=oracle_registry, goal_id=goal_id)
- fields = canonical_todo_summary_fields(
- canonical["todos"], rollout_events=events,
- goal_acceptance_contract=acceptance,
- goal_acceptance_work_guards=canonical.get("goal_acceptance_work_guards"),
- ) if canonical is not None else None
+ else:
+ fields = None
try:
text = state_path.read_text(encoding="utf-8")
except FileNotFoundError:
diff --git a/tests/control_plane/canonical_authority_fixture.py b/tests/control_plane/canonical_authority_fixture.py
index f2deeb13d8..9b3d3e396e 100644
--- a/tests/control_plane/canonical_authority_fixture.py
+++ b/tests/control_plane/canonical_authority_fixture.py
@@ -12,6 +12,15 @@
import tempfile
+def assert_todo_read_unchanged(current, previous):
+ """Compare durable/read content while checking the separate observation scope."""
+ assert current["projection_envelope"]["schema_version"] == "loopx_projection_envelope_v0"
+ if "projection_envelope" in previous:
+ assert current["projection_envelope"]["coverage"] == previous["projection_envelope"]["coverage"]
+ assert {key: value for key, value in current.items() if key != "projection_envelope"} == {
+ key: value for key, value in previous.items() if key != "projection_envelope"}
+
+
def initialize_canonical_authority(runtime_root: Path, goal_id: str, projection: dict, *, state_path: Path, provider: str = "file") -> dict:
repository = Path(__file__).resolve().parents[2]
module = repository / "loopx/control_plane/coordination/file_authority_store.ts"
diff --git a/tests/control_plane/test_cli_output_differential.py b/tests/control_plane/test_cli_output_differential.py
index d97bd723ff..605290e96c 100644
--- a/tests/control_plane/test_cli_output_differential.py
+++ b/tests/control_plane/test_cli_output_differential.py
@@ -1391,6 +1391,23 @@ def test_projection_envelope_migration_is_bounded_and_one_time(
assert not compare_cli_output_receipts(_receipt(outside_base), _receipt(outside))["ok"]
+@pytest.mark.parametrize("row_prefix", [
+ "surface/todo_list", "variant/todo_list_limited", "variant/todo_list_thin",
+])
+def test_todo_envelope_migration_retains_a_bounded_one_time_allowance(row_prefix):
+ base = _row(row_id=f"{row_prefix}/small/json", surface_id="todo_list")
+ candidate = {**base,
+ "projection_envelope_schema_versions": ["loopx_projection_envelope_v0"],
+ **{metric: base[metric] + 896 for metric in ("chars", "utf8_bytes", "compact_payload_chars")}}
+ assert compare_cli_output_receipts(_receipt(base), _receipt(candidate))["ok"]
+ excessive = {**candidate, "chars": candidate["chars"] + 1}
+ assert not compare_cli_output_receipts(_receipt(base), _receipt(excessive))["ok"]
+ repeated = {**candidate, "chars": candidate["chars"] + 896}
+ assert not compare_cli_output_receipts(_receipt(candidate), _receipt(repeated))["ok"]
+ unsupported = {**candidate, "projection_envelope_schema_versions": ["loopx_projection_envelope_v1"]}
+ assert not compare_cli_output_receipts(_receipt(candidate), _receipt(unsupported))["ok"]
+
+
def test_both_command_routes_are_counted_in_either_order() -> None:
text = (
"loopx --registry '/tmp/registry path' --runtime-root /tmp/root refresh-state\n"
diff --git a/tests/control_plane/test_explore_result_writeback.py b/tests/control_plane/test_explore_result_writeback.py
index 7aa6ea04a7..9d125ef3b9 100644
--- a/tests/control_plane/test_explore_result_writeback.py
+++ b/tests/control_plane/test_explore_result_writeback.py
@@ -1,3 +1,5 @@
+
+from canonical_authority_fixture import assert_todo_read_unchanged
from copy import deepcopy
import json
@@ -994,7 +996,7 @@ def test_corrupt_graph_cannot_report_success_on_replay_and_recovers_after_repair
assert not result["ok"], "A partial log must not become a successful delivery receipt"
assert result["primary_committed"] and result["retryable"]
assert log.read_bytes() == corrupt
- assert list_goal_todos(registry_path=args["registry_path"], goal_id=args["goal_id"]) == before
+ assert_todo_read_unchanged(list_goal_todos(registry_path=args["registry_path"], goal_id=args["goal_id"]), before)
log.write_bytes(valid)
assert deliver_result_attachment(payload=payload(), **args)["idempotent_replay"]
assert log.read_bytes() == valid
diff --git a/tests/control_plane/test_frontier_revision_scope.py b/tests/control_plane/test_frontier_revision_scope.py
index e2edf99b90..1531fdec9f 100644
--- a/tests/control_plane/test_frontier_revision_scope.py
+++ b/tests/control_plane/test_frontier_revision_scope.py
@@ -27,8 +27,8 @@ def test_summary_adapter_rejects_non_object_batched_frontier(monkeypatch, index)
def invalid_frontier(method, request, **kwargs):
result = original(method, request, **kwargs)
- if method == "todo.summary.project":
- result["fields"]["advancement_frontier_revision_index"] = index
+ if method == "todo.read":
+ result["summary"]["advancement_frontier_revision_index"] = index
return result
monkeypatch.setattr(effect_runtime, "effect_runtime_result", invalid_frontier)
diff --git a/tests/control_plane/test_handoff_context_projection.py b/tests/control_plane/test_handoff_context_projection.py
index 214073822a..eab7c2892c 100644
--- a/tests/control_plane/test_handoff_context_projection.py
+++ b/tests/control_plane/test_handoff_context_projection.py
@@ -227,7 +227,7 @@ def track(method, request, **kwargs):
assert result["done_count"] == 1000 and result["total_count"] == 1000
assert len(result["items"]) == 12
assert result["recent_completed_advancement_items"][0]["handoff_note"]["summary"] == "a" * 279 + "..."
- assert calls == ["todo.summary.project", "todo.context.page"]
+ assert calls == ["todo.read"]
def test_byte_bounded_context_batches_retain_every_record(monkeypatch):
diff --git a/tests/control_plane/test_local_coordination_authority.py b/tests/control_plane/test_local_coordination_authority.py
index e73e677587..3756bbee32 100644
--- a/tests/control_plane/test_local_coordination_authority.py
+++ b/tests/control_plane/test_local_coordination_authority.py
@@ -61,6 +61,12 @@
)
+def _assert_same_todo_content(current, expected):
+ assert current["projection_envelope"]["coverage"] == expected["projection_envelope"]["coverage"]
+ assert {key: value for key, value in current.items() if key != "projection_envelope"} == {
+ key: value for key, value in expected.items() if key != "projection_envelope"}
+
+
def _engage_fence(runtime_root: Path, goal_id: str = "goal-a") -> None:
path = legacy_coordination_writer_fence_path(
runtime_root=runtime_root,
@@ -1116,29 +1122,13 @@ def test_todo_list_uses_provider_after_cutover_even_when_markdown_disagrees(
),
encoding="utf-8",
)
- _engage_fence(runtime_root)
+ projection = build_todo_runtime_shadow_projection(goal_id="goal-a", todos=[{
+ "schema_version": "todo_item_v0", "todo_id": "todo_provider", "index": 1,
+ "role": "agent", "status": "open", "done": False, "text": "provider Todo",
+ "task_class": "advancement_task", "archive_state": "active", "source_section": "Agent Todo",
+ }])
+ initialize_canonical_authority(runtime_root, "goal-a", projection, state_path=state_file)
state_file.unlink()
- monkeypatch.setattr(
- "loopx.control_plane.coordination.local_authority.effect_runtime_result",
- lambda method, params, **_kwargs: single_snapshot_page({
- "status": "loaded",
- "todos": [
- {
- "todo_id": "todo_provider",
- "role": "agent",
- "status": "open",
- "text": "provider Todo",
- }
- ],
- "todo_read_model": _todo_read_model(1),
- "provider_revision": "file:2",
- "cursor": "2",
- "source_authority": "file_v0",
- "decision_read_from_provider": True,
- "legacy_fallback_used": False,
- }),
- )
-
result = list_goal_todos(registry_path=registry_path, goal_id="goal-a")
assert result["source"] == "file_authority"
@@ -1255,7 +1245,7 @@ def test_canonical_hard_lease_claim_cli_atomically_acquires_ownership(
assert preview["status"] == "planned"
assert preview["todo_changed"] is True
assert preview["lease_changed"] is True
- assert list_goal_todos(registry_path=registry_path, goal_id="goal-a") == before
+ _assert_same_todo_content(list_goal_todos(registry_path=registry_path, goal_id="goal-a"), before)
applied = json.loads(
subprocess.run(
@@ -1298,7 +1288,7 @@ def invoke_edit(argv):
preview_edit = invoke_edit([*edit, "--dry-run"])
assert preview_edit.returncode == 0, preview_edit.stdout + preview_edit.stderr
assert json.loads(preview_edit.stdout)["status"] == "planned"
- assert list_goal_todos(registry_path=registry_path, goal_id="goal-a") == after
+ _assert_same_todo_content(list_goal_todos(registry_path=registry_path, goal_id="goal-a"), after)
first_edit = invoke_edit(edit)
assert first_edit.returncode == 0, first_edit.stdout + first_edit.stderr
assert json.loads(first_edit.stdout)["status"] == "applied"
diff --git a/tests/control_plane/test_monitor_followthrough_contract.py b/tests/control_plane/test_monitor_followthrough_contract.py
index 80ea23edd7..9f02e0ee47 100644
--- a/tests/control_plane/test_monitor_followthrough_contract.py
+++ b/tests/control_plane/test_monitor_followthrough_contract.py
@@ -1,5 +1,7 @@
from __future__ import annotations
+from canonical_authority_fixture import assert_todo_read_unchanged
+
import json
from pathlib import Path
@@ -1103,13 +1105,10 @@ def test_material_poll_user_followup_requires_explicit_class_without_writeback(
)
assert "todo_writeback" not in result
assert state.read_text(encoding="utf-8") == before_state
- assert (
- list_goal_todos(
+ assert_todo_read_unchanged(list_goal_todos(
registry_path=registry,
goal_id=GOAL_ID,
- )
- == before_todos
- )
+ ), before_todos)
def test_material_poll_explicit_user_gate_blocks_bound_agent(
diff --git a/tests/control_plane/test_native_todo_planning_update.py b/tests/control_plane/test_native_todo_planning_update.py
index 26f4c4ceb4..6e897c1f31 100644
--- a/tests/control_plane/test_native_todo_planning_update.py
+++ b/tests/control_plane/test_native_todo_planning_update.py
@@ -1,6 +1,8 @@
"""Real CLI and canonical FileAuthorityStore; no production registry or promotion."""
from __future__ import annotations
+from canonical_authority_fixture import assert_todo_read_unchanged
+
import json
import shlex
import subprocess
@@ -797,7 +799,7 @@ def test_public_cli_external_wait_uses_current_successor_source(
display = state.read_bytes() if state.exists() else None
rejected = update(registry, *args, ok=False)
assert "external-wait successor must be runnable, not resume-gated" in json.dumps(rejected)
- assert list_goal_todos(registry_path=registry, goal_id="goal-a") == source
+ assert_todo_read_unchanged(list_goal_todos(registry_path=registry, goal_id="goal-a"), source)
assert (state.read_bytes() if state.exists() else None) == display
return
update(registry, *args, "--dry-run")
diff --git a/tests/control_plane/test_quota_authority_settlement_journey.py b/tests/control_plane/test_quota_authority_settlement_journey.py
index 9e720c0161..e87a908641 100644
--- a/tests/control_plane/test_quota_authority_settlement_journey.py
+++ b/tests/control_plane/test_quota_authority_settlement_journey.py
@@ -1,6 +1,8 @@
"""Select current work and execute its returned settlement, using real stores/CLI."""
from __future__ import annotations
+from canonical_authority_fixture import assert_todo_read_unchanged
+
import json
import shlex
from datetime import datetime, timedelta, timezone
@@ -98,8 +100,8 @@ def test_canonical_todos_cannot_replace_missing_goal_intent(tmp_path, provider):
path.unlink()
# A read-only canonical Todo lookup still works; it cannot authorize a
# delivery whose Goal objective and constraints are unavailable.
- assert list_goal_todos(registry_path=registry, goal_id=cli.GOAL_ID,
- runtime_root_arg=str(runtime), todo_id=cli.TODO_ID) == before
+ assert_todo_read_unchanged(list_goal_todos(registry_path=registry, goal_id=cli.GOAL_ID,
+ runtime_root_arg=str(runtime), todo_id=cli.TODO_ID), before)
code, guard = _guard(project, runtime, registry)
assert code == 1
assert guard["should_run"] is False
@@ -107,8 +109,8 @@ def test_canonical_todos_cannot_replace_missing_goal_intent(tmp_path, provider):
assert cli._heartbeat_receipt_count(runtime, cli.TURN_ID) == 0
assert cli._spend_run_count(runtime) == 0
assert not path.exists()
- assert list_goal_todos(registry_path=registry, goal_id=cli.GOAL_ID,
- runtime_root_arg=str(runtime), todo_id=cli.TODO_ID) == before
+ assert_todo_read_unchanged(list_goal_todos(registry_path=registry, goal_id=cli.GOAL_ID,
+ runtime_root_arg=str(runtime), todo_id=cli.TODO_ID), before)
@pytest.mark.parametrize("provider", ["file", "sqlite"])
diff --git a/tests/control_plane/test_quota_settlement_cli.py b/tests/control_plane/test_quota_settlement_cli.py
index f4ad1af48c..ace3fae70e 100644
--- a/tests/control_plane/test_quota_settlement_cli.py
+++ b/tests/control_plane/test_quota_settlement_cli.py
@@ -1983,15 +1983,15 @@ def test_blocked_wait_transports_complete_lifecycle_facts_without_prose(
cadence="5m", expires_at="2026-01-02T00:00:00Z", watch_only=True)
source = {"agent_todos": {"items": [waiting, target]}, "user_todos": {"items": []}}
before = json.dumps(source, sort_keys=True)
- invoke = effect_runtime._request_with_info
+ invoke = effect_runtime.effect_runtime_request
requests = []
- def observe_request(info, **kwargs):
- if kwargs["method"] == "quota.settlement.read":
- requests.append(kwargs["params"])
- return invoke(info, **kwargs)
+ def observe_request(method, params, **kwargs):
+ if method == "quota.settlement.read":
+ requests.append(params)
+ return invoke(method, params, **kwargs)
- monkeypatch.setattr(effect_runtime, "_request_with_info", observe_request)
+ monkeypatch.setattr(effect_runtime, "effect_runtime_request", observe_request)
result = require_blocked_retry_wait(source, todo_id=TODO_ID, observed_at=observed,
allow_turn_settlement_retry=kind == "implicit")
assert len(requests) == 1
diff --git a/tests/control_plane/test_retained_team_plan_receipt.py b/tests/control_plane/test_retained_team_plan_receipt.py
index fa41ed520b..acc0789856 100644
--- a/tests/control_plane/test_retained_team_plan_receipt.py
+++ b/tests/control_plane/test_retained_team_plan_receipt.py
@@ -1,4 +1,7 @@
+
"""Recover a fixed 1.x receipt through the current public adapter."""
+
+from canonical_authority_fixture import assert_todo_read_unchanged
import json
from pathlib import Path
@@ -41,5 +44,5 @@ def replay(**overrides):
apply_team_plan(registry_path=registry, goal_id="goal-a", agent_id=None,
proposal={**history["plan"], "objective": "Changed original intent"},
expected_state_fingerprint=history["basis"])
- assert list_goal_todos(registry_path=registry, goal_id="goal-a") == before
+ assert_todo_read_unchanged(list_goal_todos(registry_path=registry, goal_id="goal-a"), before)
assert state.read_bytes() == display
diff --git a/tests/control_plane/test_settled_monitor_user_gate.py b/tests/control_plane/test_settled_monitor_user_gate.py
index f301ffedf9..64482db94f 100644
--- a/tests/control_plane/test_settled_monitor_user_gate.py
+++ b/tests/control_plane/test_settled_monitor_user_gate.py
@@ -1,6 +1,8 @@
"""Exact monitor closeout wins over an unrelated scoped user gate."""
from __future__ import annotations
+from canonical_authority_fixture import assert_todo_read_unchanged
+
import json
import pytest
@@ -162,7 +164,7 @@ def call(command):
before = list_goal_todos(registry_path=source, goal_id=GOAL_ID)
rc, denied = call("monitor-poll")
assert rc == 1 and denied["error_code"] == "monitor_poll_admission_rejected", denied
- assert list_goal_todos(registry_path=source, goal_id=GOAL_ID) == before
+ assert_todo_read_unchanged(list_goal_todos(registry_path=source, goal_id=GOAL_ID), before)
index = runtime / "goals" / GOAL_ID / "runs" / "index.jsonl"
assert not index.exists()
if gate_kind != "linked":
@@ -176,7 +178,7 @@ def call(command):
committed = list_goal_todos(registry_path=source, goal_id=GOAL_ID)
rc, replay = call("monitor-poll")
assert rc == 0 and replay["replayed"] is True and replay["appended"] is False
- assert list_goal_todos(registry_path=source, goal_id=GOAL_ID) == committed
+ assert_todo_read_unchanged(list_goal_todos(registry_path=source, goal_id=GOAL_ID), committed)
rows = [json.loads(line) for line in index.read_text().splitlines()]
assert [row["classification"] for row in rows] == ["quota_monitor_poll"]
diff --git a/tests/control_plane/test_todo_completion_state_runtime.py b/tests/control_plane/test_todo_completion_state_runtime.py
index 58fc60d0c9..a86d75510d 100644
--- a/tests/control_plane/test_todo_completion_state_runtime.py
+++ b/tests/control_plane/test_todo_completion_state_runtime.py
@@ -1,3 +1,4 @@
+"""Persisted completion spellings decode without a field-level runtime call."""
from __future__ import annotations
import pytest
@@ -6,37 +7,20 @@
from loopx.control_plane.todos import completion_state
-def _result(**values: object) -> dict[str, object]:
- return {
- "schema_version": completion_state.TODO_COMPLETION_STATE_RESULT_SCHEMA,
- **values,
- }
+@pytest.mark.parametrize(("raw", "expected"), [
+ ("YES", True), ("no-followup", True), ("FALSE", False), (False, False),
+ (None, None), ("unrecognized", None), ("\ufeffYES\ufeff", True), ("\x1cYES\x1c", None),
+])
+def test_metadata_spellings_do_not_start_the_runtime(monkeypatch, raw, expected):
+ def unexpected(*args, **kwargs):
+ raise AssertionError("metadata codecs must not call the runtime")
+ monkeypatch.setattr(effect_runtime, "effect_runtime_request", unexpected)
+ assert completion_state.normalize_todo_no_followup(raw) is expected
-def test_scalar_normalization_uses_bounded_process_cache(monkeypatch) -> None:
- calls = 0
- def call(_method: str, params: dict[str, object]) -> dict[str, object]:
- nonlocal calls
- calls += 1
- return _result(kind=params["kind"], value=True)
-
- completion_state._normalize_cached.cache_clear()
- monkeypatch.setattr(completion_state, "effect_runtime_result", call)
-
- assert completion_state.normalize_todo_no_followup("YES") is True
- assert completion_state.normalize_todo_no_followup("YES") is True
- assert calls == 1
- completion_state._normalize_cached.cache_clear()
-
-
-def test_python_facade_preserves_typed_rejection_as_value_error(monkeypatch) -> None:
- def reject(_method: str, _params: dict[str, object]) -> object:
- raise effect_runtime.EffectRuntimeRejected("typed state rejected")
-
- monkeypatch.setattr(completion_state, "effect_runtime_result", reject)
-
- with pytest.raises(ValueError, match="typed state rejected"):
- completion_state.require_todo_completion_metadata(
- "completion_continuation", "implicit",
- )
+def test_completion_metadata_rejects_unknown_continuation_without_inventing_recovery():
+ assert completion_state.require_todo_completion_metadata("completion_continuation", " successor ") == "successor"
+ assert completion_state.require_todo_completion_metadata("completion_recovery", "") is None
+ with pytest.raises(ValueError, match="completion_continuation must be one of"):
+ completion_state.require_todo_completion_metadata("completion_continuation", "implicit")
diff --git a/tests/control_plane/test_todo_consumer_scope.py b/tests/control_plane/test_todo_consumer_scope.py
index 5f3566af72..3730978a2f 100644
--- a/tests/control_plane/test_todo_consumer_scope.py
+++ b/tests/control_plane/test_todo_consumer_scope.py
@@ -89,51 +89,28 @@ def read(*args):
assert (state.read_bytes() if state.exists() else None) == before
-def test_filter_composes_with_existing_lane_call_and_rejects_downgraded_response(monkeypatch):
+def test_filter_uses_one_read_transaction_and_rejects_a_downgraded_response(monkeypatch):
from loopx.control_plane import effect_runtime
source = compact_todo_group([{"todo_id": "todo_one", "text": "Review result", "role": "user",
"status": "open", "task_class": "user_action", "bound_agent": "agent-a"}],
role="user", source_section="User Todo", item_limit=None)
original = effect_runtime.effect_runtime_result
requests = []
+
def track(method, request, **kwargs):
- if method == "todo.summary.project":
- requests.append(request)
+ requests.append((method, request))
return original(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_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"]
- assert all(len(cells) == len(requests[0]["columns"]) for cells in requests[0]["rows"])
+ assert len(requests) == 1 and requests[0][0] == "todo.read"
+ assert requests[0][1]["schema_version"] == "todo_read_request_v0"
+
def downgrade(method, request, **kwargs):
result = original(method, request, **kwargs)
- if method == "todo.summary.project":
- result.pop("source_indices", None)
+ result.pop("schema_version", None)
return result
- monkeypatch.setattr(effect_runtime, "effect_runtime_result", downgrade)
- with pytest.raises(ValueError, match="selection ordinals"):
- filtered_todo_summary(source, role="user", agent_id="agent-a")
-
-@pytest.mark.parametrize("corruption", ["duplicate", "boolean", "outside_selection"])
-def test_typed_lane_response_cannot_alias_or_escape_selected_source(monkeypatch, corruption):
- from loopx.control_plane import effect_runtime
- source = compact_todo_group([
- {"todo_id": "todo_owned", "text": "Review result", "role": "user", "status": "open",
- "task_class": "user_action", "bound_agent": "agent-a"},
- {"todo_id": "todo_other", "text": "Review peer result", "role": "user", "status": "open",
- "task_class": "user_action", "bound_agent": "agent-b"}],
- role="user", source_section="User Todo", item_limit=None)
- outside = next(index for index, item in enumerate(source["items"]) if item["todo_id"] == "todo_other")
- original = effect_runtime.effect_runtime_result
- def corrupt(method, request, **kwargs):
- result = original(method, request, **kwargs)
- if method == "todo.summary.project":
- result["lanes"]["first_open_items"]["indices"] = {"duplicate": [0, 0], "boolean": [False], "outside_selection": [outside]}[corruption]
- return result
- monkeypatch.setattr(effect_runtime, "effect_runtime_result", corrupt)
- with pytest.raises(ValueError, match="source ordinal|escaped the selected source"):
+ monkeypatch.setattr(effect_runtime, "effect_runtime_result", downgrade)
+ with pytest.raises(ValueError, match="invalid typed Todo read result"):
filtered_todo_summary(source, role="user", agent_id="agent-a")
diff --git a/tests/control_plane/test_todo_conversation_context.py b/tests/control_plane/test_todo_conversation_context.py
index cbbc506dd5..ed6382f7cd 100644
--- a/tests/control_plane/test_todo_conversation_context.py
+++ b/tests/control_plane/test_todo_conversation_context.py
@@ -1,5 +1,8 @@
+
"""Conversation evidence reads use real canonical providers without granting work."""
+from canonical_authority_fixture import assert_todo_read_unchanged
+
import json
import subprocess
import sys
@@ -54,7 +57,7 @@ def test_common_context_recovers_constraints_from_canonical_authority(tmp_path,
allowed[0] = False
assert not tool.read(CONTEXT_TOOL_NAME, {"view": "todos", "goal_id": "goal-a"})["ok"]
assert len(observed) == 4
- assert list_goal_todos(registry_path=registry, goal_id="goal-a") == canonical
+ assert_todo_read_unchanged(list_goal_todos(registry_path=registry, goal_id="goal-a"), canonical)
assert not state.exists()
# The shipped CLI uses the same exact selector but an external audience.
diff --git a/tests/control_plane/test_todo_list_explicit_limit.py b/tests/control_plane/test_todo_list_explicit_limit.py
index 9989027c4f..f10a7d33f8 100644
--- a/tests/control_plane/test_todo_list_explicit_limit.py
+++ b/tests/control_plane/test_todo_list_explicit_limit.py
@@ -201,8 +201,7 @@ def test_default_payload_keeps_the_pre_limit_shape(tmp_path: Path) -> None:
payload = list_goal_todos(registry_path=registry_path, goal_id=GOAL_ID)
- # The no-limit cold path is byte-identical to the pre---limit shape:
- # no explicit-limit disclosure fields appear.
+ # The cold view retains its fields and adds the required read provenance.
assert set(payload) == {
"ok",
"dry_run",
@@ -218,7 +217,9 @@ def test_default_payload_keeps_the_pre_limit_shape(tmp_path: Path) -> None:
"project",
"user_todos",
"agent_todos",
+ "projection_envelope",
}
+ assert payload["projection_envelope"]["coverage"]["expected_count"] == 16
assert payload["todo_count"] == 16
assert len(payload["todos"]) == 16
for absent_key in (
diff --git a/tests/control_plane/test_todo_list_read_transport.py b/tests/control_plane/test_todo_list_read_transport.py
index a3033c5d95..f44d48f37d 100644
--- a/tests/control_plane/test_todo_list_read_transport.py
+++ b/tests/control_plane/test_todo_list_read_transport.py
@@ -3,15 +3,19 @@
import json
import os
-from pathlib import Path
import subprocess
import sys
+from pathlib import Path
import pytest
+from canonical_authority_fixture import (
+ initialize_canonical_authority,
+ isolate_sqlite_runtime,
+)
-from canonical_authority_fixture import initialize_canonical_authority, isolate_sqlite_runtime
-from loopx.control_plane.coordination.runtime_shadow import build_todo_runtime_shadow_projection
-
+from loopx.control_plane.coordination.runtime_shadow import (
+ build_todo_runtime_shadow_projection,
+)
REPO = Path(__file__).resolve().parents[2]
GOAL = "read-transport"
@@ -68,8 +72,8 @@ def run(*tail, format_name="json", expected_exit=0, isolated=False):
yield request.param, run, prefix, state
subprocess.run(
- [sys.executable, "-c", "from loopx.control_plane.effect_runtime import effect_runtime_result;"
- "effect_runtime_result('runtime.shutdown', {}, retry_safe=False)"],
+ [sys.executable, "-c", ("from loopx.control_plane.effect_runtime import effect_runtime_result;"
+ "effect_runtime_result('runtime.shutdown', {}, retry_safe=False)")],
cwd=REPO, capture_output=True, text=True, timeout=30, check=True,
)
@@ -107,7 +111,7 @@ def test_read_filters_errors_and_fresh_provider(read_cli):
added = subprocess.run(
[sys.executable, "-m", "loopx.cli", *prefix, "--format", "json", "todo", "add",
"--goal-id", GOAL, "--role", "agent", "--text", "Fresh canonical requirement",
- "--claimed-by", "agent-a"], cwd=REPO, capture_output=True, text=True, timeout=45,
+ "--claimed-by", "agent-a"], cwd=REPO, capture_output=True, text=True, timeout=45, check=False,
)
assert added.returncode == 0, added.stdout + added.stderr
fresh = run()
@@ -123,6 +127,21 @@ def test_read_filters_errors_and_fresh_provider(read_cli):
assert fresh["authority_read"]["legacy_fallback_used"] is False
+def test_display_limits_report_complete_sources_and_actual_shown_items(read_cli):
+ _, run, _, _ = read_cli
+ for options, available in [((), 3), (("--agent-id", "agent-a"), 2),
+ (("--limit", "1"), 3), (("--thin",), 3)]:
+ payload = run(*options)
+ envelope = payload["projection_envelope"]
+ coverage = envelope["coverage"]
+ assert envelope["alert"] is False
+ assert coverage["complete"] is True and coverage["omitted"] == []
+ assert coverage["expected_count"] == coverage["included_count"] == available
+ assert coverage["available_count"] == available
+ assert coverage["shown_count"] == len(payload["todos"])
+ assert coverage["truncated"] == (len(payload["todos"]) < available)
+
+
def test_read_entrypoint_does_not_load_mutation_owners(read_cli):
_, run, _, _ = read_cli
assert run(isolated=True)["todo_count"] == 3
@@ -170,5 +189,5 @@ def test_todo_help_does_not_load_mutation_owners():
"\nassert 'loopx.state_refresh' not in sys.modules"
)
result = subprocess.run([sys.executable, "-c", script], cwd=REPO,
- capture_output=True, text=True, timeout=45)
+ capture_output=True, text=True, timeout=45, check=False)
assert result.returncode == 0, result.stdout + result.stderr
diff --git a/tests/control_plane/test_todo_list_record_references.py b/tests/control_plane/test_todo_list_record_references.py
index 9a067cc0d3..7ad53db447 100644
--- a/tests/control_plane/test_todo_list_record_references.py
+++ b/tests/control_plane/test_todo_list_record_references.py
@@ -47,17 +47,17 @@ def cli(registry, runtime, flags, output_format="json"):
def test_projection_failure_returns_the_standard_cli_error_without_source_changes(tmp_path, monkeypatch, capsys):
from loopx.cli import main
- from loopx.cli_commands import todo_list as todo_command
+ from loopx.control_plane import effect_runtime
isolate_sqlite_runtime(tmp_path, monkeypatch)
registry, state = _write_fixture(tmp_path)
before = state.read_bytes()
def unavailable(operation, payload, **kwargs):
- assert operation == "todo.context.page" and payload["list_payload"]["ok"]
+ assert operation == "todo.read" and payload["kind"] == "query"
raise RuntimeError("Todo projection unavailable")
- monkeypatch.setattr(todo_command, "effect_runtime_result", unavailable)
+ monkeypatch.setattr(effect_runtime, "effect_runtime_result", unavailable)
result = main(["--registry", str(registry), "todo", "list", "--format", "json",
"--goal-id", GOAL_ID])
error = json.loads(capsys.readouterr().out)
@@ -91,6 +91,10 @@ def test_real_cli_references_reconstruct_all_scoped_lanes_and_counts(tmp_path, m
]:
native = list_goal_todos(registry_path=registry, goal_id=GOAL_ID, **options)
wire = cli(registry, runtime, flags)
+ native_envelope = native.pop("projection_envelope")
+ wire_envelope = wire.pop("projection_envelope")
+ assert native_envelope["schema_version"] == wire_envelope["schema_version"] == "loopx_projection_envelope_v0"
+ assert native_envelope["coverage"] == wire_envelope["coverage"]
disclosure = wire.pop("todo_list_record_references")
assert disclosure["replaced_view_records"] > 0
assert expand_references(wire, wire) == native
@@ -101,7 +105,9 @@ def test_real_cli_references_reconstruct_all_scoped_lanes_and_counts(tmp_path, m
assert cli(registry, runtime, flags, "markdown") == render_todo_markdown(native) + "\n"
thin = cli(registry, runtime, ["--thin"])
assert "todo_list_record_references" not in thin
- assert thin == list_goal_todos(registry_path=registry, goal_id=GOAL_ID, thin=True)
+ direct_thin = list_goal_todos(registry_path=registry, goal_id=GOAL_ID, thin=True)
+ assert thin.pop("projection_envelope")["coverage"] == direct_thin.pop("projection_envelope")["coverage"]
+ assert thin == direct_thin
exact = cli(registry, runtime, ["--todo-id", "todo_agent_current"])
assert "todo_list_record_references" not in exact
assert exact["todo"]["text"] == "Execute the current agent advancement."
diff --git a/tests/control_plane/test_todo_list_thin.py b/tests/control_plane/test_todo_list_thin.py
index 17c7cdf236..45c12038ed 100644
--- a/tests/control_plane/test_todo_list_thin.py
+++ b/tests/control_plane/test_todo_list_thin.py
@@ -12,9 +12,7 @@
measure_cli_output,
)
from loopx.control_plane.todos.contract import encode_metadata_value
-from loopx.control_plane.todos.list_projection import (
- compact_thin_todo_list_payload,
-)
+from loopx.control_plane.todos.goal_todo_projection import project_todo_read
from loopx.control_plane.todos.markdown import render_todo_markdown
from loopx.todos import list_goal_todos
@@ -496,15 +494,28 @@ def test_thin_intrinsically_bounds_high_cardinality_and_reports_overflow(
)
-def test_thin_widest_retained_shape_stays_inside_fixed_output_budget() -> None:
- payload = compact_thin_todo_list_payload(
- _widest_retained_thin_payload(items_per_role=5)
- )
+def test_thin_widest_retained_shape_stays_inside_fixed_output_budget(tmp_path: Path) -> None:
+ source = _widest_retained_thin_payload(items_per_role=5)
+ groups = [{"role": role, "source_section": f"{role.title()} Todo",
+ "items": source[f"{role}_todos"]["items"], "include_empty_source": True}
+ for role in ("user", "agent")]
+ payload = project_todo_read({
+ "kind": "query", "source": {"runtime_root": str(tmp_path), "goal_id": GOAL_ID},
+ "observed_at": 1767225600, "evaluated_at": "2026-01-01T00:00:00Z",
+ "legacy_source": {"groups": groups, "lineage": [item for group in groups for item in group["items"]],
+ "source_count": 10},
+ "query": {"goal_id": GOAL_ID, "read_scope": "active", "thin": True,
+ "state_file": str(tmp_path / "state.md")},
+ })["payload"]
assert payload["returned_todo_count"] == 4
assert payload["omitted_todo_count"] == 6
assert payload["todo_list_field_projection"]["item_limit_per_role"] == 2
assert all("title" not in item for item in payload["todos"])
+ coverage = payload["projection_envelope"]["coverage"]
+ assert coverage["complete"] is True
+ assert coverage["included_count"] == coverage["available_count"] == 10
+ assert coverage["shown_count"] == 4 and coverage["truncated"] is True
spec = CLI_OUTPUT_MODE_VARIANT_BY_ID["todo_list_thin"]
rendered_by_format = {
diff --git a/tests/control_plane/test_todo_summary_projection.py b/tests/control_plane/test_todo_summary_projection.py
index bd62cf4394..d1bbe59500 100644
--- a/tests/control_plane/test_todo_summary_projection.py
+++ b/tests/control_plane/test_todo_summary_projection.py
@@ -84,12 +84,12 @@ def test_a_selection_cannot_restore_a_lost_full_source_proof():
def test_long_history_stays_inside_the_runtime_request_budget(monkeypatch):
"""A whole-source batch must not outgrow the co-deployed runtime's request."""
from loopx.control_plane import effect_runtime
- from loopx.control_plane.effect_runtime import MAX_REQUEST_BYTES
+ from loopx.control_plane.effect_runtime import MAX_LOCAL_SNAPSHOT_BYTES
requests = []
original = effect_runtime.effect_runtime_result
def track(method, request, **kwargs):
- if method == "todo.summary.project":
+ if method == "todo.read":
requests.append(request)
return original(method, request, **kwargs)
@@ -100,7 +100,7 @@ def track(method, request, **kwargs):
assert result["done_count"] == 4096
request = requests[0]
encoded = json.dumps(request, separators=(",", ":")).encode()
- assert len(encoded) < MAX_REQUEST_BYTES
+ assert len(encoded) < MAX_LOCAL_SNAPSHOT_BYTES
def test_no_wait_skips_resume_preparation_but_keeps_archived_successor(monkeypatch):
@@ -194,7 +194,7 @@ def track(method, request, **kwargs):
monkeypatch.setattr(succession_warning, "effect_runtime_result", track)
selected = filtered_todo_summary(source, role="agent", todo_id="todo_summary_1")
assert selected["done_count"] == 1 and not selected.get("completed_without_successor_count")
- assert calls.count("todo.summary.project") == 1
+ assert calls.count("todo.read") == 1
assert "todo.succession.project" not in calls
next(item for item in source["items"] if item["todo_id"] == "todo_summary_1")["no_followup"] = True
import pytest
@@ -237,7 +237,7 @@ def track(method, request, **kwargs):
monkeypatch.setattr(effect_runtime, "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 calls == ["todo.read"]
assert not hasattr(frontier_revision, "attach_advancement_frontier_revision_index")
assert not hasattr(frontier_revision, "build_advancement_frontier_revision_index")
@@ -251,9 +251,9 @@ def test_missing_batched_frontier_never_falls_back_to_another_owner_call(monkeyp
original = effect_runtime.effect_runtime_result
def drop_index(method, request, **kwargs):
- assert method == "todo.summary.project"
+ assert method == "todo.read"
result = original(method, request, **kwargs)
- result["fields"].pop("advancement_frontier_revision_index")
+ result["summary"].pop("advancement_frontier_revision_index")
return result
monkeypatch.setattr(effect_runtime, "effect_runtime_result", drop_index)
diff --git a/tests/control_plane_ts/todo_completion_state.test.ts b/tests/control_plane_ts/todo_completion_state.test.ts
index 03c7668f7e..0f722fb7ce 100644
--- a/tests/control_plane_ts/todo_completion_state.test.ts
+++ b/tests/control_plane_ts/todo_completion_state.test.ts
@@ -4,8 +4,10 @@ import test from "node:test";
import {
buildTodoCompletionMetadataUpdates,
- normalizeTodoCompletionValue,
- requireTodoCompletionMetadataValue,
+ normalizeTodoNoFollowup,
+ normalizeTodoCompletionContinuation,
+ normalizeTodoCompletionRecovery,
+ requireTodoCompletionMetadata,
selectTodoCompletionState,
TODO_COMPLETION_STATE_REQUEST_SCHEMA,
} from "../../loopx/control_plane/todos/completion_state.ts";
@@ -35,14 +37,22 @@ const fixture = JSON.parse(
}>;
};
+function normalizeValue(request: Record) {
+ const value = request.kind === "no_followup" ? normalizeTodoNoFollowup(request.value) :
+ request.kind === "continuation" ? normalizeTodoCompletionContinuation(request.value) : normalizeTodoCompletionRecovery(request.value);
+ return {schema_version: "loopx_todo_completion_state_result_v0", kind: request.kind, value};
+}
+function requireValue(request: Record) {
+ return {schema_version: "loopx_todo_completion_state_result_v0", value: requireTodoCompletionMetadata(String(request.key), request.value)};
+}
const methods = {
- normalize: normalizeTodoCompletionValue,
- require_metadata: requireTodoCompletionMetadataValue,
+ normalize: normalizeValue,
+ require_metadata: requireValue,
evaluate: selectTodoCompletionState,
metadata_updates: buildTodoCompletionMetadataUpdates,
} as const;
-test("pinned Python completion-state characterization remains exact", () => {
+test("persisted completion spellings and lifecycle compatibility remain exact", () => {
assert.equal(
fixture.schema_version,
"loopx_todo_completion_state_characterization_v0",
@@ -86,22 +96,12 @@ test("whole completion owner selects continuation and rejects contradictory inte
}), /cannot record both no_followup and a successor/);
});
-test("retired continuation-only RPC fails closed; live metadata decoding remains", async () => {
- const handlers = createEffectRuntimeHandlers({
- fingerprint: "completion-retirement-test", requestShutdown: () => undefined,
- });
- await assert.rejects(dispatchEffectRuntimeMethod(
- handlers, "todo.completion_state.continuation_for_write", {
- schema_version: TODO_COMPLETION_STATE_REQUEST_SCHEMA,
- no_followup: false, has_successor: false,
- },
- ), /unsupported Effect runtime method/);
- assert.deepEqual(await dispatchEffectRuntimeMethod(
- handlers, "todo.completion_state.require_metadata", {
- schema_version: TODO_COMPLETION_STATE_REQUEST_SCHEMA,
- key: "completion_continuation", value: "SUCCESSOR",
- },
- ), { schema_version: "loopx_todo_completion_state_result_v0", value: "successor" });
+test("retired scalar and summary RPCs fail closed", async () => {
+ const handlers = createEffectRuntimeHandlers({fingerprint: "completion-retirement-test", requestShutdown: () => undefined});
+ for (const method of ["todo.completion_state.continuation_for_write", "todo.completion_state.normalize",
+ "todo.completion_state.require_metadata", "todo.summary.project"]) {
+ await assert.rejects(dispatchEffectRuntimeMethod(handlers, method, {}), /unsupported Effect runtime method/);
+ }
});
test("runtime boundary rejects malformed authority input", () => {
diff --git a/tests/control_plane_ts/todo_read.test.ts b/tests/control_plane_ts/todo_read.test.ts
new file mode 100644
index 0000000000..6aea33361e
--- /dev/null
+++ b/tests/control_plane_ts/todo_read.test.ts
@@ -0,0 +1,172 @@
+import assert from "node:assert/strict";
+import {mkdtemp, writeFile, rm} from "node:fs/promises";
+import {tmpdir} from "node:os";
+import {join} from "node:path";
+import test from "node:test";
+import {readTodo, TODO_READ_REQUEST} from "../../loopx/control_plane/todos/read_projection.ts";
+import type {JsonObject} from "../../loopx/control_plane/effect_program.ts";
+import {openLocalAuthorityStore, selectLocalSqliteAuthority} from "../../loopx/control_plane/coordination/local_authority_provider.ts";
+import {engageLegacyCoordinationWriterFence} from "../../loopx/control_plane/coordination/legacy_writer_fence.ts";
+import {canonicalAuthoritySha256} from "../../loopx/control_plane/coordination/authority_store_codec.ts";
+import {coordinationTodoReadModel} from "../../loopx/control_plane/coordination/coordination_projection.ts";
+import {TODO_CANONICAL_READ_RECORD_SCHEMA} from "../../loopx/control_plane/coordination/coordination_state_contract.ts";
+
+const now = "2026-10-19T07:00:00Z";
+const row = (todo_id: string, extra: JsonObject = {}): JsonObject => ({schema_version: "todo_item_v0", todo_id,
+ index: 1, role: "agent", status: "open", done: false, text: "Review accepted evidence", task_class: "advancement_task",
+ archive_state: "active", source_section: "Agent Todo", ...extra});
+const group = (items: JsonObject[], extra: JsonObject = {}) => readTodo({schema_version: TODO_READ_REQUEST,
+ kind: "group", items, lineage: [], evaluated: false, role: "agent", source_section: "Agent Todo",
+ item_limit: 1, observed_at: Date.parse(now) / 1000, evaluated_at: now, ...extra});
+
+test("current acceptance checks survive full-source reads and selection without an enable flag", async () => {
+ const acceptance = {criteria: [], revision: 1, digest: "sha256:" + "a".repeat(64)};
+ const fields = await readTodo({schema_version: TODO_READ_REQUEST, kind: "fields",
+ todos: [row("todo_guarded")], goal_acceptance_contract: acceptance,
+ goal_acceptance_work_guards: {todo_guarded: {allowed: false}},
+ observed_at: Date.parse(now) / 1000, evaluated_at: now});
+ const summary = (fields.fields as JsonObject).agent_todos as JsonObject;
+ assert.deepEqual(summary.goal_acceptance_contract, acceptance);
+ assert.equal(((summary.items as JsonObject[])[0].goal_acceptance_guard as JsonObject).allowed, false);
+ const selected = await readTodo({schema_version: TODO_READ_REQUEST, kind: "select",
+ fields: fields.fields, field_evaluations: fields.field_evaluations, roles: ["agent"],
+ source_name: "file_authority", observed_at: Date.parse(now) / 1000, evaluated_at: now});
+ const summaries = (selected.projection as JsonObject).summaries as JsonObject;
+ assert.deepEqual((summaries.agent_todos as JsonObject).goal_acceptance_contract, acceptance);
+});
+
+for (const provider of ["file", "sqlite"] as const) {
+ test(`${provider} full read keeps archived dependencies and fresh provider results without Markdown`, async t => {
+ const root = await mkdtemp(join(tmpdir(), "loopx-todo-read-"));
+ t.after(() => rm(root, {recursive: true, force: true}));
+ const state = join(root, "state.md"), goal = "goal-a";
+ await writeFile(state, "# Synthetic Goal\n");
+ if (provider === "sqlite") await selectLocalSqliteAuthority(root, goal, true);
+ const store = await openLocalAuthorityStore(root, goal);
+ const body = "Verify the original acceptance requirement 😀. ".repeat(40);
+ const todos = [row("todo_waiting", {status: "deferred", done: true, text: body,
+ resume_when: "todo_done:todo_archived"}), row("todo_archived", {role: "user", status: "done", done: true,
+ task_class: "user_action", archive_state: "archive", source_section: "Completed Work Archive"})]
+ .sort((a, b) => String(a.todo_id).localeCompare(String(b.todo_id)));
+ const projection = {goal_id: goal, handoff_mode: "soft_claim", todos, leases: [],
+ todo_read_model: coordinationTodoReadModel(todos, TODO_CANONICAL_READ_RECORD_SCHEMA)};
+ const committed = await store.commitAuthority({expected_provider_revision: null, operation_id: "seed",
+ events: [], next_projection: projection, receipts: []});
+ assert.equal(committed.status, "applied");
+ if (committed.status !== "applied") return;
+ const fence = await engageLegacyCoordinationWriterFence({schema_version: "loopx_legacy_coordination_writer_fence_engage_request_v0",
+ runtime_root: root, goal_id: goal, state_path: state, fence: {schema_version: "loopx_legacy_coordination_writer_fence_v0",
+ state: "engaged", goal_id: goal, fence_id: "fixture", source_version: "fixture",
+ source_projection_sha256: canonicalAuthoritySha256(projection), expected_shadow_provider_revision: committed.provider_revision}});
+ assert.equal(fence.status, "applied");
+ await writeFile(state, new Uint8Array([255]));
+ const request = {schema_version: TODO_READ_REQUEST, kind: "query", source: {runtime_root: root, goal_id: goal},
+ observed_at: Date.parse(now) / 1000, evaluated_at: now,
+ query: {goal_id: goal, role: null, status: null, todo_id: "todo_waiting", agent_id: null, limit: null,
+ read_scope: "active", json_references: true, thin: false, state_file: state, project: root}};
+ const result = await readTodo(request), payload = result.payload as JsonObject;
+ assert.ok(payload, JSON.stringify(result));
+ assert.equal(payload.read_only, true);
+ assert.equal((payload.todo as JsonObject).text, body);
+ assert.equal((payload.todo as JsonObject).resume_ready, true);
+ assert.equal((payload.authority_read as JsonObject).provider_revision, committed.provider_revision);
+ assert.equal((await store.loadAuthority()).status, "loaded");
+ const laterTodos = [row("todo_waiting", {status: "blocked", text: "Changed current requirement"}), todos.find(item => item.todo_id === "todo_archived")!]
+ .sort((a, b) => String(a.todo_id).localeCompare(String(b.todo_id)));
+ const changed = {...projection, todos: laterTodos,
+ todo_read_model: coordinationTodoReadModel(laterTodos, TODO_CANONICAL_READ_RECORD_SCHEMA)};
+ const later = await store.commitAuthority({expected_provider_revision: committed.provider_revision,
+ operation_id: "later", events: [], next_projection: changed, receipts: []});
+ assert.equal(later.status, "applied");
+ const reread = (await readTodo(request)).payload as JsonObject;
+ assert.equal((reread.todo as JsonObject).status, "blocked");
+ assert.equal((reread.todo as JsonObject).text, "Changed current requirement");
+ assert.notEqual((reread.authority_read as JsonObject).provider_revision, committed.provider_revision);
+ });
+}
+
+test("display limits retain full-source closure and reject altered evaluation evidence", async () => {
+ const items = Array.from({length: 4096}, (_, i) => row(`todo_completed_${i}`, {index: i + 1,
+ status: "done", done: true, no_followup: true}));
+ const result = await group(items), summary = result.summary as JsonObject;
+ assert.equal(summary.total_count, 4096);
+ assert.equal((summary.items as unknown[]).length, 1);
+ assert.equal((summary.terminal_closure_proof as JsonObject).item_count, 4096);
+ const first = await group([row("todo_owned", {status: "done", done: true, claimed_by: "agent-a"})]);
+ const changed = {...(first.items as JsonObject[])[0], no_followup: true};
+ await assert.rejects(() => group([changed], {evaluated: true, evaluations: first.evaluations}), /matching full-source/);
+});
+
+test("controller validation commands remain private while their declaration identity survives", async () => {
+ const result = await readTodo({schema_version: TODO_READ_REQUEST, kind: "fields",
+ todos: [row("todo_validation", {validation_command: " printf synthetic ", validation_label: "check"})],
+ observed_at: Date.parse(now) / 1000, evaluated_at: now});
+ const summary = (result.fields as JsonObject).agent_todos as JsonObject;
+ const item = (summary.items as JsonObject[])[0]!;
+ assert.equal(item.completion_validation_required, true);
+ // Pinned to the independent Python declaration codec, including null fields.
+ assert.equal(item.completion_validation_sha256, "31a837db75ed8cf0143228f0d3e94dae3886f0e79c496f7f4c335644f4c04020");
+ assert.equal(JSON.stringify(result).includes("printf synthetic"), false);
+});
+
+test("a receiver view cannot include a peer's addressed work in any selected lane", async () => {
+ const first = await group([row("todo_owned", {role: "user", task_class: "user_action", bound_agent: "agent-a"}),
+ row("todo_peer", {role: "user", task_class: "user_action", bound_agent: "agent-b", index: 2})],
+ {role: "user", source_section: "User Todo", item_limit: null});
+ const selected = await group(first.items as JsonObject[], {evaluated: true, evaluations: first.evaluations,
+ role: "user", source_section: "User Todo", item_limit: null,
+ selection: {role: "user", status: null, todo_id: null, agent_id: "agent-a"}});
+ assert.deepEqual((selected.items as JsonObject[]).map(item => item.todo_id), ["todo_owned"]);
+ for (const value of Object.values(selected.summary as JsonObject)) {
+ if (Array.isArray(value)) for (const item of value as JsonObject[]) {
+ if (item.todo_id) assert.equal(item.todo_id, "todo_owned");
+ }
+ }
+});
+
+test("source capture selects an evaluated view without reusing unrelated lineage carriers", async () => {
+ const item = row("todo_source_capture");
+ const result = await readTodo({schema_version: TODO_READ_REQUEST, kind: "source_fields",
+ groups: [{role: "agent", source_section: "Agent Todo", items: [item], include_empty_source: true}],
+ lineage: [item], evaluated_at: now, observed_at: Date.parse(now) / 1000,
+ projection_query: {roles: ["agent"], status: null, todo_id: null, agent_id: null, limit: null, source_name: "markdown_active_state"}});
+ const projected = result.projection as JsonObject;
+ assert.equal(projected.uncapped_todo_count, 1);
+ assert.equal((projected.todos as JsonObject[])[0]!.todo_id, item.todo_id);
+});
+
+test("query display limits preserve complete source coverage and report the final view", async t => {
+ const root = await mkdtemp(join(tmpdir(), "loopx-todo-coverage-"));
+ t.after(() => rm(root, {recursive: true, force: true}));
+ const items = Array.from({length: 30}, (_, i) => row(`todo_coverage_${i}`, {
+ index: i + 1, role: i < 15 ? "user" : "agent", task_class: i < 15 ? "user_action" : "advancement_task",
+ }));
+ const legacy = {groups: ["user", "agent"].map(role => ({role, source_section: `${role} Todo`,
+ items: items.filter(item => item.role === role), include_empty_source: true})),
+ lineage: items, full_bodies: items, source_count: items.length};
+ const cases = [
+ {name: "ordinary", query: {}, available: 30, shown: 30},
+ {name: "hot", query: {agent_id: "agent-a"}, available: 30, shown: 24},
+ {name: "limited", query: {limit: 1}, available: 30, shown: 2},
+ {name: "thin", query: {thin: true}, available: 30, shown: 4},
+ {name: "references", query: {json_references: true, limit: 1}, available: 30, shown: 2},
+ {name: "exact", query: {todo_id: items[0].todo_id}, available: 1, shown: 1},
+ {name: "missing", query: {todo_id: "todo_missing"}, available: 0, shown: 0},
+ ];
+ for (const scenario of cases) await t.test(scenario.name, async () => {
+ const read = await readTodo({schema_version: TODO_READ_REQUEST, kind: "query",
+ source: {runtime_root: root, goal_id: "coverage"}, legacy_source: legacy,
+ observed_at: Date.parse(now) / 1000, evaluated_at: now,
+ query: {goal_id: "coverage", read_scope: "active", state_file: join(root, "state.md"), ...scenario.query}}, now);
+ const payload = read.payload as JsonObject, envelope = payload.projection_envelope as JsonObject;
+ const coverage = envelope.coverage as JsonObject;
+ assert.equal(envelope.alert, false);
+ assert.equal(coverage.complete, true);
+ assert.equal(coverage.expected_count, scenario.available);
+ assert.equal(coverage.included_count, scenario.available);
+ assert.deepEqual(coverage.omitted, []);
+ assert.equal(coverage.shown_count, scenario.shown);
+ assert.equal(coverage.available_count, scenario.available);
+ assert.equal(coverage.truncated, scenario.shown < scenario.available);
+ });
+});