diff --git a/docs/architecture/rfcs/ledger/typescript-control-plane-migration-v0/2026-11-02-completion-evidence-read.md b/docs/architecture/rfcs/ledger/typescript-control-plane-migration-v0/2026-11-02-completion-evidence-read.md new file mode 100644 index 0000000000..298d220d12 --- /dev/null +++ b/docs/architecture/rfcs/ledger/typescript-control-plane-migration-v0/2026-11-02-completion-evidence-read.md @@ -0,0 +1,67 @@ +# Complete durable-completion evidence read (2026-11-02) + +The built-in `turn run-once` provider is the only production consumer of the +Python durable-completion projection. Its intent, post-completion, ordinary +recovery and terminal recovery now use the existing `todo.read` transaction. +`todos/read_transaction.ts` collects canonical source pages in process; +`todos/read_projection.ts` composes the existing completion fence and owns +continuation/recovery validation, successor existence and result construction. +Canonical refusal never falls back to stale Markdown. Fence rejection retains +precedence over continuation projection during recovery. + +## Economics and retained boundary + +Instrumented File/SQLite reads measure ordinary recovery **2 → 1**, terminal +recovery **2 → 1**, intent **1 → 1**, post-completion **1 → 1**, and explicit legacy +materialized reads **0 → 1** Python/TypeScript calls. Canonical collection stays +inside the request, including pagination. The legacy increase removes its Python +semantic authority; it is not a latency improvement claim. + +Deleted Python authority includes `_project_durable_completion`, terminal fence +composition, the separate Turn fence call and the entire `completion_fence.py` +facade, and the obsolete public persisted-record readers. No string-dispatch consumer remains for `todo.completion_fence.evaluate`, +so its registration is removed. Native fence imports and tests remain. + +Python retains registry/path resolution, legacy Markdown observation/decoding, +Python scalar/container encoding, rejection exception compatibility and transport. +The canonical source-result decoder is shared with existing authority reads to +preserve unavailable/refusal error types without an extra provider request. +Compatibility projection functions delegate to the same complete native owner; +they do not interpret continuation or recovery. + +The facade exits when `turn run-once` is native and no Python caller requires +legacy persisted-format observation or the projection import contract. The +completion-evidence read kind can then become an in-process call; the ordinary +`todo.read` operation remains for other Python consumers. Characterization +fixtures may retire with that facade after independent native coverage retains +its negative cases. No long-horizon effectiveness/efficiency benefit is claimed. + +## Evidence and scope + +The pre-cutover corpus pins 83 complete JSON observations, exact diagnostics, +field absence and ordered missing-successor lists. Baseline instrumentation and +existing focused tests passed before authority moved; expected bytes remain +unchanged. Native fence tests remain independent of that corpus. Real CLI checks +cover File/SQLite crash-after-completion recovery and stale-display identity. +The delivery report records exact commands, results, source line deltas and any +unqualified gates. No storage format, write authority, scoring or default setting +changes are intended. This internal read refactor uses the existing CLI journey +and adds no capability or configuration surface. + +Wheel and sdist were installed into separate fresh Python 3.12 environments. +Each installed package executed completion evidence, canonical refusal and a +real canonical recovery while importing solely from `site-packages`; all three +checks passed. + +Matched default-SQLite qualification used the pinned base `867615e2c` and this +head with the same synthetic fixture, 32 cold starts, 128 warm typed requests, +64 complete canonical transactions, 64 full-CLI recovery transactions and a +256-request burst. The base → head results are: cold-start p50/p95 +115.522/128.518 → 102.829/135.269 ms; complete transaction +68.774/103.729 → 9.806/14.320 ms; full CLI recovery +358.661/611.397 → 264.324/334.687 ms; process-tree RSS +30/30 → 32/33 MiB idle/burst. The cold-start p95 difference is +6.747 ms +(+5.25%) with a lower median, below the 25 ms additive allowance. The typed +warm request includes the persisted remote read; it is not the non-durable +in-process kernel budget. The complete transaction and operator CLI both +improve materially on the matched SQLite baseline. diff --git a/docs/architecture/rfcs/ledger/typescript-control-plane-migration-v0/2026-11-02-completion-evidence-read.zh-CN.md b/docs/architecture/rfcs/ledger/typescript-control-plane-migration-v0/2026-11-02-completion-evidence-read.zh-CN.md new file mode 100644 index 0000000000..f632f18a4b --- /dev/null +++ b/docs/architecture/rfcs/ledger/typescript-control-plane-migration-v0/2026-11-02-completion-evidence-read.zh-CN.md @@ -0,0 +1,52 @@ +# 完整 durable-completion 证据读取(2026-11-02) + +内置 `turn run-once` provider 是 Python durable-completion 投影的唯一生产消费者。 +其 intent、完成后读取、普通恢复和终态恢复现在复用既有 `todo.read` 事务。 +`todos/read_transaction.ts` 在进程内收集 canonical 来源分页; +`todos/read_projection.ts` 组合既有 completion fence,负责 continuation/recovery +验证、后继存在性检查和结果构造。canonical 拒绝不会回退到陈旧 Markdown。 +恢复时 fence 拒绝仍优先于 continuation 投影。 + +## 经济性与保留边界 + +File/SQLite 插桩测得 Python/TypeScript 调用数:普通恢复 **2 → 1**、终态恢复 +**2 → 1**、intent **1 → 1**、完成后读取 **1 → 1**、显式 legacy materialized +读取 **0 → 1**。canonical 收集(含分页)留在请求内部。legacy 增加一次调用是为了 +删除 Python 语义权限,不据此声称延迟改善。 + +删除的 Python 权限包括 `_project_durable_completion`、终态 fence 组合、Turn +独立 fence 调用、整个 `completion_fence.py` facade 及已过时的公开持久记录读取接口。 +`todo.completion_fence.evaluate` 已无字符串分派消费者,因此删除其注册;原生 +fence 导入和测试保留。 + +Python 保留 registry/路径解析、legacy Markdown 观察与解码、Python 标量/容器 +编码、拒绝异常兼容和传输。canonical 来源结果解码器与现有权限读取共享,以在 +不增加 provider 请求的情况下保留不可用/拒绝的异常类型。兼容投影函数委托同一个 +完整原生 owner,不解释 continuation 或 recovery。 + +当 `turn run-once` 原生化,且没有 Python 调用者需要 legacy 持久格式观察或投影 +导入契约时,删除 facade。completion-evidence 读取种类届时可改成进程内调用; +其他 Python 消费者使用的普通 `todo.read` 操作继续保留。独立原生覆盖保留负例后, +刻画 fixture 可随该 facade 退役。不声称长程效果/效率收益。 + +## 证据与范围 + +迁移前语料固定 83 个完整 JSON 观察、精确诊断、字段缺席及有序缺失后继列表。 +权限迁移前,基线插桩和既有聚焦测试通过;期望字节保持不变。原生 fence 测试 +独立于该语料。真实 CLI 检查覆盖 File/SQLite 完成后崩溃恢复和陈旧展示身份。 +交付报告记录精确命令、结果、源码行增减及尚未合格的门禁。不计划改变存储格式、 +写入权限、计分或默认设置。此内部读取重构复用现有 CLI 旅程,不增加能力或配置界面。 + +wheel 与 sdist 分别安装到全新的 Python 3.12 环境。两个安装包均只从 +`site-packages` 导入,并执行 completion evidence、canonical 拒绝和真实 +canonical recovery;三项检查全部通过。 + +默认 SQLite 的同条件资格使用固定 base `867615e2c` 与当前 head,以及相同合成 +fixture、32 次冷启动、128 次 warm typed 请求、64 次完整 canonical transaction、 +64 次完整 CLI recovery 和 256 次请求 burst。base → head 结果为:冷启动 +p50/p95 115.522/128.518 → 102.829/135.269 ms;完整 transaction +68.774/103.729 → 9.806/14.320 ms;完整 CLI recovery +358.661/611.397 → 264.324/334.687 ms;进程树 RSS 的 idle/burst +30/30 → 32/33 MiB。冷启动 p95 差异为 +6.747 ms(+5.25%),但中位数下降且 +低于 25 ms 加性余量。warm typed 请求包含持久化远程读取,不套用非持久化进程内核 +预算;完整 transaction 和操作者 CLI 在同条件 SQLite 基线上均明显改善。 diff --git a/docs/architecture/rfcs/shared-goal-authority-state-provider-v0.md b/docs/architecture/rfcs/shared-goal-authority-state-provider-v0.md index 4cc3374bb6..e2d75641a9 100644 --- a/docs/architecture/rfcs/shared-goal-authority-state-provider-v0.md +++ b/docs/architecture/rfcs/shared-goal-authority-state-provider-v0.md @@ -2129,16 +2129,22 @@ when Markdown is absent, and carries an authority receipt in the response. Provider missing, corruption, protocol drift, or Todo read-model semantic drift all fail closed; no post-cutover Markdown fallback is permitted. -Durable completion continuation read-back -(`durable_completion.py`: `read_persisted_todo_record` / -`project_durable_completion_outcome`) is a provider read point: it re-reads -the persisted lifecycle record (Markdown first, event projection fallback) -after the completion write and before settlement. The persisted record -carries an explicit `completion_continuation`; the seam fails closed when a -done record omits it or when it contradicts the recorded successor or -no-follow-up fields, so a provider that holds completion state must store -that field byte for byte. Once a remote provider becomes canonical, this seam -flips to provider-first without changing the typed outcome contract below. +Durable completion continuation read-back uses `read_durable_completion_evidence` +in `loopx/control_plane/todos/durable_completion.py` as the provider read point +after the completion write and before settlement. With `runtime_root` and +`goal_id`, the adapter reads the persisted lifecycle record through the +TypeScript `todo.read` transaction of kind `completion_evidence`: +`readCompletionEvidence` in `loopx/control_plane/todos/read_transaction.ts` +first collects the canonical provider snapshot, then selects the Todo and +passes its record and the existing Todo ids to `projectCompletionEvidence` in +`loopx/control_plane/todos/read_projection.ts`. This canonical path has no +Markdown or event-projection fallback; the adapter's explicit legacy path +reads Markdown only. The persisted record carries an explicit +`completion_continuation`; the seam fails closed when a done record omits it +or when it contradicts the recorded successor or no-follow-up fields, so a +provider that holds completion state must store that field byte for byte. +Once a remote provider becomes canonical, this seam flips to provider-first +without changing the typed outcome contract below. #### Stage 2 slice status (2026-08-23) diff --git a/docs/architecture/rfcs/shared-goal-authority-state-provider-v0.zh-CN.md b/docs/architecture/rfcs/shared-goal-authority-state-provider-v0.zh-CN.md index 64f640adce..fc29411566 100644 --- a/docs/architecture/rfcs/shared-goal-authority-state-provider-v0.zh-CN.md +++ b/docs/architecture/rfcs/shared-goal-authority-state-provider-v0.zh-CN.md @@ -1704,14 +1704,20 @@ pipeline;Markdown 缺失时仍可执行 provider-only read,并在响应中 receipt。provider 缺失、损坏、协议漂移或 Todo read-model 语义漂移都会 fail closed; cutover 后禁止回退 Markdown。 -完成续接(continuation)的持久化读回 -(`durable_completion.py`:`read_persisted_todo_record` / -`project_durable_completion_outcome`)是一个 provider read point:它在完成写入之后、 -settlement 之前重新读取已落盘的 lifecycle record(Markdown 优先,event projection -兜底)。落盘记录带有显式的 `completion_continuation`;done 记录缺少该字段、或该字段 -与 successor / no-follow-up 字段矛盾时,seam 都 fail closed,因此持有完成状态的 -provider 必须逐字节保存这个字段。一旦远端 provider 成为 canonical,这个 seam 翻转为 -provider-first,且不改变下述 typed outcome 合同。 +完成续接(continuation)的持久化读回以 +`loopx/control_plane/todos/durable_completion.py` 中的 +`read_durable_completion_evidence` 为 provider read point,在完成写入之后、 +settlement 之前执行。提供 `runtime_root` 和 `goal_id` 时,adapter 通过 kind 为 +`completion_evidence` 的 TypeScript `todo.read` transaction 读取已落盘的 lifecycle +record:`loopx/control_plane/todos/read_transaction.ts` 中的 +`readCompletionEvidence` 先收集 canonical provider snapshot,再选出 Todo,并将其 +record 与已有 Todo id 传给 `loopx/control_plane/todos/read_projection.ts` 中的 +`projectCompletionEvidence`。这条 canonical 路径不回退 Markdown 或 event projection; +adapter 的显式 legacy 路径只读取 Markdown。落盘记录带有显式的 +`completion_continuation`;done 记录缺少该字段、或该字段与 successor / no-follow-up +字段矛盾时,seam 都 fail closed,因此持有完成状态的 provider 必须逐字节保存这个 +字段。一旦远端 provider 成为 canonical,这个 seam 翻转为 provider-first,且不改变 +下述 typed outcome 合同。 #### Stage 2 切片状态(2026-08-23) diff --git a/docs/architecture/rfcs/typescript-control-plane-migration-v0.md b/docs/architecture/rfcs/typescript-control-plane-migration-v0.md index 3819b26645..0bb0622ea4 100644 --- a/docs/architecture/rfcs/typescript-control-plane-migration-v0.md +++ b/docs/architecture/rfcs/typescript-control-plane-migration-v0.md @@ -2842,3 +2842,4 @@ with shared write/read status-phase checks. See the [2026-10-02 checkpoint](ledg - Quota summary composition in one `todo.quota_planning.project` crossing with source references: [2026-11-02 cutover](ledger/typescript-control-plane-migration-v0/2026-11-02-quota-summary-transaction.md). - Complete global summary and gate read transactions: [2026-11-02 checkpoint](ledger/typescript-control-plane-migration-v0/2026-11-02-global-manager-reads.md). - Complete global-risk read ownership: [2026-11-02](ledger/typescript-control-plane-migration-v0/2026-11-02-global-risks-read.md). +- Complete durable-completion evidence read: [2026-11-02 owner and crossings ledger](ledger/typescript-control-plane-migration-v0/2026-11-02-completion-evidence-read.md). 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 3835ca6295..6a6e54ce0d 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 @@ -2107,3 +2107,4 @@ SQLite 中位数略升,收益不能外推为全面加速。这只资格化本 - 配额摘要经来源引用在一次 `todo.quota_planning.project` 跨运行时调用内完成组合:[2026-11-02 切换记录](ledger/typescript-control-plane-migration-v0/2026-11-02-quota-summary-transaction.zh-CN.md)。 - 完整全局摘要与门禁读取事务:[2026-11-02 检查点](ledger/typescript-control-plane-migration-v0/2026-11-02-global-manager-reads.zh-CN.md)。 - 完整全局风险读取所有权:[2026-11-02](ledger/typescript-control-plane-migration-v0/2026-11-02-global-risks-read.zh-CN.md)。 +- 完整 durable-completion 证据读取:[2026-11-02 owner 与调用边界记录](ledger/typescript-control-plane-migration-v0/2026-11-02-completion-evidence-read.zh-CN.md)。 diff --git a/loopx/cli_commands/turn_run_once.py b/loopx/cli_commands/turn_run_once.py index f908d58fc6..11db4e23cf 100644 --- a/loopx/cli_commands/turn_run_once.py +++ b/loopx/cli_commands/turn_run_once.py @@ -27,14 +27,7 @@ from ..control_plane.work_items.autonomous_replan_obligation import ( replan_obligation_id_from_packet, ) -from ..control_plane.todos.durable_completion import ( - project_durable_completion_intent, - project_durable_completion_outcome, - project_durable_terminal_completion_readback, - read_persisted_todo_record, - read_persisted_todo_record_with_source, -) -from ..control_plane.todos.completion_fence import evaluate_todo_completion_fence +from ..control_plane.todos.durable_completion import read_durable_completion_evidence from ..control_plane.turn_driver import ( build_loopx_turn_command_validator, codex_cli_session_binding, @@ -329,17 +322,9 @@ def completion_intent(_result: dict[str, object]) -> dict[str, object]: project=None, state_file=None, ) - durable_todo, existing_todo_ids = read_persisted_todo_record( - state_file, - todo_id=todo_id, - registry_path=registry_path, - goal_id=args.goal_id, - runtime_root=runtime_root, - ) - return project_durable_completion_intent( - todo=durable_todo, - expected_todo_id=todo_id, - existing_todo_ids=existing_todo_ids, + return read_durable_completion_evidence( + state_file, todo_id=todo_id, registry_path=registry_path, + goal_id=args.goal_id, runtime_root=runtime_root, operation="intent", ) def todo_completion( @@ -386,17 +371,9 @@ def todo_completion( ), } try: - durable_todo, existing_todo_ids = read_persisted_todo_record( - Path(state_file), - todo_id=todo_id, - registry_path=registry_path, - goal_id=args.goal_id, - runtime_root=runtime_root, - ) - completion_outcome = project_durable_completion_outcome( - todo=durable_todo, - expected_todo_id=todo_id, - existing_todo_ids=existing_todo_ids, + completion_outcome = read_durable_completion_evidence( + Path(state_file), todo_id=todo_id, registry_path=registry_path, + goal_id=args.goal_id, runtime_root=runtime_root, operation="outcome", ) except ValueError as exc: return { @@ -555,23 +532,10 @@ def completion_readback() -> dict[str, object] | None: project=None, state_file=None, ) - durable_todo, existing_todo_ids, projection_source = read_persisted_todo_record_with_source( - state_file, - todo_id=todo_id, - registry_path=registry_path, - goal_id=args.goal_id, - runtime_root=runtime_root, - ) - evaluate_todo_completion_fence( - todo=durable_todo, - projection_source=projection_source, + return read_durable_completion_evidence( + state_file, todo_id=todo_id, registry_path=registry_path, + goal_id=args.goal_id, runtime_root=runtime_root, operation="outcome", completion_turn_key=settlement_identity.turn_instance_id, - no_followup=False, - ) - return project_durable_completion_outcome( - todo=durable_todo, - expected_todo_id=todo_id, - existing_todo_ids=existing_todo_ids, ) except (OSError, ValueError): return None @@ -586,23 +550,10 @@ def terminal_completion_readback() -> dict[str, object] | None: project=None, state_file=None, ) - ( - durable_todo, - existing_todo_ids, - projection_source, - ) = read_persisted_todo_record_with_source( - state_file, - todo_id=todo_id, - registry_path=registry_path, - goal_id=args.goal_id, - runtime_root=runtime_root, - ) - readback = project_durable_terminal_completion_readback( - todo=durable_todo, - expected_todo_id=todo_id, - expected_completion_turn_key=settlement_identity.turn_instance_id, - projection_source=projection_source, - existing_todo_ids=existing_todo_ids, + readback = read_durable_completion_evidence( + state_file, todo_id=todo_id, registry_path=registry_path, + goal_id=args.goal_id, runtime_root=runtime_root, operation="terminal", + completion_turn_key=settlement_identity.turn_instance_id, ) if readback["kind"] == "absent": return None diff --git a/loopx/control_plane/coordination/local_authority.py b/loopx/control_plane/coordination/local_authority.py index 485b5f4d50..0cf337488b 100644 --- a/loopx/control_plane/coordination/local_authority.py +++ b/loopx/control_plane/coordination/local_authority.py @@ -285,63 +285,8 @@ def read_canonical_todos_if_promoted( 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", - code="local_authority_todo_list_invalid_result", - payload={"source_authority": "file_v0"}, - ) - payload = dict(result) - if payload.get("reason_code") == "canonical_authority_required": - raise CanonicalAuthorityRequired( - goal_id=goal_id, operation="todo_read", payload=payload, - ) - todos = payload.get("todos") - todo_read_model = payload.get("todo_read_model") - if ( - payload.get("status") != "loaded" - or payload.get("source_authority") not in LOCAL_AUTHORITY_SOURCES - or payload.get("decision_read_from_provider") is not True - or payload.get("legacy_fallback_used") is not False - or not isinstance(todos, list) - or any(not isinstance(item, Mapping) for item in todos) - or not isinstance(todo_read_model, Mapping) - or todo_read_model.get("schema_version") - not in { - TODO_CANONICAL_READ_RECORD_SCHEMA_VERSION, - TODO_DOMAIN_READ_RECORD_SCHEMA_VERSION, - } - or todo_read_model.get("todo_count") != len(todos) - ): - raise LocalCoordinationAuthorityUnavailable( - str(payload.get("reason") or "canonical Todo authority is unavailable"), - code=str( - payload.get("reason_code") or "local_authority_todo_list_unavailable" - ), - payload=payload, - ) - payload["todos"] = [dict(item) for item in todos] - if include_leases and ( - not isinstance(payload.get("leases"), list) - or any(not isinstance(item, Mapping) for item in payload["leases"]) - or not isinstance(payload.get("provider_revision"), str) - ): - raise LocalCoordinationAuthorityUnavailable( - "canonical Todo/lease snapshot is incomplete", code="local_authority_snapshot_incomplete", - payload=payload, - ) - if projection_readback is not None: - confirmation = payload.get("projection_readback") - if (not isinstance(confirmation, Mapping) - or confirmation.get("provider_revision") != projection_readback["provider_revision"] - or confirmation.get("observed_provider_revision") != payload.get("provider_revision") - or confirmation.get("status") not in {"pending", "delivered", "current"} - or confirmation.get("next_action") not in {"retry", "finish"} - or (confirmation.get("next_action") == "retry" and confirmation.get("status") != "pending")): - raise LocalCoordinationAuthorityUnavailable( - "canonical projection confirmation is missing or invalid", - code="local_authority_projection_confirmation_invalid", payload=payload, - ) + payload = validate_canonical_todo_read(result, goal_id=goal_id, include_leases=include_leases, + projection_readback=projection_readback) 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) @@ -404,6 +349,71 @@ def read(self, *, runtime_root: Path, goal_id: str) -> dict[str, Any]: return deepcopy(result) +def validate_canonical_todo_read( + result: Any, *, goal_id: str, include_leases: bool = False, + projection_readback: Mapping[str, Any] | None = None, +) -> dict[str, Any]: + """Decode a native source result without adding another provider read.""" + if not isinstance(result, Mapping): + raise LocalCoordinationAuthorityUnavailable( + "local coordination authority returned an invalid Todo list", + code="local_authority_todo_list_invalid_result", + payload={"source_authority": "file_v0"}, + ) + payload = dict(result) + if payload.get("reason_code") == "canonical_authority_required": + raise CanonicalAuthorityRequired( + goal_id=goal_id, operation="todo_read", payload=payload, + ) + todos = payload.get("todos") + todo_read_model = payload.get("todo_read_model") + if ( + payload.get("status") != "loaded" + or payload.get("source_authority") not in LOCAL_AUTHORITY_SOURCES + or payload.get("decision_read_from_provider") is not True + or payload.get("legacy_fallback_used") is not False + or not isinstance(todos, list) + or any(not isinstance(item, Mapping) for item in todos) + or not isinstance(todo_read_model, Mapping) + or todo_read_model.get("schema_version") + not in { + TODO_CANONICAL_READ_RECORD_SCHEMA_VERSION, + TODO_DOMAIN_READ_RECORD_SCHEMA_VERSION, + } + or todo_read_model.get("todo_count") != len(todos) + ): + raise LocalCoordinationAuthorityUnavailable( + str(payload.get("reason") or "canonical Todo authority is unavailable"), + code=str( + payload.get("reason_code") or "local_authority_todo_list_unavailable" + ), + payload=payload, + ) + payload["todos"] = [dict(item) for item in todos] + if include_leases and ( + not isinstance(payload.get("leases"), list) + or any(not isinstance(item, Mapping) for item in payload["leases"]) + or not isinstance(payload.get("provider_revision"), str) + ): + raise LocalCoordinationAuthorityUnavailable( + "canonical Todo/lease snapshot is incomplete", code="local_authority_snapshot_incomplete", + payload=payload, + ) + if projection_readback is not None: + confirmation = payload.get("projection_readback") + if (not isinstance(confirmation, Mapping) + or confirmation.get("provider_revision") != projection_readback["provider_revision"] + or confirmation.get("observed_provider_revision") != payload.get("provider_revision") + or confirmation.get("status") not in {"pending", "delivered", "current"} + or confirmation.get("next_action") not in {"retry", "finish"} + or (confirmation.get("next_action") == "retry" and confirmation.get("status") != "pending")): + raise LocalCoordinationAuthorityUnavailable( + "canonical projection confirmation is missing or invalid", + code="local_authority_projection_confirmation_invalid", payload=payload, + ) + return payload + + def read_canonical_todo_fields_if_promoted( *, runtime_root: Path, goal_id: str, rollout_events: list[dict[str, Any]] | None = None, diff --git a/loopx/control_plane/effect_runtime_handlers.ts b/loopx/control_plane/effect_runtime_handlers.ts index 258d0972ac..f4a37a5f4f 100644 --- a/loopx/control_plane/effect_runtime_handlers.ts +++ b/loopx/control_plane/effect_runtime_handlers.ts @@ -269,7 +269,6 @@ export function createEffectRuntimeHandlers( ["turn_journal.write", lazyHandler(() => import("./turn_driver/turn_journal_effects.ts"), ({commitTurnJournal}) => commitTurnJournal)], ["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.priority.plan", lazyHandler(() => import("./todos/priority.ts"), ({evaluateTodoPriority}) => evaluateTodoPriority)], ["todo.read", lazyHandler(() => import("./todos/read_transport.ts"), ({readTodoRuntime}) => readTodoRuntime)], ["capabilities.periodic_report.progress.select", lazyHandler(() => import("./capabilities/periodic_report_progress.ts"), ({selectPeriodicReportProgress}) => selectPeriodicReportProgress)], diff --git a/loopx/control_plane/todos/completion_fence.py b/loopx/control_plane/todos/completion_fence.py deleted file mode 100644 index 120d429e0f..0000000000 --- a/loopx/control_plane/todos/completion_fence.py +++ /dev/null @@ -1,78 +0,0 @@ -from __future__ import annotations - -from collections.abc import Mapping -from typing import Any - -from ..effect_runtime import EffectRuntimeRejected, effect_runtime_result - - -TODO_COMPLETION_FENCE_REQUEST_SCHEMA = "loopx_todo_completion_fence_request_v0" -TODO_COMPLETION_FENCE_RESULT_SCHEMA = "loopx_todo_completion_fence_result_v0" - - -def _json_successor_value(value: Any) -> Any: - if isinstance(value, (tuple, set)): - return list(value) - return value - - -def evaluate_todo_completion_fence( - *, - todo: Mapping[str, Any], - projection_source: str, - completion_turn_key: str | None, - no_followup: bool, - goal_id: str | None = None, - todo_id: str | None = None, - completion_identity_source: str | None = None, -) -> dict[str, Any]: - """Ask the TypeScript Todo owner for the canonical replay-fence decision.""" - - raw_no_followup = todo.get("no_followup") - if raw_no_followup is not None and not isinstance(raw_no_followup, (bool, str)): - raw_no_followup = str(raw_no_followup) - raw_continuation = todo.get("completion_continuation") - if raw_continuation is not None and not isinstance(raw_continuation, str): - raw_continuation = str(raw_continuation) - raw_turn_key = todo.get("completion_turn_key") - if raw_turn_key is not None and not isinstance(raw_turn_key, str): - raw_turn_key = str(raw_turn_key) - try: - result = effect_runtime_result( - "todo.completion_fence.evaluate", - { - "schema_version": TODO_COMPLETION_FENCE_REQUEST_SCHEMA, - "projection_source": projection_source, - "todo": { - "status": str(todo.get("status") or "open"), - "no_followup": raw_no_followup, - "completion_continuation": raw_continuation, - "completion_turn_key": raw_turn_key, - "successor_todo_ids": _json_successor_value( - todo.get("successor_todo_ids") - ), - }, - "requested_no_followup": no_followup, - "requested_completion_turn_key": completion_turn_key, - "requested_completion_identity_source": completion_identity_source, - "goal_id": goal_id, - "todo_id": todo_id, - }, - ) - except EffectRuntimeRejected as exc: - raise ValueError(str(exc)) from None - if not isinstance(result, Mapping): - raise RuntimeError("TypeScript Todo completion fence result must be an object") - if ( - result.get("schema_version") != TODO_COMPLETION_FENCE_RESULT_SCHEMA - or result.get("outcome") not in {"continue", "replay"} - or not isinstance(result.get("reason"), str) - or not isinstance(result.get("status"), str) - or not isinstance(result.get("terminal_before_request"), bool) - or not ( - result.get("completion_continuation") is None - or isinstance(result.get("completion_continuation"), str) - ) - ): - raise RuntimeError("TypeScript Todo completion fence result shape mismatch") - return dict(result) diff --git a/loopx/control_plane/todos/durable_completion.py b/loopx/control_plane/todos/durable_completion.py index 77c32b73fe..605a65afb1 100644 --- a/loopx/control_plane/todos/durable_completion.py +++ b/loopx/control_plane/todos/durable_completion.py @@ -23,98 +23,18 @@ section_bounds, todo_blocks, ) -from .contract import ( - TODO_STATUS_DONE, - normalize_todo_id, - normalize_todo_id_list, - normalize_todo_no_followup, - normalize_todo_status, -) -from .completion_state import ( - TodoCompletionContinuation, - normalize_todo_completion_continuation, - normalize_todo_completion_recovery, -) -from .completion_fence import evaluate_todo_completion_fence +from .contract import normalize_todo_id +from ..effect_runtime import EffectRuntimeRejected +from .goal_todo_projection import project_todo_read TodoCompletionProjectionSource = Literal["materialized"] -def read_persisted_todo_record( - state_file: Path, - *, - todo_id: str, - registry_path: Path | None = None, - goal_id: str | None = None, - runtime_root: Path | None = None, -) -> tuple[dict[str, Any], set[str]]: - """Read one durable Todo record and the goal-state Todo id universe. - - Returns the parsed Todo record for ``todo_id`` together with the set of - Todo ids present in the persisted goal state. Like - ``complete_goal_todo``, the record is looked up in the active Markdown - blocks first and falls back to the event projection for event-sourced - goals. Raises ``ValueError`` when the Todo cannot be located in the - durable lifecycle, so the caller can fail closed instead of projecting a - fabricated continuation. - """ - todo, existing_todo_ids, _projection_source = ( - read_persisted_todo_record_with_source( - state_file, - todo_id=todo_id, - registry_path=registry_path, - goal_id=goal_id, - runtime_root=runtime_root, - ) - ) - return todo, existing_todo_ids - - -def read_persisted_todo_record_with_source( - state_file: Path, - *, - todo_id: str, - registry_path: Path | None = None, - goal_id: str | None = None, - runtime_root: Path | None = None, +def _read_legacy_todo_record( + state_file: Path, *, todo_id: str, ) -> tuple[dict[str, Any], set[str], TodoCompletionProjectionSource]: - """Read one durable Todo together with its authoritative projection source. - - A promoted canonical provider is the primary persisted lifecycle. Markdown - and event projection remain the pre-promotion sources only; a stale - Markdown projection must never hide a canonical commit during Turn retry. - """ - - if runtime_root is not None and goal_id is not None: - from ..coordination.local_authority import ( - read_canonical_todos_if_promoted, - ) - - canonical = read_canonical_todos_if_promoted( - runtime_root=runtime_root, - goal_id=goal_id, - ) - if canonical is not None: - records = [ - dict(item) - for item in canonical.get("todos", []) - if isinstance(item, Mapping) - ] - canonical_todo_ids = { - normalized - for item in records - if (normalized := normalize_todo_id(item.get("todo_id"))) - } - normalized_todo_id = normalize_todo_id(todo_id) or todo_id - for item in records: - if normalize_todo_id(item.get("todo_id")) == normalized_todo_id: - return item, canonical_todo_ids, "materialized" - raise ValueError( - f"durable completion todo_id {normalized_todo_id!r} was not found " - "in canonical provider state" - ) - + """Decode explicit legacy Markdown; canonical callers never enter this IO path.""" lines = state_file.read_text(encoding="utf-8").splitlines() block: dict[str, Any] | None = None projection_source: TodoCompletionProjectionSource = "materialized" @@ -140,192 +60,160 @@ def read_persisted_todo_record_with_source( return block, existing_todo_ids, projection_source -def project_durable_completion_outcome( +def _completion_facts(todo: Mapping[str, Any]) -> dict[str, Any]: + """Preserve Python scalar/container text at the legacy format boundary.""" + successors = todo.get("successor_todo_ids") + values = successors if isinstance(successors, (list, tuple, set)) else [successors] + return { + **{ + key: str(todo.get(key) or "") + for key in ( + "todo_id", + "status", + "completion_continuation", + "completion_recovery", + ) + }, + "no_followup": todo.get("no_followup") + if isinstance(todo.get("no_followup"), bool) + else str(todo.get("no_followup") or ""), + "successor_todo_ids": [str(value or "") for value in values], + } + + +def _completion_request(**facts: Any) -> dict[str, Any]: + try: + result = project_todo_read({"kind": "completion_evidence", **facts}) + except EffectRuntimeRejected as exc: + raise ValueError(str(exc)) from None + if not isinstance(result, Mapping): + raise RuntimeError("TypeScript durable completion result must be an object") + if "canonical" in result: + from ..coordination.local_authority import validate_canonical_todo_read + + validate_canonical_todo_read( + result["canonical"], goal_id=facts["source"]["goal_id"] + ) + if "completion_error" in result: + raise ValueError(result["completion_error"]) + completion = result.get("completion") + if not isinstance(completion, dict): + raise RuntimeError("TypeScript durable completion result must be an object") + return completion + + +def _project_completion_view( *, todo: Mapping[str, Any], expected_todo_id: str, - existing_todo_ids: Collection[str] | None = None, + existing_todo_ids: Collection[str] | None, + operation: str, + completion_turn_key: str | None = None, + projection_source: str = "materialized", ) -> dict[str, Any]: - """Project the typed completion continuation recorded by the Todo lifecycle. - - ``todo`` is the durable Todo record re-read from persisted lifecycle state - after ``complete_goal_todo``. The outcome is one of: + fence_todo = {} + for key in ("no_followup", "completion_continuation", "completion_turn_key"): + value = todo.get(key) + fence_todo[key] = ( + value + if value is None + or isinstance(value, str) + or (key == "no_followup" and isinstance(value, bool)) + else str(value) + ) + successors = todo.get("successor_todo_ids") + fence_todo.update( + status=str(todo.get("status") or "open"), + successor_todo_ids=( + list(successors) if isinstance(successors, (tuple, set)) else successors + ), + ) + return _completion_request( + todo=_completion_facts(todo), + expected_todo_id=str(expected_todo_id or ""), + existing_todo_ids=list(existing_todo_ids) + if existing_todo_ids is not None + else None, + operation=operation, + completion_turn_key=completion_turn_key, + projection_source=projection_source, + fence_todo=fence_todo, + ) - - ``successor`` when the Todo durably declares ``successor_todo_ids``; - every declared successor must exist in the goal state - (``existing_todo_ids``) or projection fails closed; - - ``no_followup`` when the Todo durably records ``no_followup``; - - ``active_goal`` when that value is durably recorded. - Raises ``ValueError`` (fail closed) when the durable record contradicts - itself, omits its explicit completion continuation, declares a dangling - successor, does not match ``expected_todo_id``, or is not durably done. - """ - return _project_durable_completion( +def project_durable_completion_outcome( + *, + todo: Mapping[str, Any], + expected_todo_id: str, + existing_todo_ids: Collection[str] | None = None, +) -> dict[str, Any]: + return _project_completion_view( todo=todo, expected_todo_id=expected_todo_id, existing_todo_ids=existing_todo_ids, - require_done=True, + operation="outcome", ) -def project_durable_terminal_completion_readback( +def project_durable_completion_intent( *, todo: Mapping[str, Any], expected_todo_id: str, - expected_completion_turn_key: str, - projection_source: TodoCompletionProjectionSource, existing_todo_ids: Collection[str] | None = None, ) -> dict[str, Any]: - """Resolve terminal completion evidence under the canonical Turn fence. - - A still-open Todo or a same-Turn terminal upgrade proves the terminal - effect absent. A completed no-follow-up Todo proves the effect committed - only when its durable ``completion_turn_key`` matches the recovering Turn. - The TypeScript fence raises for missing, cross-Turn, or contradictory - completion identity so callers can map uncertainty to fail-closed recovery. - """ - - fence = evaluate_todo_completion_fence( - todo=todo, - projection_source=projection_source, - completion_turn_key=expected_completion_turn_key, - no_followup=True, - ) - if fence["outcome"] == "continue": - if fence["reason"] == "same_turn_terminal_upgrade" or ( - fence["reason"] == "not_terminal" and fence["status"] == "open" - ): - return {"kind": "absent"} - raise ValueError("durable terminal completion is not safely replayable") - if normalize_todo_status(todo.get("status")) != TODO_STATUS_DONE: - raise ValueError("durable terminal completion is not safely replayable") - - completion = project_durable_completion_outcome( + return _project_completion_view( todo=todo, expected_todo_id=expected_todo_id, existing_todo_ids=existing_todo_ids, + operation="intent", ) - if completion.get("continuation") != TodoCompletionContinuation.NO_FOLLOWUP.value: - raise ValueError("durable terminal completion is not no-follow-up") - return {"kind": "committed", "completion": completion} -def project_durable_completion_intent( +def project_durable_terminal_completion_readback( *, todo: Mapping[str, Any], expected_todo_id: str, + expected_completion_turn_key: str, + projection_source: TodoCompletionProjectionSource, existing_todo_ids: Collection[str] | None = None, ) -> dict[str, Any]: - """Project the declared continuation before applying lifecycle closeout. - - Turn settlement uses this read-only projection to distinguish an ordinary - completion from the terminal ``no_followup`` effect. It validates the same - continuation invariants as :func:`project_durable_completion_outcome` but - deliberately accepts an open Todo so terminal mutation can be deferred - until matching writeback and spend receipts exist. - """ - - return _project_durable_completion( + return _project_completion_view( todo=todo, expected_todo_id=expected_todo_id, existing_todo_ids=existing_todo_ids, - require_done=False, + operation="terminal", + completion_turn_key=expected_completion_turn_key, + projection_source=projection_source, ) -def _project_durable_completion( +def read_durable_completion_evidence( + state_file: Path, *, - todo: Mapping[str, Any], - expected_todo_id: str, - existing_todo_ids: Collection[str] | None, - require_done: bool, + todo_id: str, + operation: str, + registry_path: Path | None = None, + goal_id: str | None = None, + runtime_root: Path | None = None, + completion_turn_key: str | None = None, ) -> dict[str, Any]: - normalized_expected_todo_id = normalize_todo_id(expected_todo_id) - if not normalized_expected_todo_id: - raise ValueError("durable completion requires a public Todo id") - durable_todo_id = normalize_todo_id(todo.get("todo_id")) - if durable_todo_id != normalized_expected_todo_id: - raise ValueError( - "durable completion todo_id does not match the selected Todo" - ) - if require_done and normalize_todo_status(todo.get("status")) != TODO_STATUS_DONE: - raise ValueError( - "durable completion requires the selected Todo to be durably done" + """Read canonical evidence in one native transaction; decode legacy IO locally.""" + if runtime_root is not None and goal_id is not None: + return _completion_request( + source={ + "runtime_root": str(runtime_root.expanduser().resolve(strict=False)), + "goal_id": goal_id, + }, + expected_todo_id=todo_id, + operation=operation, + completion_turn_key=completion_turn_key, ) - successor_todo_ids = normalize_todo_id_list(todo.get("successor_todo_ids")) - no_followup = normalize_todo_no_followup(todo.get("no_followup")) - completion_continuation = normalize_todo_completion_continuation( - todo.get("completion_continuation") - ) - raw_completion_recovery = todo.get("completion_recovery") - completion_recovery = normalize_todo_completion_recovery( - raw_completion_recovery + todo, existing, source = _read_legacy_todo_record(state_file, todo_id=todo_id) + return _project_completion_view( + todo=todo, + expected_todo_id=todo_id, + existing_todo_ids=existing, + operation=operation, + completion_turn_key=completion_turn_key, + projection_source=source, ) - if raw_completion_recovery and completion_recovery is None: - raise ValueError("durable completion_recovery is invalid") - if require_done and completion_continuation is None: - raise ValueError( - "durable completion is missing completion_continuation; repair the " - "Todo with `loopx todo complete`" - ) - if completion_continuation is None: - if no_followup is True: - completion_continuation = TodoCompletionContinuation.NO_FOLLOWUP.value - elif successor_todo_ids: - completion_continuation = TodoCompletionContinuation.SUCCESSOR.value - else: - completion_continuation = TodoCompletionContinuation.ACTIVE_GOAL.value - if no_followup is True: - if successor_todo_ids: - raise ValueError( - "durable completion records both no_followup and " - "successor_todo_ids" - ) - if completion_continuation != TodoCompletionContinuation.NO_FOLLOWUP.value: - raise ValueError( - "durable completion_continuation contradicts no_followup" - ) - return { - "todo_id": normalized_expected_todo_id, - "continuation": "no_followup", - } - if successor_todo_ids: - if completion_recovery is not None: - raise ValueError( - "durable completion_recovery requires a no_followup continuation" - ) - if completion_continuation != TodoCompletionContinuation.SUCCESSOR.value: - raise ValueError( - "durable completion_continuation contradicts successor_todo_ids" - ) - if existing_todo_ids is not None: - existing = set(existing_todo_ids) - missing_todo_ids = [ - successor_todo_id - for successor_todo_id in successor_todo_ids - if successor_todo_id not in existing - ] - if missing_todo_ids: - raise ValueError( - "durable completion declares missing successor Todo ids: " - + ", ".join(missing_todo_ids) - ) - return { - "todo_id": normalized_expected_todo_id, - "continuation": "successor", - "successor_todo_ids": successor_todo_ids, - } - if completion_continuation != TodoCompletionContinuation.ACTIVE_GOAL.value: - raise ValueError( - "durable completion_continuation requires matching no_followup or " - "successor_todo_ids" - ) - if completion_recovery is not None: - raise ValueError( - "durable completion_recovery requires a no_followup continuation" - ) - return { - "todo_id": normalized_expected_todo_id, - "continuation": "active_goal", - } diff --git a/loopx/control_plane/todos/read_projection.ts b/loopx/control_plane/todos/read_projection.ts index 160df0f247..2da9d71f26 100644 --- a/loopx/control_plane/todos/read_projection.ts +++ b/loopx/control_plane/todos/read_projection.ts @@ -1,4 +1,7 @@ /** One full-source Todo read reduction; the existing owners decide every lane. */ +import {evaluateTodoCompletionFence, TODO_COMPLETION_FENCE_REQUEST_SCHEMA} from "./completion_fence.ts"; +import {normalizeTodoNoFollowup, normalizeTodoCompletionContinuation, normalizeTodoCompletionRecovery} from "./completion_state.ts"; +import {pythonFieldText} from "./read_codec.ts"; import {authorityUnicodeCompare} from "../coordination/authority_store_codec.ts"; import {stripPythonWhitespace} from "../coordination/todo_agents.ts"; import {compactTodoReadSummary} from "./read_presentation.ts"; @@ -307,9 +310,13 @@ export async function projectSourceTodoFields(input: JsonObject): 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))) { + if (input.schema_version !== TODO_READ_REQUEST || !["group", "fields", "canonical_fields", "select", "source_fields", "query", "historical_fields", "completion_evidence"].includes(text(input.kind))) { throw new EffectRuntimeRequestError("Todo read request schema mismatch"); } + if (input.kind === "completion_evidence") { + const {readCompletionEvidence} = await import("./read_transaction.ts"); + return readCompletionEvidence(input); + } if (input.kind === "canonical_fields" || input.kind === "query") { const {readCanonicalTodoProjection} = await import("./read_transaction.ts"); return readCanonicalTodoProjection({...input, read_observed_at: observedAt}); @@ -322,3 +329,68 @@ export async function readTodo(value: unknown, observedAt = new Date().toISOStri if (input.kind === "source_fields") return projectSourceTodoFields(input); return input.kind === "fields" ? projectCanonicalTodoFields(input) : projectTodoReadGroup(input); } + +/** Complete continuation read, including the original fence-first diagnostics. */ +export function projectCompletionEvidence(input: JsonObject): JsonObject { + const todo = requireJsonObject(input.todo, "durable completion Todo"); + const operation = input.operation; + if (!["intent", "outcome", "terminal"].includes(String(operation))) throw new EffectRuntimeRequestError("unsupported completion evidence operation"); + const terminal = operation === "terminal"; + if (terminal || input.completion_turn_key != null) { + const fenceTodo = input.fence_todo == null ? { + status: pythonFieldText(todo, "status") || "open", + no_followup: typeof todo.no_followup === "boolean" ? todo.no_followup : pythonFieldText(todo, "no_followup") || null, + completion_continuation: pythonFieldText(todo, "completion_continuation") || null, + completion_turn_key: pythonFieldText(todo, "completion_turn_key") || null, + successor_todo_ids: todo.successor_todo_ids ?? null, + } : requireJsonObject(input.fence_todo, "completion fence Todo"); + const fence = evaluateTodoCompletionFence({schema_version: TODO_COMPLETION_FENCE_REQUEST_SCHEMA, + projection_source: input.projection_source ?? "materialized", todo: fenceTodo, + requested_no_followup: terminal, requested_completion_turn_key: input.completion_turn_key ?? null, + requested_completion_identity_source: null, goal_id: null, todo_id: null}); + if (terminal && fence.outcome === "continue") { + if (fence.reason === "same_turn_terminal_upgrade" || fence.reason === "not_terminal" && fence.status === "open") return {kind: "absent"}; + throw new EffectRuntimeRequestError("durable terminal completion is not safely replayable"); + } + if (terminal && stripPythonWhitespace(pythonFieldText(todo, "status")).toLowerCase() !== "done") + throw new EffectRuntimeRequestError("durable terminal completion is not safely replayable"); + } + const expected = id(input.expected_todo_id); + if (!expected) throw new EffectRuntimeRequestError("durable completion requires a public Todo id"); + if (id(pythonFieldText(todo, "todo_id")) !== expected) + throw new EffectRuntimeRequestError("durable completion todo_id does not match the selected Todo"); + const requireDone = operation !== "intent"; + if (requireDone && stripPythonWhitespace(pythonFieldText(todo, "status")).toLowerCase() !== "done") + throw new EffectRuntimeRequestError("durable completion requires the selected Todo to be durably done"); + const rawSuccessors = todo.successor_todo_ids; + const successorValues = Array.isArray(rawSuccessors) ? rawSuccessors.map((_, i) => pythonFieldText(rawSuccessors, i)) : [pythonFieldText(todo, "successor_todo_ids")]; + const successors = ids(successorValues); + const noFollowup = normalizeTodoNoFollowup(typeof todo.no_followup === "boolean" ? todo.no_followup : pythonFieldText(todo, "no_followup")); + let continuation = normalizeTodoCompletionContinuation(pythonFieldText(todo, "completion_continuation")); + const rawRecovery = pythonFieldText(todo, "completion_recovery"), recovery = normalizeTodoCompletionRecovery(rawRecovery); + if (rawRecovery && recovery === null) throw new EffectRuntimeRequestError("durable completion_recovery is invalid"); + if (requireDone && continuation === null) throw new EffectRuntimeRequestError("durable completion is missing completion_continuation; repair the Todo with `loopx todo complete`"); + continuation ??= noFollowup === true ? "no_followup" : successors.length ? "successor" : "active_goal"; + let completion: JsonObject; + if (noFollowup === true) { + if (successors.length) throw new EffectRuntimeRequestError("durable completion records both no_followup and successor_todo_ids"); + if (continuation !== "no_followup") throw new EffectRuntimeRequestError("durable completion_continuation contradicts no_followup"); + completion = {todo_id: expected, continuation: "no_followup"}; + } else if (successors.length) { + if (recovery !== null) throw new EffectRuntimeRequestError("durable completion_recovery requires a no_followup continuation"); + if (continuation !== "successor") throw new EffectRuntimeRequestError("durable completion_continuation contradicts successor_todo_ids"); + if (input.existing_todo_ids != null) { + if (!Array.isArray(input.existing_todo_ids)) throw new EffectRuntimeRequestError("existing Todo ids must be an array"); + const existing = new Set(input.existing_todo_ids), missing = successors.filter(value => !existing.has(value)); + if (missing.length) throw new EffectRuntimeRequestError("durable completion declares missing successor Todo ids: " + missing.join(", ")); + } + completion = {todo_id: expected, continuation: "successor", successor_todo_ids: successors}; + } else { + if (continuation !== "active_goal") throw new EffectRuntimeRequestError("durable completion_continuation requires matching no_followup or successor_todo_ids"); + if (recovery !== null) throw new EffectRuntimeRequestError("durable completion_recovery requires a no_followup continuation"); + completion = {todo_id: expected, continuation: "active_goal"}; + } + if (!terminal) return completion; + if (completion.continuation !== "no_followup") throw new EffectRuntimeRequestError("durable terminal completion is not no-follow-up"); + return {kind: "committed", completion}; +} diff --git a/loopx/control_plane/todos/read_transaction.ts b/loopx/control_plane/todos/read_transaction.ts index a14e58b55c..98c65abb8f 100644 --- a/loopx/control_plane/todos/read_transaction.ts +++ b/loopx/control_plane/todos/read_transaction.ts @@ -6,22 +6,34 @@ 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, projectTodoReadGroup, TODO_READ_RESULT} from "./read_projection.ts"; -import {canonicalTodoReadItems, structureCanonicalTodo} from "./read_codec.ts"; +import {projectCompletionEvidence, projectCanonicalTodoFields, selectTodoReadFields, projectTodoReadGroup, TODO_READ_RESULT} from "./read_projection.ts"; +import {id, canonicalTodoReadItems, structureCanonicalTodo} from "./read_codec.ts"; import {compactTodoReadSummary, compactThinTodoRead, compactTodoQueueSummary, 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 canonical = await collectCanonicalTodoSource(requireJsonObject(input.source, "canonical Todo read source")); + if (canonical.status !== "loaded") return {schema_version: TODO_READ_RESULT, canonical}; + const todos = canonical.todos; + const guards = canonical.goal_acceptance_work_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); +} + +async function collectCanonicalTodoSource(source: JsonObject): Promise { 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 (page.status !== "page") return { + ...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); @@ -29,7 +41,7 @@ export async function readCanonicalTodoProjection(input: JsonObject): Promise requireJsonObject(value, "canonical Todo").todo_id), store_identity: snapshot.store_identity, @@ -37,13 +49,6 @@ export async function readCanonicalTodoProjection(input: JsonObject): Promise Array.isArray(value) ? value.map(row => requireJsonObject(row, "Todo source row")) : []; @@ -169,3 +174,17 @@ async function todoListPayload(input: JsonObject, original: JsonObject[], fields goal_acceptance_oracle_observation_basis: canonical.goal_acceptance_oracle_observation_basis, }} : {})}; } + +export async function readCompletionEvidence(input: JsonObject): Promise { + let facts = input; + if (input.source != null) { + const canonical = await collectCanonicalTodoSource(requireJsonObject(input.source, "completion evidence source")); + if (canonical.status !== "loaded") return {schema_version: TODO_READ_RESULT, canonical}; + const records = originalRecords(canonical.todos), expected = id(input.expected_todo_id) ?? input.expected_todo_id; + const todo = records.find(row => id(row.todo_id) === expected); + if (!todo) return {schema_version: TODO_READ_RESULT, completion_error: + `durable completion todo_id '${String(expected)}' was not found in canonical provider state`}; + facts = {...input, todo, existing_todo_ids: records.map(row => id(row.todo_id)).filter(Boolean)}; + } + return {schema_version: TODO_READ_RESULT, completion: projectCompletionEvidence(facts)}; +} diff --git a/tests/control_plane/test_completion_evidence_read.py b/tests/control_plane/test_completion_evidence_read.py new file mode 100644 index 0000000000..8ff312feb9 --- /dev/null +++ b/tests/control_plane/test_completion_evidence_read.py @@ -0,0 +1,165 @@ +"""Frozen pre-cutover completion observations; synthetic migration evidence.""" + +from __future__ import annotations + +import json +from pathlib import Path + +import pytest + +from loopx.control_plane import effect_runtime +from loopx.control_plane.todos import durable_completion as completion + +FIXTURE = ( + Path(__file__).parents[1] / "fixtures/control_plane/completion_evidence_read.json" +) +TODO = "todo_fixture0001" + + +def observe(case): + kwargs = { + "todo": case["todo"], + "expected_todo_id": case.get("expected", TODO), + "existing_todo_ids": case.get("existing"), + } + try: + if case["operation"] == "terminal": + result = completion.project_durable_terminal_completion_readback( + **kwargs, + expected_completion_turn_key="turn-a", + projection_source="materialized", + ) + elif case["operation"] == "intent": + result = completion.project_durable_completion_intent(**kwargs) + else: + result = completion.project_durable_completion_outcome(**kwargs) + return {"result": result} + except ValueError as exc: + return {"error_type": type(exc).__name__, "message": str(exc)} + + +@pytest.mark.parametrize( + "case", json.loads(FIXTURE.read_text())["cases"], ids=lambda c: c["name"] +) +def test_complete_observation_bytes(case): + assert ( + json.dumps(observe(case), ensure_ascii=False, separators=(",", ":")) + == case["bytes"] + ) + + +@pytest.mark.parametrize("provider", ["file", "sqlite"]) +@pytest.mark.parametrize( + "operation", ["ordinary", "terminal", "intent", "post_completion", "legacy"] +) +def test_complete_read_crossings(tmp_path, monkeypatch, provider, operation): + from loopx.control_plane.coordination.runtime_shadow import ( + build_todo_runtime_shadow_projection, + ) + from tests.control_plane.canonical_authority_fixture import ( + initialize_canonical_authority, + isolate_sqlite_runtime, + read_fixture_todos, + ) + + isolate_sqlite_runtime(tmp_path, monkeypatch) + todo = { + "todo_id": TODO, + "status": "done", + "role": "agent", + "text": "Synthetic completed work", + "completion_continuation": "no_followup" + if operation == "terminal" + else "active_goal", + "completion_turn_key": "turn-a", + "no_followup": operation == "terminal", + } + state = tmp_path / "ACTIVE_GOAL_STATE.md" + state.write_text( + "# Fixture\n\n## Agent Todo\n\n- [x] Synthetic completed work\n" + " \n" + ) + todo = {**read_fixture_todos(state)[0], **todo} + runtime = tmp_path / "runtime" + if operation != "legacy": + initialize_canonical_authority( + runtime, + "goal-fixture", + build_todo_runtime_shadow_projection( + goal_id="goal-fixture", todos=[todo], handoff_mode="soft_claim" + ), + state_path=state, + provider=provider, + ) + state.write_text("# Stale display with no Todo\n") + calls = [] + original = effect_runtime.effect_runtime_request + + def counted(method, *args, **kwargs): + calls.append(method) + return original(method, *args, **kwargs) + + monkeypatch.setattr(effect_runtime, "effect_runtime_request", counted) + result = completion.read_durable_completion_evidence( + state, + todo_id=TODO, + runtime_root=runtime if operation != "legacy" else None, + goal_id="goal-fixture", + operation="outcome" + if operation in {"ordinary", "post_completion", "legacy"} + else operation, + completion_turn_key="turn-a" + if operation in {"ordinary", "terminal"} + else None, + ) + assert len(calls) == 1, calls + expected = { + "todo_id": TODO, + "continuation": "no_followup" if operation == "terminal" else "active_goal", + } + assert result == ( + {"kind": "committed", "completion": expected} + if operation == "terminal" + else expected + ) + + +@pytest.mark.parametrize("malformed", [None, {}, {"completion": []}]) +def test_complete_read_rejects_malformed_result(monkeypatch, malformed): + monkeypatch.setattr(completion, "project_todo_read", lambda _: malformed) + with pytest.raises(RuntimeError): + completion.project_durable_completion_outcome(todo={}, expected_todo_id=TODO) + + +def test_complete_read_preserves_value_error(monkeypatch): + def reject(_): + raise effect_runtime.EffectRuntimeRejected("typed completion rejected") + + monkeypatch.setattr(completion, "project_todo_read", reject) + with pytest.raises(ValueError, match="^typed completion rejected$"): + completion.project_durable_completion_outcome(todo={}, expected_todo_id=TODO) + + +def test_canonical_refusal_never_falls_back_to_completed_markdown(tmp_path): + from loopx.control_plane.coordination.local_authority import CanonicalAuthorityRequired + + state = tmp_path / "ACTIVE_GOAL_STATE.md" + state.write_text( + "# Fixture\n\n## Agent Todo\n\n- [x] Synthetic completed work\n" + " \n" + ) + with pytest.raises(CanonicalAuthorityRequired): + completion.read_durable_completion_evidence( + state, todo_id=TODO, operation="outcome", goal_id="goal-fixture", + runtime_root=tmp_path / "runtime", + ) + + +@pytest.mark.parametrize("container", [tuple, set]) +def test_python_container_codec_preserves_frozen_output(container): + case = next( + case for case in json.loads(FIXTURE.read_text())["cases"] + if case["name"] == "container-successors-['todo_aaa']" + ) + case["todo"]["successor_todo_ids"] = container(["todo_aaa"]) + assert json.dumps(observe(case), ensure_ascii=False, separators=(",", ":")) == case["bytes"] diff --git a/tests/control_plane/test_durable_completion_projection.py b/tests/control_plane/test_durable_completion_projection.py index 659f807d60..abd023a34a 100644 --- a/tests/control_plane/test_durable_completion_projection.py +++ b/tests/control_plane/test_durable_completion_projection.py @@ -8,8 +8,7 @@ project_durable_completion_intent, project_durable_completion_outcome, project_durable_terminal_completion_readback, - read_persisted_todo_record, - read_persisted_todo_record_with_source, + read_durable_completion_evidence, ) TODO_DONE = { @@ -246,7 +245,7 @@ def test_fails_closed_when_todo_is_not_durably_done() -> None: ) -def test_read_persisted_todo_record_projects_declared_successor( +def test_read_durable_completion_evidence_projects_declared_successor( tmp_path: Path, ) -> None: state_file = tmp_path / "ACTIVE_GOAL_STATE.md" @@ -271,16 +270,8 @@ def test_read_persisted_todo_record_projects_declared_successor( ), encoding="utf-8", ) - todo, existing_todo_ids, projection_source = read_persisted_todo_record_with_source( - state_file, - todo_id="todo_fixture0001", - ) - assert projection_source == "materialized" - assert existing_todo_ids == {"todo_fixture0001", "todo_fixture0002"} - outcome = project_durable_completion_outcome( - todo=todo, - expected_todo_id="todo_fixture0001", - existing_todo_ids=existing_todo_ids, + outcome = read_durable_completion_evidence( + state_file, todo_id="todo_fixture0001", operation="outcome", ) assert outcome == { "todo_id": "todo_fixture0001", @@ -289,7 +280,7 @@ def test_read_persisted_todo_record_projects_declared_successor( } -def test_read_persisted_todo_record_fails_closed_when_todo_is_missing( +def test_read_durable_completion_evidence_fails_closed_when_todo_is_missing( tmp_path: Path, ) -> None: state_file = tmp_path / "ACTIVE_GOAL_STATE.md" @@ -313,7 +304,6 @@ def test_read_persisted_todo_record_fails_closed_when_todo_is_missing( ValueError, match="was not found in persisted Todo lifecycle state", ): - read_persisted_todo_record( - state_file, - todo_id="todo_fixture0001", + read_durable_completion_evidence( + state_file, todo_id="todo_fixture0001", operation="outcome", ) diff --git a/tests/control_plane/test_todo_completion_fence_runtime.py b/tests/control_plane/test_todo_completion_fence_runtime.py deleted file mode 100644 index 6e44939c66..0000000000 --- a/tests/control_plane/test_todo_completion_fence_runtime.py +++ /dev/null @@ -1,104 +0,0 @@ -from __future__ import annotations - -import pytest - -from loopx.control_plane import effect_runtime -from loopx.control_plane.todos import completion_fence - - -def _runtime_result(**overrides: object) -> dict[str, object]: - return { - "schema_version": completion_fence.TODO_COMPLETION_FENCE_RESULT_SCHEMA, - "outcome": "replay", - "reason": "already_terminal", - "status": "done", - "terminal_before_request": True, - "completion_continuation": "no_followup", - **overrides, - } - - -def test_python_facade_sends_one_coarse_typed_request(monkeypatch) -> None: - captured: dict[str, object] = {} - - def call(method: str, params: dict[str, object]) -> dict[str, object]: - captured["method"] = method - captured["params"] = params - return _runtime_result() - - monkeypatch.setattr(completion_fence, "effect_runtime_result", call) - - result = completion_fence.evaluate_todo_completion_fence( - todo={ - "status": "done", - "no_followup": "true", - "completion_continuation": "no_followup", - "completion_turn_key": "turn-a", - "successor_todo_ids": (), - }, - projection_source="materialized", - completion_turn_key="turn-a", - no_followup=True, - ) - - assert result == _runtime_result() - assert captured["method"] == "todo.completion_fence.evaluate" - assert captured["params"] == { - "schema_version": completion_fence.TODO_COMPLETION_FENCE_REQUEST_SCHEMA, - "projection_source": "materialized", - "todo": { - "status": "done", - "no_followup": "true", - "completion_continuation": "no_followup", - "completion_turn_key": "turn-a", - "successor_todo_ids": [], - }, - "requested_no_followup": True, - "requested_completion_turn_key": "turn-a", - "requested_completion_identity_source": None, - "goal_id": None, - "todo_id": None, - } - - -@pytest.mark.parametrize( - "malformed", - [ - None, - {}, - _runtime_result(schema_version="loopx_todo_completion_fence_result_v1"), - _runtime_result(terminal_before_request="true"), - _runtime_result(completion_continuation=[]), - ], -) -def test_python_facade_rejects_malformed_runtime_results( - monkeypatch, malformed: object -) -> None: - monkeypatch.setattr( - completion_fence, - "effect_runtime_result", - lambda _method, _params: malformed, - ) - - with pytest.raises(RuntimeError, match="result (must be|shape mismatch)"): - completion_fence.evaluate_todo_completion_fence( - todo={"status": "open"}, - projection_source="materialized", - completion_turn_key=None, - no_followup=False, - ) - - -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 fence rejected") - - monkeypatch.setattr(completion_fence, "effect_runtime_result", reject) - - with pytest.raises(ValueError, match="typed fence rejected"): - completion_fence.evaluate_todo_completion_fence( - todo={"status": "done"}, - projection_source="materialized", - completion_turn_key="turn-b", - no_followup=True, - ) diff --git a/tests/control_plane_ts/completion_evidence_read.test.ts b/tests/control_plane_ts/completion_evidence_read.test.ts new file mode 100644 index 0000000000..998e2714ad --- /dev/null +++ b/tests/control_plane_ts/completion_evidence_read.test.ts @@ -0,0 +1,17 @@ +import assert from "node:assert/strict"; +import test from "node:test"; +import fixture from "../fixtures/control_plane/completion_evidence_read.json" with {type: "json"}; +import {readTodo, TODO_READ_REQUEST} from "../../loopx/control_plane/todos/read_projection.ts"; + +for (const sample of fixture.cases) { + test(`native complete evidence: ${sample.name}`, async () => { + const expected = JSON.parse(sample.bytes); + const input = {schema_version: TODO_READ_REQUEST, kind: "completion_evidence", + operation: sample.operation, todo: sample.todo, + expected_todo_id: "expected" in sample ? sample.expected : "todo_fixture0001", + existing_todo_ids: "existing" in sample ? sample.existing : null, + ...(sample.operation === "terminal" ? {completion_turn_key: "turn-a"} : {})}; + if (expected.error_type) await assert.rejects(readTodo(input), {message: expected.message}); + else assert.deepEqual((await readTodo(input)).completion, expected.result); + }); +} diff --git a/tests/fixtures/control_plane/completion_evidence_read.json b/tests/fixtures/control_plane/completion_evidence_read.json new file mode 100644 index 0000000000..d1be7fd093 --- /dev/null +++ b/tests/fixtures/control_plane/completion_evidence_read.json @@ -0,0 +1,1059 @@ +{ + "crossings_before": { + "ordinary": 2, + "terminal": 2, + "intent": 1, + "post_completion": 1, + "legacy": 0 + }, + "cases": [ + { + "name": "active", + "operation": "outcome", + "todo": { + "todo_id": "todo_fixture0001", + "status": "done", + "completion_continuation": "active_goal", + "completion_turn_key": "turn-a" + }, + "bytes": "{\"result\":{\"todo_id\":\"todo_fixture0001\",\"continuation\":\"active_goal\"}}" + }, + { + "name": "successors", + "operation": "outcome", + "todo": { + "todo_id": "todo_fixture0001", + "status": "done", + "completion_continuation": "successor", + "completion_turn_key": "turn-a", + "successor_todo_ids": [ + "todo_zzz", + "todo_aaa", + "todo_zzz" + ] + }, + "existing": [ + "todo_zzz", + "todo_aaa" + ], + "bytes": "{\"result\":{\"todo_id\":\"todo_fixture0001\",\"continuation\":\"successor\",\"successor_todo_ids\":[\"todo_zzz\",\"todo_aaa\"]}}" + }, + { + "name": "ordered-missing", + "operation": "outcome", + "todo": { + "todo_id": "todo_fixture0001", + "status": "done", + "completion_continuation": "successor", + "completion_turn_key": "turn-a", + "successor_todo_ids": [ + "todo_zzz", + "todo_aaa", + "todo_zzz" + ] + }, + "existing": [], + "bytes": "{\"error_type\":\"ValueError\",\"message\":\"durable completion declares missing successor Todo ids: todo_zzz, todo_aaa\"}" + }, + { + "name": "identity", + "operation": "outcome", + "todo": { + "todo_id": "todo_other", + "status": "done", + "completion_continuation": "active_goal", + "completion_turn_key": "turn-a" + }, + "bytes": "{\"error_type\":\"ValueError\",\"message\":\"durable completion todo_id does not match the selected Todo\"}" + }, + { + "name": "bad-expected", + "operation": "outcome", + "todo": { + "todo_id": "todo_fixture0001", + "status": "done", + "completion_continuation": "active_goal", + "completion_turn_key": "turn-a" + }, + "expected": "wrong", + "bytes": "{\"error_type\":\"ValueError\",\"message\":\"durable completion requires a public Todo id\"}" + }, + { + "name": "continuation-'DELETE'", + "operation": "outcome", + "todo": { + "todo_id": "todo_fixture0001", + "status": "done", + "completion_turn_key": "turn-a" + }, + "bytes": "{\"error_type\":\"ValueError\",\"message\":\"durable completion is missing completion_continuation; repair the Todo with `loopx todo complete`\"}" + }, + { + "name": "intent-continuation-'DELETE'", + "operation": "intent", + "todo": { + "todo_id": "todo_fixture0001", + "status": "done", + "completion_turn_key": "turn-a" + }, + "bytes": "{\"result\":{\"todo_id\":\"todo_fixture0001\",\"continuation\":\"active_goal\"}}" + }, + { + "name": "recovery-'DELETE'", + "operation": "outcome", + "todo": { + "todo_id": "todo_fixture0001", + "status": "done", + "completion_continuation": "active_goal", + "completion_turn_key": "turn-a" + }, + "bytes": "{\"result\":{\"todo_id\":\"todo_fixture0001\",\"continuation\":\"active_goal\"}}" + }, + { + "name": "continuation-'invalid'", + "operation": "outcome", + "todo": { + "todo_id": "todo_fixture0001", + "status": "done", + "completion_continuation": "invalid", + "completion_turn_key": "turn-a" + }, + "bytes": "{\"error_type\":\"ValueError\",\"message\":\"durable completion is missing completion_continuation; repair the Todo with `loopx todo complete`\"}" + }, + { + "name": "intent-continuation-'invalid'", + "operation": "intent", + "todo": { + "todo_id": "todo_fixture0001", + "status": "done", + "completion_continuation": "invalid", + "completion_turn_key": "turn-a" + }, + "bytes": "{\"result\":{\"todo_id\":\"todo_fixture0001\",\"continuation\":\"active_goal\"}}" + }, + { + "name": "recovery-'invalid'", + "operation": "outcome", + "todo": { + "todo_id": "todo_fixture0001", + "status": "done", + "completion_continuation": "active_goal", + "completion_turn_key": "turn-a", + "completion_recovery": "invalid" + }, + "bytes": "{\"error_type\":\"ValueError\",\"message\":\"durable completion_recovery is invalid\"}" + }, + { + "name": "continuation-''", + "operation": "outcome", + "todo": { + "todo_id": "todo_fixture0001", + "status": "done", + "completion_continuation": "", + "completion_turn_key": "turn-a" + }, + "bytes": "{\"error_type\":\"ValueError\",\"message\":\"durable completion is missing completion_continuation; repair the Todo with `loopx todo complete`\"}" + }, + { + "name": "intent-continuation-''", + "operation": "intent", + "todo": { + "todo_id": "todo_fixture0001", + "status": "done", + "completion_continuation": "", + "completion_turn_key": "turn-a" + }, + "bytes": "{\"result\":{\"todo_id\":\"todo_fixture0001\",\"continuation\":\"active_goal\"}}" + }, + { + "name": "recovery-''", + "operation": "outcome", + "todo": { + "todo_id": "todo_fixture0001", + "status": "done", + "completion_continuation": "active_goal", + "completion_turn_key": "turn-a", + "completion_recovery": "" + }, + "bytes": "{\"result\":{\"todo_id\":\"todo_fixture0001\",\"continuation\":\"active_goal\"}}" + }, + { + "name": "continuation-None", + "operation": "outcome", + "todo": { + "todo_id": "todo_fixture0001", + "status": "done", + "completion_continuation": null, + "completion_turn_key": "turn-a" + }, + "bytes": "{\"error_type\":\"ValueError\",\"message\":\"durable completion is missing completion_continuation; repair the Todo with `loopx todo complete`\"}" + }, + { + "name": "intent-continuation-None", + "operation": "intent", + "todo": { + "todo_id": "todo_fixture0001", + "status": "done", + "completion_continuation": null, + "completion_turn_key": "turn-a" + }, + "bytes": "{\"result\":{\"todo_id\":\"todo_fixture0001\",\"continuation\":\"active_goal\"}}" + }, + { + "name": "recovery-None", + "operation": "outcome", + "todo": { + "todo_id": "todo_fixture0001", + "status": "done", + "completion_continuation": "active_goal", + "completion_turn_key": "turn-a", + "completion_recovery": null + }, + "bytes": "{\"result\":{\"todo_id\":\"todo_fixture0001\",\"continuation\":\"active_goal\"}}" + }, + { + "name": "continuation-[]", + "operation": "outcome", + "todo": { + "todo_id": "todo_fixture0001", + "status": "done", + "completion_continuation": [], + "completion_turn_key": "turn-a" + }, + "bytes": "{\"error_type\":\"ValueError\",\"message\":\"durable completion is missing completion_continuation; repair the Todo with `loopx todo complete`\"}" + }, + { + "name": "intent-continuation-[]", + "operation": "intent", + "todo": { + "todo_id": "todo_fixture0001", + "status": "done", + "completion_continuation": [], + "completion_turn_key": "turn-a" + }, + "bytes": "{\"result\":{\"todo_id\":\"todo_fixture0001\",\"continuation\":\"active_goal\"}}" + }, + { + "name": "recovery-[]", + "operation": "outcome", + "todo": { + "todo_id": "todo_fixture0001", + "status": "done", + "completion_continuation": "active_goal", + "completion_turn_key": "turn-a", + "completion_recovery": [] + }, + "bytes": "{\"result\":{\"todo_id\":\"todo_fixture0001\",\"continuation\":\"active_goal\"}}" + }, + { + "name": "continuation-{}", + "operation": "outcome", + "todo": { + "todo_id": "todo_fixture0001", + "status": "done", + "completion_continuation": {}, + "completion_turn_key": "turn-a" + }, + "bytes": "{\"error_type\":\"ValueError\",\"message\":\"durable completion is missing completion_continuation; repair the Todo with `loopx todo complete`\"}" + }, + { + "name": "intent-continuation-{}", + "operation": "intent", + "todo": { + "todo_id": "todo_fixture0001", + "status": "done", + "completion_continuation": {}, + "completion_turn_key": "turn-a" + }, + "bytes": "{\"result\":{\"todo_id\":\"todo_fixture0001\",\"continuation\":\"active_goal\"}}" + }, + { + "name": "recovery-{}", + "operation": "outcome", + "todo": { + "todo_id": "todo_fixture0001", + "status": "done", + "completion_continuation": "active_goal", + "completion_turn_key": "turn-a", + "completion_recovery": {} + }, + "bytes": "{\"result\":{\"todo_id\":\"todo_fixture0001\",\"continuation\":\"active_goal\"}}" + }, + { + "name": "continuation-True", + "operation": "outcome", + "todo": { + "todo_id": "todo_fixture0001", + "status": "done", + "completion_continuation": true, + "completion_turn_key": "turn-a" + }, + "bytes": "{\"error_type\":\"ValueError\",\"message\":\"durable completion is missing completion_continuation; repair the Todo with `loopx todo complete`\"}" + }, + { + "name": "intent-continuation-True", + "operation": "intent", + "todo": { + "todo_id": "todo_fixture0001", + "status": "done", + "completion_continuation": true, + "completion_turn_key": "turn-a" + }, + "bytes": "{\"result\":{\"todo_id\":\"todo_fixture0001\",\"continuation\":\"active_goal\"}}" + }, + { + "name": "recovery-True", + "operation": "outcome", + "todo": { + "todo_id": "todo_fixture0001", + "status": "done", + "completion_continuation": "active_goal", + "completion_turn_key": "turn-a", + "completion_recovery": true + }, + "bytes": "{\"error_type\":\"ValueError\",\"message\":\"durable completion_recovery is invalid\"}" + }, + { + "name": "continuation-1", + "operation": "outcome", + "todo": { + "todo_id": "todo_fixture0001", + "status": "done", + "completion_continuation": 1, + "completion_turn_key": "turn-a" + }, + "bytes": "{\"error_type\":\"ValueError\",\"message\":\"durable completion is missing completion_continuation; repair the Todo with `loopx todo complete`\"}" + }, + { + "name": "intent-continuation-1", + "operation": "intent", + "todo": { + "todo_id": "todo_fixture0001", + "status": "done", + "completion_continuation": 1, + "completion_turn_key": "turn-a" + }, + "bytes": "{\"result\":{\"todo_id\":\"todo_fixture0001\",\"continuation\":\"active_goal\"}}" + }, + { + "name": "recovery-1", + "operation": "outcome", + "todo": { + "todo_id": "todo_fixture0001", + "status": "done", + "completion_continuation": "active_goal", + "completion_turn_key": "turn-a", + "completion_recovery": 1 + }, + "bytes": "{\"error_type\":\"ValueError\",\"message\":\"durable completion_recovery is invalid\"}" + }, + { + "name": "recovery-active-same_turn_terminal_closeout", + "operation": "outcome", + "todo": { + "todo_id": "todo_fixture0001", + "status": "done", + "completion_continuation": "active_goal", + "completion_turn_key": "turn-a", + "completion_recovery": "same_turn_terminal_closeout" + }, + "bytes": "{\"error_type\":\"ValueError\",\"message\":\"durable completion_recovery requires a no_followup continuation\"}" + }, + { + "name": "recovery-terminal-same_turn_terminal_closeout", + "operation": "outcome", + "todo": { + "todo_id": "todo_fixture0001", + "status": "done", + "completion_continuation": "no_followup", + "completion_turn_key": "turn-a", + "completion_recovery": "same_turn_terminal_closeout", + "no_followup": true + }, + "bytes": "{\"result\":{\"todo_id\":\"todo_fixture0001\",\"continuation\":\"no_followup\"}}" + }, + { + "name": "recovery-active-lifecycle_reentry_terminal_closeout", + "operation": "outcome", + "todo": { + "todo_id": "todo_fixture0001", + "status": "done", + "completion_continuation": "active_goal", + "completion_turn_key": "turn-a", + "completion_recovery": "lifecycle_reentry_terminal_closeout" + }, + "bytes": "{\"error_type\":\"ValueError\",\"message\":\"durable completion_recovery requires a no_followup continuation\"}" + }, + { + "name": "recovery-terminal-lifecycle_reentry_terminal_closeout", + "operation": "outcome", + "todo": { + "todo_id": "todo_fixture0001", + "status": "done", + "completion_continuation": "no_followup", + "completion_turn_key": "turn-a", + "completion_recovery": "lifecycle_reentry_terminal_closeout", + "no_followup": true + }, + "bytes": "{\"result\":{\"todo_id\":\"todo_fixture0001\",\"continuation\":\"no_followup\"}}" + }, + { + "name": "status-open", + "operation": "outcome", + "todo": { + "todo_id": "todo_fixture0001", + "status": "open", + "completion_continuation": "active_goal", + "completion_turn_key": "turn-a" + }, + "bytes": "{\"error_type\":\"ValueError\",\"message\":\"durable completion requires the selected Todo to be durably done\"}" + }, + { + "name": "terminal-open", + "operation": "terminal", + "todo": { + "todo_id": "todo_fixture0001", + "status": "open", + "completion_continuation": "no_followup", + "completion_turn_key": "turn-a", + "no_followup": true + }, + "bytes": "{\"result\":{\"kind\":\"absent\"}}" + }, + { + "name": "status-blocked", + "operation": "outcome", + "todo": { + "todo_id": "todo_fixture0001", + "status": "blocked", + "completion_continuation": "active_goal", + "completion_turn_key": "turn-a" + }, + "bytes": "{\"error_type\":\"ValueError\",\"message\":\"durable completion requires the selected Todo to be durably done\"}" + }, + { + "name": "terminal-blocked", + "operation": "terminal", + "todo": { + "todo_id": "todo_fixture0001", + "status": "blocked", + "completion_continuation": "no_followup", + "completion_turn_key": "turn-a", + "no_followup": true + }, + "bytes": "{\"error_type\":\"ValueError\",\"message\":\"durable terminal completion is not safely replayable\"}" + }, + { + "name": "status-deferred", + "operation": "outcome", + "todo": { + "todo_id": "todo_fixture0001", + "status": "deferred", + "completion_continuation": "active_goal", + "completion_turn_key": "turn-a" + }, + "bytes": "{\"error_type\":\"ValueError\",\"message\":\"durable completion requires the selected Todo to be durably done\"}" + }, + { + "name": "terminal-deferred", + "operation": "terminal", + "todo": { + "todo_id": "todo_fixture0001", + "status": "deferred", + "completion_continuation": "no_followup", + "completion_turn_key": "turn-a", + "no_followup": true + }, + "bytes": "{\"error_type\":\"ValueError\",\"message\":\"durable terminal completion is not safely replayable\"}" + }, + { + "name": "status-done", + "operation": "outcome", + "todo": { + "todo_id": "todo_fixture0001", + "status": "done", + "completion_continuation": "active_goal", + "completion_turn_key": "turn-a" + }, + "bytes": "{\"result\":{\"todo_id\":\"todo_fixture0001\",\"continuation\":\"active_goal\"}}" + }, + { + "name": "terminal-done", + "operation": "terminal", + "todo": { + "todo_id": "todo_fixture0001", + "status": "done", + "completion_continuation": "no_followup", + "completion_turn_key": "turn-a", + "no_followup": true + }, + "bytes": "{\"result\":{\"kind\":\"committed\",\"completion\":{\"todo_id\":\"todo_fixture0001\",\"continuation\":\"no_followup\"}}}" + }, + { + "name": "status-invalid", + "operation": "outcome", + "todo": { + "todo_id": "todo_fixture0001", + "status": "invalid", + "completion_continuation": "active_goal", + "completion_turn_key": "turn-a" + }, + "bytes": "{\"error_type\":\"ValueError\",\"message\":\"durable completion requires the selected Todo to be durably done\"}" + }, + { + "name": "terminal-invalid", + "operation": "terminal", + "todo": { + "todo_id": "todo_fixture0001", + "status": "invalid", + "completion_continuation": "no_followup", + "completion_turn_key": "turn-a", + "no_followup": true + }, + "bytes": "{\"error_type\":\"ValueError\",\"message\":\"todo.status is unsupported\"}" + }, + { + "name": "terminal-upgrade", + "operation": "terminal", + "todo": { + "todo_id": "todo_fixture0001", + "status": "done", + "completion_continuation": "active_goal", + "completion_turn_key": "turn-a" + }, + "bytes": "{\"result\":{\"kind\":\"absent\"}}" + }, + { + "name": "terminal-cross-turn", + "operation": "terminal", + "todo": { + "todo_id": "todo_fixture0001", + "status": "done", + "completion_continuation": "active_goal", + "completion_turn_key": "turn-b", + "completion_recovery": "invalid" + }, + "bytes": "{\"error_type\":\"ValueError\",\"message\":\"todo is already completed under a different completion_turn_key\"}" + }, + { + "name": "terminal-missing-turn", + "operation": "terminal", + "todo": { + "todo_id": "todo_fixture0001", + "status": "done", + "completion_continuation": "active_goal" + }, + "bytes": "{\"error_type\":\"ValueError\",\"message\":\"todo is already completed under a different completion_turn_key\"}" + }, + { + "name": "terminal-fence-precedence", + "operation": "terminal", + "todo": { + "todo_id": "wrong", + "status": "done", + "completion_continuation": "active_goal", + "completion_turn_key": "turn-b" + }, + "bytes": "{\"error_type\":\"ValueError\",\"message\":\"todo is already completed under a different completion_turn_key\"}" + }, + { + "name": "contradictory-successors", + "operation": "outcome", + "todo": { + "todo_id": "todo_fixture0001", + "status": "done", + "completion_continuation": "no_followup", + "completion_turn_key": "turn-a", + "no_followup": true, + "successor_todo_ids": [ + "todo_aaa" + ] + }, + "bytes": "{\"error_type\":\"ValueError\",\"message\":\"durable completion records both no_followup and successor_todo_ids\"}" + }, + { + "name": "contradictory-no-followup", + "operation": "outcome", + "todo": { + "todo_id": "todo_fixture0001", + "status": "done", + "completion_continuation": "active_goal", + "completion_turn_key": "turn-a", + "no_followup": true + }, + "bytes": "{\"error_type\":\"ValueError\",\"message\":\"durable completion_continuation contradicts no_followup\"}" + }, + { + "name": "missing-matching-fields", + "operation": "outcome", + "todo": { + "todo_id": "todo_fixture0001", + "status": "done", + "completion_continuation": "successor", + "completion_turn_key": "turn-a" + }, + "bytes": "{\"error_type\":\"ValueError\",\"message\":\"durable completion_continuation requires matching no_followup or successor_todo_ids\"}" + }, + { + "name": "scalar-no-followup-True", + "operation": "outcome", + "todo": { + "todo_id": "todo_fixture0001", + "status": "done", + "completion_continuation": "active_goal", + "completion_turn_key": "turn-a", + "no_followup": true + }, + "bytes": "{\"error_type\":\"ValueError\",\"message\":\"durable completion_continuation contradicts no_followup\"}" + }, + { + "name": "container-successors-True", + "operation": "outcome", + "todo": { + "todo_id": "todo_fixture0001", + "status": "done", + "completion_continuation": "successor", + "completion_turn_key": "turn-a", + "successor_todo_ids": true + }, + "existing": [ + "todo_aaa", + "todo_zzz" + ], + "bytes": "{\"error_type\":\"ValueError\",\"message\":\"durable completion_continuation requires matching no_followup or successor_todo_ids\"}" + }, + { + "name": "terminal-scalar-True", + "operation": "terminal", + "todo": { + "todo_id": "todo_fixture0001", + "status": "done", + "completion_continuation": "active_goal", + "completion_turn_key": "turn-a", + "no_followup": true + }, + "bytes": "{\"error_type\":\"ValueError\",\"message\":\"durable completion_continuation contradicts no_followup\"}" + }, + { + "name": "scalar-no-followup-False", + "operation": "outcome", + "todo": { + "todo_id": "todo_fixture0001", + "status": "done", + "completion_continuation": "active_goal", + "completion_turn_key": "turn-a", + "no_followup": false + }, + "bytes": "{\"result\":{\"todo_id\":\"todo_fixture0001\",\"continuation\":\"active_goal\"}}" + }, + { + "name": "container-successors-False", + "operation": "outcome", + "todo": { + "todo_id": "todo_fixture0001", + "status": "done", + "completion_continuation": "successor", + "completion_turn_key": "turn-a", + "successor_todo_ids": false + }, + "existing": [ + "todo_aaa", + "todo_zzz" + ], + "bytes": "{\"error_type\":\"ValueError\",\"message\":\"durable completion_continuation requires matching no_followup or successor_todo_ids\"}" + }, + { + "name": "terminal-scalar-False", + "operation": "terminal", + "todo": { + "todo_id": "todo_fixture0001", + "status": "done", + "completion_continuation": "active_goal", + "completion_turn_key": "turn-a", + "no_followup": false + }, + "bytes": "{\"result\":{\"kind\":\"absent\"}}" + }, + { + "name": "scalar-no-followup-1", + "operation": "outcome", + "todo": { + "todo_id": "todo_fixture0001", + "status": "done", + "completion_continuation": "active_goal", + "completion_turn_key": "turn-a", + "no_followup": 1 + }, + "bytes": "{\"error_type\":\"ValueError\",\"message\":\"durable completion_continuation contradicts no_followup\"}" + }, + { + "name": "container-successors-1", + "operation": "outcome", + "todo": { + "todo_id": "todo_fixture0001", + "status": "done", + "completion_continuation": "successor", + "completion_turn_key": "turn-a", + "successor_todo_ids": 1 + }, + "existing": [ + "todo_aaa", + "todo_zzz" + ], + "bytes": "{\"error_type\":\"ValueError\",\"message\":\"durable completion_continuation requires matching no_followup or successor_todo_ids\"}" + }, + { + "name": "terminal-scalar-1", + "operation": "terminal", + "todo": { + "todo_id": "todo_fixture0001", + "status": "done", + "completion_continuation": "active_goal", + "completion_turn_key": "turn-a", + "no_followup": 1 + }, + "bytes": "{\"error_type\":\"ValueError\",\"message\":\"durable completion_continuation contradicts no_followup\"}" + }, + { + "name": "scalar-no-followup-0", + "operation": "outcome", + "todo": { + "todo_id": "todo_fixture0001", + "status": "done", + "completion_continuation": "active_goal", + "completion_turn_key": "turn-a", + "no_followup": 0 + }, + "bytes": "{\"result\":{\"todo_id\":\"todo_fixture0001\",\"continuation\":\"active_goal\"}}" + }, + { + "name": "container-successors-0", + "operation": "outcome", + "todo": { + "todo_id": "todo_fixture0001", + "status": "done", + "completion_continuation": "successor", + "completion_turn_key": "turn-a", + "successor_todo_ids": 0 + }, + "existing": [ + "todo_aaa", + "todo_zzz" + ], + "bytes": "{\"error_type\":\"ValueError\",\"message\":\"durable completion_continuation requires matching no_followup or successor_todo_ids\"}" + }, + { + "name": "terminal-scalar-0", + "operation": "terminal", + "todo": { + "todo_id": "todo_fixture0001", + "status": "done", + "completion_continuation": "active_goal", + "completion_turn_key": "turn-a", + "no_followup": 0 + }, + "bytes": "{\"result\":{\"kind\":\"absent\"}}" + }, + { + "name": "scalar-no-followup-' YES '", + "operation": "outcome", + "todo": { + "todo_id": "todo_fixture0001", + "status": "done", + "completion_continuation": "active_goal", + "completion_turn_key": "turn-a", + "no_followup": " YES " + }, + "bytes": "{\"error_type\":\"ValueError\",\"message\":\"durable completion_continuation contradicts no_followup\"}" + }, + { + "name": "container-successors-' YES '", + "operation": "outcome", + "todo": { + "todo_id": "todo_fixture0001", + "status": "done", + "completion_continuation": "successor", + "completion_turn_key": "turn-a", + "successor_todo_ids": " YES " + }, + "existing": [ + "todo_aaa", + "todo_zzz" + ], + "bytes": "{\"error_type\":\"ValueError\",\"message\":\"durable completion_continuation requires matching no_followup or successor_todo_ids\"}" + }, + { + "name": "terminal-scalar-' YES '", + "operation": "terminal", + "todo": { + "todo_id": "todo_fixture0001", + "status": "done", + "completion_continuation": "active_goal", + "completion_turn_key": "turn-a", + "no_followup": " YES " + }, + "bytes": "{\"error_type\":\"ValueError\",\"message\":\"durable completion_continuation contradicts no_followup\"}" + }, + { + "name": "scalar-no-followup-'false'", + "operation": "outcome", + "todo": { + "todo_id": "todo_fixture0001", + "status": "done", + "completion_continuation": "active_goal", + "completion_turn_key": "turn-a", + "no_followup": "false" + }, + "bytes": "{\"result\":{\"todo_id\":\"todo_fixture0001\",\"continuation\":\"active_goal\"}}" + }, + { + "name": "container-successors-'false'", + "operation": "outcome", + "todo": { + "todo_id": "todo_fixture0001", + "status": "done", + "completion_continuation": "successor", + "completion_turn_key": "turn-a", + "successor_todo_ids": "false" + }, + "existing": [ + "todo_aaa", + "todo_zzz" + ], + "bytes": "{\"error_type\":\"ValueError\",\"message\":\"durable completion_continuation requires matching no_followup or successor_todo_ids\"}" + }, + { + "name": "terminal-scalar-'false'", + "operation": "terminal", + "todo": { + "todo_id": "todo_fixture0001", + "status": "done", + "completion_continuation": "active_goal", + "completion_turn_key": "turn-a", + "no_followup": "false" + }, + "bytes": "{\"result\":{\"kind\":\"absent\"}}" + }, + { + "name": "scalar-no-followup-[]", + "operation": "outcome", + "todo": { + "todo_id": "todo_fixture0001", + "status": "done", + "completion_continuation": "active_goal", + "completion_turn_key": "turn-a", + "no_followup": [] + }, + "bytes": "{\"result\":{\"todo_id\":\"todo_fixture0001\",\"continuation\":\"active_goal\"}}" + }, + { + "name": "container-successors-[]", + "operation": "outcome", + "todo": { + "todo_id": "todo_fixture0001", + "status": "done", + "completion_continuation": "successor", + "completion_turn_key": "turn-a", + "successor_todo_ids": [] + }, + "existing": [ + "todo_aaa", + "todo_zzz" + ], + "bytes": "{\"error_type\":\"ValueError\",\"message\":\"durable completion_continuation requires matching no_followup or successor_todo_ids\"}" + }, + { + "name": "terminal-scalar-[]", + "operation": "terminal", + "todo": { + "todo_id": "todo_fixture0001", + "status": "done", + "completion_continuation": "active_goal", + "completion_turn_key": "turn-a", + "no_followup": [] + }, + "bytes": "{\"result\":{\"kind\":\"absent\"}}" + }, + { + "name": "scalar-no-followup-{}", + "operation": "outcome", + "todo": { + "todo_id": "todo_fixture0001", + "status": "done", + "completion_continuation": "active_goal", + "completion_turn_key": "turn-a", + "no_followup": {} + }, + "bytes": "{\"result\":{\"todo_id\":\"todo_fixture0001\",\"continuation\":\"active_goal\"}}" + }, + { + "name": "container-successors-{}", + "operation": "outcome", + "todo": { + "todo_id": "todo_fixture0001", + "status": "done", + "completion_continuation": "successor", + "completion_turn_key": "turn-a", + "successor_todo_ids": {} + }, + "existing": [ + "todo_aaa", + "todo_zzz" + ], + "bytes": "{\"error_type\":\"ValueError\",\"message\":\"durable completion_continuation requires matching no_followup or successor_todo_ids\"}" + }, + { + "name": "terminal-scalar-{}", + "operation": "terminal", + "todo": { + "todo_id": "todo_fixture0001", + "status": "done", + "completion_continuation": "active_goal", + "completion_turn_key": "turn-a", + "no_followup": {} + }, + "bytes": "{\"result\":{\"kind\":\"absent\"}}" + }, + { + "name": "scalar-no-followup-['todo_aaa']", + "operation": "outcome", + "todo": { + "todo_id": "todo_fixture0001", + "status": "done", + "completion_continuation": "active_goal", + "completion_turn_key": "turn-a", + "no_followup": [ + "todo_aaa" + ] + }, + "bytes": "{\"result\":{\"todo_id\":\"todo_fixture0001\",\"continuation\":\"active_goal\"}}" + }, + { + "name": "container-successors-['todo_aaa']", + "operation": "outcome", + "todo": { + "todo_id": "todo_fixture0001", + "status": "done", + "completion_continuation": "successor", + "completion_turn_key": "turn-a", + "successor_todo_ids": [ + "todo_aaa" + ] + }, + "existing": [ + "todo_aaa", + "todo_zzz" + ], + "bytes": "{\"result\":{\"todo_id\":\"todo_fixture0001\",\"continuation\":\"successor\",\"successor_todo_ids\":[\"todo_aaa\"]}}" + }, + { + "name": "terminal-scalar-['todo_aaa']", + "operation": "terminal", + "todo": { + "todo_id": "todo_fixture0001", + "status": "done", + "completion_continuation": "active_goal", + "completion_turn_key": "turn-a", + "no_followup": [ + "todo_aaa" + ] + }, + "bytes": "{\"result\":{\"kind\":\"absent\"}}" + }, + { + "name": "scalar-no-followup-{'todo_zzz': 'todo_aaa'}", + "operation": "outcome", + "todo": { + "todo_id": "todo_fixture0001", + "status": "done", + "completion_continuation": "active_goal", + "completion_turn_key": "turn-a", + "no_followup": { + "todo_zzz": "todo_aaa" + } + }, + "bytes": "{\"result\":{\"todo_id\":\"todo_fixture0001\",\"continuation\":\"active_goal\"}}" + }, + { + "name": "container-successors-{'todo_zzz': 'todo_aaa'}", + "operation": "outcome", + "todo": { + "todo_id": "todo_fixture0001", + "status": "done", + "completion_continuation": "successor", + "completion_turn_key": "turn-a", + "successor_todo_ids": { + "todo_zzz": "todo_aaa" + } + }, + "existing": [ + "todo_aaa", + "todo_zzz" + ], + "bytes": "{\"result\":{\"todo_id\":\"todo_fixture0001\",\"continuation\":\"successor\",\"successor_todo_ids\":[\"todo_zzz\",\"todo_aaa\"]}}" + }, + { + "name": "terminal-scalar-{'todo_zzz': 'todo_aaa'}", + "operation": "terminal", + "todo": { + "todo_id": "todo_fixture0001", + "status": "done", + "completion_continuation": "active_goal", + "completion_turn_key": "turn-a", + "no_followup": { + "todo_zzz": "todo_aaa" + } + }, + "bytes": "{\"result\":{\"kind\":\"absent\"}}" + }, + { + "name": "scalar-no-followup-['todo_zzz', 'todo_aaa']", + "operation": "outcome", + "todo": { + "todo_id": "todo_fixture0001", + "status": "done", + "completion_continuation": "active_goal", + "completion_turn_key": "turn-a", + "no_followup": [ + "todo_zzz", + "todo_aaa" + ] + }, + "bytes": "{\"result\":{\"todo_id\":\"todo_fixture0001\",\"continuation\":\"active_goal\"}}" + }, + { + "name": "container-successors-['todo_zzz', 'todo_aaa']", + "operation": "outcome", + "todo": { + "todo_id": "todo_fixture0001", + "status": "done", + "completion_continuation": "successor", + "completion_turn_key": "turn-a", + "successor_todo_ids": [ + "todo_zzz", + "todo_aaa" + ] + }, + "existing": [ + "todo_aaa", + "todo_zzz" + ], + "bytes": "{\"result\":{\"todo_id\":\"todo_fixture0001\",\"continuation\":\"successor\",\"successor_todo_ids\":[\"todo_zzz\",\"todo_aaa\"]}}" + }, + { + "name": "terminal-scalar-['todo_zzz', 'todo_aaa']", + "operation": "terminal", + "todo": { + "todo_id": "todo_fixture0001", + "status": "done", + "completion_continuation": "active_goal", + "completion_turn_key": "turn-a", + "no_followup": [ + "todo_zzz", + "todo_aaa" + ] + }, + "bytes": "{\"result\":{\"kind\":\"absent\"}}" + } + ] +} diff --git a/tests/test_loopx_turn_driver.py b/tests/test_loopx_turn_driver.py index 17cf8e5eb9..2bba0864af 100644 --- a/tests/test_loopx_turn_driver.py +++ b/tests/test_loopx_turn_driver.py @@ -16,6 +16,7 @@ from loopx.cli_commands.turn_rendering import render_loopx_turn_execution_markdown from tests.control_plane.canonical_authority_fixture import ( initialize_canonical_authority, + isolate_sqlite_runtime, ) from loopx.cli import main as cli_main from loopx.control_plane.coordination.runtime_shadow import ( @@ -2618,11 +2619,14 @@ def test_turn_run_once_cli_completes_selected_todo_after_validation( assert not any(replayed["effects"].values()) +@pytest.mark.parametrize("provider", ["file", "sqlite"]) def test_promoted_turn_completion_replays_after_commit_before_journal_crash( tmp_path: Path, monkeypatch: pytest.MonkeyPatch, + provider: str, ) -> None: - project, runtime, registry = _write_live_fixture(tmp_path) + isolate_sqlite_runtime(tmp_path, monkeypatch) + project, runtime, registry = _write_live_fixture(tmp_path, provider=provider) host_project = project / "isolated-host-workspace" host_project.mkdir() host_script, validation_script = _completion_host_and_validation_scripts() @@ -2953,13 +2957,18 @@ def test_turn_run_once_cli_projects_durable_no_followup_continuation( [None, "sha256:different-turn"], ids=["missing-turn-key", "different-turn-key"], ) +@pytest.mark.parametrize("provider", ["file", "sqlite"]) def test_turn_run_once_cli_terminal_recovery_ignores_stale_display_identity( tmp_path: Path, display_completion_turn_key: str | None, + monkeypatch: pytest.MonkeyPatch, + provider: str, ) -> None: + isolate_sqlite_runtime(tmp_path, monkeypatch) project, runtime, registry = _write_live_fixture( tmp_path, todo_metadata_extra="no_followup=true", + provider=provider, ) host_project = project / "isolated-host-workspace" host_project.mkdir()