From 0867f772324f18afcee49e6b72fc8abb8c003943 Mon Sep 17 00:00:00 2001 From: huangruiteng <14976749+huangruiteng@users.noreply.github.com> Date: Thu, 24 Sep 2026 22:53:57 +0800 Subject: [PATCH 1/2] Retire post-commit observation in favor of transaction-bound shadow capture Signed-off-by: huangruiteng <14976749+huangruiteng@users.noreply.github.com> --- .../capability-localization.ts | 10 +- .../capability-workbench.tsx | 1 + .../personal-workspace-browser/fixture.mjs | 2 +- .../typed-actions.mjs | 17 +- examples/project/configure-goal-smoke.py | 9 +- loopx/bootstrap.py | 3 +- loopx/capabilities/configuration_ui.py | 4 +- .../cli_commands/registry_admin_configure.py | 4 +- loopx/configuration_catalog.py | 37 +- .../coordination/local_authority_shadow.ts | 315 +--------- .../local_authority_shadow_adapter.py | 11 +- .../local_authority_shadow_observation.py | 479 -------------- .../coordination/runtime_shadow.py | 51 +- .../runtime_shadow_writer_adapter.py | 13 +- .../testing/authority_e2e_fixtures.py | 90 +-- .../testing/authority_e2e_ladder.py | 130 +--- .../testing/authority_e2e_row_support.py | 70 --- .../testing/authority_e2e_rows_stage2c.py | 519 +-------------- loopx/control_plane/todos/handoff_mode.py | 15 +- .../work_items/task_lease_acquire_adapter.py | 64 -- .../work_items/team_plan_adapter.py | 2 +- .../project_registry_io_manifest_v1.json | 30 +- loopx/state_migration.py | 105 +--- loopx/todos.py | 12 +- .../test_coordination_state_contract.py | 2 +- .../test_local_authority_shadow_cli_e2e.py | 594 ++---------------- .../test_local_authority_shadow_config.py | 163 ++--- .../test_local_authority_shadow_runtime.py | 513 +-------------- .../test_shared_goal_authority_e2e.py | 16 +- .../test_state_migration_authority_shadow.py | 152 +---- .../local_authority_shadow.test.ts | 261 +------- 31 files changed, 362 insertions(+), 3332 deletions(-) delete mode 100644 loopx/control_plane/coordination/local_authority_shadow_observation.py diff --git a/apps/presentation/dashboard/src/features/personal-workspace/capability-localization.ts b/apps/presentation/dashboard/src/features/personal-workspace/capability-localization.ts index 72b1e78fae..191a51e8fb 100644 --- a/apps/presentation/dashboard/src/features/personal-workspace/capability-localization.ts +++ b/apps/presentation/dashboard/src/features/personal-workspace/capability-localization.ts @@ -47,8 +47,9 @@ const capabilityCopy: Record> = { description: "Synchronizes accepted LoopX work state to the configured Lark Kanban heartbeat surface.", }, local_authority_shadow: { - displayName: "Local authority shadow", - description: "Observes post-commit Todo and task-lease state through the shared authority contract without taking write authority.", + displayName: "Retired authority observation", + description: "No longer writes observations. Retained records are read-only, not promotion evidence.", + readOnlyReason: "Clear the old setting with configure-goal --clear-local-authority-shadow. Runtime shadow requires separate configuration and bootstrap.", }, coordination_runtime_shadow: { displayName: "Coordination runtime shadow", @@ -111,8 +112,9 @@ const capabilityCopy: Record> = { description: "把 LoopX 已接受的工作状态同步到配置好的飞书看板心跳界面。", }, local_authority_shadow: { - displayName: "本地 Authority 影子观测", - description: "通过共享 Authority contract 观测提交后的 Todo 与 task lease 状态,但不取得写入权。", + displayName: "已退役的 Authority 观测", + description: "不再写入观测。保留记录只读,不能作为晋升证据。", + readOnlyReason: "通过 configure-goal --clear-local-authority-shadow 清理旧设置;Runtime shadow 需要另行配置与 bootstrap。", }, coordination_runtime_shadow: { displayName: "协调 Runtime 影子", diff --git a/apps/presentation/dashboard/src/features/personal-workspace/capability-workbench.tsx b/apps/presentation/dashboard/src/features/personal-workspace/capability-workbench.tsx index cc3d37f608..d05301ab25 100644 --- a/apps/presentation/dashboard/src/features/personal-workspace/capability-workbench.tsx +++ b/apps/presentation/dashboard/src/features/personal-workspace/capability-workbench.tsx @@ -50,6 +50,7 @@ export function CapabilityEditorStatus({ available, description, t }: Readonly<{ } function capabilityPresentationTier(capability: CapabilityDescriptor) { + if (capability.availability === "retired") return 5; if (capability.availability?.includes("experimental")) return 4; if (capability.capability_id === "multi_subagent") return 3; if (capability.configuration_editor.writable_scopes.length === 0) return 2; diff --git a/examples/personal-workspace-browser/fixture.mjs b/examples/personal-workspace-browser/fixture.mjs index 9191a2e968..9e0398d801 100644 --- a/examples/personal-workspace-browser/fixture.mjs +++ b/examples/personal-workspace-browser/fixture.mjs @@ -188,7 +188,7 @@ export function goalCapabilityCatalog(multiSubagentConfiguration) { fields: [{ key: "coordinator_agent_id", label: "Coordinator Agent", description: "", input_kind: "text", required: false }], }), multiSubagentCapability({ current: multiSubagentConfiguration }), - goalCapability({ availability: "experimental_opt_in", capabilityId: "local_authority_shadow", displayName: "Local authority shadow" }), + goalCapability({ availability: "retired", capabilityId: "local_authority_shadow", displayName: "Retired authority observation", fields: [], readOnlyReason: "Clear retired observation config explicitly; bootstrap runtime shadow separately." }), goalCapability({ availability: "experimental_opt_in", capabilityId: "reward_memory", diff --git a/examples/personal-workspace-browser/typed-actions.mjs b/examples/personal-workspace-browser/typed-actions.mjs index f193a0d0f7..8eb7d74d11 100644 --- a/examples/personal-workspace-browser/typed-actions.mjs +++ b/examples/personal-workspace-browser/typed-actions.mjs @@ -1031,7 +1031,7 @@ export const typedActionsScenario = { if (await page.locator(".personal-capability-editor-status").count()) throw new Error("Editable Goal settings must not show internal editor-contract notices"); const expectedGoalCapabilities = [ "变更质量验证", "Goal 复核周期", "探索图谱", "探索 Harness", "飞书事件收件箱", - "飞书看板心跳同步", "本地 Authority 影子观测", "自适应子 Agent 容量", + "飞书看板心跳同步", "已退役的 Authority 观测", "自适应子 Agent 容量", "已注册 Peer 任务协调", "周期报告", "Reward Memory 实验", ]; if (JSON.stringify([...goalCapabilityOrder].sort()) !== JSON.stringify(expectedGoalCapabilities.sort())) { @@ -1042,10 +1042,23 @@ export const typedActionsScenario = { const capabilityIndex = (name) => goalCapabilityOrder.indexOf(name); if (capabilityIndex("周期报告") >= capabilityIndex("探索 Harness") || capabilityIndex("自适应子 Agent 容量") <= capabilityIndex("探索 Harness") - || capabilityIndex("自适应子 Agent 容量") >= capabilityIndex("本地 Authority 影子观测") + || capabilityIndex("自适应子 Agent 容量") >= capabilityIndex("已退役的 Authority 观测") || capabilityIndex("自适应子 Agent 容量") >= capabilityIndex("Reward Memory 实验")) { throw new Error(`Goal capability maturity ordering drifted: ${JSON.stringify(goalCapabilityOrder)}`); } + await page.locator(".personal-capability-list").getByRole("button", { name: "已退役的 Authority 观测" }).click(); + const retiredDetail = page.locator(".personal-capability-detail"); + await retiredDetail.getByText(/--clear-local-authority-shadow/).waitFor(); + if (await retiredDetail.getByRole("checkbox").count() + || await retiredDetail.getByRole("button", { name: "预览变更", exact: true }).count()) { + throw new Error("Retired observation must not expose activation controls"); + } + await page.screenshot({ path: resolve(outputDir, "retired-observation-settings.png"), fullPage: false, animations: "disabled" }); + await page.setViewportSize({ width: 390, height: 844 }); + await retiredDetail.getByText(/--clear-local-authority-shadow/).waitFor(); + await page.screenshot({ path: resolve(outputDir, "retired-observation-settings-mobile.png"), fullPage: false, animations: "disabled" }); + await page.setViewportSize(desktopViewport); + await page.locator(".personal-capability-list").getByRole("button", { name: "周期报告" }).click(); for (const label of [/^启用$/u, /^报告 Profile/u, /^Goal Channel 路由/u, /^时区/u]) { await page.getByLabel(label).waitFor({ state: "visible" }); } diff --git a/examples/project/configure-goal-smoke.py b/examples/project/configure-goal-smoke.py index 3c606531aa..b255bc880d 100644 --- a/examples/project/configure-goal-smoke.py +++ b/examples/project/configure-goal-smoke.py @@ -324,18 +324,15 @@ def main() -> int: assert "--clear-execution-replan-after-todos" in replan_commands["preview_disable"] assert "--execute" not in replan_commands["preview_disable"] assert "--execute" in replan_commands["apply_disable"] - assert features["local_authority_shadow"]["availability"] == "experimental_opt_in" + assert features["local_authority_shadow"]["availability"] == "retired" assert features["local_authority_shadow"]["default"] == {"enabled": False} assert features["local_authority_shadow"]["current"] == { "enabled": False, "mode": None, "status": "disabled", } - assert "--local-authority-shadow-file" in features[ - "local_authority_shadow" - ]["commands"]["preview_enable"] - assert "--execute" not in features["local_authority_shadow"]["commands"]["preview_enable"] - assert "--execute" in features["local_authority_shadow"]["commands"]["apply_enable"] + assert "preview_enable" not in features["local_authority_shadow"]["commands"] + assert "apply_enable" not in features["local_authority_shadow"]["commands"] assert features["periodic_report"]["availability"] == "supported_explicit_override" assert features["periodic_report"]["default"] == {"enabled": False, "timezone": "UTC"} # A Goal without an explicit override follows the machine default, so the diff --git a/loopx/bootstrap.py b/loopx/bootstrap.py index 8106dcc7bb..aa571d9a6a 100644 --- a/loopx/bootstrap.py +++ b/loopx/bootstrap.py @@ -552,8 +552,7 @@ def bootstrap_project( registry_transaction.commit(registry) if shadow_capture is not None: shadow_evidence = settle_todo_runtime_shadow_capture({}, registry_path=registry_path, - runtime_root=runtime_root, goal_id=goal_id, write_class="bootstrap_state", - capture=shadow_capture, observe_legacy=False, emit_disabled=False) + runtime_root=runtime_root, goal_id=goal_id, capture=shadow_capture, emit_disabled=False) if sync_global: global_sync = sync_project_registry_to_global( registry_path=registry_path, diff --git a/loopx/capabilities/configuration_ui.py b/loopx/capabilities/configuration_ui.py index 0c5af03015..16fdb51f96 100644 --- a/loopx/capabilities/configuration_ui.py +++ b/loopx/capabilities/configuration_ui.py @@ -361,8 +361,8 @@ def capability_configuration_editor( }, "local_authority_shadow": { "supported_scopes": ["goal"], - "writable_scopes": ["goal"], - "fields": [_field("enabled", "Enabled", "boolean")], + "writable_scopes": [], + "fields": [], }, "coordination_runtime_shadow": { "supported_scopes": ["goal"], diff --git a/loopx/cli_commands/registry_admin_configure.py b/loopx/cli_commands/registry_admin_configure.py index 9abf3c2e0f..454dc62f12 100644 --- a/loopx/cli_commands/registry_admin_configure.py +++ b/loopx/cli_commands/registry_admin_configure.py @@ -349,8 +349,8 @@ def register_configure_goal_command(subparsers: argparse._SubParsersAction) -> N "--local-authority-shadow-file", action="store_true", help=( - "Enable default-off, one-way capture of post-commit local snapshots " - "in FileAuthorityStore. This does not compare source and candidate." + "Retired; rejected without writing. Use --coordination-runtime-shadow-file " + "and explicit coordination-shadow bootstrap for transaction-bound capture." ), ) configure_goal_parser.add_argument( diff --git a/loopx/configuration_catalog.py b/loopx/configuration_catalog.py index 737776844c..d4d9d1f4d2 100644 --- a/loopx/configuration_catalog.py +++ b/loopx/configuration_catalog.py @@ -184,43 +184,20 @@ def build_goal_configuration_catalog( }, { "feature_id": "local_authority_shadow", - "display_name": "Local post-commit authority observation", - "availability": "experimental_opt_in", + "display_name": "Retired post-commit authority observation", + "availability": "retired", "default": {"enabled": False}, "current": { "enabled": local_authority_shadow.get("enabled") is True, "mode": local_authority_shadow.get("mode"), "status": local_authority_shadow.get("status", "disabled"), }, - "consider_when": ( - "A Goal needs to exercise the first Stage 2C observation " - "plumbing while legacy local writers remain authoritative." - ), - "effect": ( - "Captures a best-effort post-commit snapshot of Todo and " - "task-lease state through the FileAuthorityStore contract." - ), - "does_not": [ - "read the candidate for lifecycle decisions", - "write candidate state back into Markdown or task-lease files", - "promote shared authority or fence legacy writers", - "bind the snapshot to the exact primary transaction", - "guarantee delivery through a durable outbox", - "compare source and candidate or issue a parity verdict", - ], + "consider_when": "Clear retained observation configuration before an explicit runtime-shadow bootstrap.", + "effect": "No new observations are written. Retained data stays read-only and cannot qualify promotion.", + "does_not": ["enable or bootstrap runtime shadow", "delete retained observations", "grant promotion evidence"], "commands": { - "preview_enable": _configure_command( - goal_id, "--local-authority-shadow-file" - ), - "apply_enable": _configure_command( - goal_id, "--local-authority-shadow-file", execute=True - ), - "preview_disable": _configure_command( - goal_id, "--clear-local-authority-shadow" - ), - "apply_disable": _configure_command( - goal_id, "--clear-local-authority-shadow", execute=True - ), + "preview_disable": _configure_command(goal_id, "--clear-local-authority-shadow"), + "apply_disable": _configure_command(goal_id, "--clear-local-authority-shadow", execute=True), "verify": [inspect_command], }, "documentation": { diff --git a/loopx/control_plane/coordination/local_authority_shadow.ts b/loopx/control_plane/coordination/local_authority_shadow.ts index 7e8c8f1588..5721496eb3 100644 --- a/loopx/control_plane/coordination/local_authority_shadow.ts +++ b/loopx/control_plane/coordination/local_authority_shadow.ts @@ -50,320 +50,19 @@ export { LOCAL_AUTHORITY_SHADOW_REQUEST_SCHEMA, }; -const REQUEST_FIELDS = new Set([ - "schema_version", - "mode", - "runtime_root", - "goal_id", - "observation_id", - "observation_trigger", - "source_digest", - "source_projection", -]); -export type LocalAuthorityShadowOutcome = - | "captured" - | "replayed" - | "ambiguous_reconciled" - | "ambiguous_unproved" - | "unavailable" - | "failed" - | "protocol_mismatch" - | "conflict_retry_required"; - -export interface LocalAuthorityShadowEvidence extends JsonObject { - schema_version: typeof LOCAL_AUTHORITY_SHADOW_EVIDENCE_SCHEMA; - outcome: LocalAuthorityShadowOutcome; - reason_code: string | null; - goal_id: string; - observation_id: string; - source_digest: string; - capture_kind: "post_commit_snapshot"; - source_transaction_correlated: false; - durable_source_outbox: false; - source_candidate_compared: false; - parity_verdict: "not_evaluated"; - primary_authority: "legacy_local"; - candidate_provider: "file"; - candidate_read_for_decision: false; - provider_to_local_writes: false; - primary_writeback_preserved: true; - store_identity: string | null; - provider_revision: string | null; - cursor: string | null; -} - -interface LocalAuthorityShadowRequest { - mode: "file_one_way"; - runtime_root: string; - goal_id: string; - observation_id: string; - observation_trigger: string; - source_digest: string; - source_projection: JsonObject; +/** Compatibility tombstone for older clients: never open or mint a store. */ +export async function recordLocalAuthorityShadow(_value: unknown): Promise { + throw new EffectRuntimeRequestError( + "Post-commit observation is retired; configure runtime shadow and explicitly bootstrap its source lineage.", + "local_authority_shadow_retired", + ); } export interface LocalAuthorityShadowDependencies { openStore?: (directory: string, goalId: string) => AuthorityStore; } -function decodeRequest(value: unknown): LocalAuthorityShadowRequest { - const request = requireJsonObject(value, "local authority shadow request"); - const unexpected = Object.keys(request).filter((field) => !REQUEST_FIELDS.has(field)); - if (unexpected.length > 0) { - const listed = [...unexpected].sort((left, right) => (left < right ? -1 : left > right ? 1 : 0)); - throw new EffectRuntimeRequestError( - `Local authority shadow request has unsupported fields: ${listed.join(", ")}`, - ); - } - if (request.schema_version !== LOCAL_AUTHORITY_SHADOW_REQUEST_SCHEMA) { - throw new EffectRuntimeRequestError("Local authority shadow request schema mismatch"); - } - if (request.mode !== "file_one_way") { - throw new EffectRuntimeRequestError("Local authority shadow mode must be file_one_way"); - } - const goalId = requireNonEmptyString(request.goal_id, "goal_id"); - if (goalId === "." || goalId === ".." || goalId.includes("/") || goalId.includes("\\")) { - throw new EffectRuntimeRequestError( - "Local authority shadow goal id must be a single path segment", - ); - } - const projection = requireJsonObject(request.source_projection, "source_projection"); - if ( - projection.schema_version !== LOCAL_AUTHORITY_SHADOW_PROJECTION_SCHEMA || - projection.goal_id !== goalId - ) { - throw new EffectRuntimeRequestError( - "Local authority shadow projection schema or goal identity mismatch", - ); - } - const sourceDigest = requireNonEmptyString(request.source_digest, "source_digest"); - if (!/^sha256:[a-f0-9]{64}$/u.test(sourceDigest)) { - throw new EffectRuntimeRequestError("source_digest must be sha256:<64 lowercase hex>"); - } - return { - mode: "file_one_way", - runtime_root: requireNonEmptyString(request.runtime_root, "runtime_root"), - goal_id: goalId, - observation_id: requireNonEmptyString(request.observation_id, "observation_id"), - observation_trigger: requireNonEmptyString( - request.observation_trigger, - "observation_trigger", - ), - source_digest: sourceDigest, - source_projection: structuredClone(projection), - }; -} - -function evidence( - request: LocalAuthorityShadowRequest, - outcome: LocalAuthorityShadowOutcome, - options: { - reasonCode?: string | null; - storeIdentity?: string | null; - providerRevision?: string | null; - cursor?: string | null; - } = {}, -): LocalAuthorityShadowEvidence { - return { - schema_version: LOCAL_AUTHORITY_SHADOW_EVIDENCE_SCHEMA, - outcome, - reason_code: options.reasonCode ?? null, - goal_id: request.goal_id, - observation_id: request.observation_id, - source_digest: request.source_digest, - capture_kind: "post_commit_snapshot", - source_transaction_correlated: false, - durable_source_outbox: false, - source_candidate_compared: false, - parity_verdict: "not_evaluated", - primary_authority: "legacy_local", - candidate_provider: "file", - candidate_read_for_decision: false, - provider_to_local_writes: false, - primary_writeback_preserved: true, - store_identity: options.storeIdentity ?? null, - provider_revision: options.providerRevision ?? null, - cursor: options.cursor ?? null, - }; -} - -function readFailureEvidence( - request: LocalAuthorityShadowRequest, - result: Extract, - storeIdentity: string | null, -): LocalAuthorityShadowEvidence { - return evidence(request, result.status, { - reasonCode: result.reason_code, - storeIdentity, - }); -} - -function receiptMatches( - request: LocalAuthorityShadowRequest, - result: Extract, -): boolean { - return result.receipts.some((raw) => { - const receipt = raw as Record; - return receipt.schema_version === LOCAL_AUTHORITY_SHADOW_OBSERVATION_RECEIPT_SCHEMA && - receipt.observation_id === request.observation_id && - receipt.source_digest === request.source_digest && - receipt.primary_authority === "legacy_local" && - receipt.provider_to_local_writes === false; - }); -} - -async function reconcileReceipt( - store: AuthorityStore, - request: LocalAuthorityShadowRequest, - storeIdentity: string, - reconciledOutcome: "replayed" | "ambiguous_reconciled", -): Promise { - const result = await store.readReceipt(request.observation_id); - if (result.status === "found" && receiptMatches(request, result)) { - return evidence(request, reconciledOutcome, { - storeIdentity, - providerRevision: result.provider_revision, - cursor: result.cursor, - }); - } - if (result.status === "unavailable") { - return evidence(request, "unavailable", { - reasonCode: result.reason_code, - storeIdentity, - }); - } - if (result.status === "failed") { - return evidence(request, "failed", { - reasonCode: result.reason_code, - storeIdentity, - }); - } - return evidence( - request, - reconciledOutcome === "ambiguous_reconciled" - ? "ambiguous_unproved" - : "protocol_mismatch", - { - reasonCode: result.status === "missing" - ? "observation_receipt_missing" - : "observation_receipt_mismatch", - storeIdentity, - }, - ); -} - -/** - * Record a post-commit observation in a candidate AuthorityStore. - * - * The legacy local writers remain the only decision authority. This function - * receives a completed source projection and has no route back to those files. - */ -export async function recordLocalAuthorityShadow( - value: unknown, - dependencies: LocalAuthorityShadowDependencies = {}, -): Promise { - const request = decodeRequest(value); - let store: AuthorityStore; - try { - const providerDirectory = join( - request.runtime_root, - "authority-shadow", - "file", - request.goal_id, - ); - store = (dependencies.openStore ?? ((directory, goalId) => - new FileAuthorityStore(directory, goalId)))( - providerDirectory, - request.goal_id, - ); - } catch { - return evidence(request, "unavailable", { - reasonCode: "provider_construction_failed", - }); - } - - try { - const identity = await store.storeIdentity(); - if (identity.status !== "available") { - return evidence(request, identity.status, { reasonCode: identity.reason_code }); - } - const storeIdentity = identity.store_identity; - const receipt = { - schema_version: LOCAL_AUTHORITY_SHADOW_OBSERVATION_RECEIPT_SCHEMA, - observation_id: request.observation_id, - source_digest: request.source_digest, - observation_trigger: request.observation_trigger, - source_transaction_correlated: false, - parity_verdict: "not_evaluated", - primary_authority: "legacy_local", - candidate_read_for_decision: false, - provider_to_local_writes: false, - }; - const event = { - schema_version: "loopx_local_authority_shadow_event_v0", - kind: "post_commit_snapshot_captured", - observation_id: request.observation_id, - observation_trigger: request.observation_trigger, - source_digest: request.source_digest, - }; - - const loaded = await store.loadAuthority(); - if (loaded.status === "unavailable" || loaded.status === "failed") { - return readFailureEvidence(request, loaded, storeIdentity); - } - const committed = await store.commitAuthority({ - expected_provider_revision: - loaded.status === "loaded" ? loaded.provider_revision : null, - operation_id: request.observation_id, - events: [event], - next_projection: request.source_projection, - receipts: [receipt], - }); - if (committed.status === "applied") { - return evidence(request, "captured", { - storeIdentity, - providerRevision: committed.provider_revision, - cursor: committed.cursor, - }); - } - if (committed.status === "ambiguous") { - return await reconcileReceipt( - store, - request, - storeIdentity, - "ambiguous_reconciled", - ); - } - if (committed.status === "failed") { - return evidence(request, "failed", { - reasonCode: committed.reason_code, - storeIdentity, - }); - } - if (committed.conflict_kind === "operation_id_exists") { - return await reconcileReceipt(store, request, storeIdentity, "replayed"); - } - return evidence(request, "conflict_retry_required", { - reasonCode: "provider_revision_mismatch", - storeIdentity, - providerRevision: committed.current_provider_revision, - cursor: committed.current_cursor, - }); - } catch { - return evidence(request, "unavailable", { - reasonCode: "provider_call_failed", - }); - } -} - -// --------------------------------------------------------------------------- -// Transaction-bound entries (Stage 2C second half). -// -// A drained outbox entry becomes exactly one candidate transaction whose -// operation_id is the entry id, so a receipt names the primary transaction it -// records instead of a post-commit snapshot that may include other writers. -// --------------------------------------------------------------------------- - +// Transaction-bound entries are the only writable shadow lineage. export const LOCAL_AUTHORITY_SHADOW_PROJECTION_SCHEMA_V1 = LOCAL_AUTHORITY_SHADOW_TRANSACTION_PROJECTION_SCHEMA; export { LOCAL_AUTHORITY_SHADOW_COMMIT_ENTRY_RESULT_SCHEMA }; diff --git a/loopx/control_plane/coordination/local_authority_shadow_adapter.py b/loopx/control_plane/coordination/local_authority_shadow_adapter.py index 87697e2af7..5ace9dc6d1 100644 --- a/loopx/control_plane/coordination/local_authority_shadow_adapter.py +++ b/loopx/control_plane/coordination/local_authority_shadow_adapter.py @@ -1,7 +1,7 @@ """Transaction capture evidence, receipt-proven drain, and operator readback. -The independent compatibility observation path lives in -local_authority_shadow_observation; this owner only delivers durable entries. +Historical observations remain readable; this owner only delivers durable +transaction-bound entries. """ from __future__ import annotations @@ -40,7 +40,7 @@ from .shadow_management import read_shadow_capture_binding -from .local_authority_shadow_observation import local_authority_shadow_summary +from .runtime_shadow import local_authority_shadow_summary def effective_runtime_root( @@ -609,7 +609,6 @@ def _drain_prelude( resolve_coordination_runtime_shadow_config( find_registry_goal(registry, goal_id) ).enabled - or local_authority_shadow_summary(find_registry_goal(registry, goal_id) or {})["enabled"] is True ) resolved = ( runtime_root @@ -783,7 +782,7 @@ def local_authority_shadow_status( runtime_config = resolve_coordination_runtime_shadow_config(goal) management = read_shadow_capture_binding(runtime_root, goal_id) legacy_observation = ( - config["enabled"] is True + config.get("configured") is True and not runtime_config.enabled and management["status"] == "missing" ) @@ -844,6 +843,8 @@ def local_authority_shadow_status( "partitions": None, "codec_agreement": None, } + candidate["store_kind"] = "legacy_observation" if legacy_observation else "runtime_shadow" + candidate["historical_only"] = legacy_observation try: store_bytes = _store_bytes( runtime_root, goal_id, legacy_observation=legacy_observation diff --git a/loopx/control_plane/coordination/local_authority_shadow_observation.py b/loopx/control_plane/coordination/local_authority_shadow_observation.py deleted file mode 100644 index bae192ad0b..0000000000 --- a/loopx/control_plane/coordination/local_authority_shadow_observation.py +++ /dev/null @@ -1,479 +0,0 @@ -"""Post-commit bridge from legacy local authority into a file shadow. - -The adapter deliberately owns no lifecycle decision. It is entered only after -the existing Markdown or task-lease writer has succeeded, projects public-safe -facts, and asks the TypeScript authority-store boundary to retain an -observation. Missing configuration is a zero-effect fast path. -""" - -from __future__ import annotations - -import hashlib -import json -from collections.abc import Mapping -from pathlib import Path -from typing import Any - -from ...file_lock import ( - LockAcquireTimeoutError, - exclusive_file_lock, -) -from ...history import load_registry -from ...paths import resolve_runtime_root -from ...registry import find_registry_goal -from ..effect_runtime import effect_runtime_result -from .coordination_state_contract_generated import ( - LOCAL_AUTHORITY_SHADOW_CONFIG_SCHEMA, - LOCAL_AUTHORITY_SHADOW_EVIDENCE_SCHEMA, - LOCAL_AUTHORITY_SHADOW_PROJECTION_SCHEMA, - LOCAL_AUTHORITY_SHADOW_REQUEST_SCHEMA, -) - - -_CONFIG_FIELDS = {"schema_version", "mode"} -_PROJECTION_ATTEMPTS = 3 -_CONFLICT_RETRY_ATTEMPTS = 3 -_EVIDENCE_OUTCOMES = { - "captured", - "replayed", - "ambiguous_reconciled", - "ambiguous_unproved", - "unavailable", - "failed", - "protocol_mismatch", - "conflict_retry_required", -} -_TODO_FIELDS = ( - "todo_id", - "role", - "status", - "claimed_by", - "bound_agent", - "goal_bound", - "blocks_agent", - "excluded_agents", - "global_gate", - "task_class", - "action_kind", - "required_write_scopes", - "required_capabilities", - "continuation_policy", - "successor_todo_ids", - "no_followup", - "completion_continuation", -) -_LEASE_FIELDS = ( - "todo_id", - "owner", - "idempotency_key", - "write_scopes", - "version", - "lease_epoch", - "acquired_at", - "updated_at", - "expires_at", - "released_at", - "status", -) - - -def local_authority_shadow_summary(goal: Mapping[str, Any]) -> dict[str, Any]: - """Project the closed local-shadow configuration for operator readback.""" - - coordination = ( - goal.get("coordination") - if isinstance(goal.get("coordination"), Mapping) - else {} - ) - raw = coordination.get("authority_shadow") - if raw is None: - return {"enabled": False, "mode": None, "status": "disabled"} - valid = bool( - isinstance(raw, Mapping) - and set(raw) == _CONFIG_FIELDS - and raw.get("schema_version") == LOCAL_AUTHORITY_SHADOW_CONFIG_SCHEMA - and raw.get("mode") == "file_one_way" - ) - return { - "enabled": valid, - "mode": raw.get("mode") if isinstance(raw, Mapping) else None, - "status": "enabled" if valid else "invalid", - } - - -def validate_local_authority_shadow_change( - enable_file: bool, - clear: bool, -) -> None: - """Reject contradictory CLI intent before reading or mutating the registry.""" - - if enable_file and clear: - raise ValueError( - "--local-authority-shadow-file cannot be combined with " - "--clear-local-authority-shadow" - ) - - -def apply_local_authority_shadow_change( - goal: dict[str, Any], - enable_file: bool, - clear: bool, -) -> None: - """Apply a validated default-off local-shadow configuration change.""" - - if not enable_file and not clear: - return - coordination = ( - goal.get("coordination") if isinstance(goal.get("coordination"), dict) else {} - ) - if clear: - coordination.pop("authority_shadow", None) - else: - coordination["authority_shadow"] = { - "schema_version": LOCAL_AUTHORITY_SHADOW_CONFIG_SCHEMA, - "mode": "file_one_way", - } - if coordination: - goal["coordination"] = coordination - else: - goal.pop("coordination", None) - - - - -def _base_evidence( - *, - goal_id: str, - outcome: str, - reason_code: str, -) -> dict[str, Any]: - return { - "schema_version": LOCAL_AUTHORITY_SHADOW_EVIDENCE_SCHEMA, - "outcome": outcome, - "reason_code": reason_code, - "goal_id": goal_id, - "observation_id": None, - "source_digest": None, - "capture_kind": "post_commit_snapshot", - "source_transaction_correlated": False, - "durable_source_outbox": False, - "source_candidate_compared": False, - "parity_verdict": "not_evaluated", - "primary_authority": "legacy_local", - "candidate_provider": "file", - "candidate_read_for_decision": False, - "provider_to_local_writes": False, - "primary_writeback_preserved": True, - "store_identity": None, - "provider_revision": None, - "cursor": None, - } - - -def _shadow_config(registry: dict[str, Any], goal_id: str) -> dict[str, str] | None: - goal = find_registry_goal(registry, goal_id) - coordination = goal.get("coordination") if isinstance(goal, dict) else None - if not isinstance(coordination, dict) or "authority_shadow" not in coordination: - return None - raw = coordination.get("authority_shadow") - if ( - not isinstance(raw, dict) - or set(raw) != _CONFIG_FIELDS - or raw.get("schema_version") != LOCAL_AUTHORITY_SHADOW_CONFIG_SCHEMA - or raw.get("mode") != "file_one_way" - ): - raise ValueError("authority_shadow must be a closed file_one_way config") - return {"mode": "file_one_way"} - - -def _canonical(value: object) -> bytes: - return json.dumps( - value, - ensure_ascii=False, - sort_keys=True, - separators=(",", ":"), - allow_nan=False, - ).encode("utf-8") - - -def _compact_todo(raw: object) -> dict[str, Any] | None: - if not isinstance(raw, dict): - return None - todo_id = str(raw.get("todo_id") or "").strip() - if not todo_id: - return None - compact = {field: raw[field] for field in _TODO_FIELDS if field in raw} - compact["todo_id"] = todo_id - if "status" not in compact and isinstance(raw.get("done"), bool): - compact["status"] = "done" if raw["done"] else "open" - return json.loads(_canonical(compact)) - - -def _compact_lease(path: Path, *, goal_id: str) -> dict[str, Any]: - raw = json.loads(path.read_text(encoding="utf-8")) - if not isinstance(raw, dict): - raise ValueError("task lease must contain an object") - if raw.get("goal_id") != goal_id or raw.get("todo_id") != path.stem: - raise ValueError("task lease identity does not match its shadow source") - return json.loads( - _canonical({field: raw[field] for field in _LEASE_FIELDS if field in raw}) - ) - - -def _source_projection( - *, - registry_path: Path, - runtime_root: Path, - goal_id: str, -) -> dict[str, Any]: - from ...control_plane.todos.handoff_mode import goal_handoff_mode_for_goal - from ...todos import list_goal_todos - - todo_payload = list_goal_todos( - registry_path=registry_path, - goal_id=goal_id, - runtime_root_arg=str(runtime_root), - ) - todos = [ - compact - for raw in todo_payload.get("todos") or [] - if (compact := _compact_todo(raw)) is not None - ] - todos.sort(key=lambda item: str(item["todo_id"])) - lease_dir = runtime_root / "goals" / goal_id / "task-leases" - leases = ( - [ - _compact_lease(path, goal_id=goal_id) - for path in sorted(lease_dir.glob("*.json")) - ] - if lease_dir.exists() - else [] - ) - projection = { - "schema_version": LOCAL_AUTHORITY_SHADOW_PROJECTION_SCHEMA, - "goal_id": goal_id, - "handoff_mode": goal_handoff_mode_for_goal( - registry_path=registry_path, - goal_id=goal_id, - ), - "todos": todos, - "leases": leases, - } - return json.loads(_canonical(projection)) - - -def _stable_projection( - *, - registry_path: Path, - runtime_root: Path, - goal_id: str, -) -> dict[str, Any]: - previous = _source_projection( - registry_path=registry_path, - runtime_root=runtime_root, - goal_id=goal_id, - ) - for _attempt in range(_PROJECTION_ATTEMPTS): - current = _source_projection( - registry_path=registry_path, - runtime_root=runtime_root, - goal_id=goal_id, - ) - if current == previous: - return current - previous = current - raise RuntimeError("local authority sources did not stabilize for shadowing") - - -def _valid_evidence( - result: object, - *, - goal_id: str, - observation_id: str, - source_digest: str, -) -> bool: - if not isinstance(result, dict): - return False - return ( - result.get("schema_version") == LOCAL_AUTHORITY_SHADOW_EVIDENCE_SCHEMA - and result.get("outcome") in _EVIDENCE_OUTCOMES - and result.get("goal_id") == goal_id - and result.get("observation_id") == observation_id - and result.get("source_digest") == source_digest - and result.get("capture_kind") == "post_commit_snapshot" - and result.get("source_transaction_correlated") is False - and result.get("durable_source_outbox") is False - and result.get("source_candidate_compared") is False - and result.get("parity_verdict") == "not_evaluated" - and result.get("primary_authority") == "legacy_local" - and result.get("candidate_provider") == "file" - and result.get("candidate_read_for_decision") is False - and result.get("provider_to_local_writes") is False - and result.get("primary_writeback_preserved") is True - and ( - result.get("reason_code") is None - or isinstance(result.get("reason_code"), str) - ) - ) - - -def observe_local_authority_commit( - *, - registry_path: Path, - runtime_root: Path | None, - goal_id: str, - observation_trigger: str, -) -> dict[str, Any] | None: - """Capture a best-effort post-commit snapshot without changing its verdict. - - ``observation_trigger`` is diagnostic context, not a primary transaction - identity. The snapshot may include commits that landed after that trigger. - """ - - if not goal_id or goal_id in {".", ".."} or "/" in goal_id or "\\" in goal_id: - return _base_evidence( - goal_id=goal_id, - outcome="failed", - reason_code="invalid_shadow_goal_id", - ) - try: - registry = load_registry(registry_path) - config = _shadow_config(registry, goal_id) - except Exception: - return _base_evidence( - goal_id=goal_id, - outcome="failed", - reason_code="invalid_shadow_config", - ) - if config is None: - return None - - try: - if runtime_root is None: - runtime_root = resolve_runtime_root( - registry, - None, - registry_path=registry_path, - ) - # Candidate-provider bytes live outside the legacy per-goal runtime - # tree. State migration may copy that tree, but it must never copy a - # store identity or revision and accidentally create a second lineage. - shadow_root = runtime_root / "authority-shadow" / "file" / goal_id - with exclusive_file_lock( - shadow_root / "observation", - timeout_seconds=1.0, - operation="local_authority_shadow_observe", - ): - result: dict[str, Any] | None = None - for _attempt in range(_CONFLICT_RETRY_ATTEMPTS): - projection = _stable_projection( - registry_path=registry_path, - runtime_root=runtime_root, - goal_id=goal_id, - ) - source_digest = ( - "sha256:" + hashlib.sha256(_canonical(projection)).hexdigest() - ) - observation_id = ( - "local-shadow:" - + hashlib.sha256( - _canonical( - { - "goal_id": goal_id, - "observation_trigger": observation_trigger, - "source_digest": source_digest, - } - ) - ).hexdigest() - ) - raw_result = effect_runtime_result( - "coordination.local_authority_shadow.record", - { - "schema_version": LOCAL_AUTHORITY_SHADOW_REQUEST_SCHEMA, - "mode": config["mode"], - "runtime_root": str(runtime_root), - "goal_id": goal_id, - "observation_id": observation_id, - "observation_trigger": observation_trigger, - "source_digest": source_digest, - "source_projection": projection, - }, - timeout=15.0, - ) - if not _valid_evidence( - raw_result, - goal_id=goal_id, - observation_id=observation_id, - source_digest=source_digest, - ): - return _base_evidence( - goal_id=goal_id, - outcome="failed", - reason_code="shadow_observation_result_invalid", - ) - result = dict(raw_result) - if result["outcome"] != "conflict_retry_required": - return result - if result is not None: - return result - except LockAcquireTimeoutError: - return _base_evidence( - goal_id=goal_id, - outcome="unavailable", - reason_code="shadow_observation_lock_timeout", - ) - except Exception: - return _base_evidence( - goal_id=goal_id, - outcome="failed", - reason_code="shadow_observation_failed", - ) - return _base_evidence( - goal_id=goal_id, - outcome="failed", - reason_code="shadow_observation_failed", - ) - - -def observe_todo_local_authority_commit( - payload: dict[str, Any], - registry_path: Path, - goal_id: str, - write_class: str, - *, - runtime_root: Path | None = None, -) -> dict[str, Any]: - """Attach post-commit shadow evidence without changing the Todo verdict. - - ``runtime_root`` is the effective root the writer resolved for this call; - ``None`` falls back to the registry root exactly as the other hooks do. - """ - - changed = any( - payload.get(field) - for field in ("changed", "added", "metadata_updated", "completed", "superseded") - ) - if payload.get("dry_run") or not changed: - return payload - todo_id = str(payload.get("todo_id") or "none") - updated_at = str(payload.get("updated_at") or "unknown") - evidence = observe_local_authority_commit( - registry_path=registry_path, - runtime_root=runtime_root, - goal_id=goal_id, - observation_trigger=f"{write_class}:{todo_id}:{updated_at}", - ) - if evidence is not None: - payload["authority_shadow"] = evidence - return payload - - -__all__ = [ - "LOCAL_AUTHORITY_SHADOW_CONFIG_SCHEMA", - "LOCAL_AUTHORITY_SHADOW_EVIDENCE_SCHEMA", - "apply_local_authority_shadow_change", - "local_authority_shadow_summary", - "observe_local_authority_commit", - "observe_todo_local_authority_commit", - "validate_local_authority_shadow_change", -] diff --git a/loopx/control_plane/coordination/runtime_shadow.py b/loopx/control_plane/coordination/runtime_shadow.py index 8d668fc1bf..ba58feb7c2 100644 --- a/loopx/control_plane/coordination/runtime_shadow.py +++ b/loopx/control_plane/coordination/runtime_shadow.py @@ -15,7 +15,6 @@ from typing import Any from .local_authority_shadow_projection import source_effect_runtime_result as effect_runtime_result -from . import local_authority_shadow_observation from .coordination_state_contract_generated import ( COORDINATION_RUNTIME_SHADOW_BOOTSTRAP_REQUEST_SCHEMA as RUNTIME_SHADOW_BOOTSTRAP_REQUEST_SCHEMA_VERSION, COORDINATION_RUNTIME_SHADOW_BOOTSTRAP_RESULT_SCHEMA, @@ -28,6 +27,7 @@ COORDINATION_RUNTIME_SHADOW_ROLLBACK_RESULT_SCHEMA, COORDINATION_RUNTIME_SHADOW_TODO_READ_REQUEST_SCHEMA as RUNTIME_SHADOW_TODO_READ_REQUEST_SCHEMA_VERSION, COORDINATION_RUNTIME_SHADOW_TODO_READ_RESULT_SCHEMA, + LOCAL_AUTHORITY_SHADOW_CONFIG_SCHEMA, LOCAL_COORDINATION_PROMOTION_REVIEW_REQUEST_SCHEMA, LOCAL_COORDINATION_PROMOTION_REVIEW_RESULT_SCHEMA, ) @@ -50,13 +50,50 @@ class CoordinationRuntimeShadowConfig: reason_code: str +def local_authority_shadow_summary(goal: Mapping[str, Any] | None) -> dict[str, Any]: + """Recognize retained config without granting it a writer or capture lineage.""" + coordination = goal.get("coordination") if isinstance(goal, Mapping) else None + if not isinstance(coordination, Mapping) or "authority_shadow" not in coordination: + return {"enabled": False, "mode": None, "status": "disabled"} + raw = coordination["authority_shadow"] + valid = (isinstance(raw, Mapping) and set(raw) == {"schema_version", "mode"} + and raw.get("schema_version") == LOCAL_AUTHORITY_SHADOW_CONFIG_SCHEMA + and raw.get("mode") == "file_one_way") + return {"enabled": False, "mode": raw.get("mode") if isinstance(raw, Mapping) else None, + "status": "retired" if valid else "invalid", "configured": True, + "replacement": "coordination_runtime_shadow"} + + +def validate_local_authority_shadow_change(enable_file: bool, clear: bool) -> None: + if enable_file and clear: + raise ValueError("--local-authority-shadow-file cannot be combined with --clear-local-authority-shadow") + if enable_file: + raise ValueError( + "local_authority_shadow_retired: post-commit observation is retired; " + "clear it with --clear-local-authority-shadow; explicitly configure " + "--coordination-runtime-shadow-file and run coordination-shadow bootstrap " + "before transaction-bound capture. Retained observations are not migration evidence." + ) + + +def apply_local_authority_shadow_change(goal: dict[str, Any], enable_file: bool, clear: bool) -> None: + validate_local_authority_shadow_change(enable_file, clear) + if not clear: + return + coordination = goal.get("coordination") + if isinstance(coordination, dict): + coordination.pop("authority_shadow", None) + if not coordination: + goal.pop("coordination", None) + + def coordination_shadow_summaries( goal: Mapping[str, Any] | None, ) -> dict[str, dict[str, object]]: - """Project both distinct pre-promotion shadow configurations.""" + """Report the active capture configuration and any retired observation setting.""" return { - "local_authority_shadow": local_authority_shadow_observation.local_authority_shadow_summary( + "local_authority_shadow": local_authority_shadow_summary( goal ), "coordination_runtime_shadow": coordination_runtime_shadow_summary(goal), @@ -69,9 +106,9 @@ def validate_coordination_shadow_changes( runtime_enable_file: bool, runtime_clear: bool, ) -> None: - """Validate both default-off shadow configuration seams.""" + """Reject retired activation before any registry mutation.""" - local_authority_shadow_observation.validate_local_authority_shadow_change( + validate_local_authority_shadow_change( local_enable_file, local_clear ) validate_coordination_runtime_shadow_change(runtime_enable_file, runtime_clear) @@ -84,9 +121,9 @@ def apply_coordination_shadow_changes( runtime_enable_file: bool, runtime_clear: bool, ) -> None: - """Apply observation and transaction-bound shadow settings together.""" + """Clear retired settings and configure the transaction-bound shadow independently.""" - local_authority_shadow_observation.apply_local_authority_shadow_change( + apply_local_authority_shadow_change( goal, local_enable_file, local_clear ) apply_coordination_runtime_shadow_change(goal, runtime_enable_file, runtime_clear) diff --git a/loopx/control_plane/coordination/runtime_shadow_writer_adapter.py b/loopx/control_plane/coordination/runtime_shadow_writer_adapter.py index 1544322f50..e7bab728ae 100644 --- a/loopx/control_plane/coordination/runtime_shadow_writer_adapter.py +++ b/loopx/control_plane/coordination/runtime_shadow_writer_adapter.py @@ -141,14 +141,11 @@ def settle_todo_runtime_shadow_capture( registry_path: Path, runtime_root: Path, goal_id: str, - write_class: str, capture: outbox.TodoPartitionCapture, - observe_legacy: bool = True, emit_disabled: bool = True, ) -> dict[str, Any]: """Boundedly drain one transaction capture after releasing the Todo lock.""" - from .local_authority_shadow_observation import observe_todo_local_authority_commit from .local_authority_shadow_adapter import capture_evidence, drain_local_authority_shadow_outbox drain = ( @@ -164,15 +161,7 @@ def settle_todo_runtime_shadow_capture( payload["coordination_runtime_shadow"] = capture_evidence( goal_id=goal_id, capture=capture.outcome, drain=drain, ) - if not observe_legacy: - return payload - return observe_todo_local_authority_commit( - payload, - registry_path, - goal_id, - write_class, - runtime_root=runtime_root, - ) + return payload def settle_lease_runtime_shadow_capture( diff --git a/loopx/control_plane/testing/authority_e2e_fixtures.py b/loopx/control_plane/testing/authority_e2e_fixtures.py index 5ced733753..ffe2283135 100644 --- a/loopx/control_plane/testing/authority_e2e_fixtures.py +++ b/loopx/control_plane/testing/authority_e2e_fixtures.py @@ -15,13 +15,11 @@ import sys import time import uuid -from collections.abc import Callable, Iterator, Mapping, Sequence -from contextlib import contextmanager +from collections.abc import Callable, Mapping, Sequence from dataclasses import dataclass from pathlib import Path from typing import Any, Protocol -from ...file_lock import exclusive_file_lock from ..coordination.authority_core import HandoffMode from ..coordination.coordination_state_contract_generated import ( LOCAL_AUTHORITY_SHADOW_CONFIG_SCHEMA, @@ -95,10 +93,6 @@ class GoalWorkspace: def shadow_directory(self) -> Path: return self.runtime_root / "authority-shadow" / "file" / self.goal_id - @property - def observation_lock_target(self) -> Path: - return self.shadow_directory / "observation" - def cli_prefix(self) -> list[str]: prefix = ["--registry", str(self.registry_path)] if self.runtime_root_binding in ("cli_override", "cli_override_divergent"): @@ -132,54 +126,6 @@ def cli_prefix(self) -> list[str]: return ["--registry", str(self.target_registry), "--format", "json"] -@dataclass(frozen=True) -class CandidateDocument: - """Stable fields of the single ``authority-store-*.json`` candidate document.""" - - path: Path - document: JsonObject - - @property - def cursor(self) -> str: - return str(self.document.get("cursor")) - - @property - def store_identity(self) -> str: - return str(self.document.get("store_identity")) - - @property - def head(self) -> JsonObject: - head = self.document.get("head") - return dict(head) if isinstance(head, dict) else {} - - @property - def operation_ids(self) -> list[str]: - committed = self.document.get("committed") - if not isinstance(committed, list): - return [] - return [ - str(entry.get("operation_id")) - for entry in committed - if isinstance(entry, dict) - ] - - @property - def todo_ids(self) -> list[str]: - return [ - str(todo.get("todo_id")) - for todo in self.head.get("todos") or [] - if isinstance(todo, dict) - ] - - @property - def leases(self) -> list[JsonObject]: - return [ - dict(lease) - for lease in self.head.get("leases") or [] - if isinstance(lease, dict) - ] - - @dataclass(frozen=True) class TapSummary: """The ``# pass`` / ``# fail`` / ``# skipped`` trailer of a node TAP run.""" @@ -473,33 +419,6 @@ def kill_now(process: subprocess.Popen[str]) -> None: process.communicate(timeout=5) -@contextmanager -def hold_observation_lock(workspace: GoalWorkspace) -> Iterator[Path]: - """Hold the observer's own lock so a primary commit cannot be observed.""" - - with exclusive_file_lock( - workspace.observation_lock_target, - operation="e2e_window", - ) as lock_path: - yield lock_path - - -def candidate_store_paths(workspace: GoalWorkspace) -> list[Path]: - return sorted(workspace.shadow_directory.glob("authority-store-*.json")) - - -def candidate_document(workspace: GoalWorkspace) -> CandidateDocument: - """Return the single candidate document; zero or several is a failure.""" - - paths = candidate_store_paths(workspace) - if len(paths) != 1: - raise AssertionError(f"expected exactly one candidate document, found {len(paths)}") - return CandidateDocument( - path=paths[0], - document=parse_json_object(paths[0].read_text(encoding="utf-8")), - ) - - def node_executable() -> str | None: return shutil.which("node") @@ -509,6 +428,7 @@ def ts_readback( *, receipt: str | None = None, page_size: int = 2, + directory: Path | None = None, ) -> JsonObject | None: """Read the candidate back through ``FileAuthorityStore``; ``None`` without node.""" @@ -521,7 +441,7 @@ def ts_readback( "--experimental-strip-types", str(REPO_ROOT / TS_READBACK_PROBE), "--directory", - str(workspace.shadow_directory), + str(directory or workspace.shadow_directory), "--goal-id", workspace.goal_id, "--page-size", @@ -600,7 +520,6 @@ def tap_summary( __all__ = [ - "CandidateDocument", "CliCommandError", "CliOutputError", "CliWorkspace", @@ -617,11 +536,8 @@ def tap_summary( "TapSummary", "build_goal_workspace", "build_legacy_migration_source", - "candidate_document", - "candidate_store_paths", "cli_command", "cli_env", - "hold_observation_lock", "kill_now", "node_executable", "parse_json_object", diff --git a/loopx/control_plane/testing/authority_e2e_ladder.py b/loopx/control_plane/testing/authority_e2e_ladder.py index e11e4e3c35..a2416b3d26 100644 --- a/loopx/control_plane/testing/authority_e2e_ladder.py +++ b/loopx/control_plane/testing/authority_e2e_ladder.py @@ -34,22 +34,15 @@ RowOutcome, acquire_lease, add_todo, - committed_observation, expect, passed, sha256_hex, unverified, ) -from .authority_e2e_rows_stage2c import ( - row_candidate_failure_preserves_primary, - row_configure_enable_disable_roundtrip, - row_crash_gap_loses_observation, - row_default_off_isolation, - row_dual_runtime_root_consistency, - row_every_writer_family_captures, - row_migration_seeds_new_lineage, -) +from .authority_e2e_rows_stage2c import row_retired_observation_upgrade from .authority_e2e_rows_stage2c2 import ( + capture_workspace, + delivered, row_archive_after_leased_completion_parity, row_drain_idempotent, row_event_only_todo_source_holds, @@ -67,8 +60,6 @@ CliOutputError, TS_READBACK_PROBE, JsonObject, - build_goal_workspace, - candidate_document, node_executable, parse_json_object, run_cli, @@ -319,14 +310,8 @@ def _row_nokv_live_matrix(context: RowContext) -> RowOutcome: def _row_cli_document_decodes_through_ts_store(context: RowContext) -> RowOutcome: if node_executable() is None: return unverified("node_missing") - workspace = build_goal_workspace( - context.root, - goal_id=unique_goal_id("ladder-s1"), - handoff_mode="hard_lease", - shadow_enabled=True, - runtime_root_binding="registry", - ) - added = add_todo(workspace, "Decode this observation through the TypeScript store.") + workspace = capture_workspace(context, "ladder-s1") + added = add_todo(workspace, "Decode this source transaction through the TypeScript store.") todo_id = str(added["todo_id"]) acquired = acquire_lease( workspace, @@ -347,37 +332,28 @@ def _row_cli_document_decodes_through_ts_store(context: RowContext) -> RowOutcom "--agent-id", AGENT_A, ) - observations = [ - committed_observation(payload, label=label) - for label, payload in (("todo add", added), ("task-lease acquire", acquired), ("todo update", updated)) - ] - expect( - all(evidence["outcome"] == "captured" for evidence in observations), - "three distinct CLI writes must each be captured", - ) - observation_ids = [str(evidence["observation_id"]) for evidence in observations] - probe = ts_readback(workspace, receipt=observation_ids[0]) + for label, payload in (("todo add", added), ("task-lease acquire", acquired), ("todo update", updated)): + delivered(payload, label=label) + probe = ts_readback(workspace, directory=workspace.runtime_root / "authority-shadow" / "file-v0") expect(probe is not None, "read-back probe requires node") assert probe is not None - load = probe.get("load") - scan = probe.get("scan") - receipt = probe.get("receipt") + load, scan = probe.get("load"), probe.get("scan") expect(isinstance(load, dict) and load.get("status") == "loaded", "TS store must load the CLI document") - expect(isinstance(load, dict) and load.get("cursor") == "3", "TS store cursor must be 3 after three writes") - expect( - isinstance(load, dict) - and load.get("provider_revision") == observations[-1]["provider_revision"], - "TS store head revision must equal the last observation revision", - ) - expect( - isinstance(scan, dict) and scan.get("operation_ids") == observation_ids, - "scanCommitted must page through the three observation ids in order", - ) - expect(isinstance(receipt, dict) and receipt.get("status") == "found", "readReceipt must find the first observation") - document = candidate_document(workspace) - expect(document.cursor == "3" and document.operation_ids == observation_ids, "candidate bytes must match the probe") + expect(isinstance(load, dict) and load.get("cursor") == "4", "bootstrap and three writes must advance four cursors") + expect(isinstance(scan, dict), "scan must return a page summary") assert isinstance(scan, dict) - return passed(cursor="3", scan_pages=scan.get("pages"), observation_count=len(observation_ids)) + operations = scan.get("operation_ids") + expect(isinstance(operations, list) and len(operations) == len(set(operations)) == 4, + "paged scan must return four distinct transactions") + expect(scan.get("cursors") == ["1", "2", "3", "4"], "transactions must retain source order") + assert isinstance(operations, list) + receipt_probe = ts_readback(workspace, receipt=str(operations[1]), + directory=workspace.runtime_root / "authority-shadow" / "file-v0") + expect(isinstance(receipt_probe, dict), "receipt probe must load") + assert isinstance(receipt_probe, dict) + receipt = receipt_probe.get("receipt") + expect(isinstance(receipt, dict) and receipt.get("status") == "found", "readReceipt must find the first source write") + return passed(cursor="4", scan_pages=scan.get("pages"), transaction_count=4) # --------------------------------------------------------------------------- @@ -610,67 +586,13 @@ def _row_postgresql_conformance_live(context: RowContext) -> RowOutcome: run=_row_postgresql_conformance_live, ), LadderRow( - id="s2c1.configure_enable_disable_roundtrip", - stage="2c1", - title="configure-goal previews, enables, reads back, and disables the observer", - product_path="real_cli", - gate="deterministic", - posix_only=False, - run=row_configure_enable_disable_roundtrip, - ), - LadderRow( - id="s2c1.every_writer_family_captures", - stage="2c1", - title="Every local writer family records a post-commit observation", - product_path="real_cli", - gate="deterministic", - posix_only=False, - run=row_every_writer_family_captures, - ), - LadderRow( - id="s2c1.default_off_isolation", - stage="2c1", - title="Default-off goals produce identical responses and no candidate storage", - product_path="real_cli", - gate="deterministic", - posix_only=False, - run=row_default_off_isolation, - ), - LadderRow( - id="s2c1.candidate_failure_preserves_primary", - stage="2c1", - title="Candidate construction failure never reverses the primary commit", - product_path="real_cli", - gate="deterministic", - posix_only=False, - run=row_candidate_failure_preserves_primary, - ), - LadderRow( - id="s2c1.crash_gap_loses_observation", - stage="2c1", - title="A SIGKILL between primary commit and observer loses only that observation", - product_path="real_cli", - gate="deterministic", - posix_only=True, - run=row_crash_gap_loses_observation, - ), - LadderRow( - id="s2c1.dual_runtime_root_consistency", - stage="2c1", - title="A --runtime-root override that differs from common_runtime_root keeps one candidate lineage", - product_path="real_cli", - gate="deterministic", - posix_only=False, - run=row_dual_runtime_root_consistency, - ), - LadderRow( - id="s2c1.migration_seeds_new_lineage", + id="s2c1.retired_observation_upgrade", stage="2c1", - title="migrate-state excludes the legacy lineage and seeds a fresh candidate", + title="Retired settings cannot write; explicit replacement bootstrap captures the next transaction", product_path="real_cli", gate="deterministic", posix_only=False, - run=row_migration_seeds_new_lineage, + run=row_retired_observation_upgrade, ), LadderRow( id="s2c2.outbox_prepared_then_committed_entries", diff --git a/loopx/control_plane/testing/authority_e2e_row_support.py b/loopx/control_plane/testing/authority_e2e_row_support.py index b64a38de6b..50fcfc7233 100644 --- a/loopx/control_plane/testing/authority_e2e_row_support.py +++ b/loopx/control_plane/testing/authority_e2e_row_support.py @@ -20,36 +20,6 @@ AGENT_B = "agent-b" -PRIMARY_VISIBILITY_TIMEOUT_SECONDS = 15.0 - - -COMMITTED_OBSERVATION_OUTCOMES = frozenset({"captured", "replayed", "ambiguous_reconciled"}) - - -LOCAL_SHADOW_SUMMARY_ENABLED = { - "enabled": True, - "mode": "file_one_way", - "status": "enabled", -} - - -DEFAULT_OFF_PARITY_FIELDS: tuple[str, ...] = ( - "ok", - "added", - "already_exists", - "metadata_updated", - "status_changed", - "role", - "status", - "task_class", - "action_kind", - "continuation_policy", -) - - -MIGRATION_SEED_SCHEMA = "loopx_state_migration_shadow_seed_evidence_v0" - - class RowAssertionError(AssertionError): """A row invariant failed; the message is written to be public-safe.""" @@ -89,34 +59,6 @@ def sha256_hex(value: str | bytes) -> str: return hashlib.sha256(payload).hexdigest() -def shadow_evidence(payload: Mapping[str, object], *, label: str) -> JsonObject: - evidence = payload.get("authority_shadow") - expect(isinstance(evidence, dict), f"{label} must carry authority_shadow evidence") - assert isinstance(evidence, dict) - return {str(key): value for key, value in evidence.items()} - - -def committed_observation(payload: Mapping[str, object], *, label: str) -> JsonObject: - evidence = shadow_evidence(payload, label=label) - expect( - evidence.get("outcome") in COMMITTED_OBSERVATION_OUTCOMES, - f"{label} observation outcome must be captured, replayed, or ambiguous_reconciled", - ) - expect( - evidence.get("primary_writeback_preserved") is True, - f"{label} must preserve the primary writeback", - ) - expect( - evidence.get("provider_to_local_writes") is False, - f"{label} must never write from provider to local state", - ) - expect( - evidence.get("candidate_read_for_decision") is False, - f"{label} must never read the candidate for a decision", - ) - return evidence - - def add_todo(workspace: GoalWorkspace, text: str) -> JsonObject: return run_cli( workspace, @@ -157,10 +99,6 @@ def acquire_lease( ) -def configure_shadow(workspace: GoalWorkspace, *flags: str) -> JsonObject: - return run_cli(workspace, "configure-goal", "--goal-id", workspace.goal_id, *flags) - - def lease_version(payload: Mapping[str, object], *, label: str) -> str: lease = payload.get("lease") expect(isinstance(lease, dict), f"{label} must return a lease record") @@ -171,22 +109,14 @@ def lease_version(payload: Mapping[str, object], *, label: str) -> str: __all__ = [ "AGENT_A", "AGENT_B", - "COMMITTED_OBSERVATION_OUTCOMES", - "DEFAULT_OFF_PARITY_FIELDS", - "LOCAL_SHADOW_SUMMARY_ENABLED", - "MIGRATION_SEED_SCHEMA", - "PRIMARY_VISIBILITY_TIMEOUT_SECONDS", "RowAssertionError", "RowContext", "RowOutcome", "acquire_lease", "add_todo", - "committed_observation", - "configure_shadow", "expect", "lease_version", "passed", - "shadow_evidence", "sha256_hex", "unverified", ] diff --git a/loopx/control_plane/testing/authority_e2e_rows_stage2c.py b/loopx/control_plane/testing/authority_e2e_rows_stage2c.py index 15f3edb7a7..4090cc8b1d 100644 --- a/loopx/control_plane/testing/authority_e2e_rows_stage2c.py +++ b/loopx/control_plane/testing/authority_e2e_rows_stage2c.py @@ -1,492 +1,31 @@ -"""Stage 2C observation-foundation rows of the shared-goal-authority ladder. - -These rows port the local-shadow CLI E2E and migration assertions onto the -ladder: every write goes through the real ``python -m loopx.cli`` and the -candidate is read back only from its retained bytes. -""" - +"""Upgrade acceptance for the retired, post-commit observation lineage.""" from __future__ import annotations -import json -from collections.abc import Mapping -from dataclasses import dataclass, field - -from .authority_e2e_fixtures import ( - GoalWorkspace, - JsonObject, - LegacyMigrationSource, - build_goal_workspace, - build_legacy_migration_source, - candidate_document, - candidate_store_paths, - hold_observation_lock, - kill_now, - parse_json_object, - run_cli, - spawn_cli, - unique_goal_id, - wait_until, -) -from .authority_e2e_row_support import ( - AGENT_A, - AGENT_B, - DEFAULT_OFF_PARITY_FIELDS, - LOCAL_SHADOW_SUMMARY_ENABLED, - MIGRATION_SEED_SCHEMA, - PRIMARY_VISIBILITY_TIMEOUT_SECONDS, - RowContext, - RowOutcome, - acquire_lease, - add_todo, - committed_observation, - configure_shadow, - expect, - lease_version, - passed, - shadow_evidence, -) - -def shadow_workspace(context: RowContext, prefix: str, *, shadow_enabled: bool) -> GoalWorkspace: - return build_goal_workspace( - context.root, - goal_id=unique_goal_id(prefix), - handoff_mode="hard_lease", - shadow_enabled=shadow_enabled, - runtime_root_binding="cli_override", - ) - - -def row_configure_enable_disable_roundtrip(context: RowContext) -> RowOutcome: - workspace = shadow_workspace(context, "ladder-configure", shadow_enabled=False) - preview = configure_shadow(workspace, "--local-authority-shadow-file") - expect(preview.get("dry_run") is True and preview.get("written") is False, "preview must not write") - enabled = configure_shadow(workspace, "--local-authority-shadow-file", "--execute") - expect(enabled.get("written") is True, "enable must write the registry") - - observed = add_todo(workspace, "Capture one post-commit observation through the product CLI.") - evidence = committed_observation(observed, label="todo add") - expect(evidence["outcome"] == "captured", "first observation must be captured") - expect(evidence["parity_verdict"] == "not_evaluated", "observation must not claim parity") - lease = acquire_lease( - workspace, - todo_id=str(observed["todo_id"]), - owner=AGENT_A, - idempotency_key="ladder-configure-lease", - ) - expect(lease.get("acquired") is True, "lease must be acquired") - expect(committed_observation(lease, label="task-lease acquire")["outcome"] == "captured", "lease observation must be captured") - document = candidate_document(workspace) - expect(len(document.todo_ids) == 1 and len(document.leases) == 1, "candidate head must hold one todo and one lease") - - inspected = configure_shadow(workspace) - after = inspected.get("after") - expect( - isinstance(after, dict) and after.get("local_authority_shadow") == LOCAL_SHADOW_SUMMARY_ENABLED, - "configure-goal must read back the enabled shadow summary", - ) - disabled = configure_shadow(workspace, "--clear-local-authority-shadow", "--execute") - expect(disabled.get("written") is True, "disable must write the registry") - before_disabled_write = document.path.read_bytes() - after_disable = add_todo(workspace, "This local lifecycle write must not execute the observer.") - expect(after_disable.get("ok") is True and after_disable.get("added") is True, "disabled write must still commit") - expect("authority_shadow" not in after_disable, "disabled write must not observe") - expect(document.path.read_bytes() == before_disabled_write, "candidate bytes must not change once disabled") - return passed(candidate_cursor=document.cursor, head_todos=1, head_leases=1) - - -def row_default_off_isolation(context: RowContext) -> RowOutcome: - enabled = shadow_workspace(context, "ladder-enabled", shadow_enabled=True) - baseline = shadow_workspace(context, "ladder-baseline", shadow_enabled=False) - text = "Capture one post-commit observation through the product CLI." - observed = add_todo(enabled, text) - committed_observation(observed, label="enabled todo add") - default_off = add_todo(baseline, text) - expect("authority_shadow" not in default_off, "default-off write must not carry observation evidence") - differing = [field for field in DEFAULT_OFF_PARITY_FIELDS if observed.get(field) != default_off.get(field)] - expect(not differing, f"default-off response fields must match the observed response: {differing}") - expect(not (baseline.runtime_root / "authority-shadow").exists(), "default-off must not create candidate storage") - return passed(compared_fields=len(DEFAULT_OFF_PARITY_FIELDS)) - - -def row_candidate_failure_preserves_primary(context: RowContext) -> RowOutcome: - workspace = shadow_workspace(context, "ladder-failure", shadow_enabled=False) - configure_shadow(workspace, "--local-authority-shadow-file", "--execute") - workspace.runtime_root.mkdir(parents=True, exist_ok=True) - (workspace.runtime_root / "authority-shadow").write_text("block candidate directory", encoding="utf-8") - result = add_todo(workspace, "The primary write survives a candidate construction failure.") - expect(result.get("ok") is True and result.get("added") is True, "primary write must commit") - evidence = shadow_evidence(result, label="todo add") - expect(evidence.get("outcome") == "failed", "candidate failure must be reported as failed") - expect(evidence.get("reason_code") == "shadow_observation_failed", "candidate failure must carry its typed reason") - expect(evidence.get("primary_writeback_preserved") is True, "candidate failure must preserve the primary writeback") - expect( - str(result["todo_id"]) in workspace.state_path.read_text(encoding="utf-8"), - "the committed todo must be present in the primary state", - ) - return passed(outcome="failed", reason_code="shadow_observation_failed") - - -def row_crash_gap_loses_observation(context: RowContext) -> RowOutcome: - workspace = shadow_workspace(context, "ladder-crash-gap", shadow_enabled=False) - configure_shadow(workspace, "--local-authority-shadow-file", "--execute") - first_text = "Primary commit that loses its post-commit observation." - with hold_observation_lock(workspace): - process = spawn_cli( - workspace, - "todo", - "add", - "--goal-id", - workspace.goal_id, - "--role", - "agent", - "--text", - first_text, - "--task-class", - "advancement_task", - ) - try: - visible = wait_until( - lambda: first_text in workspace.state_path.read_text(encoding="utf-8"), - PRIMARY_VISIBILITY_TIMEOUT_SECONDS, - ) - expect(visible, "primary Todo commit did not become visible") - expect(process.poll() is None, "writer must still be blocked on the observation lock") - finally: - kill_now(process) - expect(not candidate_store_paths(workspace), "a killed writer must leave no candidate document") - recovered = add_todo(workspace, "A later primary commit refreshes the current full snapshot.") - evidence = committed_observation(recovered, label="recovering todo add") - expect(evidence["outcome"] == "captured", "recovery observation must be captured") - expect(evidence["durable_source_outbox"] is False, "no durable outbox may be claimed") - expect(evidence["source_transaction_correlated"] is False, "no transaction correlation may be claimed") - expect(evidence["parity_verdict"] == "not_evaluated", "recovery must not claim parity") - document = candidate_document(workspace) - expect(len(document.todo_ids) == 2, "the refreshed snapshot must include both primary commits") - return passed(lost_observations=1, refreshed_head_todos=2, candidate_cursor=document.cursor) - - -@dataclass -class _WriterSequence: - workspace: GoalWorkspace - committed: list[tuple[str, JsonObject]] = field(default_factory=list) - - def commit(self, label: str, payload: JsonObject, *, flag: str) -> JsonObject: - expect(payload.get(flag) is True, f"{label} must report {flag}=true") - self.committed.append((label, payload)) - return payload - - def cli(self, *args: str) -> JsonObject: - return run_cli(self.workspace, *args, "--goal-id", self.workspace.goal_id) - - -def _writer_sequence_lease_lifecycle(sequence: _WriterSequence) -> str: - """Add a todo, then acquire, update, renew, transfer, and complete it.""" - - first = sequence.commit( - "todo add", - add_todo(sequence.workspace, "Deliver one bounded control-plane change."), - flag="added", - ) - todo_id = str(first["todo_id"]) - acquired = sequence.commit( - "task-lease acquire", - acquire_lease(sequence.workspace, todo_id=todo_id, owner=AGENT_A, idempotency_key="ladder-lease-a"), - flag="acquired", - ) - sequence.commit( - "todo update", - sequence.cli("todo", "update", "--todo-id", todo_id, "--note", "A public-safe update.", "--agent-id", AGENT_A), - flag="changed", - ) - renewed = sequence.commit( - "task-lease renew", - sequence.cli( - "task-lease", "renew", "--todo-id", todo_id, "--owner", AGENT_A, - "--idempotency-key", "ladder-lease-a", - "--expected-version", lease_version(acquired, label="acquire"), - "--ttl-seconds", "120", - ), - flag="renewed", - ) - transferred = sequence.commit( - "task-lease transfer", - sequence.cli( - "task-lease", "transfer", "--todo-id", todo_id, "--owner", AGENT_A, - "--idempotency-key", "ladder-lease-a", "--new-owner", AGENT_B, - "--new-idempotency-key", "ladder-lease-b", - "--expected-version", lease_version(renewed, label="renew"), - "--ttl-seconds", "120", - ), - flag="transferred", - ) - sequence.commit( - "todo complete", - sequence.cli( - "todo", "complete", "--todo-id", todo_id, "--agent-id", AGENT_B, - "--task-lease-idempotency-key", "ladder-lease-b", - "--task-lease-expected-version", lease_version(transferred, label="transfer"), - "--evidence", "validation://ladder-complete", "--no-follow-up", - ), - flag="completed", - ) - return todo_id - - -def _writer_sequence_supersede_and_hygiene(sequence: _WriterSequence) -> str: - """Add a second todo, replay its acquire, supersede it, then run hygiene writers.""" - - second = sequence.commit( - "todo add (second)", - add_todo(sequence.workspace, "Replace this bounded work with a successor."), - flag="added", - ) - todo_id = str(second["todo_id"]) - acquired = sequence.commit( - "task-lease acquire (second)", - acquire_lease(sequence.workspace, todo_id=todo_id, owner=AGENT_A, idempotency_key="ladder-lease-c"), - flag="acquired", - ) - replayed = acquire_lease(sequence.workspace, todo_id=todo_id, owner=AGENT_A, idempotency_key="ladder-lease-c") - expect(replayed.get("idempotent") is True, "re-acquire with the same key must be idempotent") - expect("authority_shadow" not in replayed, "an idempotent re-acquire must not observe") - sequence.commit( - "todo supersede", - sequence.cli( - "todo", "supersede", "--todo-id", todo_id, "--agent-id", AGENT_A, - "--reason", "Replace obsolete work.", - "--next-agent-todo", "Carry the bounded work forward.", - "--task-lease-idempotency-key", "ladder-lease-c", - "--task-lease-expected-version", lease_version(acquired, label="acquire (second)"), - ), - flag="superseded", - ) - sequence.commit( - "todo add (verification)", - add_todo(sequence.workspace, "Verify the migrated authority projection."), - flag="added", - ) - sequence.commit( - "todo archive-completed", - sequence.cli("todo", "archive-completed", "--max-active-done", "0", "--execute"), - flag="changed", - ) - return todo_id - - -def row_every_writer_family_captures(context: RowContext) -> RowOutcome: - workspace = build_goal_workspace( - context.root, - goal_id=unique_goal_id("ladder-writers"), - handoff_mode="legacy", - shadow_enabled=True, - runtime_root_binding="cli_override", - ) - sequence = _WriterSequence(workspace) - sequence.commit( - "handoff-mode set", - sequence.cli("handoff-mode", "set", "--mode", "hard_lease"), - flag="changed", - ) - first_todo = _writer_sequence_lease_lifecycle(sequence) - second_todo = _writer_sequence_supersede_and_hygiene(sequence) - - observations = [committed_observation(payload, label=label) for label, payload in sequence.committed] - captured = [evidence for evidence in observations if evidence["outcome"] == "captured"] - document = candidate_document(workspace) - expect(document.cursor == str(len(captured)), "candidate cursor must equal the number of captured observations") - expect( - set(document.operation_ids) == {str(evidence["observation_id"]) for evidence in observations}, - "candidate operation ids must be exactly the observation ids", - ) - expect( - {str(lease.get("todo_id")) for lease in document.leases} == {first_todo, second_todo}, - "candidate head must retain the lease records of both leased todos", - ) - expect( - all(lease.get("status") == "released" for lease in document.leases), - "no time-active lease may remain in the candidate head", - ) - listed = sequence.cli("todo", "list") - listed_ids = sorted(str(todo.get("todo_id")) for todo in listed.get("todos") or [] if isinstance(todo, dict)) - expect(sorted(document.todo_ids) == listed_ids, "candidate head todos must equal the projected todo list") - return passed( - writer_families=len(sequence.committed), - captured=len(captured), - outcomes=sorted({str(evidence["outcome"]) for evidence in observations}), - candidate_cursor=document.cursor, - head_todos=len(document.todo_ids), - head_leases=len(document.leases), - ) - - -def _migration_arguments(source: LegacyMigrationSource) -> list[str]: - return [ - "migrate-state", - "--legacy-registry", str(source.legacy_registry), - "--legacy-runtime-root", str(source.legacy_runtime), - "--target-runtime-root", str(source.target_runtime), - "--goal-id", source.old_goal_id, - "--goal-id-map", f"{source.old_goal_id}={source.new_goal_id}", - "--path-map", f"{source.source_repo}={source.target_repo}", - "--copy-active-state", - "--copy-runtime", - "--no-global-sync", - ] - - -def _first_entry(payload: Mapping[str, object], key: str, *, label: str) -> JsonObject: - entries = payload.get(key) - expect(isinstance(entries, list) and len(entries) == 1, f"{label} must report exactly one {key} entry") - assert isinstance(entries, list) - entry = entries[0] - expect(isinstance(entry, dict), f"{label} {key} entry must be an object") - assert isinstance(entry, dict) - return {str(field): value for field, value in entry.items()} - - -def _assert_migration_preview(source: LegacyMigrationSource, preview: JsonObject, sentinel: bytes) -> None: - expect(preview.get("ok") is True and preview.get("dry_run") is True, "migration preview must be a dry run") - expect(source.target_registry.read_bytes() == sentinel, "dry run must not write the target registry") - expect(not source.target_runtime.exists(), "dry run must not create the target runtime") - runtime_result = _first_entry(preview, "runtime_goals", label="migration preview") - expect(runtime_result.get("copied_file_count") == 0, "dry run must copy no runtime files") - seed = _first_entry(preview, "authority_shadow_seeds", label="migration preview") - expect( - seed - == { - "schema_version": MIGRATION_SEED_SCHEMA, - "goal_id": source.new_goal_id, - "attempted": False, - "outcome": "planned", - "reason_code": None, - }, - "dry run must plan, not attempt, the shadow seed", - ) - expect(source.private_marker not in json.dumps(preview, sort_keys=True), "preview must not leak private provider bytes") - - -def _assert_migration_executed(source: LegacyMigrationSource, executed: JsonObject) -> JsonObject: - expect(executed.get("ok") is True and executed.get("wrote_project_registry") is True, "execute must write the registry") - runtime_result = _first_entry(executed, "runtime_goals", label="migration execute") - expect(runtime_result.get("copied") is True, "execute must copy the runtime goal directory") - lease_path = source.target_runtime / "goals" / source.new_goal_id / "task-leases" / "safe-local.json" - copied_lease = parse_json_object(lease_path.read_text(encoding="utf-8")) - expect(copied_lease.get("goal_id") == source.new_goal_id, "copied lease must carry the migrated goal id") - identity = (source.target_shadow_directory / "store-identity").read_text(encoding="utf-8") - expect(identity.startswith("file:") and identity != source.old_store_identity, "target lineage must be fresh") - store_paths = sorted(source.target_shadow_directory.glob("authority-store-*.json")) - expect(len(store_paths) == 1, "execute must seed exactly one candidate document") - store = parse_json_object(store_paths[0].read_text(encoding="utf-8")) - expect(store.get("goal_id") == source.new_goal_id and store.get("store_identity") == identity, "seeded store must bind the new lineage") - committed = store.get("committed") - expect(store.get("cursor") == "1" and isinstance(committed, list) and len(committed) == 1, "seed must be the first and only commit") - serialized = json.dumps(store, sort_keys=True) - for forbidden in (source.old_store_identity, source.legacy_revision, str(source.source_repo), source.private_marker): - expect(forbidden not in serialized, "seeded store must not carry any legacy lineage or private byte") - expect(not (source.target_shadow_directory / "authority-store-legacy.json").exists(), "legacy document must not migrate") - seed = _first_entry(executed, "authority_shadow_seeds", label="migration execute") - expect(seed.get("goal_id") == source.new_goal_id and seed.get("attempted") is True, "seed must target the migrated goal") - expect(seed.get("outcome") == "captured", "seed must be captured") - return store - - -def row_migration_seeds_new_lineage(context: RowContext) -> RowOutcome: - source = build_legacy_migration_source( - context.root, - old_goal_id=unique_goal_id("legacy"), - new_goal_id=unique_goal_id("migrated"), - ) - sentinel = b'{"schema_version":"existing","goals":[]}\n' - source.target_registry.write_bytes(sentinel) - arguments = _migration_arguments(source) - _assert_migration_preview(source, run_cli(source, *arguments), sentinel) - store = _assert_migration_executed(source, run_cli(source, *arguments, "--execute")) - return passed(seed_outcome="captured", seeded_cursor=str(store.get("cursor")), legacy_lineage_excluded=True) - - -def row_dual_runtime_root_consistency(context: RowContext) -> RowOutcome: - """``--runtime-root`` differs from ``common_runtime_root``: one lineage per goal.""" - - workspace = build_goal_workspace( - context.root, - goal_id=unique_goal_id("ladder-one-root"), - handoff_mode="hard_lease", - shadow_enabled=True, - runtime_root_binding="cli_override_divergent", - ) - expect( - workspace.registry_runtime_root != workspace.runtime_root, - "fixture must register a different common_runtime_root than the override", - ) - added = add_todo(workspace, "Every hook of one CLI call shares one runtime root.") - todo_id = str(added["todo_id"]) - acquired = acquire_lease( - workspace, - todo_id=todo_id, - owner=AGENT_A, - idempotency_key="ladder-one-root-lease", - ) - updated = run_cli( - workspace, "todo", "update", "--goal-id", workspace.goal_id, "--todo-id", todo_id, - "--note", "Observed under the override root.", "--agent-id", AGENT_A, - ) - second_add = add_todo(workspace, "Keep one candidate lineage per goal.") - completed = run_cli( - workspace, "todo", "complete", "--goal-id", workspace.goal_id, "--todo-id", todo_id, - "--agent-id", AGENT_A, "--task-lease-idempotency-key", "ladder-one-root-lease", - "--task-lease-expected-version", lease_version(acquired, label="acquire"), - "--evidence", "validation://ladder-one-root-complete", "--no-follow-up", - ) - observations = [ - committed_observation(payload, label=label) - for label, payload in ( - ("todo add", added), - ("task-lease acquire", acquired), - ("todo update", updated), - ("todo add (second)", second_add), - ("todo complete", completed), - ) - ] - identities = {str(evidence.get("store_identity")) for evidence in observations} - expect(len(identities) == 1, "every writer family must observe into one store identity") - document = candidate_document(workspace) - expect(document.store_identity in identities, "candidate bytes must carry the observed identity") - expect(document.cursor == str(len(observations)), "candidate cursor must equal the observation count") - expect( - todo_id in document.todo_ids and len(document.todo_ids) == 2, - "head must hold the completed todo and the second added todo", - ) - expect( - [lease.get("todo_id") for lease in document.leases] == [todo_id] - and document.leases[0].get("status") == "released", - "head must hold exactly the released lease of the completed todo", - ) - lease_path = workspace.runtime_root / "goals" / workspace.goal_id / "task-leases" / f"{todo_id}.json" - expect(lease_path.exists(), "lease state must live under the override root") - expect( - not (workspace.registry_runtime_root / "authority-shadow").exists(), - "the registry root must not gain a candidate lineage", - ) - expect( - not (workspace.registry_runtime_root / "goals").exists(), - "the registry root must not gain lease state", - ) - return passed( - observations=len(observations), - store_identities=len(identities), - candidate_cursor=document.cursor, - head_todos=len(document.todo_ids), - head_leases=len(document.leases), - ) - - -__all__ = [ - "row_candidate_failure_preserves_primary", - "row_configure_enable_disable_roundtrip", - "row_crash_gap_loses_observation", - "row_default_off_isolation", - "row_dual_runtime_root_consistency", - "row_every_writer_family_captures", - "row_migration_seeds_new_lineage", - "shadow_workspace", -] +from .authority_e2e_fixtures import build_goal_workspace, run_cli, unique_goal_id +from .authority_e2e_row_support import RowContext, RowOutcome, add_todo, expect, passed +from .authority_e2e_rows_stage2c2 import bootstrap_capture, delivered, goal_cli + + +def row_retired_observation_upgrade(context: RowContext) -> RowOutcome: + workspace = build_goal_workspace(context.root, goal_id=unique_goal_id("retired-observer"), + handoff_mode="hard_lease", shadow_enabled=True, + runtime_root_binding="cli_override") + original = workspace.registry_path.read_bytes() + rejected = goal_cli(workspace, "configure-goal", "--local-authority-shadow-file", "--execute", check=False) + expect(rejected.get("ok") is False and "local_authority_shadow_retired" in str(rejected.get("error")), + "retired enable must fail with actionable context") + expect(workspace.registry_path.read_bytes() == original, "rejection must not change configuration") + added = add_todo(workspace, "Continue work before explicit capture upgrade.") + expect("authority_shadow" not in added, "a retained setting cannot restore observation writes") + status = goal_cli(workspace, "authority-shadow", "status") + expect(status["config"]["status"] == "retired" and status["config"]["enabled"] is False, + "status must disclose the inactive historical configuration") + expect(not (workspace.runtime_root / "authority-shadow").exists(), "primary work must create no shadow lineage") + run_cli(workspace, "configure-goal", "--goal-id", workspace.goal_id, + "--clear-local-authority-shadow", "--coordination-runtime-shadow-file", "--execute") + expect(not (workspace.runtime_root / "authority-shadow" / "file-v0").exists(), + "configuration must not implicitly bootstrap a source lineage") + bootstrap_capture(workspace) + delivered(add_todo(workspace, "Capture a transaction after explicit bootstrap."), label="upgraded write") + expect(not workspace.shadow_directory.exists(), "replacement capture must not recreate historical observation storage") + return passed(retired_enable_rejected=True, implicit_capture=False, explicit_bootstrap=True) diff --git a/loopx/control_plane/todos/handoff_mode.py b/loopx/control_plane/todos/handoff_mode.py index 367c8b2bb5..873e94f064 100644 --- a/loopx/control_plane/todos/handoff_mode.py +++ b/loopx/control_plane/todos/handoff_mode.py @@ -410,20 +410,9 @@ def set_goal_handoff_mode( state_path=resolved_state_file, write_class="handoff_mode_set", original_text=original) write_captured_todo_state(capture, runtime_root=runtime_root, goal_id=goal_id, state_path=resolved_state_file, text=plan["next_frontmatter_text"] + body) - previous = str(plan["previous_mode"]) payload["changed"] = True - from ..coordination.local_authority_shadow_observation import observe_local_authority_commit - - evidence = observe_local_authority_commit( - registry_path=registry_path, - runtime_root=runtime_root, - goal_id=goal_id, - observation_trigger=f"handoff_mode_set:{previous}:{requested}", - ) - if evidence is not None: - payload["authority_shadow"] = evidence return settle_todo_runtime_shadow_capture( payload, registry_path=registry_path, runtime_root=runtime_root, - goal_id=goal_id, write_class="handoff_mode_set", capture=capture, - observe_legacy=False, emit_disabled=False, + goal_id=goal_id, capture=capture, + emit_disabled=False, ) diff --git a/loopx/control_plane/work_items/task_lease_acquire_adapter.py b/loopx/control_plane/work_items/task_lease_acquire_adapter.py index cd5a87d5ba..162fd1e0ff 100644 --- a/loopx/control_plane/work_items/task_lease_acquire_adapter.py +++ b/loopx/control_plane/work_items/task_lease_acquire_adapter.py @@ -33,44 +33,6 @@ TASK_LEASE_AUTHORITY_SNAPSHOT_ATTEMPTS = 3 -def _attach_local_authority_shadow( - result: dict[str, Any], - *, - registry_path: Path | None, - runtime_root: Path, - goal_id: str, - todo_id: str, - operation: str, -) -> dict[str, Any]: - """Observe a committed public lease mutation without changing its verdict.""" - - if registry_path is None: - return result - lease = result.get("lease") if isinstance(result.get("lease"), dict) else {} - observation_trigger = ":".join( - ( - f"task_lease_{operation}", - str(todo_id), - str(lease.get("version") or "none"), - str(lease.get("lease_epoch") or "none"), - str(lease.get("updated_at") or lease.get("released_at") or "unknown"), - ) - ) - from ..coordination.local_authority_shadow_observation import ( - observe_local_authority_commit, - ) - - evidence = observe_local_authority_commit( - registry_path=registry_path, - runtime_root=runtime_root, - goal_id=str(goal_id), - observation_trigger=observation_trigger, - ) - if evidence is not None: - result["authority_shadow"] = evidence - return result - - def _authority_source_receipt(source_id: str, path: Path) -> dict[str, Any]: resolved = path.expanduser().resolve(strict=False) try: @@ -391,14 +353,6 @@ def _finalize_native_acquire_result( runtime_root=runtime_root, goal_id=goal_id, ) - result = _attach_local_authority_shadow( - result, - registry_path=registry_path, - runtime_root=runtime_root, - goal_id=goal_id, - todo_id=todo_id, - operation="acquire", - ) return result @@ -812,24 +766,6 @@ def execute_native_task_lease_lifecycle( runtime_root=runtime_root, goal_id=str(goal_id), ) - committed_mutation = ( - normalized_operation == "renew" and result.get("renewed") is True - ) or ( - normalized_operation == "transfer" - and result.get("transferred") is True - ) or ( - normalized_operation == "release" - and result.get("released") is True - ) - if committed_mutation and result.get("idempotent") is not True: - result = _attach_local_authority_shadow( - result, - registry_path=registry_path, - runtime_root=runtime_root, - goal_id=str(goal_id), - todo_id=str(todo_id), - operation=normalized_operation, - ) # lock_token is an internal bridge value. Callers that need a held # fence read it from the nested native payload before redacting it. return result diff --git a/loopx/control_plane/work_items/team_plan_adapter.py b/loopx/control_plane/work_items/team_plan_adapter.py index 0f88f097b3..2320b887ac 100644 --- a/loopx/control_plane/work_items/team_plan_adapter.py +++ b/loopx/control_plane/work_items/team_plan_adapter.py @@ -150,4 +150,4 @@ def previous_receipt(original: str) -> dict[str, Any] | None: state_path=state, text=text) verify_state_text_durable(state, text) return settle_todo_runtime_shadow_capture(result, registry_path=registry_path, - runtime_root=runtime, goal_id=goal_id, write_class="todo_add", capture=capture) + runtime_root=runtime, goal_id=goal_id, capture=capture) diff --git a/loopx/semantics/project_registry_io_manifest_v1.json b/loopx/semantics/project_registry_io_manifest_v1.json index 4a423e20f2..d536e03aa3 100644 --- a/loopx/semantics/project_registry_io_manifest_v1.json +++ b/loopx/semantics/project_registry_io_manifest_v1.json @@ -871,23 +871,15 @@ }, { "site": "loopx/control_plane/coordination/local_authority_shadow_adapter.py::.local_authority_shadow_status::codec_read:load_registry#1", - "line": 776, + "line": 775, "column": 16, "kind": "codec_read", "api": "load_registry", "classification": "codec_api" }, - { - "site": "loopx/control_plane/coordination/local_authority_shadow_observation.py::.observe_local_authority_commit::codec_read:load_registry#1", - "line": 340, - "column": 20, - "kind": "codec_read", - "api": "load_registry", - "classification": "codec_api" - }, { "site": "loopx/control_plane/coordination/runtime_shadow.py::.build_runtime_shadow_source_snapshot::codec_read:load_registry#1", - "line": 325, + "line": 362, "column": 16, "kind": "codec_read", "api": "load_registry", @@ -1399,7 +1391,7 @@ }, { "site": "loopx/control_plane/work_items/task_lease_acquire_adapter.py::._canonical_lease_authority_facts::codec_read:load_registry#1", - "line": 352, + "line": 314, "column": 31, "kind": "codec_read", "api": "load_registry", @@ -1407,7 +1399,7 @@ }, { "site": "loopx/control_plane/work_items/task_lease_acquire_adapter.py::._task_lease_authority_snapshot_attempt::codec_read:load_registry#1", - "line": 267, + "line": 229, "column": 16, "kind": "codec_read", "api": "load_registry", @@ -1415,7 +1407,7 @@ }, { "site": "loopx/control_plane/work_items/task_lease_acquire_adapter.py::._task_lease_authority_snapshot_attempt::codec_read:load_registry#2", - "line": 290, + "line": 252, "column": 22, "kind": "codec_read", "api": "load_registry", @@ -1423,7 +1415,7 @@ }, { "site": "loopx/control_plane/work_items/task_lease_acquire_adapter.py::.execute_native_task_lease_acquire::codec_read:load_registry#1", - "line": 443, + "line": 397, "column": 20, "kind": "codec_read", "api": "load_registry", @@ -1431,7 +1423,7 @@ }, { "site": "loopx/control_plane/work_items/task_lease_acquire_adapter.py::.execute_native_task_lease_lifecycle::codec_read:load_registry#1", - "line": 755, + "line": 709, "column": 24, "kind": "codec_read", "api": "load_registry", @@ -1735,7 +1727,7 @@ }, { "site": "loopx/state_migration.py::.legacy_registry_goal_ids::direct_json_read:read_json_object#1", - "line": 72, + "line": 62, "column": 16, "kind": "direct_json_read", "api": "read_json_object", @@ -1743,7 +1735,7 @@ }, { "site": "loopx/state_migration.py::.migrate_legacy_state::codec_transaction:project_registry_transaction#1", - "line": 359, + "line": 273, "column": 25, "kind": "codec_transaction", "api": "project_registry_transaction", @@ -1751,7 +1743,7 @@ }, { "site": "loopx/state_migration.py::.migrate_legacy_state::direct_json_read:read_json_object#1", - "line": 376, + "line": 290, "column": 18, "kind": "direct_json_read", "api": "read_json_object", @@ -1759,7 +1751,7 @@ }, { "site": "loopx/state_migration.py::.migrate_legacy_state::codec_read:load_project_registry#1", - "line": 407, + "line": 321, "column": 18, "kind": "codec_read", "api": "load_project_registry", diff --git a/loopx/state_migration.py b/loopx/state_migration.py index 774e6f8a82..4a35203a86 100644 --- a/loopx/state_migration.py +++ b/loopx/state_migration.py @@ -30,16 +30,6 @@ LEGACY_RUNTIME_ROOT = Path.home() / ".codex" / "goal-harness" LEGACY_GLOBAL_REGISTRY = LEGACY_RUNTIME_ROOT / "registry.global.json" MIGRATION_SHADOW_SEED_EVIDENCE_SCHEMA = "loopx_state_migration_shadow_seed_evidence_v0" -_SHADOW_EVIDENCE_OUTCOMES = { - "captured", - "replayed", - "ambiguous_reconciled", - "ambiguous_unproved", - "unavailable", - "failed", - "protocol_mismatch", - "conflict_retry_required", -} def now_local() -> str: @@ -243,88 +233,12 @@ def _uses_file_authority_shadow(goal: dict[str, Any]) -> bool: ) -def _shadow_seed_evidence( - *, - goal_id: str, - attempted: bool, - outcome: str, - reason_code: str | None = None, -) -> dict[str, Any]: - return { - "schema_version": MIGRATION_SHADOW_SEED_EVIDENCE_SCHEMA, - "goal_id": goal_id, - "attempted": attempted, - "outcome": outcome, - "reason_code": reason_code, - } - - -def _shadow_seed_result(*, goal_id: str, result: object) -> dict[str, Any]: - outcome = result.get("outcome") if isinstance(result, dict) else None - if outcome not in _SHADOW_EVIDENCE_OUTCOMES: - return _shadow_seed_evidence( - goal_id=goal_id, - attempted=True, - outcome="failed", - reason_code="post_migration_shadow_seed_invalid_evidence", - ) - - reason_code = ( - None - if outcome in {"captured", "replayed", "ambiguous_reconciled"} - else f"post_migration_shadow_seed_{outcome}" - ) - return _shadow_seed_evidence( - goal_id=goal_id, - attempted=True, - outcome=str(outcome), - reason_code=reason_code, - ) - - -def seed_migrated_authority_shadows( - *, - goals: list[dict[str, Any]], - target_registry_path: Path, - target_runtime_root: Path, - execute: bool, -) -> list[dict[str, Any]]: - """Plan or seed fresh candidate lineage from migrated local authority.""" - - results: list[dict[str, Any]] = [] - for goal in goals: - if not _uses_file_authority_shadow(goal): - continue - goal_id = str(goal.get("id") or "") - if not execute: - results.append( - _shadow_seed_evidence( - goal_id=goal_id, - attempted=False, - outcome="planned", - ) - ) - continue - try: - from .control_plane.coordination.local_authority_shadow_observation import observe_local_authority_commit - - result = observe_local_authority_commit( - registry_path=target_registry_path, - runtime_root=target_runtime_root, - goal_id=goal_id, - observation_trigger="state_migration_seed", - ) - results.append(_shadow_seed_result(goal_id=goal_id, result=result)) - except Exception: - results.append( - _shadow_seed_evidence( - goal_id=goal_id, - attempted=True, - outcome="failed", - reason_code="post_migration_shadow_seed_failed", - ) - ) - return results +def retired_authority_shadow_notices(goals: list[dict[str, Any]]) -> list[dict[str, Any]]: + """Keep the migration response field, but never seed the retired observer.""" + return [{"schema_version": MIGRATION_SHADOW_SEED_EVIDENCE_SCHEMA, + "goal_id": str(goal.get("id") or ""), "attempted": False, + "outcome": "retired", "reason_code": "local_authority_shadow_retired"} + for goal in goals if _uses_file_authority_shadow(goal)] def migrate_legacy_state( @@ -478,12 +392,7 @@ def migrate_legacy_state( ) target_transaction.commit(target_payload) - authority_shadow_seeds = seed_migrated_authority_shadows( - goals=incoming_goals, - target_registry_path=target_registry_path, - target_runtime_root=target_runtime_root, - execute=execute, - ) + authority_shadow_seeds = retired_authority_shadow_notices(incoming_goals) return { "ok": True, diff --git a/loopx/todos.py b/loopx/todos.py index fbb5bc438f..005964eab4 100644 --- a/loopx/todos.py +++ b/loopx/todos.py @@ -1027,7 +1027,7 @@ def add_goal_todo( ) return settle_todo_runtime_shadow_capture( payload, registry_path=registry_path, runtime_root=shadow_runtime_root, - goal_id=goal_id, write_class="todo_add", capture=shadow_capture, + goal_id=goal_id, capture=shadow_capture, ) @@ -1541,7 +1541,7 @@ def update_goal_todo( ) return settle_todo_runtime_shadow_capture( payload, registry_path=registry_path, runtime_root=shadow_runtime_root, - goal_id=goal_id, write_class=write_class, capture=shadow_capture, + goal_id=goal_id, capture=shadow_capture, ) @@ -1798,7 +1798,7 @@ def complete_goal_todo( return settle_todo_runtime_shadow_capture( event_result, registry_path=registry_path, runtime_root=shadow_runtime_root, goal_id=goal_id, - write_class="todo_complete_event_projection", capture=shadow_capture, + capture=shadow_capture, ) if not isinstance(completion_state, dict): raise RuntimeError( @@ -1931,7 +1931,7 @@ def complete_goal_todo( result["self_merged"] = effective_self_merged return settle_todo_runtime_shadow_capture( result, registry_path=registry_path, runtime_root=shadow_runtime_root, - goal_id=goal_id, write_class="todo_complete", capture=shadow_capture, + goal_id=goal_id, capture=shadow_capture, ) @provider_first_terminal_lifecycle("supersede") @@ -2091,7 +2091,7 @@ def supersede_goal_todo( } return settle_todo_runtime_shadow_capture( result, registry_path=registry_path, runtime_root=shadow_runtime_root, - goal_id=goal_id, write_class="todo_supersede", capture=shadow_capture, + goal_id=goal_id, capture=shadow_capture, ) @@ -2157,5 +2157,5 @@ def archive_completed_todos( } return settle_todo_runtime_shadow_capture( result, registry_path=registry_path, runtime_root=shadow_runtime_root, - goal_id=goal_id, write_class="todo_archive_completed", capture=shadow_capture, + goal_id=goal_id, capture=shadow_capture, ) diff --git a/tests/control_plane/test_coordination_state_contract.py b/tests/control_plane/test_coordination_state_contract.py index f19fef5031..0eae9336e0 100644 --- a/tests/control_plane/test_coordination_state_contract.py +++ b/tests/control_plane/test_coordination_state_contract.py @@ -33,7 +33,7 @@ LEGACY_COORDINATION_WRITE_CHECK_REQUEST_SCHEMA, ) from loopx.control_plane.turn_driver import delivery_continuity -from loopx.control_plane.coordination.local_authority_shadow_observation import LOCAL_AUTHORITY_SHADOW_EVIDENCE_SCHEMA as BRIDGE_SHADOW_EVIDENCE_SCHEMA, LOCAL_AUTHORITY_SHADOW_REQUEST_SCHEMA as BRIDGE_SHADOW_REQUEST_SCHEMA +from loopx.control_plane.coordination.coordination_state_contract_generated import LOCAL_AUTHORITY_SHADOW_EVIDENCE_SCHEMA as BRIDGE_SHADOW_EVIDENCE_SCHEMA, LOCAL_AUTHORITY_SHADOW_REQUEST_SCHEMA as BRIDGE_SHADOW_REQUEST_SCHEMA from loopx.control_plane.coordination.local_authority_shadow_outbox import ( OUTBOX_ENTRY_SCHEMA, ) diff --git a/tests/control_plane/test_local_authority_shadow_cli_e2e.py b/tests/control_plane/test_local_authority_shadow_cli_e2e.py index 8773e2425f..4e05b66688 100644 --- a/tests/control_plane/test_local_authority_shadow_cli_e2e.py +++ b/tests/control_plane/test_local_authority_shadow_cli_e2e.py @@ -1,542 +1,64 @@ +"""Real CLI upgrade from a retired setting to an explicit capture lineage.""" from __future__ import annotations import json -import os from pathlib import Path -import subprocess -import sys -import time - -import pytest - -from loopx.file_lock import exclusive_file_lock - - -REPO_ROOT = Path(__file__).resolve().parents[2] -# Keep the crash-gap observation window aligned with the CLI subprocess timeout; -# importing the Python CLI can exceed a short local polling budget on CI. -CLI_TIMEOUT_SECONDS = 30 - - -def _workspace(tmp_path: Path, *, goal_id: str) -> tuple[Path, Path, Path]: - repo = tmp_path / goal_id - repo.mkdir() - state = repo / "ACTIVE_GOAL_STATE.md" - state.write_text( - "---\n" - f"goal_id: {goal_id}\n" - "handoff_mode: hard_lease\n" - "updated_at: 2026-09-02T00:00:00+00:00\n" - "---\n\n" - "## Agent Todo\n\n", - encoding="utf-8", - ) - runtime_root = tmp_path / f"{goal_id}-runtime" - registry = tmp_path / f"{goal_id}-registry.json" - registry.write_text( - json.dumps( - { - "common_runtime_root": str(runtime_root), - "goals": [ - { - "id": goal_id, - "status": "active", - "repo": str(repo), - "state_file": state.name, - "coordination": { - "agent_model": "peer_v1", - "registered_agents": ["agent-a", "agent-b"], - }, - } - ], - } - ), - encoding="utf-8", - ) - return registry, state, runtime_root - - -def _command(registry: Path, runtime_root: Path, *args: str) -> list[str]: - return [ - sys.executable, - "-m", - "loopx.cli", - "--registry", - str(registry), - "--runtime-root", - str(runtime_root), - "--format", - "json", - *args, - ] - - -def _env() -> dict[str, str]: - env = os.environ.copy() - env["PYTHONPATH"] = str(REPO_ROOT) - return env - - -def _cli(registry: Path, runtime_root: Path, *args: str) -> dict[str, object]: - completed = subprocess.run( - _command(registry, runtime_root, *args), - cwd=REPO_ROOT, - env=_env(), - check=True, - capture_output=True, - text=True, - timeout=CLI_TIMEOUT_SECONDS, - ) - return json.loads(completed.stdout) - - -def _store_document(runtime_root: Path, goal_id: str) -> tuple[Path, dict[str, object]]: - paths = list( - (runtime_root / "authority-shadow" / "file" / goal_id).glob( - "authority-store-*.json" - ) - ) - assert len(paths) == 1 - return paths[0], json.loads(paths[0].read_text(encoding="utf-8")) - - -def _add_todo( - registry: Path, - runtime_root: Path, - *, - goal_id: str, - text: str, -) -> dict[str, object]: - return _cli( - registry, - runtime_root, - "todo", - "add", - "--goal-id", - goal_id, - "--role", - "agent", - "--text", - text, - "--task-class", - "advancement_task", - ) - - -def test_product_cli_configure_capture_readback_disable_and_default_off_lifecycle_isolation( - tmp_path: Path, -) -> None: - goal_id = "shadow-cli-e2e" - registry, _state, runtime_root = _workspace(tmp_path, goal_id=goal_id) - - preview = _cli( - registry, - runtime_root, - "configure-goal", - "--goal-id", - goal_id, - "--local-authority-shadow-file", - ) - assert preview["dry_run"] is True - assert preview["written"] is False - - enabled = _cli( - registry, - runtime_root, - "configure-goal", - "--goal-id", - goal_id, - "--local-authority-shadow-file", - "--execute", - ) - assert enabled["written"] is True - - text = "Capture one post-commit observation through the product CLI." - observed = _add_todo( - registry, - runtime_root, - goal_id=goal_id, - text=text, - ) - assert observed["authority_shadow"]["outcome"] == "captured" # type: ignore[index] - assert observed["authority_shadow"]["parity_verdict"] == "not_evaluated" # type: ignore[index] - lease = _cli( - registry, - runtime_root, - "task-lease", - "acquire", - "--goal-id", - goal_id, - "--todo-id", - str(observed["todo_id"]), - "--owner", - "agent-a", - "--idempotency-key", - "shadow-cli-lease", - "--ttl-seconds", - "120", - ) - assert lease["acquired"] is True - assert lease["authority_shadow"]["outcome"] == "captured" # type: ignore[index] - store_path, store = _store_document(runtime_root, goal_id) - assert len(store["head"]["todos"]) == 1 # type: ignore[index] - assert len(store["head"]["leases"]) == 1 # type: ignore[index] - - inspected = _cli( - registry, - runtime_root, - "configure-goal", - "--goal-id", - goal_id, - ) - assert inspected["after"]["local_authority_shadow"] == { # type: ignore[index] - "enabled": True, - "mode": "file_one_way", - "status": "enabled", - } - - disabled = _cli( - registry, - runtime_root, - "configure-goal", - "--goal-id", - goal_id, - "--clear-local-authority-shadow", - "--execute", - ) - assert disabled["written"] is True - candidate_before_disabled_write = store_path.read_bytes() - after_disable = _add_todo( - registry, - runtime_root, - goal_id=goal_id, - text="This local lifecycle write must not execute the observer.", - ) - assert after_disable["ok"] is True - assert after_disable["added"] is True - assert "authority_shadow" not in after_disable - assert store_path.read_bytes() == candidate_before_disabled_write - - baseline_registry, _baseline_state, baseline_runtime = _workspace( - tmp_path, goal_id="shadow-cli-baseline" - ) - baseline = _add_todo( - baseline_registry, - baseline_runtime, - goal_id="shadow-cli-baseline", - text=text, - ) - for field in ( - "ok", - "added", - "already_exists", - "metadata_updated", - "status_changed", - "role", - "status", - "task_class", - "action_kind", - "continuation_policy", - ): - assert observed[field] == baseline[field] - assert not (baseline_runtime / "authority-shadow").exists() - - -def test_product_cli_candidate_failure_preserves_the_primary_lifecycle_commit( - tmp_path: Path, -) -> None: - goal_id = "shadow-cli-failure" - registry, state, runtime_root = _workspace(tmp_path, goal_id=goal_id) - _cli( - registry, - runtime_root, - "configure-goal", - "--goal-id", - goal_id, - "--local-authority-shadow-file", - "--execute", - ) - runtime_root.mkdir(parents=True, exist_ok=True) - (runtime_root / "authority-shadow").write_text("block candidate directory", encoding="utf-8") - - result = _add_todo( - registry, - runtime_root, - goal_id=goal_id, - text="The primary write survives a candidate construction failure.", - ) +from tests.control_plane.shadow_e2e_fixture import workspace + + +def test_retired_setting_cannot_enable_capture_or_satisfy_bootstrap(tmp_path: Path) -> None: + ws = workspace(tmp_path, bootstrap=False) + data = json.loads(ws.registry.read_text()) + config = data["goals"][0]["coordination"] + config.pop("runtime_shadow") + config["authority_shadow"] = {"schema_version": "loopx_local_authority_shadow_config_v0", "mode": "file_one_way"} + ws.registry.write_text(json.dumps(data)) + original = ws.registry.read_bytes() + rejected = ws.cli("configure-goal", "--local-authority-shadow-file", "--execute", success=False) + assert rejected["ok"] is False + assert "local_authority_shadow_retired" in rejected["error"] + assert ws.registry.read_bytes() == original + initial = ws.add("Continue work with a retained retired configuration.") + assert "authority_shadow" not in initial + status = ws.cli("authority-shadow", "status") + assert status["config"]["status"] == "retired" + assert status["config"]["enabled"] is False + assert status["candidate"]["status"] == "missing" + assert status["management"]["status"] == "missing" + drain = ws.drain() + assert drain["config_enabled"] is False + assert not (ws.runtime / "authority-shadow").exists() + + # Configuration is not a source transaction or an automatic bootstrap. + ws.cli("configure-goal", "--clear-local-authority-shadow", "--coordination-runtime-shadow-file", "--execute") + assert not (ws.runtime / "authority-shadow" / "file-v0").exists() + boot = ws.cli("coordination-shadow", "bootstrap", "--execute") + assert boot["bootstrap"]["status"] == "applied" + added = ws.add("Capture the next transaction using the existing outbox.") + assert added["coordination_runtime_shadow"]["outcome"] in {"delivered", "replayed"} + assert "authority_shadow" not in added + status = ws.cli("authority-shadow", "status") + assert status["config"]["status"] == "disabled" + assert status["management"]["status"] == "active" + assert status["candidate"]["codec_agreement"] is True + assert not (ws.runtime / "authority-shadow" / "file" / ws.goal).exists() + + +def test_clearing_old_setting_does_not_retire_an_active_capture(tmp_path: Path) -> None: + ws = workspace(tmp_path) + data = json.loads(ws.registry.read_text()) + data["goals"][0]["coordination"]["authority_shadow"] = { + "schema_version": "loopx_local_authority_shadow_config_v0", "mode": "file_one_way"} + ws.registry.write_text(json.dumps(data)) + before = ws.cli("authority-shadow", "status")["management"] + ws.cli("configure-goal", "--clear-local-authority-shadow", "--execute") + after = ws.cli("authority-shadow", "status")["management"] + assert after == before + # Existing process-loss recovery still owns the source transaction. + crashed = ws.crash("before_marker", "todo", "add", "--role", "agent", "--text", "Recover captured source.") + assert crashed is not None + result = ws.drain() assert result["ok"] is True - assert result["added"] is True - assert result["authority_shadow"]["outcome"] == "failed" # type: ignore[index] - assert result["authority_shadow"]["reason_code"] == "shadow_observation_failed" # type: ignore[index] - assert str(result["todo_id"]) in state.read_text(encoding="utf-8") - - -@pytest.mark.skipif(os.name == "nt", reason="requires POSIX cross-process flock and SIGKILL") -def test_product_cli_loses_capture_between_commit_and_observer_then_refreshes_snapshot( - tmp_path: Path, -) -> None: - goal_id = "shadow-cli-crash-gap" - registry, state, runtime_root = _workspace(tmp_path, goal_id=goal_id) - _cli( - registry, - runtime_root, - "configure-goal", - "--goal-id", - goal_id, - "--local-authority-shadow-file", - "--execute", - ) - first_text = "Primary commit that loses its post-commit observation." - observation_lock_target = ( - runtime_root / "authority-shadow" / "file" / goal_id / "observation" - ) - - with exclusive_file_lock(observation_lock_target, operation="e2e_crash_gap"): - process = subprocess.Popen( - _command( - registry, - runtime_root, - "todo", - "add", - "--goal-id", - goal_id, - "--role", - "agent", - "--text", - first_text, - "--task-class", - "advancement_task", - ), - cwd=REPO_ROOT, - env=_env(), - stdout=subprocess.PIPE, - stderr=subprocess.PIPE, - text=True, - ) - deadline = time.monotonic() + CLI_TIMEOUT_SECONDS - while first_text not in state.read_text(encoding="utf-8"): - returncode = process.poll() - if returncode is not None: - stdout, stderr = process.communicate(timeout=5) - raise AssertionError( - "primary Todo CLI exited before its commit became visible " - f"(returncode={returncode}, stdout={stdout!r}, stderr={stderr!r})" - ) - if time.monotonic() >= deadline: - process.kill() - stdout, stderr = process.communicate(timeout=5) - raise AssertionError( - "primary Todo commit did not become visible within " - f"{CLI_TIMEOUT_SECONDS}s (stdout={stdout!r}, stderr={stderr!r})" - ) - time.sleep(0.01) - assert process.poll() is None - process.kill() - process.communicate(timeout=5) - - assert not list( - (runtime_root / "authority-shadow" / "file" / goal_id).glob( - "authority-store-*.json" - ) - ) - - # Allow the OS to release the observation lock fully before the recovery write. - time.sleep(0.1) - - recovered = _add_todo( - registry, - runtime_root, - goal_id=goal_id, - text="A later primary commit refreshes the current full snapshot.", - ) - assert recovered["authority_shadow"]["outcome"] == "captured" # type: ignore[index] - assert recovered["authority_shadow"]["durable_source_outbox"] is False # type: ignore[index] - assert recovered["authority_shadow"]["source_transaction_correlated"] is False # type: ignore[index] - assert recovered["authority_shadow"]["parity_verdict"] == "not_evaluated" # type: ignore[index] - _store_path, store = _store_document(runtime_root, goal_id) - assert len(store["head"]["todos"]) == 2 # type: ignore[index] - - -def test_product_cli_runtime_root_override_keeps_one_candidate_lineage( - tmp_path: Path, -) -> None: - """``--runtime-root`` differs from ``common_runtime_root``: one lineage, one head. - - Every writer family of one CLI invocation must observe into the same - candidate store: Todo add, task-lease acquire, Todo update, follow-up - capture, and a leased completion. The registry root must not gain a - candidate lineage of its own. - """ - - goal_id = "shadow-cli-one-root" - registry, state, registry_runtime = _workspace(tmp_path, goal_id=goal_id) - override_runtime = tmp_path / f"{goal_id}-override-runtime" - assert override_runtime != registry_runtime - _cli( - registry, - override_runtime, - "configure-goal", - "--goal-id", - goal_id, - "--local-authority-shadow-file", - "--execute", - ) - - added = _add_todo( - registry, - override_runtime, - goal_id=goal_id, - text="Every hook of this call shares one runtime root.", - ) - todo_id = str(added["todo_id"]) - lease = _cli( - registry, - override_runtime, - "task-lease", - "acquire", - "--goal-id", - goal_id, - "--todo-id", - todo_id, - "--owner", - "agent-a", - "--idempotency-key", - "one-root-lease", - "--ttl-seconds", - "120", - ) - assert lease["acquired"] is True - updated = _cli( - registry, - override_runtime, - "todo", - "update", - "--goal-id", - goal_id, - "--todo-id", - todo_id, - "--note", - "Observed under the override root.", - "--agent-id", - "agent-a", - ) - assert updated["changed"] is True - second_add = _cli( - registry, - override_runtime, - "todo", - "add", - "--goal-id", - goal_id, - "--role", - "agent", - "--text", - "Verify that one goal keeps one candidate lineage.", - "--evidence", - "validation://one-root-add", - ) - assert second_add["added"] is True - completed = _cli( - registry, - override_runtime, - "todo", - "complete", - "--goal-id", - goal_id, - "--todo-id", - todo_id, - "--agent-id", - "agent-a", - "--task-lease-idempotency-key", - "one-root-lease", - "--task-lease-expected-version", - str(lease["lease"]["version"]), # type: ignore[index] - "--evidence", - "validation://one-root-complete", - "--no-follow-up", - ) - assert completed["completed"] is True - - responses = { - "todo add": added, - "task-lease acquire": lease, - "todo update": updated, - "todo add (second)": second_add, - "todo complete": completed, - } - for label, payload in responses.items(): - evidence = payload["authority_shadow"] - assert evidence["outcome"] == "captured", label # type: ignore[index] - identities = {payload["authority_shadow"]["store_identity"] for payload in responses.values()} # type: ignore[index] - assert len(identities) == 1 - store_path, store = _store_document(override_runtime, goal_id) - assert store["store_identity"] == identities.pop() - assert store["cursor"] == str(len(responses)) - head_todo_ids = {todo["todo_id"] for todo in store["head"]["todos"]} # type: ignore[index] - assert todo_id in head_todo_ids - assert len(head_todo_ids) == 2 - [lease_record] = store["head"]["leases"] # type: ignore[index] - assert lease_record["todo_id"] == todo_id - assert lease_record["status"] == "released" - assert (override_runtime / "goals" / goal_id / "task-leases" / f"{todo_id}.json").exists() - assert not (registry_runtime / "authority-shadow").exists() - assert not (registry_runtime / "goals" / goal_id / "task-leases").exists() - assert store_path.is_relative_to(override_runtime) - assert todo_id in state.read_text(encoding="utf-8") - - -def test_product_cli_authority_shadow_status_and_drain_read_without_creating_lineage( - tmp_path: Path, -) -> None: - goal_id = "shadow-cli-drain" - registry, _state, runtime_root = _workspace(tmp_path, goal_id=goal_id) - - default_off = _cli(registry, runtime_root, "authority-shadow", "status", "--goal-id", goal_id) - assert default_off["ok"] is True - assert default_off["schema_version"] == "loopx_authority_shadow_cli_v0" - assert default_off["config"] == {"enabled": False, "mode": None, "status": "disabled"} # type: ignore[index] - assert default_off["candidate"]["status"] == "missing" # type: ignore[index] - assert default_off["store_bytes"] == 0 - assert not (runtime_root / "authority-shadow").exists() - idle = _cli(registry, runtime_root, "authority-shadow", "drain", "--goal-id", goal_id) - assert idle["ok"] is True - assert idle["outcome"] == "nothing_pending" - assert idle["drained_count"] == 0 - assert idle["config_enabled"] is False - assert not (runtime_root / "authority-shadow").exists() - - _cli( - registry, - runtime_root, - "configure-goal", - "--goal-id", - goal_id, - "--local-authority-shadow-file", - "--execute", - ) - observed = _add_todo(registry, runtime_root, goal_id=goal_id, text="Observed through the v0 path.") - assert observed["authority_shadow"]["outcome"] == "captured" # type: ignore[index] - - status = _cli(registry, runtime_root, "authority-shadow", "status", "--goal-id", goal_id) - assert status["ok"] is True - assert status["config"]["status"] == "enabled" # type: ignore[index] - assert status["outbox"]["todos"]["committed_pending"] == 0 # type: ignore[index] - assert status["outbox"]["leases"]["prepared_only"] == 0 # type: ignore[index] - candidate = status["candidate"] - assert candidate["status"] == "loaded" # type: ignore[index] - assert candidate["cursor"] == "1" # type: ignore[index] - assert candidate["head_schema_version"] == "loopx_local_authority_shadow_projection_v0" # type: ignore[index] - assert candidate["codec_agreement"] is True # type: ignore[index] - assert candidate["partitions"] == {"todos": None, "leases": None} # type: ignore[index] - assert status["store_bytes"] > 0 - assert status["retention_pressure"] is False - assert str(runtime_root) not in json.dumps(status) - - drained = _cli(registry, runtime_root, "authority-shadow", "drain", "--goal-id", goal_id) - assert drained["ok"] is True - assert drained["outcome"] == "nothing_pending" - assert drained["config_enabled"] is True - _store_path, store = _store_document(runtime_root, goal_id) - assert store["cursor"] == "1" + assert result["drained_count"] == 1 + assert ws.drain()["drained_count"] == 0 diff --git a/tests/control_plane/test_local_authority_shadow_config.py b/tests/control_plane/test_local_authority_shadow_config.py index 50cc8c0dd0..97014d26b3 100644 --- a/tests/control_plane/test_local_authority_shadow_config.py +++ b/tests/control_plane/test_local_authority_shadow_config.py @@ -45,61 +45,30 @@ def _registry(tmp_path: Path) -> Path: return registry -def test_configure_goal_enables_and_clears_closed_file_shadow_config( - tmp_path: Path, -) -> None: +@pytest.mark.parametrize("execute", [False, True]) +def test_retired_enable_rejects_without_rewriting_registry(tmp_path: Path, execute: bool) -> None: registry = _registry(tmp_path) + before = registry.read_bytes() + with pytest.raises(ValueError, match="local_authority_shadow_retired"): + configure_goal(registry_path=registry, goal_id=GOAL_ID, + local_authority_shadow_file=True, execute=execute) + assert registry.read_bytes() == before - preview = configure_goal( - registry_path=registry, - goal_id=GOAL_ID, - local_authority_shadow_file=True, - execute=False, - ) - - assert preview["changed_fields"] == ["local_authority_shadow"] - assert preview["before"]["local_authority_shadow"] == { - "enabled": False, - "mode": None, - "status": "disabled", - } - assert preview["after"]["local_authority_shadow"] == { - "enabled": True, - "mode": "file_one_way", - "status": "enabled", - } - - applied = configure_goal( - registry_path=registry, - goal_id=GOAL_ID, - local_authority_shadow_file=True, - execute=True, - ) - assert applied["written"] is True - goal = json.loads(registry.read_text(encoding="utf-8"))["goals"][0] - assert goal["coordination"]["authority_shadow"] == { - "schema_version": "loopx_local_authority_shadow_config_v0", - "mode": "file_one_way", - } - assert goal["coordination"]["agent_model"] == "peer_v1" - - repeated = configure_goal( - registry_path=registry, - goal_id=GOAL_ID, - local_authority_shadow_file=True, - execute=True, - ) - assert repeated["written"] is False - cleared = configure_goal( - registry_path=registry, - goal_id=GOAL_ID, - clear_local_authority_shadow=True, - execute=True, - ) - assert cleared["changed_fields"] == ["local_authority_shadow"] - goal = json.loads(registry.read_text(encoding="utf-8"))["goals"][0] +def test_clear_retired_setting_preserves_runtime_config_and_peer_registration(tmp_path: Path) -> None: + registry = _registry(tmp_path) + data = json.loads(registry.read_text()) + data["goals"][0]["coordination"]["authority_shadow"] = { + "schema_version": "loopx_local_authority_shadow_config_v0", "mode": "file_one_way"} + registry.write_text(json.dumps(data)) + result = configure_goal(registry_path=registry, goal_id=GOAL_ID, + clear_local_authority_shadow=True, coordination_runtime_shadow_file=True, execute=True) + assert result["before"]["local_authority_shadow"]["status"] == "retired" + assert result["before"]["local_authority_shadow"]["enabled"] is False + assert result["after"]["local_authority_shadow"]["status"] == "disabled" + goal = json.loads(registry.read_text())["goals"][0] assert "authority_shadow" not in goal["coordination"] + assert goal["coordination"]["runtime_shadow"]["enabled"] is True assert goal["coordination"]["registered_agents"] == ["agent-a", "agent-b"] @@ -176,70 +145,30 @@ def test_configure_goal_exposes_transaction_bound_runtime_shadow_separately( assert "runtime_shadow" not in goal["coordination"] -def test_configure_goal_cli_exposes_default_off_shadow_boundary( - tmp_path: Path, - capsys: pytest.CaptureFixture[str], -) -> None: +def test_cli_rejects_retired_activation_and_exposes_no_enable_action(tmp_path: Path, capsys) -> None: registry = _registry(tmp_path) - - exit_code = main( - [ - "--registry", - str(registry), - "--runtime-root", - str(tmp_path / "runtime"), - "--format", - "json", - "configure-goal", - "--goal-id", - GOAL_ID, - "--local-authority-shadow-file", - ] - ) - - assert exit_code == 0 - payload = json.loads(capsys.readouterr().out) - assert payload["dry_run"] is True - assert payload["written"] is False - assert payload["after"]["local_authority_shadow"]["enabled"] is True - feature = next( - item - for item in payload["configuration_catalog"]["features"] - if item["feature_id"] == "local_authority_shadow" - ) - assert feature["display_name"] == "Local post-commit authority observation" - assert feature["availability"] == "experimental_opt_in" - assert "parity" not in feature["consider_when"].lower() - assert "post-commit snapshot" in feature["effect"] - assert feature["does_not"] == [ - "read the candidate for lifecycle decisions", - "write candidate state back into Markdown or task-lease files", - "promote shared authority or fence legacy writers", - "bind the snapshot to the exact primary transaction", - "guarantee delivery through a durable outbox", - "compare source and candidate or issue a parity verdict", - ] - assert feature["commands"]["apply_disable"].endswith( - "--clear-local-authority-shadow --execute" - ) - assert "authority_shadow" not in json.loads( - registry.read_text(encoding="utf-8") - )["goals"][0]["coordination"] - - -def test_rfc_disambiguates_historical_and_current_stage_numbering() -> None: - english = ( - REPO_ROOT - / "docs/architecture/rfcs/shared-goal-authority-state-provider-v0.md" - ).read_text(encoding="utf-8") - chinese = ( - REPO_ROOT - / "docs/architecture/rfcs/shared-goal-authority-state-provider-v0.zh-CN.md" - ).read_text(encoding="utf-8") - - assert "historical #3669 implementation sequence" in english - assert "part of the Stage 0 reference foundation" in english - assert "not the Stage 3 remote-shadow phase in Section 11" in english - assert "#3669 历史实施序列" in chinese - assert "属于 Stage 0 reference foundation" in chinese - assert "不是第 11 节的 Stage 3 远端 shadow 阶段" in chinese + before = registry.read_bytes() + code = main(["--registry", str(registry), "--format", "json", "configure-goal", + "--goal-id", GOAL_ID, "--local-authority-shadow-file"]) + assert code != 0 + assert "local_authority_shadow_retired" in capsys.readouterr().out + assert registry.read_bytes() == before + result = configure_goal(registry_path=registry, goal_id=GOAL_ID) + feature = next(row for row in result["configuration_catalog"]["features"] + if row["feature_id"] == "local_authority_shadow") + assert feature["availability"] == "retired" + assert "apply_enable" not in feature["commands"] + assert "--clear-local-authority-shadow" in feature["commands"]["apply_disable"] + from loopx.capabilities.configuration_ui import capability_configuration_editor + assert capability_configuration_editor("local_authority_shadow")["writable_scopes"] == [] + + +@pytest.mark.parametrize("raw", [None, {}, {"mode": "other"}]) +def test_malformed_retained_config_can_be_cleared_without_activation(tmp_path: Path, raw) -> None: + registry = _registry(tmp_path) + data = json.loads(registry.read_text()) + data["goals"][0]["coordination"]["authority_shadow"] = raw + registry.write_text(json.dumps(data)) + result = configure_goal(registry_path=registry, goal_id=GOAL_ID, clear_local_authority_shadow=True, execute=True) + assert result["before"]["local_authority_shadow"]["status"] == "invalid" + assert result["after"]["local_authority_shadow"]["status"] == "disabled" diff --git a/tests/control_plane/test_local_authority_shadow_runtime.py b/tests/control_plane/test_local_authority_shadow_runtime.py index b848152971..9a5629e2af 100644 --- a/tests/control_plane/test_local_authority_shadow_runtime.py +++ b/tests/control_plane/test_local_authority_shadow_runtime.py @@ -5,25 +5,13 @@ import pytest -from loopx.control_plane.coordination.local_authority_shadow_observation import LOCAL_AUTHORITY_SHADOW_EVIDENCE_SCHEMA, observe_local_authority_commit -from loopx.control_plane.todos.handoff_mode import set_goal_handoff_mode from loopx.control_plane.work_items.task_lease import ( acquire_task_lease, release_task_lease, renew_task_lease, - transfer_task_lease, -) -from loopx.event_sourced_state import ( - TODO_ADDED, - AppendOnlyStateEventStore, - make_state_event, ) from loopx.todos import ( add_goal_todo, - archive_completed_todos, - complete_goal_todo, - supersede_goal_todo, - update_goal_todo, ) @@ -83,482 +71,37 @@ def _add(registry: Path) -> dict: registry_path=registry, goal_id=GOAL_ID, role="agent", - text="Exercise one-way local authority shadowing.", + text="Continue primary work with a retained observation setting.", task_class="advancement_task", ) -def _shadow_document(runtime_root: Path) -> dict: - paths = list( - ( - runtime_root - / "authority-shadow" - / "file" - / GOAL_ID - ).glob("authority-store-*.json") - ) - assert len(paths) == 1 - return json.loads(paths[0].read_text(encoding="utf-8")) - - -def test_default_off_public_writers_never_call_shadow_runtime( - tmp_path: Path, - monkeypatch: pytest.MonkeyPatch, -) -> None: - registry, _state, runtime_root = _fixture(tmp_path, enabled=False) - calls: list[object] = [] - - def forbidden(*args: object, **kwargs: object) -> object: - calls.append((args, kwargs)) - raise AssertionError("default-off path constructed the shadow runtime") - - monkeypatch.setattr( - "loopx.control_plane.coordination.local_authority_shadow_observation.effect_runtime_result", - forbidden, - ) - - result = _add(registry) - lease_result = acquire_task_lease( - registry_path=registry, - runtime_root=runtime_root, - goal_id=GOAL_ID, - todo_id=str(result["todo_id"]), - owner=AGENT_A, - idempotency_key="default-off", - ttl_seconds=120, - ) - - assert result["ok"] is True - assert lease_result["ok"] is True - assert "authority_shadow" not in result - assert "authority_shadow" not in lease_result - assert calls == [] - assert not (runtime_root / "authority-shadow").exists() - - -def test_enabled_todo_public_facades_emit_post_commit_evidence(tmp_path: Path) -> None: - registry, _state, runtime_root = _fixture(tmp_path, enabled=True) - +@pytest.mark.parametrize("enabled", [False, True]) +def test_retired_and_absent_settings_leave_public_writers_on_primary_only(tmp_path: Path, enabled: bool) -> None: + registry, state, runtime = _fixture(tmp_path, enabled=enabled) + retained = runtime / "authority-shadow" / "file" / GOAL_ID / "historical.json" + if enabled: + retained.parent.mkdir(parents=True) + retained.write_bytes(b'{"historical":true}\n') + before = retained.read_bytes() if enabled else None added = _add(registry) - todo_id = str(added["todo_id"]) - acquired = acquire_task_lease( - registry_path=registry, - runtime_root=runtime_root, - goal_id=GOAL_ID, - todo_id=todo_id, - owner=AGENT_A, - idempotency_key="todo-terminal-a", - ttl_seconds=120, - ) - updated = update_goal_todo( - registry_path=registry, - goal_id=GOAL_ID, - todo_id=todo_id, - note="A public-safe update.", - agent_id=AGENT_A, - ) - completed = complete_goal_todo( - registry_path=registry, - goal_id=GOAL_ID, - todo_id=todo_id, - role="agent", - no_followup=True, - agent_id=AGENT_A, - task_lease_idempotency_key="todo-terminal-a", - task_lease_expected_version=int(acquired["lease"]["version"]), - ) - archived = archive_completed_todos( - registry_path=registry, - goal_id=GOAL_ID, - role="agent", - max_active_done=0, - dry_run=False, - ) - replacement = _add(registry) - replacement_lease = acquire_task_lease( - registry_path=registry, - runtime_root=runtime_root, - goal_id=GOAL_ID, - todo_id=str(replacement["todo_id"]), - owner=AGENT_A, - idempotency_key="todo-terminal-b", - ttl_seconds=120, - ) - superseded = supersede_goal_todo( - registry_path=registry, - goal_id=GOAL_ID, - todo_id=str(replacement["todo_id"]), - role="agent", - reason="Replace obsolete work.", - next_agent_todo="Carry the bounded work forward.", - agent_id=AGENT_A, - task_lease_idempotency_key="todo-terminal-b", - task_lease_expected_version=int(replacement_lease["lease"]["version"]), - ) - - for result in (added, updated, completed, archived, replacement, superseded): + todo_id = added["todo_id"] + acquired = acquire_task_lease(registry_path=registry, runtime_root=runtime, goal_id=GOAL_ID, + todo_id=todo_id, owner=AGENT_A, idempotency_key="retired-writer", ttl_seconds=120) + renewed = renew_task_lease(registry_path=registry, runtime_root=runtime, goal_id=GOAL_ID, + todo_id=todo_id, owner=AGENT_A, idempotency_key="retired-writer", + expected_version=acquired["lease"]["version"], ttl_seconds=180) + released = release_task_lease(registry_path=registry, runtime_root=runtime, goal_id=GOAL_ID, + todo_id=todo_id, owner=AGENT_A, idempotency_key="retired-writer", + expected_version=renewed["lease"]["version"]) + for result in (added, acquired, renewed, released): assert result["ok"] is True - assert result["authority_shadow"]["schema_version"] == ( - LOCAL_AUTHORITY_SHADOW_EVIDENCE_SCHEMA - ) - assert result["authority_shadow"]["primary_writeback_preserved"] is True - assert result["authority_shadow"]["provider_to_local_writes"] is False - assert result["authority_shadow"]["capture_kind"] == "post_commit_snapshot" - assert result["authority_shadow"]["source_transaction_correlated"] is False - assert result["authority_shadow"]["durable_source_outbox"] is False - assert result["authority_shadow"]["source_candidate_compared"] is False - assert result["authority_shadow"]["parity_verdict"] == "not_evaluated" - - -def test_enabled_task_lease_facades_shadow_only_committed_mutations( - tmp_path: Path, -) -> None: - registry, _state, runtime_root = _fixture(tmp_path, enabled=True) - todo_id = str(_add(registry)["todo_id"]) - - acquired = acquire_task_lease( - registry_path=registry, - runtime_root=runtime_root, - goal_id=GOAL_ID, - todo_id=todo_id, - owner=AGENT_A, - idempotency_key="lease-a", - ttl_seconds=120, - ) - replayed_acquire = acquire_task_lease( - registry_path=registry, - runtime_root=runtime_root, - goal_id=GOAL_ID, - todo_id=todo_id, - owner=AGENT_A, - idempotency_key="lease-a", - ttl_seconds=120, - ) - renewed = renew_task_lease( - registry_path=registry, - runtime_root=runtime_root, - goal_id=GOAL_ID, - todo_id=todo_id, - owner=AGENT_A, - idempotency_key="lease-a", - expected_version=1, - ttl_seconds=120, - ) - transferred = transfer_task_lease( - registry_path=registry, - runtime_root=runtime_root, - goal_id=GOAL_ID, - todo_id=todo_id, - owner=AGENT_A, - idempotency_key="lease-a", - new_owner=AGENT_B, - new_idempotency_key="lease-b", - expected_version=2, - ttl_seconds=120, - ) - released = release_task_lease( - registry_path=registry, - runtime_root=runtime_root, - goal_id=GOAL_ID, - todo_id=todo_id, - owner=AGENT_B, - idempotency_key="lease-b", - expected_version=3, - ) - - for result in (acquired, renewed, transferred, released): - assert result["authority_shadow"]["outcome"] in { - "captured", - "replayed", - "ambiguous_reconciled", - } - assert replayed_acquire["idempotent"] is True - assert "authority_shadow" not in replayed_acquire - - -def test_handoff_mode_and_todo_add_refresh_the_same_shadow( - tmp_path: Path, -) -> None: - registry, _state, runtime_root = _fixture(tmp_path, enabled=True) - - mode = set_goal_handoff_mode( - registry_path=registry, - goal_id=GOAL_ID, - mode="legacy", - ) - added = add_goal_todo( - registry_path=registry, - goal_id=GOAL_ID, - role="agent", - text="Verify the migrated authority projection.", - task_class="advancement_task", - ) - - assert mode["changed"] is True - assert mode["authority_shadow"]["outcome"] == "captured" - assert added["added"] is True - assert added["authority_shadow"]["outcome"] == "captured" - head = _shadow_document(runtime_root)["head"] - assert head["handoff_mode"] == "legacy" - assert [todo["todo_id"] for todo in head["todos"]] == [ - added["todo_id"] - ] - - -def test_event_projected_completion_refreshes_shadow_after_releasing_lease( - tmp_path: Path, -) -> None: - registry, state, runtime_root = _fixture(tmp_path, enabled=True) - todo_id = "todo_event_shadow" - AppendOnlyStateEventStore(state.with_name("events.jsonl")).append( - make_state_event( - event_id="evt-event-shadow-parent", - goal_id=GOAL_ID, - event_type=TODO_ADDED, - refs={"todo_id": todo_id}, - payload={ - "role": "agent", - "title": "Complete the event-projected shadow task.", - "task_class": "advancement_task", - "claimed_by": AGENT_A, - }, - recorded_at="2026-09-02T00:00:00+00:00", - ) - ) - lease_key = "event-shadow-instance" - acquire_task_lease( - registry_path=registry, - runtime_root=runtime_root, - goal_id=GOAL_ID, - todo_id=todo_id, - owner=AGENT_A, - idempotency_key=lease_key, - ttl_seconds=120, - ) - - completed = complete_goal_todo( - registry_path=registry, - goal_id=GOAL_ID, - todo_id=todo_id, - claimed_by=AGENT_A, - agent_id=AGENT_A, - task_lease_idempotency_key=lease_key, - task_lease_expected_version=1, - evidence="validation://event-shadow-completion", - no_followup=True, - ) - - assert completed["source"] == "event_log" - assert completed["changed"] is True - assert completed["authority_shadow"]["outcome"] == "captured" - head = _shadow_document(runtime_root)["head"] - assert len(head["leases"]) == 1 - assert head["leases"][0]["todo_id"] == todo_id - assert head["leases"][0]["status"] == "released" - projected = next(todo for todo in head["todos"] if todo["todo_id"] == todo_id) - assert projected["status"] == "done" - - -def test_candidate_failure_never_changes_committed_todo_result( - tmp_path: Path, - monkeypatch: pytest.MonkeyPatch, -) -> None: - registry, state, _runtime_root = _fixture(tmp_path, enabled=True) - - def unavailable( - _method: str, - params: dict[str, object], - **_kwargs: object, - ) -> dict[str, object]: - return { - "schema_version": LOCAL_AUTHORITY_SHADOW_EVIDENCE_SCHEMA, - "outcome": "unavailable", - "reason_code": "injected_outage", - "goal_id": params["goal_id"], - "observation_id": params["observation_id"], - "source_digest": params["source_digest"], - "primary_authority": "legacy_local", - "candidate_provider": "file", - "candidate_read_for_decision": False, - "provider_to_local_writes": False, - "primary_writeback_preserved": True, - "capture_kind": "post_commit_snapshot", - "source_transaction_correlated": False, - "durable_source_outbox": False, - "source_candidate_compared": False, - "parity_verdict": "not_evaluated", - "store_identity": None, - "provider_revision": None, - "cursor": None, - } - - monkeypatch.setattr( - "loopx.control_plane.coordination.local_authority_shadow_observation.effect_runtime_result", - unavailable, - ) - - result = _add(registry) - - assert result["ok"] is True - assert result["added"] is True - assert result["authority_shadow"]["outcome"] == "unavailable" - assert str(result["todo_id"]) in state.read_text(encoding="utf-8") - - -def test_invalid_shadow_config_is_typed_but_preserves_primary_write( - tmp_path: Path, -) -> None: - registry, state, _runtime_root = _fixture(tmp_path, enabled=True) - payload = json.loads(registry.read_text(encoding="utf-8")) - payload["goals"][0]["coordination"]["authority_shadow"]["mode"] = "remote" - registry.write_text(json.dumps(payload), encoding="utf-8") - - result = _add(registry) - - assert result["ok"] is True - assert result["authority_shadow"]["outcome"] == "failed" - assert result["authority_shadow"]["reason_code"] == "invalid_shadow_config" - assert str(result["todo_id"]) in state.read_text(encoding="utf-8") - - -def test_observer_is_default_off_without_creating_lock_or_provider_directory( - tmp_path: Path, -) -> None: - registry, _state, runtime_root = _fixture(tmp_path, enabled=False) - - assert ( - observe_local_authority_commit( - registry_path=registry, - runtime_root=runtime_root, - goal_id=GOAL_ID, - observation_trigger="todo_update", - ) - is None - ) - assert not (runtime_root / "authority-shadow").exists() - - -def test_provider_revision_conflict_resamples_source_under_same_observation_lock( - tmp_path: Path, - monkeypatch: pytest.MonkeyPatch, -) -> None: - registry, _state, runtime_root = _fixture(tmp_path, enabled=True) - projections = iter( - ( - { - "schema_version": "loopx_local_authority_shadow_projection_v0", - "goal_id": GOAL_ID, - "handoff_mode": "hard_lease", - "todos": [{"todo_id": "todo_old", "status": "open"}], - "leases": [], - }, - { - "schema_version": "loopx_local_authority_shadow_projection_v0", - "goal_id": GOAL_ID, - "handoff_mode": "hard_lease", - "todos": [{"todo_id": "todo_fresh", "status": "open"}], - "leases": [], - }, - ) - ) - requests: list[dict[str, object]] = [] - - monkeypatch.setattr( - "loopx.control_plane.coordination.local_authority_shadow_observation._stable_projection", - lambda **_kwargs: next(projections), - ) - - def conflict_then_advance( - _method: str, - params: dict[str, object], - **_kwargs: object, - ) -> dict[str, object]: - requests.append(dict(params)) - outcome = "conflict_retry_required" if len(requests) == 1 else "captured" - return { - "schema_version": LOCAL_AUTHORITY_SHADOW_EVIDENCE_SCHEMA, - "outcome": outcome, - "reason_code": ( - "provider_revision_mismatch" if len(requests) == 1 else None - ), - "goal_id": GOAL_ID, - "observation_id": params["observation_id"], - "source_digest": params["source_digest"], - "primary_authority": "legacy_local", - "candidate_provider": "file", - "candidate_read_for_decision": False, - "provider_to_local_writes": False, - "primary_writeback_preserved": True, - "capture_kind": "post_commit_snapshot", - "source_transaction_correlated": False, - "durable_source_outbox": False, - "source_candidate_compared": False, - "parity_verdict": "not_evaluated", - "store_identity": "file:test", - "provider_revision": "file:2:test", - "cursor": "2", - } - - monkeypatch.setattr( - "loopx.control_plane.coordination.local_authority_shadow_observation.effect_runtime_result", - conflict_then_advance, - ) - - result = observe_local_authority_commit( - registry_path=registry, - runtime_root=runtime_root, - goal_id=GOAL_ID, - observation_trigger="todo_update:todo_a:now", - ) - - assert result is not None - assert result["outcome"] == "captured" - assert len(requests) == 2 - assert requests[0]["source_digest"] != requests[1]["source_digest"] - assert requests[0]["observation_id"] != requests[1]["observation_id"] - assert all(request["runtime_root"] == str(runtime_root) for request in requests) - assert all("provider_directory" not in request for request in requests) - - -def test_relative_common_runtime_root_resolves_against_the_project_root_for_every_hook( - tmp_path: Path, - monkeypatch: pytest.MonkeyPatch, -) -> None: - from loopx.control_plane.work_items.task_lease import runtime_root_from_registry - from loopx.paths import registry_project_root - - registry, _state, _absolute_runtime = _fixture(tmp_path, enabled=True) - document = json.loads(registry.read_text(encoding="utf-8")) - document["common_runtime_root"] = "runtime-relative" - registry.write_text(json.dumps(document), encoding="utf-8") - elsewhere = tmp_path / "elsewhere" - elsewhere.mkdir() - monkeypatch.chdir(elsewhere) - expected_root = registry_project_root(registry) / "runtime-relative" - - added = _add(registry) - lease = acquire_task_lease( - registry_path=registry, - runtime_root=runtime_root_from_registry(registry, None), - goal_id=GOAL_ID, - todo_id=str(added["todo_id"]), - owner=AGENT_A, - idempotency_key="relative-root", - ttl_seconds=120, - ) - handoff = set_goal_handoff_mode( - registry_path=registry, - goal_id=GOAL_ID, - mode="hard_lease", - ) - - assert added["authority_shadow"]["outcome"] == "captured" - assert lease["authority_shadow"]["outcome"] == "captured" - assert handoff["changed"] is False - assert added["authority_shadow"]["store_identity"] == lease["authority_shadow"]["store_identity"] - assert (expected_root / "authority-shadow" / "file" / GOAL_ID).is_dir() - assert (expected_root / "goals" / GOAL_ID / "task-leases" / f"{added['todo_id']}.json").exists() - assert not (elsewhere / "runtime-relative").exists() - document = _shadow_document(expected_root) - assert document["cursor"] == "2" - assert len(document["head"]["leases"]) == 1 + assert "authority_shadow" not in result + assert todo_id in state.read_text() + assert not (runtime / "authority-shadow" / "outbox").exists() + assert not (runtime / "authority-shadow" / "file-v0").exists() + if enabled: + assert retained.read_bytes() == before + assert list(retained.parent.iterdir()) == [retained] + else: + assert not (runtime / "authority-shadow").exists() diff --git a/tests/control_plane/test_shared_goal_authority_e2e.py b/tests/control_plane/test_shared_goal_authority_e2e.py index 875a7a3ac7..f5a2d7e2b3 100644 --- a/tests/control_plane/test_shared_goal_authority_e2e.py +++ b/tests/control_plane/test_shared_goal_authority_e2e.py @@ -44,21 +44,7 @@ # renamed or deleted so the skip cannot outlive its coverage. CLI_E2E_TWIN_FILE = Path(__file__).with_name("test_local_authority_shadow_cli_e2e.py") CLI_E2E_COVERAGE = { - "s2c1.configure_enable_disable_roundtrip": ( - "test_product_cli_configure_capture_readback_disable_and_default_off_lifecycle_isolation" - ), - "s2c1.default_off_isolation": ( - "test_product_cli_configure_capture_readback_disable_and_default_off_lifecycle_isolation" - ), - "s2c1.candidate_failure_preserves_primary": ( - "test_product_cli_candidate_failure_preserves_the_primary_lifecycle_commit" - ), - "s2c1.crash_gap_loses_observation": ( - "test_product_cli_loses_capture_between_commit_and_observer_then_refreshes_snapshot" - ), - "s2c1.dual_runtime_root_consistency": ( - "test_product_cli_runtime_root_override_keeps_one_candidate_lineage" - ), + "s2c1.retired_observation_upgrade": "test_retired_setting_cannot_enable_capture_or_satisfy_bootstrap", } CLI_E2E_COVERED_ROW_IDS = tuple(CLI_E2E_COVERAGE) CLI_E2E_COVERAGE_REASON = ( diff --git a/tests/control_plane/test_state_migration_authority_shadow.py b/tests/control_plane/test_state_migration_authority_shadow.py index 0ce33cccb5..9237fd3025 100644 --- a/tests/control_plane/test_state_migration_authority_shadow.py +++ b/tests/control_plane/test_state_migration_authority_shadow.py @@ -1,7 +1,6 @@ from __future__ import annotations import json -from collections.abc import Iterator from pathlib import Path import pytest @@ -125,138 +124,25 @@ def _migrate(paths: dict[str, Path], *, execute: bool) -> dict[str, object]: ) -def test_execute_excludes_old_shadow_and_seeds_fresh_target_lineage( - tmp_path: Path, -) -> None: +@pytest.mark.parametrize("execute", [False, True]) +def test_migration_preserves_old_store_without_reseeding_retired_lineage(tmp_path: Path, execute: bool) -> None: paths = _migration_fixture(tmp_path) - - result = _migrate(paths, execute=True) - - assert result["ok"] is True - runtime_result = result["runtime_goals"][0] # type: ignore[index] - assert runtime_result["copied"] is True - - target_goal_runtime = paths["target_runtime"] / "goals" / NEW_GOAL_ID - copied_lease = json.loads( - (target_goal_runtime / "task-leases" / "safe-local.json").read_text( - encoding="utf-8" - ) - ) - assert copied_lease["goal_id"] == NEW_GOAL_ID - - target_store_dir = ( - paths["target_runtime"] / "authority-shadow" / "file" / NEW_GOAL_ID - ) - target_identity = (target_store_dir / "store-identity").read_text(encoding="utf-8") - assert target_identity.startswith("file:") - assert target_identity != OLD_STORE_IDENTITY - - store_paths = list(target_store_dir.glob("authority-store-*.json")) - assert len(store_paths) == 1 - store_payload = json.loads(store_paths[0].read_text(encoding="utf-8")) - assert store_payload["goal_id"] == NEW_GOAL_ID - assert store_payload["store_identity"] == target_identity - assert store_payload["cursor"] == "1" - assert len(store_payload["committed"]) == 1 - serialized_store = json.dumps(store_payload, sort_keys=True) - assert OLD_STORE_IDENTITY not in serialized_store - assert "file:99:legacy-lineage" not in serialized_store - assert str(paths["source_repo"]) not in serialized_store - assert "must-never-migrate" not in serialized_store - - seed = result["authority_shadow_seeds"][0] # type: ignore[index] - assert seed["goal_id"] == NEW_GOAL_ID - assert seed["attempted"] is True - assert seed["outcome"] == "captured" - - -def test_dry_run_reports_exclusion_and_seed_plan_without_writing( - tmp_path: Path, - monkeypatch: pytest.MonkeyPatch, -) -> None: - paths = _migration_fixture(tmp_path) - sentinel = b'{"schema_version":"existing","goals":[]}\n' - paths["target_registry"].write_bytes(sentinel) - - def forbidden_observer(**_kwargs: object) -> object: - raise AssertionError("dry-run called the shadow observer") - - original_rglob = Path.rglob - - def guarded_rglob(path: Path, pattern: str) -> Iterator[Path]: - if path.name == "authority-shadow": - raise AssertionError("migration entered candidate-provider storage") - return original_rglob(path, pattern) - - monkeypatch.setattr( - "loopx.control_plane.coordination.local_authority_shadow_observation." - "observe_local_authority_commit", - forbidden_observer, - ) - monkeypatch.setattr(Path, "rglob", guarded_rglob) - - result = _migrate(paths, execute=False) - + old_dir = paths["legacy_runtime"] / "authority-shadow" / "file" / OLD_GOAL_ID + before = {p.name: p.read_bytes() for p in old_dir.iterdir()} + result = _migrate(paths, execute=execute) assert result["ok"] is True - assert result["dry_run"] is True - assert paths["target_registry"].read_bytes() == sentinel - assert not paths["target_runtime"].exists() - runtime_result = result["runtime_goals"][0] # type: ignore[index] - assert runtime_result["copied_file_count"] == 0 - seed = result["authority_shadow_seeds"][0] # type: ignore[index] - assert seed == { + assert result["authority_shadow_seeds"] == [{ "schema_version": "loopx_state_migration_shadow_seed_evidence_v0", - "goal_id": NEW_GOAL_ID, - "attempted": False, - "outcome": "planned", - "reason_code": None, - } - rendered = render_state_migration_markdown(result) - assert "Authority Shadow Seeds" in rendered - assert "outcome=`planned`" in rendered - assert "must-never-migrate" not in json.dumps(result, sort_keys=True) - assert "must-never-migrate" not in rendered - - -def test_seed_failure_is_public_safe_evidence_and_does_not_reverse_migration( - tmp_path: Path, - monkeypatch: pytest.MonkeyPatch, -) -> None: - paths = _migration_fixture(tmp_path) - - def fail_observer(**_kwargs: object) -> object: - raise RuntimeError("credential=private-provider-value") - - monkeypatch.setattr( - "loopx.control_plane.coordination.local_authority_shadow_observation." - "observe_local_authority_commit", - fail_observer, - ) - - result = _migrate(paths, execute=True) - - assert result["ok"] is True - assert result["wrote_project_registry"] is True - migrated_registry = json.loads(paths["target_registry"].read_text(encoding="utf-8")) - assert migrated_registry["goals"][0]["id"] == NEW_GOAL_ID - assert ( - paths["target_runtime"] - / "goals" - / NEW_GOAL_ID - / "task-leases" - / "safe-local.json" - ).exists() - - seed = result["authority_shadow_seeds"][0] # type: ignore[index] - assert seed["outcome"] == "failed" - assert seed["reason_code"] == "post_migration_shadow_seed_failed" - assert seed["attempted"] is True - assert "credential" not in json.dumps(result, sort_keys=True) - assert "private-provider-value" not in json.dumps(result, sort_keys=True) - assert not ( - paths["target_runtime"] - / "authority-shadow" - / "file" - / NEW_GOAL_ID - / "authority-store-legacy.json" - ).exists() + "goal_id": NEW_GOAL_ID, "attempted": False, "outcome": "retired", + "reason_code": "local_authority_shadow_retired"}] + assert not (paths["target_runtime"] / "authority-shadow").exists() + assert {p.name: p.read_bytes() for p in old_dir.iterdir()} == before + assert "retired" in render_state_migration_markdown(result) + if execute: + target = json.loads(paths["target_registry"].read_text())["goals"][0] + assert target["id"] == NEW_GOAL_ID + assert "runtime_shadow" not in target["coordination"] + lease = paths["target_runtime"] / "goals" / NEW_GOAL_ID / "task-leases" / "safe-local.json" + assert json.loads(lease.read_text())["goal_id"] == NEW_GOAL_ID + else: + assert not paths["target_registry"].exists() diff --git a/tests/control_plane_ts/local_authority_shadow.test.ts b/tests/control_plane_ts/local_authority_shadow.test.ts index cff60b6988..d6a93256f3 100644 --- a/tests/control_plane_ts/local_authority_shadow.test.ts +++ b/tests/control_plane_ts/local_authority_shadow.test.ts @@ -1,44 +1,12 @@ import assert from "node:assert/strict"; -import { mkdtemp, rm } from "node:fs/promises"; +import { mkdtemp, readdir, rm } from "node:fs/promises"; import { tmpdir } from "node:os"; import { join } from "node:path"; import test from "node:test"; - -import type { - AuthorityStore, - AuthorityStoreCommit, - AuthorityStoreCommitResult, - AuthorityStoreIdentityResult, - AuthorityStoreLoadResult, - AuthorityStoreReceiptResult, - AuthorityStoreScanResult, -} from "../../loopx/control_plane/coordination/authority_store.ts"; +import { EffectRuntimeRequestError } from "../../loopx/control_plane/effect_runtime_errors.ts"; import { FileAuthorityStore } from "../../loopx/control_plane/coordination/file_authority_store.ts"; -import { - LOCAL_AUTHORITY_SHADOW_EVIDENCE_SCHEMA, - localAuthorityShadowHeadDigest, - localAuthorityShadowPartitionDigest, - recordLocalAuthorityShadow, -} from "../../loopx/control_plane/coordination/local_authority_shadow.ts"; - -function request(directory: string, operationId = "local-operation-a") { - return { - schema_version: "loopx_local_authority_shadow_request_v0", - mode: "file_one_way", - runtime_root: directory, - goal_id: "goal-a", - observation_id: operationId, - observation_trigger: "todo_update", - source_digest: `sha256:${"a".repeat(64)}`, - source_projection: { - schema_version: "loopx_local_authority_shadow_projection_v0", - goal_id: "goal-a", - handoff_mode: "hard_lease", - todos: [{ todo_id: "todo-a", status: "open", claimed_by: "agent-a" }], - leases: [{ todo_id: "todo-a", version: 2, lease_epoch: 1, status: "active" }], - }, - }; -} +import {localAuthorityShadowHeadDigest, localAuthorityShadowPartitionDigest, + recordLocalAuthorityShadow, readLocalAuthorityShadow} from "../../loopx/control_plane/coordination/local_authority_shadow.ts"; test("runtime shadow parity ignores only the resume evaluation observation clock", () => { const head = { @@ -90,205 +58,32 @@ test("runtime shadow parity ignores only the resume evaluation observation clock ); }); -test("one-way file shadow captures a post-commit observation without claiming parity", async (t) => { - const root = await mkdtemp(join(tmpdir(), "loopx-local-authority-shadow-")); - t.after(() => rm(root, { recursive: true, force: true })); - - const evidence = await recordLocalAuthorityShadow(request(root)); - - assert.equal(evidence.schema_version, LOCAL_AUTHORITY_SHADOW_EVIDENCE_SCHEMA); - assert.equal(evidence.outcome, "captured"); - assert.equal(evidence.primary_authority, "legacy_local"); - assert.equal(evidence.candidate_read_for_decision, false); - assert.equal(evidence.provider_to_local_writes, false); - assert.equal(evidence.primary_writeback_preserved, true); - assert.equal(evidence.capture_kind, "post_commit_snapshot"); - assert.equal(evidence.source_transaction_correlated, false); - assert.equal(evidence.durable_source_outbox, false); - assert.equal(evidence.source_candidate_compared, false); - assert.equal(evidence.parity_verdict, "not_evaluated"); - const loaded = await new FileAuthorityStore( - join(root, "authority-shadow", "file", "goal-a"), - "goal-a", - ).loadAuthority(); - assert.equal(loaded.status, "loaded"); - if (loaded.status === "loaded") { - assert.deepEqual(loaded.head, request(root).source_projection); - } -}); - -test("same observation reuses its candidate-side capture receipt", async (t) => { - const root = await mkdtemp(join(tmpdir(), "loopx-local-authority-shadow-")); - t.after(() => rm(root, { recursive: true, force: true })); - - assert.equal((await recordLocalAuthorityShadow(request(root))).outcome, "captured"); - const replay = await recordLocalAuthorityShadow(request(root)); - - assert.equal(replay.outcome, "replayed"); - const page = await new FileAuthorityStore( - join(root, "authority-shadow", "file", "goal-a"), - "goal-a", - ).scanCommitted(null, 10); - assert.equal(page.status, "page"); - if (page.status === "page") assert.equal(page.transactions.length, 1); -}); - -test("observation trigger does not imply exact source-transaction binding", async (t) => { - const root = await mkdtemp(join(tmpdir(), "loopx-local-authority-shadow-")); - t.after(() => rm(root, { recursive: true, force: true })); - const requestAfterConcurrentCommit = request(root); - requestAfterConcurrentCommit.observation_trigger = "todo_add:todo-a"; - requestAfterConcurrentCommit.source_projection.todos.push({ - todo_id: "todo-b", - status: "open", - claimed_by: "agent-b", - }); - - const observation = await recordLocalAuthorityShadow(requestAfterConcurrentCommit); - - assert.equal(observation.outcome, "captured"); - assert.equal(observation.source_transaction_correlated, false); - assert.equal(observation.parity_verdict, "not_evaluated"); - const page = await new FileAuthorityStore( - join(root, "authority-shadow", "file", "goal-a"), - "goal-a", - ).scanCommitted(null, 10); - assert.equal(page.status, "page"); - if (page.status === "page") { - assert.equal(page.transactions[0]?.events[0]?.observation_trigger, "todo_add:todo-a"); - const capturedTodos = page.transactions[0]?.projection.todos; - assert.ok(Array.isArray(capturedTodos)); - assert.equal(capturedTodos.length, 2); - } -}); - -test("legacy source_operation wording is rejected by the closed observation contract", async () => { - const legacyRequest = { ...request("/not-used") } as Record; - legacyRequest.source_operation = legacyRequest.observation_trigger; - delete legacyRequest.observation_trigger; - - await assert.rejects( - recordLocalAuthorityShadow(legacyRequest), - /unsupported fields: source_operation/u, - ); -}); - -class UnavailableStore implements AuthorityStore { - commits = 0; - - async storeIdentity(): Promise { - return { status: "unavailable", reason_code: "injected", reason: "offline" }; - } - async loadAuthority(): Promise { - return { status: "unavailable", reason_code: "injected", reason: "offline" }; - } - async commitAuthority(_commit: AuthorityStoreCommit): Promise { - this.commits += 1; - return { status: "failed", reason_code: "unexpected", reason: "must not commit" }; - } - async readReceipt(_operationId: string): Promise { - return { status: "missing" }; - } - async scanCommitted(_afterCursor: string | null, _limit: number): Promise { - return { status: "page", transactions: [], next_cursor: null, has_more: false }; - } -} - -test("candidate unavailability is typed evidence and attempts no commit", async () => { - const store = new UnavailableStore(); - const evidence = await recordLocalAuthorityShadow(request("/not-used"), { - openStore: () => store, - }); - - assert.equal(evidence.outcome, "unavailable"); - assert.equal(evidence.reason_code, "injected"); - assert.equal(evidence.primary_writeback_preserved, true); - assert.equal(store.commits, 0); -}); - -class RevisionConflictStore extends UnavailableStore { - override async storeIdentity(): Promise { - return { status: "available", store_identity: "file:test" }; - } - override async loadAuthority(): Promise { - return { - status: "loaded", - head: {}, - provider_revision: "provider-revision-a", - cursor: "1", - }; - } - override async commitAuthority( - _commit: AuthorityStoreCommit, - ): Promise { - this.commits += 1; - return { - status: "conflict", - conflict_kind: "provider_revision_mismatch", - current_provider_revision: "provider-revision-b", - current_cursor: "2", - }; - } -} - -test("provider revision conflict requests a fresh source projection without stale retry", async () => { - const store = new RevisionConflictStore(); - - const result = await recordLocalAuthorityShadow(request("/not-used"), { - openStore: () => store, - }); - - assert.equal(result.outcome, "conflict_retry_required"); - assert.equal(result.reason_code, "provider_revision_mismatch"); - assert.equal(store.commits, 1); -}); - -test("goal id cannot escape the fixed shadow directory", async () => { - await assert.rejects( - recordLocalAuthorityShadow({ - ...request("/not-used"), - goal_id: "../other-goal", - source_projection: { - ...request("/not-used").source_projection, - goal_id: "../other-goal", - }, - }), - /single path segment/u, - ); -}); - -test("ambiguous response is captured only when its candidate observation receipt is readable", async () => { - const root = await mkdtemp(join(tmpdir(), "loopx-local-authority-shadow-")); - tCleanup(root); - class AfterCommitStore extends FileAuthorityStore { - protected override async replaceDurably(path: string, payload: Uint8Array): Promise { - await super.replaceDurably(path, payload); - if (path === this.path) throw new Error("lost response after durable replace"); - } - } - const recovered = await recordLocalAuthorityShadow(request(root), { - openStore: (directory, goalId) => new AfterCommitStore(directory, goalId), - }); - assert.equal(recovered.outcome, "ambiguous_reconciled"); - const beforeRoot = await mkdtemp(join(tmpdir(), "loopx-local-authority-shadow-")); - tCleanup(beforeRoot); - class BeforeCommitStore extends FileAuthorityStore { - protected override async replaceDurably(path: string, _payload: Uint8Array): Promise { - if (path === this.path) throw new Error("failed before durable replace"); - return await super.replaceDurably(path, _payload); - } +test("retired record RPC rejects even a stale client without creating any storage", async t => { + const root = await mkdtemp(join(tmpdir(), "loopx-retired-observer-")); + t.after(() => rm(root, {recursive: true, force: true})); + for (const request of [null, {}, {schema_version: "loopx_local_authority_shadow_request_v0", + mode: "file_one_way", runtime_root: root, goal_id: "goal-a", observation_id: "old-op", + source_digest: `sha256:${"a".repeat(64)}`, observation_trigger: "todo_update", source_projection: {}}]) { + await assert.rejects(recordLocalAuthorityShadow(request), error => + error instanceof EffectRuntimeRequestError && error.code === "local_authority_shadow_retired"); } - const unproved = await recordLocalAuthorityShadow(request(beforeRoot), { - openStore: (directory, goalId) => new BeforeCommitStore(directory, goalId), - }); - assert.equal(unproved.outcome, "ambiguous_unproved"); + assert.deepEqual(await readdir(root), []); }); -const cleanupRoots: string[] = []; -function tCleanup(root: string): void { - cleanupRoots.push(root); -} -test.after(async () => { - await Promise.all(cleanupRoots.map((root) => rm(root, { recursive: true, force: true }))); +test("historical observation remains readable but is not a runtime capture lineage", async t => { + const root = await mkdtemp(join(tmpdir(), "loopx-retained-observer-")); + t.after(() => rm(root, {recursive: true, force: true})); + const store = new FileAuthorityStore(join(root, "authority-shadow", "file", "goal-a"), "goal-a"); + const head = {schema_version: "loopx_local_authority_shadow_projection_v0", goal_id: "goal-a", + handoff_mode: "legacy", todos: [], leases: []}; + assert.equal((await store.commitAuthority({expected_provider_revision: null, operation_id: "old-op", + events: [], receipts: [{observation_id: "old-op"}], next_projection: head})).status, "applied"); + const before = await store.loadAuthority(); + const view = await readLocalAuthorityShadow({schema_version: "loopx_coordination_runtime_shadow_outbox_read_v0", + runtime_root: root, goal_id: "goal-a", store_kind: "legacy_observation"}); + assert.equal(view.status, "loaded"); + assert.deepEqual(view.head, head); + assert.deepEqual(await store.loadAuthority(), before); + assert.deepEqual(await readdir(join(root, "authority-shadow")), ["file"]); }); From 1db8993464af6b8e54ffd4f110b62e601ce518e5 Mon Sep 17 00:00:00 2001 From: huangruiteng <14976749+huangruiteng@users.noreply.github.com> Date: Thu, 24 Sep 2026 22:54:20 +0800 Subject: [PATCH 2/2] Document explicit shadow upgrade and reconcile provider cutover work Signed-off-by: huangruiteng <14976749+huangruiteng@users.noreply.github.com> --- .../2026-09-24-observation-retirement.md | 67 +++++++++++++++ ...2026-09-24-observation-retirement.zh-CN.md | 46 +++++++++++ ...shared-goal-authority-state-provider-v0.md | 82 +++++++------------ ...-goal-authority-state-provider-v0.zh-CN.md | 63 +++++--------- .../typescript-control-plane-migration-v0.md | 9 ++ ...script-control-plane-migration-v0.zh-CN.md | 7 ++ .../authority-observation-retirement.md | 73 +++++++++++++++++ examples/shared-goal-authority-e2e/README.md | 28 +++---- 8 files changed, 265 insertions(+), 110 deletions(-) create mode 100644 docs/architecture/rfcs/ledger/shared-goal-authority-state-provider-v0/2026-09-24-observation-retirement.md create mode 100644 docs/architecture/rfcs/ledger/shared-goal-authority-state-provider-v0/2026-09-24-observation-retirement.zh-CN.md create mode 100644 docs/reference/authority-observation-retirement.md diff --git a/docs/architecture/rfcs/ledger/shared-goal-authority-state-provider-v0/2026-09-24-observation-retirement.md b/docs/architecture/rfcs/ledger/shared-goal-authority-state-provider-v0/2026-09-24-observation-retirement.md new file mode 100644 index 0000000000..d4c8793a85 --- /dev/null +++ b/docs/architecture/rfcs/ledger/shared-goal-authority-state-provider-v0/2026-09-24-observation-retirement.md @@ -0,0 +1,67 @@ +# One writable shadow lineage + +- Inventory baseline: `d64c4d377` (`main`), 2026-09-24. +- Goal: #4574 R5/G2, shared authority §12 question 14, TS T2/T4. +- Gap: post-commit observation still writes a second history after transaction-bound outbox capture shipped. +- Outcome: remove the obsolete writer, expose retirement at configuration/CLI/runtime/settings boundaries, preserve historical reads and require explicit replacement bootstrap. + +## Reconciled delivery plan + +Do not reuse the old “5–8” or “7–9” ranges. They mixed new implementation, +open PRs and qualification into one number. This snapshot has **four proposed +new implementation batches including this retirement**, alongside already-open +work. After this PR, **three named batches remain planned**; that is a delivery +plan, not proof that precisely three future PRs will suffice for every Goal. +Split a batch only when a demonstrated integration defect requires it, and +record that defect rather than keeping an unchanged numerical range. + +| Batch | Observable completion | Current boundary | +| --- | --- | --- | +| Retire duplicate observation | No post-commit resampling or second-store writes; old settings cannot enable it; historical data survives; explicit outbox bootstrap works. | Independent cleanup of the already-shipped capture owner; no provider-default claim. | +| Executor liveness | A real Host renews during execution and is cancelled on fence loss; expired/reclaimed execution cannot settle as its successor. | Current acquire/readback is point-in-time proof. RFC §12 question 6 remains open. External systems still need their own effect identity/fencing. | +| Event capture + whole-Goal journey | Bind actual event publication to source locks/outbox lineage; verify mixed writers, drain, reviewed promotion, consumer reads, export/rollback. | Reuse #5003; preserve `event_log_writer_not_bound` until binding is proved. | +| Default onboarding + bounded Python retirement | Qualified profile selected consistently by new Goal creation/settings/install; explicit migration for existing Goals; remove switched business writers. | Depends on integration and applicable D1–D3 gates. Python host IO/rendering/import-export need not disappear. | + +Existing open implementations are tracked separately: #5006 complete-source +transport, #5003 atomic event-owned completion, #4994 leased handoff continuation, +#4995 Monitor proof projection, #4991 reservation cleanup, #4992 deferred receipt +selection, and #4931 SQLite retained-proof optimization. Do not propose these +again. #4915 filesystem placement is not authority-provider selection. + +Merged source assembly/capture delivery (#4967/#4968), canonical pagination +(#4922), reviewed cutover/drain planning (#4888/#4920), SQLite admission (#4960) +and display recovery (#4961) are existing implementations, not missing projects. +#5006 is still open at this baseline: its larger source transport should be +integrated, not copied into this independent retirement PR. + + +SQLite D2 is evidence, not an invented PR allocation. #4224 reports failed +1 MiB receipt/scan budgets and missing workload/RSS/recovery/restore/runtime/soak +coverage; #4931 addresses proof encoding but does not certify all those rows. +At least ten days of natural soak, exact-profile qualification and D3 cohort +approval remain separate. File-only cutover, qualified SQLite default and +migration of all existing Goals are different acceptance scopes. PostgreSQL +continues to reuse the typed contract while deployed transport, tenant policy, +operations and service qualification remain medium-term work. + +## Semantics and evidence + +[Operator transition](../../../../reference/authority-observation-retirement.md) +covers rejection, clearing, bootstrap and rollback. Primary Todo/lease rules, +source checks, event holds and active capture bindings do not change. The old +settings entry is read-only in both languages; stale clients fail before writes. +The old RPC is a typed rejection tombstone. Historical codecs and reads remain, +but Python post-commit projection/retry and TS observation commit are deleted. +State migration retains its response field with `retired/attempted=false`. + +Validation exercises public CLI replacement bootstrap, lease writes with old +and absent settings, invalid-setting cleanup, retained-store readback, and +process death between source replacement and committed marker followed by +single-entry recovery. Existing transaction, source-fencing and configuration +suites remain the oracle; tests for intentionally removed observation behavior +are replaced rather than preserved as a second implementation. + +A witness-checked detached snapshot of the real local Goal exercises Todo add, +retired-setting clear/readback and unchanged historical bytes. All writes target +the disposable runtime explicitly; private source content is not published. +This proves the affected upgrade path, not full migration or D2 qualification. diff --git a/docs/architecture/rfcs/ledger/shared-goal-authority-state-provider-v0/2026-09-24-observation-retirement.zh-CN.md b/docs/architecture/rfcs/ledger/shared-goal-authority-state-provider-v0/2026-09-24-observation-retirement.zh-CN.md new file mode 100644 index 0000000000..470ed28a9a --- /dev/null +++ b/docs/architecture/rfcs/ledger/shared-goal-authority-state-provider-v0/2026-09-24-observation-retirement.zh-CN.md @@ -0,0 +1,46 @@ +# 单一可写 shadow + +- 盘点基线:`d64c4d377`(`main`),2026-09-24。 +- 目标:#4574 R5/G2、shared authority §12 第 14 问、TS T2/T4。 +- 缺口:事务绑定的 outbox 已交付,但后提交观察器仍写另一份历史。 +- 结果:删除旧 writer,配置、CLI、runtime、设置页共同声明退役;保留历史读取,新链显式 bootstrap。 + +## 校准交付计划 + +不再沿用“5–8”或“7–9”。旧估算混合了未开发、已提 PR 和资格验收三个不同单位。 +本检查点规划 **四个新增实现批次,包括本次退役**;本 PR 之后仍规划 **三个明确批次**。 +这是开发计划,不是“恰好再合三次即可迁移所有 Goal”的保证。若集成发现实际缺陷, +应列出缺陷和新增边界,不能默默维持一个固定区间。 + +| 批次 | 可验收结果 | 当前边界 | +| --- | --- | --- | +| 退役重复观测(本 PR) | 不再二次采样和写另一份历史,旧开关不能启用,旧数据保留,新链必须 bootstrap。 | 独立收尾,不宣称切默认。 | +| 执行过程的租约保护 | 真实 Host 执行期间续租,失去执行权后取消;失效执行不能冒用新执行权结算。 | 当前读回只是时点证明,外部系统仍需自己的副作用防重与 fence。 | +| 事件捕获及整 Goal 迁移旅程 | 将事件发布接入来源锁和 outbox,验证混合写入、排空、评审晋升、消费与回退。 | 复用 #5003,真实绑定证明前保留拒绝条件。 | +| 默认入口及剩余 Python 业务退役 | 新 Goal、设置、安装一致选择已合格 profile;旧 Goal 显式迁移;删除已切换的业务 writer。 | 依赖集成与 D1–D3,不要求删除保留的 Python IO、渲染与合法导入导出。 | + +单独跟踪已有在途实现:#5006 完整来源传输、#5003 事件 Todo 原子完成、#4994 租约交接、 +#4995 Monitor proof、#4991 reservation 清理、#4992 回执绑定选择、#4931 SQLite 读取优化。 +不得重复开发。#4915 是目录迁移,不是 provider 默认选择。 + +#4967/#4968 来源组装与捕获投递、#4922 分页、#4888/#4920 晋升及排空计划、#4960 SQLite +准入、#4961 展示恢复已有实现。#5006 此时仍在途;本 PR 不重复拷贝其大来源传输改动。 + +SQLite D2 不是可随意换算成 PR 的单位。#4224 报告 1 MiB 回执/扫描预算失败及真实负载、 +RSS、恢复、restore、runtime、soak 缺失;#4931 修读取证明成本,不等于所有行通过。 +至少十天自然 soak、精确 profile 资格及 D3 cohort 批准仍独立存在。File 限域切换、 +SQLite 合格默认和所有旧 Goal 迁移不是同一验收范围。PostgreSQL 复用 typed 合同, +部署传输、租户策略、运维与服务资格仍属中期目标。 + +## 语义与证据 + +[操作说明](../../../../reference/authority-observation-retirement.md)覆盖拒绝、清理、 +bootstrap 与回退。主 Todo/租约规则、来源校验、事件 hold 和活跃绑定不变。中英文 +设置中旧入口只读,旧客户端写入前拒绝;旧 RPC 保留 typed 拒绝入口。历史 codec/read +保留,删除 Python 后采样/重试与 TS observation 提交。目录迁移结果明确不再 seed。 + +验证真实 CLI bootstrap、旧配置/无配置的租约写入、非法配置清理、历史读回,以及 +主文件替换后 committed marker 前进程死亡的单次恢复。保留现有事务、fence 与配置 +验证;已退役行为的旧测试替换为升级验收,不保留第二套实现。 +本机真实 Goal 经来源 witness 校验后复制到隔离 runtime,验证 Todo 添加、旧配置清理 +及历史字节不变;写入显式指向隔离 runtime,私有内容不发布。这不是整 Goal 迁移或 D2 证明。 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 27a9c7230c..068a10f930 100644 --- a/docs/architecture/rfcs/shared-goal-authority-state-provider-v0.md +++ b/docs/architecture/rfcs/shared-goal-authority-state-provider-v0.md @@ -36,6 +36,14 @@ are not a guaranteed total PR count. Use the [reconciled inventory and exits](le `e94759d88` adds [PostgreSQL service admission](../../reference/postgresql-authority-service-v0.md), with authentication/tenant verification injection and identity rotation. It is an in-process service boundary, not a deployed network service. The P lane should reuse it and finish transport, real identity policy, pool/cancellation/failover and operations qualification rather than rebuilding admission. R7 must separately report registration, active executors and measured capacity. Directory, presence, a plan or one source read grants no shared authority. Existing fail-closed source, receipt/replay and rollback contracts remain. +## Observation retirement checkpoint (2026-09-24) + +[Current delivery inventory](ledger/shared-goal-authority-state-provider-v0/2026-09-24-observation-retirement.md) +separates merged code, open PRs and qualification gates. This delivery removes +the obsolete Python observation writer and TS observation commit path; it does +not implement executor liveness or event-writer binding. There is one writable +shadow lineage, still default-off and subject to explicit bootstrap. + ## Current implementation checkpoint The current [event transaction and default-cutover plan](ledger/shared-goal-authority-state-provider-v0/2026-09-24-event-completion-transaction.md) estimates 5–8 complete packages conditionally. #4967 source assembly and #4968 capture delivery are already delivered; event-writer binding remains open, with atomic completion repaired here as a prerequisite. Earlier counts below describe historical checkpoints, not additional current work. @@ -45,8 +53,9 @@ after promotion. Legacy event-only claims reject rather than disappear at a Markdown boundary; event append locks protect the observation through writeback. Canonical changes reuse durable command receipt recovery. This is an L2/L3 compatibility correction with Python decision deletion, not cohort migration, -SQLite D2 completion or a default flip. Remaining work is classified in the current delivery frontier rather than -counted as unchanged packages. +SQLite D2 completion or a default flip. Remaining work depends on executor/consumer closure, qualification, +integrated migration and onboarding; it is classified in the current delivery frontier rather than counted as +unchanged packages, and historical package ranges are not a current PR count. [Operation, repair and recovery](../../reference/handoff-mode.md). The terminal caller family now binds review and validation to the canonical @@ -54,7 +63,8 @@ source and recovers historical receipts independently of private argv. Agent completion and Monitor stop share current-head display acknowledgement with ordinary edits. [Caller and recovery contract](../../reference/canonical-terminal-review.md). This advances L2/L5 without closing executor-held fences, D1–D3 or default -onboarding; use the current reconciled inventory for remaining work. +onboarding; use the current reconciled inventory for remaining work, and historical package estimates below are +superseded by the current inventory. The local registry witness now spans canonical create/claim/update/Monitor poll and terminal mutations through one TS owner. File, SQLite and service-injected @@ -1570,40 +1580,13 @@ that provider metadata is absent from the logical revision projection. This is Stage 1 parity evidence, not provider promotion or a claim that all later provider profiles are qualified. -#### Stage 2C observation foundation: local post-commit capture +#### Stage 2C observation foundation: retired -The first half of Stage 2C is an explicit, default-off product path. Preview -and enable it with: - -```bash -loopx configure-goal --goal-id GOAL --local-authority-shadow-file -loopx configure-goal --goal-id GOAL --local-authority-shadow-file --execute -``` - -Todo, handoff-mode, follow-up, and task-lease facades sample the full current -local projection after their primary write returns committed, then ask -`FileAuthorityStore` to retain that snapshot. `observation_trigger` records -why sampling began; it is not the primary transaction identity. A concurrent -primary commit may therefore appear in the sampled snapshot. A `captured` or -`replayed` result proves only the candidate-side observation commit. It does -not compare the source and candidate and carries `parity_verdict=not_evaluated`. - -Candidate bytes live under -`authority-shadow/file/` outside the legacy per-Goal runtime tree, so state -migration never copies a store identity or revision; an executed migration -seeds a new target lineage from the migrated local state. Candidate failure is -reported as an observation result but never reverses the completed local write. - -Disable the observer in one command with -`loopx configure-goal --goal-id GOAL --clear-local-authority-shadow --execute`. -This is rollback of observation only: the local Markdown and task-lease files -remain canonical throughout. The slice does not read the candidate for a -decision, fence a legacy writer, qualify a remote provider, or complete the -second Stage 2C promotion. A process crash after the local commit but before -the observer call may miss that individual observation; a later committed -write or migration seed refreshes the full current projection, but no durable -shadow outbox or transaction-correlated receipt is claimed here. This plumbing -is not parity evidence and cannot by itself support Stage 2C promotion. +The historical post-commit observer has been removed. Existing configuration +is readable but inactive, enable requests reject, and state migration no longer +seeds a second observation history. Retained files are not deleted. Use the +existing transaction-bound runtime shadow after explicit bootstrap; no old +observation is promoted into evidence. [Transition and compatibility](../../reference/authority-observation-retirement.md). #### Implementation prerequisite: put local file mode behind the same coordination contract @@ -2159,6 +2142,10 @@ shipped production capability. #### Stage-ladder end-to-end evidence (2026-09-03) +Historical delivery record: the observation-writer rows described below were +retired on 2026-09-24. The [current ladder](../../../examples/shared-goal-authority-e2e/README.md) +uses transaction-bound Stage 1 readback and explicit Stage 2C upgrade acceptance. + What exists on this branch is one incremental end-to-end "stage ladder" that exercises every completed stage claim of this RFC through the real `python -m loopx.cli` and reports a machine-checkable verdict per row: @@ -2431,22 +2418,13 @@ remain reviewable in the same bounded slice. the promotion PR; two local aggregate formats cannot both be canonical. Flipping the file profile's `qualification_holds` to `[]` and its `stage` literal happens only inside that PR.* -14. `main` now carries two default-off shadow lineages for the same writers: - the observation capture of #3818 (`coordination.authority_shadow`, - `authority-shadow/file/`, projection v0) and the runtime shadow - (`coordination.runtime_shadow`, `authority-shadow/file-v0`, projection v0 - with `inspect`, `qualify`, `bootstrap`, `rollback`, and `read-candidate`). - Both re-sample the source after the primary commit, so both share the - concurrent-writer and commit-to-dispatch loss windows that the review of - #3818 named. Which lineage is Stage 2C's, and what closes those windows? - *Proposed answer: the runtime shadow is the lineage, because the parity - report, bootstrap, quarantine rollback, read shape, and promotion kernel - already bind to it. The transaction-bound outbox of the parity half - (prepared entry inside the writer's own lock, committed marker after the - primary write, bounded drain with `operation_id = entry id`) becomes the - durable capture that feeds `coordination.runtime_shadow.commit`, and the - #3818 observation path retires once that capture is wired. The RFC must - not keep two shadow record formats.* +14. Which shadow lineage remains writable? *Resolved by this retirement: + `coordination.runtime_shadow` is the sole writable lineage. Its existing + transaction-bound outbox prepares under the source writer lock and drains + by stable entry identity. The old `coordination.authority_shadow` writer + is removed; historical records remain readable, but cannot qualify capture + or promotion. Unsupported event writers still fail closed until their own + transaction boundary is bound; retiring observation does not close that gap.* --- 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 104016d419..c0c46cf263 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 @@ -26,6 +26,13 @@ 已合入实现、六个相关在途 PR、四个拟新增批次(含当前完整来源传输)和 D1–D3 验收分开记录;四批不是承诺总计只剩四个 PR。唯一当前清单见[实现核对与退出证据](ledger/shared-goal-authority-state-provider-v0/2026-09-24-default-cutover-reconciliation.zh-CN.md)。 +## 旧观测退役检查点(2026-09-24) + +[当前交付清单](ledger/shared-goal-authority-state-provider-v0/2026-09-24-observation-retirement.zh-CN.md) +区分已合入、在途 PR 与资格证据。本次删除旧 Python observer 和 TS observation 提交链, +没有将其冒充 executor 存活保护或 event writer 绑定。只保留一个可写 shadow lineage, +仍然默认关闭,且必须显式 bootstrap。 + ## 管家规模化的持久化路线(2026-09-16) [统一路线](loopx-overall-roadmap-v0.zh-CN.md) R5 复用本 RFC 的 D1 投影、D2 真实 backend/容量/适用十日 soak、D3 fenced cutover;R6 再把所选 shared profile 接入认证的本地/云端路径。R1–R3 可在已支持 profile 上前进,不等待 PostgreSQL 或整 Goal 默认晋升。 @@ -39,7 +46,7 @@ 终结 caller 现将审核与验证绑定 canonical 来源,历史回执恢复不再依赖私有 argv。 Agent 完成和 Monitor 停止复用普通编辑的当前 head 显示确认。 [调用与恢复合同](../../reference/canonical-terminal-review.zh-CN.md)。此批推进 L2/L5, -未闭合 executor-held fence、D1–D3 或默认 onboarding;剩余工作使用当前核对表。 +未闭合 executor-held fence、D1–D3 或默认 onboarding;剩余工作使用当前核对表,下文历史批次估算不再作为当前剩余 PR 数量。 本地 registry witness 现经同一 TS owner 覆盖 canonical create/claim/update、 Monitor poll 与 terminal mutation;File、SQLite、service-injected PostgreSQL @@ -1239,35 +1246,12 @@ legacy-compatible、unknown-key、malformed-list、malformed-nested 与 non-stri 这些是 Stage 1 parity 证据,不代表 provider promotion,也不代表后续 provider profile 已经完成资格化。 -#### Stage 2C 观察基础:本地提交后 capture +#### Stage 2C 旧提交后观测:已退役 -Stage 2C 的前半段是一个显式开启、默认关闭的产品路径。先预览,再开启: - -```bash -loopx configure-goal --goal-id GOAL --local-authority-shadow-file -loopx configure-goal --goal-id GOAL --local-authority-shadow-file --execute -``` - -Todo、handoff-mode、follow-up 与 task-lease facade 会在本地主写返回成功后,采样 -完整当前本地投影,再让 `FileAuthorityStore` 保存该 snapshot。 -`observation_trigger` 只记录为何开始采样,不是主写 transaction identity;并发主写 -因此可能出现在该次 snapshot 中。`captured` 或 `replayed` 只证明候选侧 observation -commit,不表示已经对比 source 与 candidate;结果明确携带 -`parity_verdict=not_evaluated`。 - -候选数据位于 legacy 单 Goal runtime tree -之外的 `authority-shadow/file/`,因此 state migration 不会复制 store identity 或 -revision;真正执行迁移时,会从迁移后的本地主状态为目标端建立一条新 lineage。 -候选失败只形成 observation result,不会推翻已经完成的本地写入。 - -用 -`loopx configure-goal --goal-id GOAL --clear-local-authority-shadow --execute` -即可关闭 observer。这里回退的只是观察路径:Markdown 与 task-lease 文件始终是 -canonical。本切片不会读取候选来决策,不会 fence legacy writer,不会资格化远端 -provider,也没有完成 Stage 2C 后半段的本地 canonical promotion。若进程恰好在本地 -提交后、observer 调用前崩溃,该次 observation 可能丢失;后续成功写入或 migration -seed 会刷新完整当前投影,但这里不宣称已有 durable shadow outbox 或与主写 transaction -关联的 receipt。这套 plumbing 不是 parity evidence,不能单独支持 Stage 2C promotion。 +旧 observer 写入已删除。旧配置可识别但不再启用,重新启用请求被拒绝;目录迁移不再 +重新创建观测历史。保留文件不删除。需要捕获时显式配置并 bootstrap 现有事务绑定 +runtime shadow;旧 observation 不会升级为晋升证据。 +[操作与兼容](../../reference/authority-observation-retirement.md)。 #### 实施前置条件:先让本地文件模式经过同一协调合同 @@ -1707,6 +1691,10 @@ retention 决策,不能用一个会制造第二 writer 的诊断 CLI 代替。 #### Stage ladder 端到端证据(2026-09-03) +历史交付记录:下述旧 observation writer 验收行已于 2026-09-24 退役。 +[当前验收梯子](../../../examples/shared-goal-authority-e2e/README.md)改为事务捕获读回与 +显式升级验收;不要求退役 writer 继续写入。 + 本分支上存在一条增量式端到端 "stage ladder":它通过真实的 `python -m loopx.cli` 逐行演练本 RFC 每个已完成阶段的声明,并按行给出可机器 判定的结论:`loopx/control_plane/testing/authority_e2e_ladder.py`(行注册表、 @@ -1923,18 +1911,11 @@ decision authority,并且 caller-visible parity 与 rollback 能在同一有 CLI 之前保持 coverage-only,先把它们的场景电池移植为 TypeScript 测试,再在 promotion PR 中删除;两种本地 aggregate 格式不能同时为准。file profile 的 `qualification_holds` 翻为 `[]` 与 `stage` 字面量的改变只在该 PR 内发生。* -14. `main` 上现在有两条针对同一批写者的默认关闭 shadow lineage:#3818 的观察捕获 - (`coordination.authority_shadow`,`authority-shadow/file/`,投影 v0)与 - runtime shadow(`coordination.runtime_shadow`,`authority-shadow/file-v0`, - 投影 v0,带 `inspect`、`qualify`、`bootstrap`、`rollback`、`read-candidate`)。 - 两者都在主写提交之后重新采样源,因此都带着 #3818 评审点名的并发写者混入与 - commit 到 dispatch 之间的丢失窗口。哪条是 Stage 2C 的 lineage,什么来关闭这两 - 个窗口?*拟议答案:runtime shadow 是 lineage,因为 parity 报告、bootstrap、隔离 - 式 rollback、读形状与 promotion kernel 已经绑定在它上面。parity 半段的事务绑 - 定 outbox(写者在自己已持有的锁内写 prepared entry,主写返回后写 committed - 标记,有界 drain 以 `operation_id = entry id` 提交)成为喂给 - `coordination.runtime_shadow.commit` 的持久捕获;该捕获接线后 #3818 的观察路径 - 退役。RFC 不得保留两种 shadow 记录格式。* +14. 哪条 shadow lineage 保持可写?*本次退役收敛:唯一可写 lineage 为 + `coordination.runtime_shadow`,复用已有的来源锁内 prepare 与稳定 entry identity + 投递。删除旧 `coordination.authority_shadow` writer;历史记录仍可读,但不能证明 + 捕获或晋升资格。尚未绑定事务边界的事件 writer 继续拒绝,删除观察器不等于关闭 + 事件捕获缺口。* --- diff --git a/docs/architecture/rfcs/typescript-control-plane-migration-v0.md b/docs/architecture/rfcs/typescript-control-plane-migration-v0.md index 4fd6300b3b..0f9a74055d 100644 --- a/docs/architecture/rfcs/typescript-control-plane-migration-v0.md +++ b/docs/architecture/rfcs/typescript-control-plane-migration-v0.md @@ -21,6 +21,15 @@ Implemented code, six relevant open PRs, four proposed new batches (including complete-source transport) and D1–D3 evidence are separate units; four batches are not a guaranteed total PR count. Use the [reconciled inventory and exits](ledger/shared-goal-authority-state-provider-v0/2026-09-24-default-cutover-reconciliation.md) as the current plan. +## Observation writer retirement (2026-09-24) + +The obsolete Python post-commit observer and TS observation commit path are +removed together. Existing runtime-shadow outbox rules remain the sole capture +owner; source adapters do not resample into a second authority. Old settings +are recognizable, inactive and explicitly clearable. This is deletion of an +obsolete path, not a claim that remaining Python business writers or the +reference executor are retired. [Delivery inventory and transition](ledger/shared-goal-authority-state-provider-v0/2026-09-24-observation-retirement.md). + ## Cross-RFC execution priority (2026-09-16) [Roadmap](loopx-overall-roadmap-v0.md) R1–R5 are current product consumers of T0–T4, not another migration ladder. The team confirmation path now uses `work_items/team_plan.ts` for preview, batch planning and immutable operation identity, plus the existing AuthorityStore receipt/CAS boundary. Python retains public-safety and legacy Markdown IO adaptation; the per-lane writer loop is retired. The R1 checkpoint records the delivered assignment/retry delta and remaining receiver/execution boundaries. 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 26caadf357..306371f9ca 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 @@ -20,6 +20,13 @@ 已合入实现、六个相关在途 PR、四个拟新增批次(含当前完整来源传输)和 D1–D3 验收分开记录;四批不是承诺总计只剩四个 PR。唯一当前清单见[实现核对与退出证据](ledger/shared-goal-authority-state-provider-v0/2026-09-24-default-cutover-reconciliation.zh-CN.md)。 +## 旧观测写入退役(2026-09-24) + +同时删除旧 Python 提交后 observer 与 TS observation 提交链,保留现有事务 outbox +作为唯一捕获 owner;source adapter 不再二次采样生成另一份历史。旧配置可识别、 +不生效、可显式清理。这是删除已被替代的路径,不代表其余 Python 业务 writer 或 +reference executor 已退役。[交付清单与操作](ledger/shared-goal-authority-state-provider-v0/2026-09-24-observation-retirement.zh-CN.md)。 + ## canonical collection 分页检查点(2026-09-23) canonical collection 跨语言传输改为 TS 一致性分页:旧 direct list 和分页共用 diff --git a/docs/reference/authority-observation-retirement.md b/docs/reference/authority-observation-retirement.md new file mode 100644 index 0000000000..ce4f260bab --- /dev/null +++ b/docs/reference/authority-observation-retirement.md @@ -0,0 +1,73 @@ +# Retiring post-commit authority observation + +The `coordination.authority_shadow` / `file_one_way` observer is retired. +Todo, handoff-mode and task-lease writes no longer re-read the Goal after commit +or create a second observation store. Their primary semantics are unchanged. +The transaction-bound `coordination.runtime_shadow` outbox remains the only +writable shadow lineage; its existing bootstrap, source locks, byte witnesses, +recovery and promotion checks remain mandatory. + +This is an intentional behavior change for Goals that opted into the old +observer. Unconfigured Goals remain default-off. A retained old setting is +reported as `enabled=false, status=retired`; malformed settings remain `invalid` +and can also be cleared. The settings catalog shows a read-only retired entry, +without an enable action. A stale settings client or the old CLI enable flag +is rejected before registry writes. An old runtime `.record` RPC returns +`request_rejected / local_authority_shadow_retired` before opening any store. +It must not be retried as a transient storage error. + +## Operator transition + +Inspect before changing configuration: + +```bash +loopx authority-shadow status --goal-id GOAL +loopx configure-goal --goal-id GOAL --clear-local-authority-shadow +loopx configure-goal --goal-id GOAL --clear-local-authority-shadow --execute +``` + +The clear command removes only the obsolete setting. It neither deletes +retained observations nor changes an existing runtime-shadow capture binding, +provider default, lease, Todo or writer fence. `authority-shadow status` can +still read the retained observer while that old configuration is present and +no runtime-shadow lineage is selected. Explicit legacy-observation reads remain +available through the existing read contract. After clear, status selects the +runtime-shadow candidate; the historical files remain untouched. + +To begin transaction-bound capture, explicitly configure and bootstrap it: + +```bash +loopx configure-goal --goal-id GOAL --coordination-runtime-shadow-file +loopx configure-goal --goal-id GOAL --coordination-runtime-shadow-file --execute +loopx coordination-shadow bootstrap --goal-id GOAL --execute +loopx authority-shadow status --goal-id GOAL +loopx coordination-shadow inspect --goal-id GOAL +``` + +Bootstrap can reject unsupported sources, source drift or a previous binding. +Resolve the reported boundary; never copy observation records into the outbox +or remove a promotion fence. A configuration update does not prove bootstrap +or qualify promotion. Runtime-shadow disable/rollback continues to use its +existing management contract; clearing the retired setting is not a rollback +of an active runtime shadow. + +State-directory migration continues to exclude historical observation-store +identity and bytes. It no longer seeds a new observer at the destination. The +retained `authority_shadow_seeds` response field reports `outcome=retired` and +`attempted=false` in preview and execution, without auto-enabling a replacement. + +Retained data remains readable by its existing format. Reverting this code +can restart the old observer if its setting is still present, so clear the +setting on installations that must not resume observation after rollback. +No format rewrite or destructive data cleanup accompanies retirement. + +## Why two matching reads were not a capture guarantee + +The observer sampled after the writer released its lock. Another writer could +commit first, so the snapshot did not identify the triggering transaction. +A crash between the primary write and the observer could omit that transaction. +Two equal samples only showed temporary stability; they did not close either +window. The existing outbox instead prepares an entry under the source writer's +lock, marks durability, then delivers/reconciles that exact entry identity. +Retiring the observer removes repeated parsing, hashing, retries and a second +candidate history without discarding that stronger capture contract. diff --git a/examples/shared-goal-authority-e2e/README.md b/examples/shared-goal-authority-e2e/README.md index f72887e37e..a9ab0ea531 100644 --- a/examples/shared-goal-authority-e2e/README.md +++ b/examples/shared-goal-authority-e2e/README.md @@ -18,15 +18,15 @@ python examples/shared-goal-authority-e2e/ladder.py --list The pytest projection is `tests/control_plane/test_shared_goal_authority_e2e.py`; there, an unverified row skips as `unverified: ` and a POSIX-only row -skips on Windows. Five `s2c1.*` rows whose assertions -`tests/control_plane/test_local_authority_shadow_cli_e2e.py` already pins -through the same product path (configure round trip, default-off isolation, -candidate failure, crash gap, dual runtime root) are skipped in the default CI -projection to stay within the pytest job budget; `LOOPX_LADDER_FULL=1` runs -them in pytest, and the example runner always runs every row. The ten -`s2c2.*` rows carry the `stage2c_e2e` marker, so CI runs them in the stage2c -correctness job next to the other real-CLI, process-death and recovery -suites rather than in the pytest shards. +skips on Windows. The `s2c1.retired_observation_upgrade` row is also covered by +`tests/control_plane/test_local_authority_shadow_cli_e2e.py` and is skipped in +the default pytest projection; `LOOPX_LADDER_FULL=1` runs it there too. The +standalone example always runs selected rows. The eleven `s2c2.*` rows run in +the stage2c correctness job alongside real-CLI process-death and recovery suites. + +The seven former `s2c1.*` observation-writer rows are retired with that writer. +Historical reports retain their meaning; current runs validate explicit upgrade +and the transaction-bound outbox instead of requiring a second writable history. ## Rows @@ -34,16 +34,10 @@ suites rather than in the pytest shards. | --- | --- | --- | --- | --- | | `s0.file_matrix_twelve_rows` | 0 | store_direct | deterministic | `examples/nokv-shadow-provider/live_e2e.py` reports exactly the twelve known file-provider scenario rows, all true | | `s0.nokv_live_matrix` | 0 | store_direct | env:nokv_legacy | the same twelve rows plus `restored_lineage_fails_closed` are true on a live NoKV stack and file/NoKV outcomes are identical | -| `s1.cli_document_decodes_through_ts_store` | 1 | real_cli | deterministic | three CLI writes (`todo add`, `task-lease acquire`, `todo update`) read back through `FileAuthorityStore`: `loadAuthority` loaded at cursor `3`, paged `scanCommitted` yields the three `observation_id`s in order, `readReceipt` finds the first | +| `s1.cli_document_decodes_through_ts_store` | 1 | real_cli | deterministic | Explicit bootstrap plus three CLI writes load at cursor `4`; paged `scanCommitted` returns four distinct transactions in source order and `readReceipt` finds the first source write | | `s2a.nokv_live_qualification` | 2a | store_direct | env:nokv_authority | runs the merged `examples/nokv-authority-store/live-qualification.ts --execute-live` against an existing workbench with a fresh tenant/goal pair; requires `ok=true`, the single-node store-conformance scope, every check `passed`, NoKV SDK `0.11.1` / API `1`, the two stale-incarnation fence checks (`stale_incarnation_fence_rejected`, `stale_incarnation_fence_left_generation_unchanged`), and no promotion or availability claim; evidence carries check ids, counts, and config and workbench digest prefixes, never a configuration value or the workbench name | | `s2b.postgresql_conformance_live` | 2b | store_direct | env:postgresql | `postgresql_authority_store.integration.test.ts` under node's TAP reporter: `# pass >= 9`, `# fail 0`, `# skipped 0` | -| `s2c1.configure_enable_disable_roundtrip` | 2c1 | real_cli | deterministic | `configure-goal` preview does not write, enable writes, captured observations for a todo and a lease, read-back summary `enabled/file_one_way`, disable writes and later writes neither observe nor touch candidate bytes | -| `s2c1.every_writer_family_captures` | 2c1 | real_cli | deterministic | handoff-mode set, todo add/update/complete/supersede/archive-completed, task-lease acquire/renew/transfer each carry `outcome in {captured, replayed, ambiguous_reconciled}`, `primary_writeback_preserved=true`, `provider_to_local_writes=false`, `candidate_read_for_decision=false`; an idempotent re-acquire carries no `authority_shadow`; candidate `cursor == captured count`, operation ids equal observation ids, no time-active lease in the head, head todos equal `todo list` | -| `s2c1.default_off_isolation` | 2c1 | real_cli | deterministic | a default-off goal returns the same response fields as an observed goal, carries no `authority_shadow`, and creates no `authority-shadow/` directory | -| `s2c1.candidate_failure_preserves_primary` | 2c1 | real_cli | deterministic | a blocked candidate directory yields `outcome=failed`, `reason_code=shadow_observation_failed`, and the committed todo is in the primary state | -| `s2c1.crash_gap_loses_observation` | 2c1 | real_cli | deterministic (POSIX) | a writer SIGKILLed while the observation lock is held commits its todo but leaves no candidate document; the next write captures the full two-todo snapshot without claiming an outbox or correlation | -| `s2c1.dual_runtime_root_consistency` | 2c1 | real_cli | deterministic | with `common_runtime_root` different from `--runtime-root`, two todo adds, task-lease acquire, todo update, and a leased completion all observe into one store identity; the head holds both todos and the released lease; the registry root gains neither a candidate lineage nor lease state | -| `s2c1.migration_seeds_new_lineage` | 2c1 | real_cli | deterministic | `migrate-state` dry run plans the seed without writing; execute seeds one fresh `file:` lineage at cursor `1` that carries no legacy identity, revision, source path, or private byte | +| `s2c1.retired_observation_upgrade` | 2c1 | real_cli | deterministic | Old enable rejects without writes; retained settings create no history; explicit clear/configure/bootstrap captures the next source transaction | | `s2c2.outbox_prepared_then_committed_entries` | 2c2 | real_cli | deterministic | with the maintenance lock held, `todo add` (Python) and `task-lease acquire` (TypeScript) report `drain_deferred/drain_lock_busy`, `status` shows one `committed_pending` entry per partition with one prepared record and one committed marker on disk; one `drain` delivers both (`delivered=2`), history holds the bootstrap plus two committed receipts from both writer runtimes, and the next write delivers inline at cursor `4` | | `s2c2.drain_idempotent` | 2c2 | real_cli | deterministic | three deferred entries: `drain --max-entries 1` delivers one (`pending_after=2`, `budget_exhausted`), the next `drain` delivers two, an idle `drain` reports `nothing_pending` with unchanged cursor, `head_digest` and `provider_revision`; receipts settle sequences 1..3; an idempotent same-key re-acquire carries no capture evidence and adds no transaction | | `s2c2.sigkill_between_primary_write_and_drain` | 2c2 | real_cli | deterministic (POSIX) | `todo add` SIGKILLed at `before_replace`, `after_replace` and `before_marker` leaves one prepared-only entry each; `drain` settles it as `abandoned` (no-op, primary unchanged) or `committed_proven_by_readback`, the projection equals the primary, and `inspect` ends `matched` |