diff --git a/docs/architecture/rfcs/monorepo-distribution-split-v0.md b/docs/architecture/rfcs/monorepo-distribution-split-v0.md index 28b5aad408..23028ad4ec 100644 --- a/docs/architecture/rfcs/monorepo-distribution-split-v0.md +++ b/docs/architecture/rfcs/monorepo-distribution-split-v0.md @@ -1822,6 +1822,109 @@ performance holds remain separate. Full source/wheel correctness and a matched, unprofiled default-SQLite whole-path comparison are still required for a new implementation; all earlier milestone, adoption and complete-cost gaps remain. +### 2026-11-07 — Reuse complete Todo display sources within a managed request + +The existing `todo.context.page` owner can compose summary, planning and retained +terminal display views from one complete supplied source. The quota preparation +caller explicitly requests the Agent source's three views and passes retained +terminal rows to its frontier consumer. User-source and general display callers +keep their requested single view. Source selection and each display rule retain +their existing owners. The orchestration fallback reuses an independent copy of +the already selected summary instead of requesting it again. + +Inside an already revision-pinned logical request, exact source JSON and request +options bound the transport memo. Changed source facts and later requests +revalidate; mutable callers receive independent decoded values. Large opt-in +inputs keep the original per-view transport, and incomplete bundles fail before +memo admission. The batch input threshold does not predict response size or +promise inline transport. An explicit multi-view request can use the existing +private response snapshot when the combined output exceeds the socket limit. + +Independent review of the initial proposal found that the generic transport +expanded single-view calls into three views. An 8,000-row delivery-response +caller retained the same final output but materialized about 11.18 MB of unused +combined response instead of its roughly 3.73 MB single-view socket response. +That performance failure is retained. The corrected caller-owned composition +removes the implicit expansion; matching single-view and complete governed +source/wheel qualification are recorded with the revision being proposed. + +The initial proposal against `main@32167518b3` passed six synthetic source and isolated-wheel +101-Todo File/SQLite governed CLI lifecycles. They retained the verified artifact, +original completion time, four settlement receipts, one debit, Effect restart +recovery and rejected evidence replacement. A dropped write-scope response +mutation fails at the independent public-guard scope check. Frozen display +fixtures and focused source-isolation tests also cover the existing three views. +One profiled SQLite guard reduced JSON parameter bytes from about 1.900 MB to +1.556 MB and calls from 89 to 87; loaded modules remained 503. These counts are +diagnostic and do not qualify complete wire cost, latency or long-horizon benefit. + + +The corrected single-call comparison uses the same synthetic source on +`main@dee549663`, the reviewed defective head and the repaired source. All three +produce the same final delivery response. The repaired request parameters and serialized response +bytes match main exactly; its nonempty response snapshot bytes are +zero for both row counts. Five warm calls per arm gave the following exploratory +medians, including the display and delivery-response projections: + +| Rows | Main seconds | Defective proposal seconds | Repair seconds | Defective response snapshot bytes | Repair snapshot bytes | +| --- | --- | --- | --- | --- | --- | +| 8,000 | 0.159 | 0.636 | 0.161 | 9,480,165 | 0 | +| 15,000 | 0.284 | 1.344 | 0.286 | 17,775,165 | 0 | + +This reproduces and removes the single-view regression. It does not qualify the +complete CLI, daemon CPU/memory, full marginal cost or long-horizon benefit. + + +The repaired source and isolated wheel pass six 101-Todo File/SQLite governed +CLI lifecycles with the verified artifact, original completion time, four +receipts, one debit, restart and refusal/replay checks. A recursive dropped-scope +response mutation fails at the independent public guard oracle. The isolated +PostgreSQL 16.15 suites pass 390 store and 11 service cases with zero skipped. + +Three interleaved unprofiled default-SQLite whole-CLI pairs have median wall time +12.24 seconds on main and 12.71 seconds on the repair. Two candidate runs are +slower; ranges overlap. A separate profiled guard reduces parameter bytes from +1,899,603 to 1,546,162 and calls from 90 to 87, with 503 modules unchanged. +These observations do not establish a whole-path performance benefit. Daemon +CPU/memory, total wire/file writes, complete marginal cost and adoption remain +unqualified; the provider-first transaction remains the next accepted boundary. + +This is a reversible display-transport increment, not the complete provider-first +Todo projection transaction. Provider reads, revisions, lease/actor fences and +state writes retain their existing owners. The remaining canonical-source +transaction, matched complete-cost qualification and affected backend acceptance +stay open. I3 remains at least one minor release; M1/#8011, full R0/V10/R1, +D2/D5/D6/D3, context/model/split-install, adoption and all frozen failures remain. + +#### Remaining provider-first transaction boundary + +The next implementation must compose the existing provider-admitted +`canonicalTodoCollection` and projection owners. The native snapshot owner binds +Goal, store incarnation, provider revision, cursor and query to complete rows, +leases, read-model validation and acceptance guards. A continuation is a position +within that read, not a reusable current-head handle; a changed snapshot restarts +the complete read. Displaying a retained source does not obtain fresh authority. + +`quota_selection.py` still sends a complete summary source alongside encoded +selection rows and source-row facts. Its request also includes the clock, +actor/profile, capabilities, closure evidence, route/handoff facts and resume +inputs. Move canonical row materialization into the existing typed owner before +collapsing that transport. Preserve the Python-value codec where strict integer, +boolean or text distinctions remain observable. Provider rows cannot substitute +for independent caller evidence or pending intent; all-done counts alone cannot +reconstruct a closure declaration. + +`goal_vision_wait` separately consumes complete lineage, scoped blockers, acceptance gaps and resume facts. The combined transaction must preserve each +causal obligation, including when two obligations share a witness. Full notes, +write scopes, archive/settled identity and source provenance remain part of its +parity oracle. + +The real PostgreSQL integration case connects the existing native snapshot read +to all three display views, checks unchanged provider/source data, observes a +new provider revision after a separate commit, and refuses a stale continuation. +This qualifies a composition prerequisite. The combined guard transaction, +PostgreSQL source/wheel host journey and complete-cost benefit remain open. + ## Appendix B: Decision log | Date | Decision | Owner / approval | Alternatives | Normative sections changed | diff --git a/docs/architecture/rfcs/monorepo-distribution-split-v0.zh-CN.md b/docs/architecture/rfcs/monorepo-distribution-split-v0.zh-CN.md index 1cd4692672..624787f7f4 100644 --- a/docs/architecture/rfcs/monorepo-distribution-split-v0.zh-CN.md +++ b/docs/architecture/rfcs/monorepo-distribution-split-v0.zh-CN.md @@ -1391,6 +1391,91 @@ lineage、pause/gates 与 pending intents。新增更多 leaf RPC 或有损裁 解决的性能 hold 保持独立。新实现仍需完整 source/wheel 正确性,以及不插桩、 匹配条件的默认 SQLite 全路径比较;既有里程碑、采纳与完整成本缺口继续开放。 +### 2026-11-07 — 在受管请求内复用完整 Todo 展示来源 + +既有 `todo.context.page` owner 可从同一完整输入来源组合 summary、planning 和 +保留 terminal 的展示视图。Quota preparation 调用方显式请求 Agent 来源的三种 +视图,并把保留 terminal 的 rows 传给 frontier 消费方。User 来源及通用展示调用方 +保留自己请求的单一视图。来源选择和每种展示规则保留原 owner。Orchestration +fallback 复用已选 summary 的独立副本,避免再次请求相同展示。 + +已经固定 runtime revision 的逻辑请求内,以完整 source JSON 和请求选项约束 +传输 memo。来源事实变化和后续请求重新验证;可变调用方获得独立解码值。较大的 +显式组合输入保留原逐视图传输,不完整组合响应在进入 memo 前失败。Batch 输入 +阈值不能预测响应大小,也不保证 socket 内联传输;显式多视图的合计输出超过 +socket 限制时,仍可能使用既有私有响应 snapshot。 + +初始提案的独立评审发现,通用传输把单视图调用扩大为三种视图。8,000 条记录的 +单次 delivery-response 调用最终输出相同,却把原约 3.73 MB 的单视图 socket +响应扩大成约 11.18 MB 的组合响应并写盘。保留这次性能失败。修复后的调用方 +显式组合移除了这种隐式扩大;单视图对照和完整受管 source/wheel 验证按本次提案 +revision 记录。 + +初始提案对照 `main@32167518b3`,源码与隔离安装 wheel 的六组 101-Todo File/SQLite +受管 CLI 生命周期通过,保留已验证产物、原完成时间、四种 settlement receipt、 +一次 debit、Effect 重启恢复及 evidence 替换拒绝。刻意丢弃 write-scope 响应字段的 +mutation 在独立公开 guard scope 检查失败。冻结展示 fixture 和聚焦来源隔离用例 +同时覆盖既有三种视图。一次带 profiler 的 SQLite guard 观察到 JSON 参数约 +1.900 MB → 1.556 MB,调用 89 → 87,已加载模块仍为 503。这些计数仅用于诊断, +不验收完整 wire 成本、延迟或长程收益。 + + +修复后的单次调用对照在 `main@dee549663`、已评审的缺陷 head 和修复源码上 +使用相同合成来源,三者最终 delivery response 相同。修复的请求参数和响应字节 +与 main 完全一致;两种记录规模的非空响应 snapshot 字节都为零。每个版本五次 +热调用的探索性中位数如下,包含 display 和 delivery-response 两种 projection: + +| Rows | Main 秒 | 缺陷提案秒 | 修复秒 | 缺陷响应 snapshot 字节 | 修复 snapshot 字节 | +| --- | --- | --- | --- | --- | --- | +| 8,000 | 0.159 | 0.636 | 0.161 | 9,480,165 | 0 | +| 15,000 | 0.284 | 1.344 | 0.286 | 17,775,165 | 0 | + +此对照复现并消除了单视图性能回归,不能验收完整 CLI、daemon CPU/内存、完整 +边际成本或长程收益。 + + +修复后的源码及隔离 wheel 通过六组 101-Todo File/SQLite 受管 CLI 生命周期, +保留已验证产物、原完成时间、四种 receipt、一次 debit、重启恢复与拒绝/重放。 +递归丢弃响应中的 write scope 的 mutation 在独立公开 guard oracle 失败。隔离 +PostgreSQL 16.15 的 390 个 store 和 11 个 service 用例通过,零 skipped。 + +三组交错、无 profiler 的默认 SQLite 完整 CLI 对照中,main 墙钟中位数为 +12.24 秒,修复为 12.71 秒;两次候选更慢,范围重叠。独立 profiled guard 的参数 +字节从 1,899,603 降到 1,546,162,调用从 90 降到 87,模块仍为 503。这些观察 +未证明完整路径性能收益。Daemon CPU/内存、总 wire/file 写入、完整边际成本及 +采纳仍未验收;完整 provider-first 事务继续作为下一验收边界。 + +这是可独立回滚的展示传输增量,尚非完整 provider-first Todo projection +transaction。Provider 读取、revision、lease/actor fence 和状态写入保留原 owner。 +后续 canonical-source 事务、匹配的完整成本对照及受影响后端验收仍开放。I3 至少 +保留一个 minor release;M1/#8011、完整 R0/V10/R1、D2/D5/D6/D3、context/model/ +split-install、adoption 和所有冻结失败继续保留。 + +#### 后续 provider-first 事务边界 + +下一实现应组合既有经过 provider 准入的 `canonicalTodoCollection` 和 projection +owner。Native snapshot owner 把 Goal、store incarnation、provider revision、cursor +及 query 绑定到完整 rows、leases、read-model 验证和 acceptance guards。Continuation +只是同一次读取中的位置,不是可复用的 current-head handle;snapshot 变化后应重新 +完整读取。展示保留来源不会获取新的 authority。 + +`quota_selection.py` 仍在 encoded selection rows 和 source-row facts 之外发送 +完整 summary source。请求还包含时钟、actor/profile、capabilities、closure evidence、 +route/handoff facts 和 resume 输入。合并传输之前,先把 canonical row materialization +放入既有 typed owner。严格 integer、boolean 或文本区别仍可观测时,保留 +Python-value codec。Provider rows 不能替代独立 caller evidence 或 pending intent; +all-done 计数本身无法重建 closure declaration。 + +`goal_vision_wait` 还独立消费完整 lineage、scoped blockers、acceptance gaps 和 +resume facts。合并事务应保留每个 causal obligation,包括两个 obligation 共用 +witness 的情况。完整 note、write scopes、archive/settled identity 和来源 provenance +仍属于 parity oracle。 + +真实 PostgreSQL 集成用例把既有 native snapshot read 接到三种展示视图,检查 +provider/source 数据不变,在独立 commit 后观察新的 provider revision,并拒绝旧 +continuation。它验证的是组合前置条件;完整 guard transaction、PostgreSQL +source/wheel host journey 和完整成本收益仍开放。 + ## 附录 B:决策日志 | 日期 | 决策 | 负责人 / 批准 | 备选 | 变更的规范章节 | diff --git a/loopx/control_plane/effect_runtime.py b/loopx/control_plane/effect_runtime.py index 040251b1de..5ec88a1080 100644 --- a/loopx/control_plane/effect_runtime.py +++ b/loopx/control_plane/effect_runtime.py @@ -1601,6 +1601,17 @@ def effect_runtime_result( large_local_snapshot: bool = False, ) -> Any: state = _REQUEST_RUNTIME_REVISION.get() if method == "todo.context.page" else None + def validate_result(result: Any) -> Any: + # A caller must explicitly request a bundle. A request scope or a small + # source does not establish that it will consume additional views. + if set(params) == {"summary_display_views"} and (not isinstance(result, dict) or any( + not isinstance(result.get(view), list) + or any(not isinstance(row, dict) for row in result[view]) + for view in ("summary", "planning", "terminal") + )): + raise RuntimeError("invalid TypeScript Todo source views") + return result + key = None if state is not None: # Preserve JSON order and scalar types, including bool/int, signed zero @@ -1613,7 +1624,7 @@ def effect_runtime_result( if key is not None: prior = state.todo_context_result(key) if prior is not None: - return json.loads(prior) # A caller cannot mutate another view. + return validate_result(json.loads(prior)) # A caller cannot mutate another view. result = effect_runtime_request( method, params, @@ -1621,6 +1632,7 @@ def effect_runtime_result( retry_safe=retry_safe, large_local_snapshot=large_local_snapshot, ).get("result") + validate_result(result) if state is not None and key is not None and isinstance(result, dict): try: encoded = json.dumps(result, ensure_ascii=False, allow_nan=False, diff --git a/loopx/control_plane/quota/should_run_prepare.py b/loopx/control_plane/quota/should_run_prepare.py index f9488fa50b..85b90cc01a 100644 --- a/loopx/control_plane/quota/should_run_prepare.py +++ b/loopx/control_plane/quota/should_run_prepare.py @@ -98,7 +98,7 @@ todo_item_task_class as projection_todo_item_task_class, ) from ..todos.quota_summary import ( - select_planning_inventory_source_items, + select_quota_todo_display_views, select_quota_todo_source_items, select_quota_todo_summary, select_task_orchestration_authority_items, @@ -237,6 +237,7 @@ def _prepare_quota_replan_context( project_asset: dict[str, Any], user_todo_summary: dict[str, Any] | None, agent_todo_summary: dict[str, Any] | None, + agent_todo_terminal_source_items: list[dict[str, Any]], authoritative_fallback_todo_items: FallbackTodoSource, effective_available_capabilities: Any, work_lane_contract: dict[str, Any] | None, @@ -251,11 +252,6 @@ def _prepare_quota_replan_context( replan_explore_reader: Callable[[str, str | None], Mapping[str, Any] | None] | None, ) -> tuple[dict[str, Any], dict[str, Any] | None]: """Project the current frontier and contextualize its receipt-bound review.""" - replan_todo_items = select_planning_inventory_source_items( - item.get("agent_todos"), - project_asset.get("agent_todos") if project_asset else None, - include_terminal=True, - ) goal_frontier_context = build_goal_frontier_projection_context_from_status( replan_explore_reader=replan_explore_reader, goal_id=safe_goal_id, @@ -265,7 +261,7 @@ def _prepare_quota_replan_context( project_asset=project_asset, user_todo_summary=user_todo_summary, agent_todo_summary=agent_todo_summary, - agent_todo_source_items=replan_todo_items, + agent_todo_source_items=agent_todo_terminal_source_items, fallback_todo_source_items=authoritative_fallback_todo_items, available_capabilities=effective_available_capabilities, work_lane_contract=work_lane_contract, @@ -718,23 +714,25 @@ def _prepare_quota_should_run_item( item.get("user_todos"), project_asset.get("user_todos") if project_asset else None, ) - agent_todo_source_items = select_quota_todo_source_items( - item.get("agent_todos"), - project_asset.get("agent_todos") if project_asset else None, - ) - agent_todo_planning_source_items = select_planning_inventory_source_items( + # Only this Agent source is consumed as summary, planning and retained + # terminal rows below. User-source and general display calls stay single-view. + agent_todo_display_views = select_quota_todo_display_views( item.get("agent_todos"), project_asset.get("agent_todos") if project_asset else None, ) + agent_todo_source_items = agent_todo_display_views["summary"] + agent_todo_planning_source_items = agent_todo_display_views["planning"] task_orchestration_agent_items = select_task_orchestration_authority_items( item.get("agent_todos"), project_asset.get("agent_todos") if project_asset else None, role="agent", + fallback_source_items=agent_todo_source_items, ) task_orchestration_user_blockers = select_task_orchestration_authority_items( item.get("user_todos"), project_asset.get("user_todos") if project_asset else None, role="user", + fallback_source_items=user_todo_source_items, ) agent_scoped_user_todo_override = _agent_scoped_user_todo_override( state=state, @@ -986,6 +984,7 @@ def _prepare_quota_should_run_item( project_asset=project_asset, user_todo_summary=user_todo_summary, agent_todo_summary=agent_todo_summary, + agent_todo_terminal_source_items=agent_todo_display_views["terminal"], authoritative_fallback_todo_items=authoritative_fallback_todo_items, effective_available_capabilities=effective_available_capabilities, work_lane_contract=work_lane_contract, diff --git a/loopx/control_plane/todos/context_projection.ts b/loopx/control_plane/todos/context_projection.ts index 2802a09ccd..a180059b2a 100644 --- a/loopx/control_plane/todos/context_projection.ts +++ b/loopx/control_plane/todos/context_projection.ts @@ -24,6 +24,19 @@ const CONTEXT_FIELDS = [ /** Select before redaction; never convert nested scopes or booleans to prose. */ export function projectTodoContextPage(input: JsonObject): JsonObject { + if (input.summary_display_views !== undefined) { + if (Object.keys(input).length !== 1) throw new EffectRuntimeRequestError("summary display views cannot be combined with other views"); + const source = requireJsonObject(input.summary_display_views, "summary display views"); + if (Object.keys(source).some(key => key !== "source" && key !== "handoff_gates")) { + throw new EffectRuntimeRequestError("invalid summary display views fields"); + } + requireJsonObject(source.source, "summary source"); + return { + summary: projectTodoSummaryDisplay(source), + planning: projectTodoSummaryDisplay({source: source.source, include_terminal: false}), + terminal: projectTodoSummaryDisplay({source: source.source, include_terminal: true}), + }; + } if (input.summary_display !== undefined) { if (Object.keys(input).length !== 1) throw new EffectRuntimeRequestError("summary display cannot be combined with other views"); return {items: projectTodoSummaryDisplay(requireJsonObject(input.summary_display, "summary display"))}; diff --git a/loopx/control_plane/todos/quota_summary.py b/loopx/control_plane/todos/quota_summary.py index a94153aa3f..b6e0034012 100644 --- a/loopx/control_plane/todos/quota_summary.py +++ b/loopx/control_plane/todos/quota_summary.py @@ -1,6 +1,7 @@ from __future__ import annotations from typing import Any, Callable +from copy import deepcopy from ..goals.goal_vision_policy import COMPLETED_TODO_CHAIN_REPLAN_THRESHOLD @@ -494,6 +495,18 @@ def select_quota_todo_source_items( return todo_summary_source_items(source) if source is not None else [] +def select_quota_todo_display_views( + canonical_value: Any, + project_asset_value: Any, +) -> dict[str, list[dict[str, Any]]]: + """Select the source once for the quota caller's three consumed displays.""" + from .summary_item import todo_source_display_views + + source = _select_todo_source(canonical_value, project_asset_value) + return (todo_source_display_views(source) if source is not None + else {"summary": [], "planning": [], "terminal": []}) + + def select_planning_inventory_source_items( canonical_value: Any, project_asset_value: Any, @@ -532,6 +545,7 @@ def select_task_orchestration_authority_items( project_asset_value: Any, *, role: str, + fallback_source_items: list[dict[str, Any]] | None = None, ) -> list[dict[str, Any]]: canonical_items = _task_orchestration_authority_items( canonical_value, @@ -551,4 +565,7 @@ def select_task_orchestration_authority_items( ) if selected is not None: return selected + if fallback_source_items is not None: + # Preserve the independent mutable display returned by the old call. + return deepcopy(fallback_source_items) return select_quota_todo_source_items(canonical_value, project_asset_value) diff --git a/loopx/control_plane/todos/summary_item.py b/loopx/control_plane/todos/summary_item.py index a730f5de6a..f9755d157a 100644 --- a/loopx/control_plane/todos/summary_item.py +++ b/loopx/control_plane/todos/summary_item.py @@ -98,3 +98,30 @@ def todo_planning_source_items(value: dict[str, Any], *, include_terminal: bool if not any(value.get(key) for key in TODO_PLANNING_SOURCE_KEYS): return [] return _project({"source": todo_read_source(value), "include_terminal": include_terminal}) + + +def todo_source_display_views(value: dict[str, Any]) -> dict[str, list[dict[str, Any]]]: + """Compose displays only for a caller consuming all three existing views.""" + from ..effect_runtime import MAX_REQUEST_BYTES, EffectRuntimeRejected, effect_runtime_result + from .handoff_gate import todo_summary_handoff_gates + + has_summary = any(value.get(key) for key in TODO_SUMMARY_SOURCE_KEYS) + has_planning = any(value.get(key) for key in TODO_PLANNING_SOURCE_KEYS) + if not has_summary and not has_planning: + return {"summary": [], "planning": [], "terminal": []} + request = {"summary_display_views": { + "source": todo_read_source(value), "handoff_gates": todo_summary_handoff_gates(value), + }} + # This bounds the opt-in batch input; it does not predict response size or + # promise inline transport. Large sources retain the existing view limits. + if len(json.dumps(request, ensure_ascii=False, separators=(",", ":")).encode("utf-8", errors="backslashreplace")) > MAX_REQUEST_BYTES // 4: + return {"summary": todo_summary_source_items(value), + "planning": todo_planning_source_items(value), + "terminal": todo_planning_source_items(value, include_terminal=True)} + try: + result = effect_runtime_result("todo.context.page", request, large_local_snapshot=True) + except EffectRuntimeRejected as error: + raise ValueError(str(error)) from error + return {"summary": result["summary"] if has_summary else [], + "planning": result["planning"] if has_planning else [], + "terminal": result["terminal"] if has_planning else []} diff --git a/tests/control_plane/test_runtime_source_read_batching.py b/tests/control_plane/test_runtime_source_read_batching.py index 434e58bb22..5a91ca1714 100644 --- a/tests/control_plane/test_runtime_source_read_batching.py +++ b/tests/control_plane/test_runtime_source_read_batching.py @@ -11,6 +11,169 @@ from loopx.control_plane import effect_runtime as runtime +def test_todo_source_views_share_only_the_exact_request_source(monkeypatch): + from loopx.control_plane.todos.summary_item import ( + todo_source_display_views, + ) + + requests = [] + original = runtime.effect_runtime_request + + def track(method, params, **kwargs): + if method == "todo.context.page": + requests.append(params) + return original(method, params, **kwargs) + + monkeypatch.setattr(runtime, "effect_runtime_request", track) + source = {"items": [{"todo_id": "todo_retained", "text": "Keep the full acceptance", + "role": "agent", "status": "done", "done": True, + "note": "Original recovery detail", "successor_todo_ids": ["todo_next"]}]} + with runtime.effect_runtime_request_scope(): + views = todo_source_display_views(source) + assert views["summary"] == [] + assert views["planning"] == [] + terminal = views["terminal"] + assert len(terminal) == 1 + assert terminal[0]["note"] == "Original recovery detail" + assert terminal[0]["successor_todo_ids"] == ["todo_next"] + assert len(requests) == 1 + terminal[0]["successor_todo_ids"].append("todo_unrelated") + assert todo_source_display_views(source)["terminal"][0]["successor_todo_ids"] == ["todo_next"] + # The same Todo id cannot alias changed full-source facts. + source["items"][0]["note"] = "Changed recovery detail" + assert todo_source_display_views(source)["terminal"][0]["note"] == "Changed recovery detail" + assert len(requests) == 2 + # A later logical request starts with a new source read and result. + with runtime.effect_runtime_request_scope(): + assert todo_source_display_views(source)["terminal"][0]["note"] == "Changed recovery detail" + assert len(requests) == 3 + + +def test_large_or_unscoped_todo_sources_keep_single_view_transport(monkeypatch): + from loopx.control_plane.todos.summary_item import todo_planning_source_items + + requests = [] + original = runtime.effect_runtime_request + + def track(method, params, **kwargs): + if method == "todo.context.page": + requests.append(params) + return original(method, params, **kwargs) + + monkeypatch.setattr(runtime, "effect_runtime_request", track) + source = {"items": [{"todo_id": "todo_retained", "text": "Keep acceptance", "status": "done", + "note": "Recovery detail " * (runtime.MAX_REQUEST_BYTES // 32)}]} + with runtime.effect_runtime_request_scope(): + assert todo_planning_source_items(source, include_terminal=True)[0]["note"] == source["items"][0]["note"] + assert set(requests[0]) == {"summary_display"} + source["items"][0]["note"] = "Small source outside the shared request" + assert todo_planning_source_items(source, include_terminal=True)[0]["note"] == source["items"][0]["note"] + assert set(requests[1]) == {"summary_display"} + + +def test_explicit_large_source_falls_back_to_existing_single_view_limits(monkeypatch): + from loopx.control_plane.todos.summary_item import todo_source_display_views + + requests = [] + original = runtime.effect_runtime_request + + def track(method, params, **kwargs): + if method == "todo.context.page": + requests.append(params) + return original(method, params, **kwargs) + + monkeypatch.setattr(runtime, "effect_runtime_request", track) + source = {"items": [{"todo_id": "todo_retained", "text": "Completed work", "status": "done", "done": True, + "note": "Complete recovery detail " * (runtime.MAX_REQUEST_BYTES // 64)}]} + with runtime.effect_runtime_request_scope(): + views = todo_source_display_views(source) + assert views["summary"] == views["planning"] == [] + assert views["terminal"][0]["note"] == source["items"][0]["note"] + assert len(requests) == 3 + assert all(set(request) == {"summary_display"} for request in requests) + + +def test_incomplete_source_views_fail_before_entering_the_request_memo(monkeypatch): + from loopx.control_plane.todos.summary_item import todo_source_display_views + + calls = [] + original = runtime.effect_runtime_request + + def response(method, params, **kwargs): + if method != "todo.context.page": + return original(method, params, **kwargs) + calls.append(params) + return {"result": {"summary": [], "planning": [], + "terminal": None if len(calls) == 1 else []}} + + monkeypatch.setattr(runtime, "effect_runtime_request", response) + source = {"items": [{"todo_id": "todo_retained", "text": "Acceptance", "status": "done"}]} + with runtime.effect_runtime_request_scope(): + with pytest.raises(RuntimeError, match="invalid TypeScript Todo source views"): + todo_source_display_views(source) + assert todo_source_display_views(source)["summary"] == [] + assert len(calls) == 2 + + +def test_single_view_delivery_does_not_compute_or_snapshot_unused_views(monkeypatch): + from loopx.control_plane.work_items.delivery_history import project_delivery_response + + requests, snapshots = [], [] + original = runtime.effect_runtime_request + read_snapshot = runtime._read_local_snapshot_response + + def track(method, params, **kwargs): + if method == "todo.context.page": + requests.append(params) + return original(method, params, **kwargs) + + def snapshot(*args, **kwargs): + snapshots.append(args) + return read_snapshot(*args, **kwargs) + + monkeypatch.setattr(runtime, "effect_runtime_request", track) + monkeypatch.setattr(runtime, "_read_local_snapshot_response", snapshot) + source = {"items": [{"todo_id": f"todo_{i}", "text": "Work", "role": "agent", + "status": "open", "successor_todo_ids": ["todo_next"]} + for i in range(8000)]} + with runtime.effect_runtime_request_scope(): + result = project_delivery_response({"todo_id": "todo_0", "agent_id": "agent-a"}, source) + assert result["schema_version"] == "delivery_response_v0" + assert len(requests) == 1 + assert set(requests[0]) == {"summary_display"} + assert requests[0]["summary_display"]["include_terminal"] is True + assert snapshots == [], "a sub-limit single display must not materialize unused views" + + +def test_quota_caller_explicitly_composes_only_the_agent_source(monkeypatch): + from loopx.control_plane.testing.quota_fixtures import quota_status_payload, quota_todo_item, quota_todo_summary + from loopx.quota import build_quota_should_run + + requests = [] + original = runtime.effect_runtime_request + + def track(method, params, **kwargs): + if method == "todo.context.page": + requests.append(params) + return original(method, params, **kwargs) + + monkeypatch.setattr(runtime, "effect_runtime_request", track) + agent = quota_todo_summary([quota_todo_item(todo_id="todo_agent", title="Agent work", claimed_by="agent-a")]) + user = quota_todo_summary([quota_todo_item(todo_id="todo_user", title="User action", role="user", blocks_agent="agent-b")]) + status = quota_status_payload(goal_id="goal-a", status="active_state_agent_todo", + recommended_action="Agent work", agent_todos=agent, user_todos=user, + coordination={"registered_agents": ["agent-a", "agent-b"]}) + with runtime.effect_runtime_request_scope(): + result = build_quota_should_run(status, goal_id="goal-a", agent_id="agent-a", available_capabilities=["shell"]) + bundles = [p["summary_display_views"] for p in requests if "summary_display_views" in p] + assert len(bundles) == 1 + assert bundles[0]["source"]["record"]["first_open_items"][0]["todo_id"] == "todo_agent" + assert any("summary_display" in p and + p["summary_display"].get("source", {}).get("record", {}).get("first_open_items", [{}])[0].get("todo_id") == "todo_user" + for p in requests) + assert result["agent_identity"]["agent_id"] == "agent-a" + + def serial_fingerprint(root: Path) -> str: """Independent reference: selected adapter, release identity and source bytes.""" digest = hashlib.sha256() diff --git a/tests/control_plane_ts/postgresql_authority_store.integration.test.ts b/tests/control_plane_ts/postgresql_authority_store.integration.test.ts index 888d1c6af7..c6e302203f 100644 --- a/tests/control_plane_ts/postgresql_authority_store.integration.test.ts +++ b/tests/control_plane_ts/postgresql_authority_store.integration.test.ts @@ -19,6 +19,8 @@ import { } from "../../loopx/control_plane/coordination/postgresql_authority_store.ts"; import { openLocalAuthorityStoreHandle } from "../../loopx/control_plane/coordination/local_authority_provider.ts"; import {readLocalCoordinationTodoSource} from "../../loopx/control_plane/coordination/local_authority_read.ts"; +import {CANONICAL_SNAPSHOT_PAGE_REQUEST, readCanonicalSnapshotFromStore} from "../../loopx/control_plane/coordination/canonical_snapshot_page.ts"; +import {projectTodoContextPage} from "../../loopx/control_plane/todos/context_projection.ts"; import {coordinationTodoReadModel} from "../../loopx/control_plane/coordination/coordination_projection.ts"; import {inspectGoalAcceptance, refineGoalAcceptanceOutcome} from "../../loopx/control_plane/goals/acceptance_authority.ts"; import {authorityProjectionFixture} from "./authority_projection_fixture.ts"; @@ -187,6 +189,71 @@ if (database && installed) { "todo_source_lineage_mismatch"); }); + test("PostgreSQL canonical snapshot feeds complete display views without becoming a fresh authority read", async t => { + await installed; + const options = {tenant_id: `tenant-${randomUUID()}`, goal_id: `goal-${randomUUID()}`}; + t.after(() => cleanScope(options.tenant_id, options.goal_id)); + const store = new PostgreSqlAuthorityStore(database, options); + const retainedNote = "Keep every acceptance clause.\nRecover the original artifact and settlement identity."; + const todos = [ + {todo_id: "todo_01_open", role: "agent", status: "open", done: false, archive_state: "active", + text: "Implement the requested outcome", task_class: "advancement_task", + required_write_scopes: ["result.json"], note: retainedNote}, + {todo_id: "todo_02_blocker", role: "agent", status: "blocked", done: false, archive_state: "active", + text: "Wait for the required input", task_class: "blocker", claimed_by: "author"}, + {todo_id: "todo_03_completed", role: "agent", status: "done", done: true, archive_state: "active", + text: "Preserve the verified artifact", task_class: "advancement_task", + note: retainedNote, evidence: "artifact:verified-result", completed_at: "2026-01-01T00:00:00Z"}, + ]; + const projection = authorityProjectionFixture(options.goal_id, todos); + const committed = await store.commitAuthority({expected_provider_revision: null, + operation_id: "seed-display-source", events: [], receipts: [], next_projection: projection}); + assert.equal(committed.status, "applied"); + if (committed.status !== "applied") return; + const request = {schema_version: CANONICAL_SNAPSHOT_PAGE_REQUEST, runtime_root: tmpdir(), + goal_id: options.goal_id, include_leases: true, projection_readback: null, after: null}; + const first = await readCanonicalSnapshotFromStore(request, store); + assert.equal(first.status, "page"); + assert.equal(first.next, null); + const source = {source: {record: {items: first.todos}}, handoff_gates: []}; + const before = JSON.stringify(first); + const views = projectTodoContextPage({summary_display_views: source}); + const ids = (rows: unknown) => (rows as JsonObject[]).map(row => row.todo_id); + assert.deepEqual(ids(views.summary), ["todo_01_open", "todo_02_blocker"]); + assert.deepEqual(ids(views.planning), ["todo_01_open", "todo_02_blocker"]); + assert.deepEqual(ids(views.terminal), ["todo_01_open", "todo_02_blocker", "todo_03_completed"]); + assert.deepEqual((views.planning as JsonObject[])[0].required_write_scopes, ["result.json"]); + const terminal = (views.terminal as JsonObject[])[2]; + assert.equal(terminal.note, retainedNote); + assert.equal(terminal.evidence, "artifact:verified-result"); + assert.equal(terminal.completed_at, "2026-01-01T00:00:00Z"); + assert.equal(JSON.stringify(first), before, "display cannot mutate the complete source snapshot"); + const unchanged = await store.loadAuthority(); + assert.equal(unchanged.status, "loaded"); + if (unchanged.status !== "loaded") return; + assert.equal(unchanged.provider_revision, committed.provider_revision); + assert.deepEqual(unchanged.head, projection, "display cannot write or compact provider records"); + + const changed = authorityProjectionFixture(options.goal_id, todos.map(row => + row.todo_id === "todo_01_open" ? {...row, status: "done", done: true} : row)); + const later = await store.commitAuthority({expected_provider_revision: committed.provider_revision, + operation_id: "advance-display-source", events: [], receipts: [], next_projection: changed}); + assert.equal(later.status, "applied"); + if (later.status !== "applied") return; + const fresh = await readCanonicalSnapshotFromStore(request, store); + assert.equal(fresh.status, "page"); + assert.notEqual((fresh.snapshot as JsonObject).provider_revision, (first.snapshot as JsonObject).provider_revision); + assert.deepEqual((fresh.todos as JsonObject[]).filter(row => row.todo_id !== "todo_01_open"), + (first.todos as JsonObject[]).filter(row => row.todo_id !== "todo_01_open"), "non-target provider rows stay unchanged"); + assert.deepEqual(ids(projectTodoContextPage({summary_display_views: { + source: {record: {items: fresh.todos}}, handoff_gates: [], + }}).planning), ["todo_02_blocker"]); + assert.deepEqual(ids(projectTodoContextPage({summary_display_views: source}).planning), ["todo_01_open", "todo_02_blocker"], + "the retained source is historical evidence, never a newly read provider head"); + await assert.rejects(readCanonicalSnapshotFromStore({...request, + after: {snapshot: first.snapshot, todo_offset: 1, lease_offset: 0}}, store), /canonical snapshot changed/); + }); + registerAuthorityStoreConformance("PostgreSQL provider", async (t) => { await installed; const tenantId = `tenant-${randomUUID()}`; diff --git a/tests/control_plane_ts/todo_context_projection.test.ts b/tests/control_plane_ts/todo_context_projection.test.ts index 85d7bf9d0d..d1f3904ad0 100644 --- a/tests/control_plane_ts/todo_context_projection.test.ts +++ b/tests/control_plane_ts/todo_context_projection.test.ts @@ -197,6 +197,19 @@ test("summary display matches pre-cutover synthetic CLI characterization", () => } }); +test("one complete source projects all existing views without changing their oracle", () => { + const source = {source: {record: structuredClone(displayFixture.source)}, handoff_gates: []}; + const before = structuredClone(source); + const result = projectTodoContextPage({summary_display_views: source}); + assert.deepEqual(result.summary, displayFixture.summary); + assert.deepEqual(result.planning, displayFixture.planning); + assert.deepEqual(result.terminal, displayFixture.terminal); + assert.deepEqual(source, before); + assert.throws(() => projectTodoContextPage({summary_display_views: source, summary_display: {}})); + assert.throws(() => projectTodoContextPage({summary_display_views: {...source, include_terminal: true}})); + assert.throws(() => projectTodoContextPage({summary_display_views: {source: null}})); +}); + test("handoff preserves identity and nested precedence without granting completion", () => { const item = {todo_id: "todo_original", role: "agent", status: "open", claimed_by: "Agent A", text: "Read the complete requirement", note: "Keep the unfinished validation", evidence: "opaque evidence",