{locale === "zh-CN"
- ? "启用 canonical 创建后,新 Goal 直接建立权威存储,并采用所选执行策略;失败时须重试原创建操作。关闭时仅固定晋升后的目标存储。已有 Goal 的升级仍需备份、停止写入和结算租约;这里不授予工具权限。"
- : "With canonical creation enabled, new Goals establish authority with the selected execution policy; retry the original operation after failure. Disabled only freezes the post-promotion target. Existing Goals still require backup, stopped writers and settled leases for upgrade; this setting grants no tool permissions."}
+ ? "2.0 新建 Goal 必须启用 canonical 创建,并采用所选执行策略。旧的关闭设置不会自动改变;启用后再重试创建。已有 Goal 需备份、停止写入并导入;这里不迁移已有数据,也不授予工具权限。"
+ : "2.0 requires canonical creation with the selected execution policy. An older disabled setting is not changed automatically; enable it before retrying creation. Back up, stop writers and import existing Goals separately. This setting neither migrates existing data nor grants tool permissions."}
) : null}
diff --git a/docs/architecture/rfcs/typescript-control-plane-migration-v0.md b/docs/architecture/rfcs/typescript-control-plane-migration-v0.md
index 1ba703beeb..5a158be19e 100644
--- a/docs/architecture/rfcs/typescript-control-plane-migration-v0.md
+++ b/docs/architecture/rfcs/typescript-control-plane-migration-v0.md
@@ -127,6 +127,14 @@ The existing delivery Todos should carry these concrete exits:
This updates the existing R5/T4 frontier and release proposal; it creates no
parallel migration owner, runtime gate or release activation.
+The retirement proposal is now reconciled through contributor-led #6396 and
+its fork supplement #2. The [absorption map](../../reference/local-authority-provider-selection.md#retirement-proposal-reconciliation)
+retains canonical ownership/terminal invariants, fixed 1.x receipt recovery,
+reviewed App creation and the actual-contention mutation oracle from #6366.
+The competing implementation stack is not an integration prerequisite; adopted
+combined-head validation, core trial, real Host and D2/release exits stay open.
+
+
Team-plan recovery prerequisite: an unpromoted plan that published its original
receipt can now recover through the Chat confirmation owner while shadow
bootstrap is interrupted. Registry/Todo/source locks still protect the read,
diff --git a/docs/reference/local-authority-provider-selection.md b/docs/reference/local-authority-provider-selection.md
index 445144f1c4..f8a34101d0 100644
--- a/docs/reference/local-authority-provider-selection.md
+++ b/docs/reference/local-authority-provider-selection.md
@@ -186,8 +186,9 @@ layout or responsive navigation is introduced.
-To disable future canonical creation, preview and apply the same v1 document
-with `canonical_creation=false`. Removing the preference restores the candidate
+In 2.0, `canonical_creation=false` preserves an inspectable preference but
+refuses new Goal creation. Enable canonical creation and review the preview
+before retrying. Removing the preference restores the candidate
SQLite/hard-lease default for future new Goals; removal is **not** opt-out.
To remove the whole preference:
@@ -247,7 +248,8 @@ CLI 使用上面的完整 JSON 和 preview/apply/inspect/bootstrap 命令;App
后的存储丢失须恢复完整备份,不能重新创建空库;通用 force 不能重建。原回执在
后续写入后仍可读回,重试不得丢失或重复 Todo。
-显式设置 `canonical_creation=false` 才关闭之后的新建;remove 删除偏好会恢复候选
+2.0 中,显式 `canonical_creation=false` 保留可查看的偏好,但拒绝之后的新建;
+须启用并审核配置,再重试创建。remove 删除偏好会恢复候选
SQLite/hard-lease 默认,不能用于关闭。两者均不会迁回已有数据、
删除 fence 或重新开放旧 writer。既有 Goal 升级仍需备份、停止写入、结算租约和
审核计划;反向迁移须保留新增写入。受支持的旧备份、格式和原回执恢复能力保留。
@@ -529,6 +531,29 @@ D2 capacity/soak qualification; missing or failed D2 evidence stays visible.
A microbenchmark failure alone does not block an unrelated, proven internal
deletion, and passing those deletions does not certify the release default.
+### Retirement proposal reconciliation
+
+The contributor-led [#6396](https://github.com/loopx-project/loopx/pull/6396)
+and [supplement #2](https://github.com/Duang777/loopx/pull/2) carry the useful
+contracts from [#6366](https://github.com/loopx-project/loopx/pull/6366).
+Reconciliation follows supported behavior, rather than preserving every old
+module, response alias or test filename:
+
+| Contract | Current owner and regression evidence |
+| --- | --- |
+| Normal writer cutoff and deletion | Canonical mutation/creation/Monitor adapters; cold-source import and writer-isolation tests. Markdown mutation producers stay retired. |
+| Imported completion, succession and policy repair | Typed terminal/update owners; shared provider conformance and actual CLI consumer smokes in the supplement. |
+| Claim/lease ownership and atomic completion | `test_canonical_handoff_invariants.py`, canonical mutation/claim-transfer tests and provider conformance. Metadata cannot implement the retired two-write handoff. |
+| Original-operation recovery | Fixed 1.x lease and team-plan artifacts guard exact replay, prepared matching effects, original release, lost close response and preservation of later state. Fresh legacy verification/grants remain retired. |
+| Reviewed App creation and configuration | Chat binds the accepted operation to the reviewed provider revision and Todo identity; stale preview rejects, corrected preview can retry. Settings preserve a disabled preference and explain the required upgrade. |
+| Mutation-oracle strength and packaging | The fence oracle observes actual exclusive-open contention, including delayed startup. Existing prerelease bundle and installed-package checks retain their independent scope. |
+
+The absorption does not qualify a release, contributor adoption, sustained D2
+operation or an unobserved Host. Full current-head CI and the combined adopted
+candidate retain their own acceptance. Historical output aliases and tests
+whose only subject was a deleted producer are superseded by the typed current
+contract and its supported recovery tests.
+
### 2.0 退役边界
正常写入现在只接受 canonical authority;旧 writer、producer 和无调用内部桥接
diff --git a/loopx/capabilities/configuration_ui.py b/loopx/capabilities/configuration_ui.py
index df00f6677a..c1850581a5 100644
--- a/loopx/capabilities/configuration_ui.py
+++ b/loopx/capabilities/configuration_ui.py
@@ -100,7 +100,7 @@ def capability_configuration_editor(
options=["file", "sqlite"], required=True,
description="Fixed at creation. Existing Goals need a separate backed-up migration."),
_field("canonical_creation", "Create canonical authority", "boolean", required=True,
- description="Enabled by default for new Goals. Disabled retains the post-promotion target behavior."),
+ description="Required for new Goals in 2.0. Enable before creating a Goal; import existing Markdown Goals separately."),
_field("new_goal_handoff_mode", "New Goal execution policy", "select",
options=["soft_claim", "hard_lease"], required=True,
description="Used only with canonical creation. Agents inherit the Goal policy; this grants no tool permissions.")],
From 1c9547cc104cb1b82ea25cf5a4cc4bd4efd17e07 Mon Sep 17 00:00:00 2001
From: LoopX Agent <337587101+loopx-agent@users.noreply.github.com>
Date: Thu, 15 Oct 2026 13:16:39 +0800
Subject: [PATCH 30/38] fix(authority): retire legacy execution policy and
source lease grants
Signed-off-by: LoopX Agent <337587101+loopx-agent@users.noreply.github.com>
---
loopx/collaboration_mcp.py | 32 +++----
.../coordination/handoff_mode_policy.ts | 10 +-
.../coordination/lease_acquisition_proof.ts | 9 +-
.../coordination/task_lease_lifecycle.ts | 5 +-
.../coordination/task_lease_proof.ts | 3 +
.../control_plane/coordination/todo_claim.ts | 4 +
.../coordination/todo_lifecycle_decision.ts | 6 +-
.../coordination/todo_monitor_cycle.ts | 6 +-
.../coordination/todo_terminal_lifecycle.ts | 4 +-
.../coordination/todo_update_admission.ts | 3 +
loopx/control_plane/todos/handoff_mode.py | 42 ++++-----
.../work_items/task_lease_acquire.ts | 9 +-
.../work_items/task_lease_acquire_adapter.py | 18 +---
.../work_items/task_lease_acquire_decision.ts | 11 ++-
.../work_items/task_lease_inspection.ts | 19 ++--
.../task_lease_lifecycle_decision.ts | 11 ++-
.../test_canonical_handoff_mode.py | 91 +++++++++++++++++++
.../test_canonical_lease_inspection.py | 19 ++--
.../control_plane/test_chat_goal_ownership.py | 4 +
.../authority_projection_fixture.ts | 3 +
.../authority_store_conformance.ts | 51 ++++++++++-
.../command_observation_scenarios.ts | 2 +-
.../coordination_command_fixture.ts | 2 +-
...gresql_authority_store.integration.test.ts | 2 +-
.../production_scale_coordination_fixture.ts | 4 +-
.../task_lease_eligibility.test.ts | 11 ++-
.../task_lease_inspection.test.ts | 30 ++++--
.../todo_terminal_decision.test.ts | 29 +++++-
tests/test_delegation_lease_lifetime.py | 36 ++++++++
29 files changed, 355 insertions(+), 121 deletions(-)
diff --git a/loopx/collaboration_mcp.py b/loopx/collaboration_mcp.py
index e23b40fbc8..5a754dbbdc 100644
--- a/loopx/collaboration_mcp.py
+++ b/loopx/collaboration_mcp.py
@@ -35,7 +35,6 @@
effect_runtime_request_scope, effect_runtime_result, EffectRuntimeRemoteError,
)
from .control_plane.coordination.local_authority import local_authority_is_promoted
-from .control_plane.todos.handoff_mode import show_goal_handoff_mode
from .control_plane.turn_driver.journal_store import (
load_turn_journal,
turn_journal_path,
@@ -1535,20 +1534,16 @@ def _acquire_delegation_lease(
) -> dict:
"""Acquire the promoted hard lease before worker or completion effects."""
- if not local_authority_is_promoted(
- runtime_root=self.root,
- goal_id=self.goal_id,
- ):
- if row.get("task_lease", {}).get("required") is True:
- raise ValueError("delegation canonical authority disappeared; reconcile the original execution")
- row["task_lease"] = {"required": False, "handoff_mode": "legacy"}
- self._fenced_write(path, row)
- return row["task_lease"]
- handoff_mode = show_goal_handoff_mode(
- registry_path=self.registry,
- runtime_root_arg=str(self.root),
- goal_id=self.goal_id,
- )["handoff_mode"]
+ from .control_plane.work_items.task_lease import inspect_task_lease
+
+ observed = inspect_task_lease(registry_path=self.registry, runtime_root=self.root,
+ goal_id=self.goal_id, todo_id=binding["todo_id"])
+ support = observed.get("execution_policy")
+ if not isinstance(support, dict) or type(support.get("supported")) is not bool:
+ raise RuntimeError("lease inspection omitted typed execution-policy support")
+ if not support["supported"]:
+ raise ValueError(str(support.get("reason") or "current execution policy is unavailable"))
+ handoff_mode = observed["handoff_mode"]
if handoff_mode != "hard_lease":
if row.get("task_lease", {}).get("required") is True:
raise ValueError("delegation authority mode changed; reconcile the original execution")
@@ -1707,9 +1702,10 @@ def _execute(self, path: Path, row: dict, binding: dict) -> None:
self._write_delegation_bootstrap(row, binding)
self._acquire_delegation_lease(path, row, binding)
self._observe(path, row, "running")
- if row.get("task_lease", {}).get("required") is True and "lease" not in row["task_lease"]:
- # Upgrade a still-current operation record by replaying its
- # original claim, not by inventing a replacement execution.
+ elif row["status"] == "running":
+ # Resumption rechecks typed policy support even for old records
+ # that said no lease was required. Hard mode replays its original
+ # claim proof; it cannot invent a replacement execution.
self._acquire_delegation_lease(path, row, binding)
if row["status"] == "running":
self._raise_if_stop_requested(path)
diff --git a/loopx/control_plane/coordination/handoff_mode_policy.ts b/loopx/control_plane/coordination/handoff_mode_policy.ts
index c768eca892..89ee2b30a6 100644
--- a/loopx/control_plane/coordination/handoff_mode_policy.ts
+++ b/loopx/control_plane/coordination/handoff_mode_policy.ts
@@ -1,4 +1,4 @@
-/** One quiescence rule for the legacy adapter and canonical mode transaction. */
+/** One policy owner: two current execution modes, historical mode decoding and migration. */
import type {JsonObject} from "../effect_program.ts";
import {requireJsonObject, requireStringArray, requireStringLiteral} from "../runtime_decode.ts";
@@ -8,6 +8,14 @@ export const HANDOFF_MODE_PLAN_SCHEMA = "loopx_handoff_mode_plan_request_v0";
import type {PersistedHandoffMode} from "./handoff_mode_facts.ts";
+export const HANDOFF_MODE_RETIRED_REASON = "Legacy ownership is historical only. Release retained execution, then review handoff-mode plan-migration/migrate to soft_claim or hard_lease; policy migration does not require another storage promotion.";
+
+/** Historical mode is readable and releasable, never present-tense execution.
+ * Receipt recovery runs before fresh admission; readback must not turn it into a grant. */
+export function retiredHandoffModeRejection(mode: unknown): "handoff_mode_retired" | null {
+ return mode === "legacy" ? "handoff_mode_retired" : null;
+}
+
export function decideHandoffMode(previous: PersistedHandoffMode, requested: HandoffMode,
claims: number, leases: number): JsonObject {
if (requested === "legacy") return {outcome: "rejected", code: "handoff_mode_retired",
diff --git a/loopx/control_plane/coordination/lease_acquisition_proof.ts b/loopx/control_plane/coordination/lease_acquisition_proof.ts
index 0dd9082ea8..280888f173 100644
--- a/loopx/control_plane/coordination/lease_acquisition_proof.ts
+++ b/loopx/control_plane/coordination/lease_acquisition_proof.ts
@@ -6,7 +6,7 @@ import type {AuthorityStore, AuthorityStoreLoadResult} from "./authority_store.t
import {canonicalAuthorityObject} from "./authority_store_codec.ts";
import {indexCoordinationProjection, validateCoordinationTodoReadModel} from "./coordination_projection.ts";
import {canonicalTaskLease, canonicalTaskLeaseAcquireFacts} from "./task_lease_state.ts";
-import {HANDOFF_MODES} from "./handoff_mode_policy.ts";
+import {HANDOFF_MODES, retiredHandoffModeRejection, HANDOFF_MODE_RETIRED_REASON} from "./handoff_mode_policy.ts";
import {requireStringLiteral} from "../runtime_decode.ts";
import {leaseOwnerRejection} from "../work_items/task_lease_eligibility.ts";
import {leaseEpoch, leaseVersion, leaseIsActive} from "../work_items/task_lease_acquire.ts";
@@ -48,10 +48,11 @@ export async function currentLeaseAcquisitionProof(store: Auth
const current = facts.current;
const details = {handoff_mode: mode, original_receipt: result.original_receipt,
current_provider_revision: head.provider_revision, current_cursor: head.cursor};
- const rejection = mode === "soft_claim" ? "handoff_mode_forbids_lease"
+ const rejection = retiredHandoffModeRejection(mode) ?? (mode === "soft_claim" ? "handoff_mode_forbids_lease"
: input.required_handoff_mode !== undefined && mode !== input.required_handoff_mode ? "claim_lease_requires_hard_lease"
- : leaseOwnerRejection(facts.todo, input.owner, input.registered_agents);
- if (rejection) return failed(rejection, `current authority rejects lease acquire replay: ${rejection}`, details);
+ : leaseOwnerRejection(facts.todo, input.owner, input.registered_agents));
+ if (rejection) return failed(rejection, rejection === "handoff_mode_retired" ? HANDOFF_MODE_RETIRED_REASON
+ : `current authority rejects lease acquire replay: ${rejection}`, details);
if (!current || !leaseIsActive(current, input.now) || current.owner !== input.owner ||
current.idempotency_key !== input.idempotency_key || leaseEpoch(current) !== leaseEpoch(original) ||
leaseVersion(current) < leaseVersion(original)) {
diff --git a/loopx/control_plane/coordination/task_lease_lifecycle.ts b/loopx/control_plane/coordination/task_lease_lifecycle.ts
index 07e27cdcba..0719fd9b13 100644
--- a/loopx/control_plane/coordination/task_lease_lifecycle.ts
+++ b/loopx/control_plane/coordination/task_lease_lifecycle.ts
@@ -11,7 +11,7 @@ import {planLeaseClaimTransfer} from "./lease_claim_transfer.ts";
import {decideTaskLeaseLifecycle, materializeTaskLeaseLifecycle,
type TaskLeaseLifecycleDecisionOperation, type TaskLeaseLifecycleDecisionCommand} from "../work_items/task_lease_lifecycle_decision.ts";
import {requireStringLiteral} from "../runtime_decode.ts";
-import {HANDOFF_MODES} from "./handoff_mode_policy.ts";
+import {HANDOFF_MODES, retiredHandoffModeRejection, HANDOFF_MODE_RETIRED_REASON} from "./handoff_mode_policy.ts";
import {leaseEpoch, leaseIsActive, leaseVersion, normalizeAgent, normalizeGoalId,
normalizeIdempotencyKey, normalizeOwner, normalizeTodoId,
normalizeTtl, utcIsoformat, TaskLeaseAcquireError, type LeaseRecord} from "../work_items/task_lease_acquire.ts";
@@ -112,6 +112,9 @@ export async function executeCanonicalTaskLeaseLifecycle(store: AuthorityStore,
const current = await store.loadAuthority();
if (current.status !== "loaded") throw new AuthorityStoreProtocolError("current lease authority unavailable");
validateCoordinationTodoReadModel(current.head, input.goal_id);
+ const retired = retiredHandoffModeRejection(current.head.handoff_mode ?? "legacy");
+ if (retired !== null) return {...failed(retired, HANDOFF_MODE_RETIRED_REASON),
+ original_receipt: result.original_receipt};
const dependency = todoExecutionDependencyRejection(
indexCoordinationProjection(current.head, input.goal_id).todos, input.todo_id, input.now);
return dependency === null ? result : {...failed(dependency.code, dependency.reason),
diff --git a/loopx/control_plane/coordination/task_lease_proof.ts b/loopx/control_plane/coordination/task_lease_proof.ts
index 0489444603..bf6ea682c0 100644
--- a/loopx/control_plane/coordination/task_lease_proof.ts
+++ b/loopx/control_plane/coordination/task_lease_proof.ts
@@ -1,5 +1,6 @@
/** Current nonterminal mutation proof. Admission stays in the lifecycle owner;
* this boundary never acquires, renews, releases or transfers a lease. */
+import {retiredHandoffModeRejection} from "./handoff_mode_policy.ts";
import type {JsonObject} from "../effect_program.ts";
import {requireJsonObject} from "../runtime_decode.ts";
import {EffectRuntimeRequestError} from "../effect_runtime_errors.ts";
@@ -70,6 +71,8 @@ export function evaluateCanonicalTaskLeaseProof(input: {
actor_agent_id: string | null; registered_agents: readonly string[];
lease_idempotency_key: string | null; lease_expected_version: number | null; now: Date;
}) {
+ const retired = retiredHandoffModeRejection(input.handoff_mode);
+ if (retired !== null) return {outcome: "rejected", code: retired};
const {lease} = input;
if (!(input.now instanceof Date) || !Number.isFinite(input.now.valueOf())) {
throw new EffectRuntimeRequestError("lease proof requires a valid transaction clock");
diff --git a/loopx/control_plane/coordination/todo_claim.ts b/loopx/control_plane/coordination/todo_claim.ts
index 2ecd77edaa..1bebb93f6c 100644
--- a/loopx/control_plane/coordination/todo_claim.ts
+++ b/loopx/control_plane/coordination/todo_claim.ts
@@ -1,3 +1,4 @@
+import {retiredHandoffModeRejection, HANDOFF_MODE_RETIRED_REASON} from "./handoff_mode_policy.ts";
import {AUTHORITY_SOURCE_CHANGED, uncheckedAuthoritySource, type AuthoritySourceCheck} from "./authority_source.ts";
import {currentLeaseAcquisitionProof} from "./lease_acquisition_proof.ts";
import {canonicalTaskLeaseAcquireFacts} from "./task_lease_state.ts";
@@ -526,6 +527,9 @@ async function executeClaimAttempt(
if (!["legacy", "soft_claim", "hard_lease"].includes(handoffMode)) {
return failure("invalid_handoff_mode", "canonical projection has an invalid handoff mode");
}
+ const retired = retiredHandoffModeRejection(handoffMode);
+ if (retired !== null) return failure(retired, HANDOFF_MODE_RETIRED_REASON,
+ {todo_id: input.todo_id, handoff_mode: handoffMode}, "decision_rejection");
if (leaseRequest !== null && handoffMode !== "hard_lease") {
return failure(
"claim_lease_requires_hard_lease",
diff --git a/loopx/control_plane/coordination/todo_lifecycle_decision.ts b/loopx/control_plane/coordination/todo_lifecycle_decision.ts
index 2e65c6dab7..1a0d63bc71 100644
--- a/loopx/control_plane/coordination/todo_lifecycle_decision.ts
+++ b/loopx/control_plane/coordination/todo_lifecycle_decision.ts
@@ -27,7 +27,7 @@ export const COORDINATION_TERMINAL_FENCE_REQUEST_SCHEMA =
"loopx_coordination_terminal_fence_request_v0";
export const COORDINATION_TERMINAL_FENCE_RESULT_SCHEMA =
"loopx_coordination_terminal_fence_result_v0";
-import {HANDOFF_MODES, type HandoffMode} from "./handoff_mode_policy.ts";
+import {HANDOFF_MODES, retiredHandoffModeRejection, type HandoffMode} from "./handoff_mode_policy.ts";
const OUTCOMES = ["approve", "reject", "cancel"] as const;
const AUTHORITY_ACTIONS = ["complete", "reassign", "supersede", "update"] as const;
const EXECUTOR_RECLAIM_ACTION = "reclaim";
@@ -385,6 +385,8 @@ function authority(request: LifecycleDecisionRequest):
| { mode: string; ownershipGate: CoordinationTodoTerminalDecisionResult["ownership_gate"] }
| CoordinationTodoTerminalDecisionResult {
const { todo, actor_agent_id: actor, registered_agents: registered } = request;
+ const retired = retiredHandoffModeRejection(request.handoff_mode);
+ if (retired !== null) return result("rejected", retired);
if (registered.length <= 1) {
if (actor !== null) {
const rejection = registeredTodoMutationRejection(todo, actor, registered);
@@ -443,6 +445,8 @@ function terminalFence(
authorityMode: string | null,
requireActiveWhenFenceSupplied = true,
): CoordinationTodoTerminalDecisionResult {
+ const retired = retiredHandoffModeRejection(request.handoff_mode);
+ if (retired !== null) return result("rejected", retired);
const lease = request.lease;
const timeActive = lease !== null && lease.present && lease.active;
const effective = timeActive && ownerEligible(request, lease.owner);
diff --git a/loopx/control_plane/coordination/todo_monitor_cycle.ts b/loopx/control_plane/coordination/todo_monitor_cycle.ts
index c62571cac8..fd8f42516f 100644
--- a/loopx/control_plane/coordination/todo_monitor_cycle.ts
+++ b/loopx/control_plane/coordination/todo_monitor_cycle.ts
@@ -2,7 +2,7 @@
* Admission and observation validation run first; all effects join their CAS. */
import {evaluateCoordinationTodoMutationDecision, COORDINATION_TODO_MUTATION_DECISION_REQUEST_SCHEMA} from "./todo_lifecycle_decision.ts";
import {evaluateCanonicalTaskLeaseProof, type TaskLeaseProof} from "./task_lease_proof.ts";
-import {HANDOFF_MODES} from "./handoff_mode_policy.ts";
+import {HANDOFF_MODES, HANDOFF_MODE_RETIRED_REASON} from "./handoff_mode_policy.ts";
import {requireStringLiteral} from "../runtime_decode.ts";
import type {JsonObject} from "../effect_program.ts";
import type {CoordinationProjectionMutation} from "./coordination_projection.ts";
@@ -62,7 +62,8 @@ export function monitorMutationRejection(input: {
});
if (decision.outcome !== "apply") {
return reject(decision.code === "claim_owner_mismatch" ? "update_owner_mismatch" : String(decision.code),
- "Monitor observation is outside the actor's registered owner/binding scope");
+ decision.code === "handoff_mode_retired" ? HANDOFF_MODE_RETIRED_REASON
+ : "Monitor observation is outside the actor's registered owner/binding scope");
}
if (input.operation === "reactivate") {
return input.proof === null ? null : reject("monitor_reactivation_execution_proof_not_allowed",
@@ -76,7 +77,6 @@ export function monitorMutationRejection(input: {
}
return executionWait();
}
- if (lease === undefined && mode !== "hard_lease" && input.proof === null) return executionWait();
const fence = evaluateCanonicalTaskLeaseProof({todo, lease, handoff_mode: mode,
actor_agent_id: actor, registered_agents: registered, now: input.now,
lease_idempotency_key: input.proof?.idempotency_key ?? null,
diff --git a/loopx/control_plane/coordination/todo_terminal_lifecycle.ts b/loopx/control_plane/coordination/todo_terminal_lifecycle.ts
index d5b06014cd..9a5656266e 100644
--- a/loopx/control_plane/coordination/todo_terminal_lifecycle.ts
+++ b/loopx/control_plane/coordination/todo_terminal_lifecycle.ts
@@ -1,3 +1,4 @@
+import {HANDOFF_MODE_RETIRED_REASON} from "./handoff_mode_policy.ts";
import {planUserCompletion, requireCompletionDecisionOutcome} from "../todos/user_completion.ts";
import {AUTHORITY_SOURCE_CHANGED, uncheckedAuthoritySource, type AuthoritySourceCheck} from "./authority_source.ts";
import {
@@ -1358,7 +1359,8 @@ export async function executeCoordinationTodoTerminalLifecycle(
if (authority.outcome === "rejected" || authority.outcome === "conflict") {
return terminalFailure(
authority.code,
- `canonical Todo terminal lifecycle rejected the request: ${authority.code}`,
+ authority.code === "handoff_mode_retired" ? HANDOFF_MODE_RETIRED_REASON
+ : `canonical Todo terminal lifecycle rejected the request: ${authority.code}`,
{terminal_decision: authority},
"decision_rejection",
);
diff --git a/loopx/control_plane/coordination/todo_update_admission.ts b/loopx/control_plane/coordination/todo_update_admission.ts
index 4f2b41a056..979376f3ed 100644
--- a/loopx/control_plane/coordination/todo_update_admission.ts
+++ b/loopx/control_plane/coordination/todo_update_admission.ts
@@ -1,6 +1,7 @@
/** Admission for Todo edits; terminal completion retains its own lease proof.
* Grants may cross a claim owner;
* exclusions, bindings and execution lineage remain independent restrictions. */
+import {retiredHandoffModeRejection, HANDOFF_MODE_RETIRED_REASON} from "./handoff_mode_policy.ts";
import {leaseRepositoryRejection} from "../work_items/task_lease_repository.ts";
import {isExploreReferenceAppend} from "../todos/field_update.ts";
import {acceptanceRestoration} from "./todo_acceptance_restoration.ts";
@@ -49,6 +50,8 @@ export function todoUpdateAdmissionRejection(
if (typeof mode !== "string" || !["legacy", "soft_claim", "hard_lease"].includes(mode)) {
return reject("invalid_handoff_mode", "canonical handoff mode is invalid");
}
+ const retired = retiredHandoffModeRejection(mode);
+ if (retired !== null) return {...reject(retired, HANDOFF_MODE_RETIRED_REASON), handoff_mode: mode};
const intent = input.planning_intent ?? {};
const ownershipMutation = ["claimed_by", "clear_claim", "excluded_agents", "bound_agent",
"goal_bound", "blocks_agent", "clear_blocks_agent", "global_gate", "clear_global_gate"]
diff --git a/loopx/control_plane/todos/handoff_mode.py b/loopx/control_plane/todos/handoff_mode.py
index 353fa78764..9732b2613b 100644
--- a/loopx/control_plane/todos/handoff_mode.py
+++ b/loopx/control_plane/todos/handoff_mode.py
@@ -1,30 +1,22 @@
"""Per-goal handoff mode: which ownership authority governs todo handoffs.
-Before promotion the active-state frontmatter declares ``handoff_mode``; afterward
-the selected canonical provider owns it. Public show/set route by that authority:
-
-* absent / ``legacy`` (default): today's dual soft-claim + hard-lease
- behavior, byte-for-byte. The known soft-claim/hard-lease split brain stays
- open in this mode by design; it is surfaced additively, never silently
- repaired.
-* ``soft_claim``: the Todo claim is the only ownership record. Task-lease
- acquire/renew/transfer are typed-rejected; release and inspect stay allowed
- for cleanup and observability of legacy leftovers.
-* ``hard_lease``: ownership changes on an existing todo require the acting
- agent to hold that todo's time-active task lease, and the completion fence
- becomes mandatory for both user-role and agent-role todos. An exact linked
- user-gate decision-scope override authorizes only the exact linked decision
- transition; it does not bypass the completion fence. The delegated
- ``coordination.todo_lifecycle_authority`` override is the one audited door
- through the gate.
-
-Canonical mode, complete Todo/lease quiescence, CAS and replay share one TypeScript
-transaction. Stale or missing Markdown and local lease files are not fallback
-sources. The legacy mode below remains a frontmatter compatibility contract.
-
-Unpromoted transitions read complete Markdown Todos and local leases under
-their writer mutexes. The same typed quiescence rule governs both paths;
-nonempty retired Todo event sources are refused, never treated as empty.
+The canonical provider owns the current policy; Markdown remains a historical
+source. Current execution uses only ``soft_claim`` or ``hard_lease``:
+
+* ``legacy`` (including an absent historical field) is decodable for import,
+ original receipts and exact lease cleanup. It cannot admit new execution or
+ turn a historical lease receipt into current proof. Existing Goals require
+ a reviewed, backed-up policy migration; their bytes are never silently changed.
+* ``soft_claim`` uses the Todo assignment. New lease acquire/renew/transfer are
+ rejected; retained release and inspection remain available.
+* ``hard_lease`` uses the current execution proof and typed ownership/terminal
+ rules. Claims, delegated actions and linked user decisions do not independently
+ grant a lease or bypass the transaction's fence.
+
+TypeScript owns policy admission, quiescence, migration, CAS and receipt recovery.
+Python retains source decoding, transport and public result adaptation. A missing
+or stale display never replaces canonical authority; unpromoted writes refuse.
+
"""
from __future__ import annotations
diff --git a/loopx/control_plane/work_items/task_lease_acquire.ts b/loopx/control_plane/work_items/task_lease_acquire.ts
index b4fc2cf9dd..b66c5781fc 100644
--- a/loopx/control_plane/work_items/task_lease_acquire.ts
+++ b/loopx/control_plane/work_items/task_lease_acquire.ts
@@ -1,3 +1,4 @@
+import {HANDOFF_MODES, type HandoffMode} from "../coordination/handoff_mode_vocabulary.ts";
import {LeaseWorktreeObservationError, observeLeaseWorktree, type LeaseWorkspace} from "./task_lease_workspace.ts";
import {isCanonicalTodoId} from "../todos/identity.ts";
import {executeCanonicalTaskLeaseAcquire} from "../coordination/task_lease_acquire.ts";
@@ -386,16 +387,16 @@ export function decodeTaskLeaseAuthority(value: unknown): AuthorityFacts {
};
}
-export function normalizeHandoffMode(value: unknown): string {
+export function normalizeHandoffMode(value: unknown): HandoffMode {
const mode = compact(value) || "legacy";
- if (!new Set(["legacy", "soft_claim", "hard_lease"]).has(mode)) {
+ if (!HANDOFF_MODES.some(candidate => candidate === mode)) {
throw new TaskLeaseAcquireError(
`unsupported handoff_mode '${mode}'; expected one of: legacy, soft_claim, hard_lease`,
"invalid_handoff_mode",
- { handoff_mode: mode, supported: ["legacy", "soft_claim", "hard_lease"] },
+ { handoff_mode: mode, supported: [...HANDOFF_MODES] },
);
}
- return mode;
+ return mode as HandoffMode;
}
function decodeRequest(value: unknown): AcquireRequest {
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 dfe42fb58d..11326a02f7 100644
--- a/loopx/control_plane/work_items/task_lease_acquire_adapter.py
+++ b/loopx/control_plane/work_items/task_lease_acquire_adapter.py
@@ -677,15 +677,14 @@ def inspect_native_task_lease(
for attempt in range(TASK_LEASE_AUTHORITY_SNAPSHOT_ATTEMPTS):
canonical = local_authority_is_promoted(runtime_root=runtime_root, goal_id=goal_id)
- # TS alone decides whether the retained lease needs current Todo facts.
- # Inactive legacy inspection must not parse the full work document.
+ # Source records are retained history, never a current execution policy.
+ # No retired Todo projection is needed to report them read-only.
authority = _canonical_lease_authority_facts(registry_path, goal_id) if canonical else task_lease_acquire_authority_facts(
registry_path=registry_path, goal_id=goal_id, todo_id=todo_id, project_todos=False,
)
request = {
"schema_version": "loopx_task_lease_inspect_request_v0",
"source": "canonical" if canonical else "legacy",
- "phase": "effective_lease" if canonical else "lease_record",
"runtime_root": str(runtime_root.resolve()), "goal_id": goal_id,
"todo_id": todo_id, "authority": authority,
}
@@ -694,19 +693,6 @@ def inspect_native_task_lease(
timeout=(CANONICAL_AUTHORITY_READ_TIMEOUT_SECONDS
if canonical else DEFAULT_REQUEST_TIMEOUT_SECONDS),
)
- if isinstance(result, dict) and result.get("todo_projection_required") is True:
- if canonical or result.get("schema_version") != TASK_LEASE_SCHEMA_VERSION or result.get("ok") is not True or result.get("action") != "inspect":
- raise RuntimeError("native lease inspection requested an invalid source projection")
- request["phase"] = "effective_lease"
- request["authority"] = task_lease_acquire_authority_facts(
- registry_path=registry_path, goal_id=goal_id, todo_id=todo_id,
- )
- # Re-read the lease and fence: neither the record nor its expiry is
- # assumed unchanged while Python prepares the Todo projection.
- result = effect_runtime_result(
- "task_lease.inspect.native", request,
- timeout=DEFAULT_REQUEST_TIMEOUT_SECONDS,
- )
if not isinstance(result, dict) or result.get("schema_version") != TASK_LEASE_SCHEMA_VERSION or result.get("action") != "inspect" or not isinstance(result.get("ok"), bool):
raise RuntimeError("native lease inspection result shape mismatch")
if result.get("error_code") == "authority_source_changed" and attempt + 1 < TASK_LEASE_AUTHORITY_SNAPSHOT_ATTEMPTS:
diff --git a/loopx/control_plane/work_items/task_lease_acquire_decision.ts b/loopx/control_plane/work_items/task_lease_acquire_decision.ts
index 02626dea63..70f3dc7ddd 100644
--- a/loopx/control_plane/work_items/task_lease_acquire_decision.ts
+++ b/loopx/control_plane/work_items/task_lease_acquire_decision.ts
@@ -1,8 +1,10 @@
+import {HANDOFF_MODES} from "../coordination/handoff_mode_vocabulary.ts";
import {leaseWorkspace, sameLeaseWorkspace, sameLeaseCheckout, type LeaseWorkspace} from "./task_lease_workspace.ts";
/** Shared acquire/reclaim admission. IO and durable receipts belong to callers. */
+import {retiredHandoffModeRejection} from "../coordination/handoff_mode_policy.ts";
import {leaseOwnerRejection as ownerRejection} from "./task_lease_eligibility.ts";
import {EffectRuntimeRequestError} from "../effect_runtime_errors.ts";
-import {requireJsonObject} from "../runtime_decode.ts";
+import {requireJsonObject, requireStringLiteral} from "../runtime_decode.ts";
import type {JsonObject} from "../effect_program.ts";
import type {TodoFact, LeaseRecord} from "./task_lease_acquire.ts";
import {normalizeTodoRepository} from "../todos/work_requirements.ts";
@@ -268,7 +270,7 @@ function decodeAcquireDecisionInput(value: unknown): AcquireDecisionInput {
};
});
return {
- handoff_mode: stringValue(input.handoff_mode, "handoff_mode"),
+ handoff_mode: requireStringLiteral(input.handoff_mode, HANDOFF_MODES, "handoff_mode"),
registered_agents: decisionStringArray(
input.registered_agents,
"registered_agents",
@@ -341,6 +343,8 @@ export function decideTaskLeaseAcquire(input: AcquireDecisionInput): AcquireDeci
if (rejection !== null) {
return acquireDecisionResult("rejected", rejection);
}
+ const retired = retiredHandoffModeRejection(input.handoff_mode);
+ if (retired !== null) return acquireDecisionResult("rejected", retired);
const actualVersion = lease !== null && lease.present ? lease.version : 0;
if (
command.expected_version !== null && command.expected_version !== actualVersion
@@ -374,9 +378,6 @@ export function decideTaskLeaseAcquire(input: AcquireDecisionInput): AcquireDeci
) {
return acquireDecisionResult("rejected", "idempotency_key_reuse");
}
- if (input.handoff_mode === "legacy") {
- return acquireDecisionResult("rejected", "handoff_mode_retired");
- }
const repository = normalizeTodoRepository(input.todo?.task_repository);
const workspace = leaseWorkspace(command.write_workspace);
if (workspace && workspace.repository.toLowerCase() !== repository?.toLowerCase()) {
diff --git a/loopx/control_plane/work_items/task_lease_inspection.ts b/loopx/control_plane/work_items/task_lease_inspection.ts
index 985d6a56c4..e5ed6dea57 100644
--- a/loopx/control_plane/work_items/task_lease_inspection.ts
+++ b/loopx/control_plane/work_items/task_lease_inspection.ts
@@ -11,6 +11,7 @@ import {openLocalAuthorityStoreHandle, localAuthorityOpenFailure,
import {decodeTaskLeaseAuthority, leaseIsActive, normalizeGoalId, normalizeTodoId, normalizeHandoffMode,
readLease, revalidateAuthoritySources, TaskLeaseAcquireError, TASK_LEASE_SCHEMA_VERSION,
type LeaseRecord, type TodoFact} from "./task_lease_acquire.ts";
+import {retiredHandoffModeRejection, HANDOFF_MODE_RETIRED_REASON} from "../coordination/handoff_mode_policy.ts";
import {leaseOwnerConstraint} from "./task_lease_eligibility.ts";
import {leaseRepositoryRejection} from "./task_lease_repository.ts";
import {todoExecutionDependencyRejection} from "../coordination/todo_execution_dependency.ts";
@@ -31,9 +32,7 @@ export async function inspectTaskLease(value: unknown,
const input = requireJsonObject(value, "task lease inspection");
if (input.schema_version !== TASK_LEASE_INSPECT_REQUEST ||
(input.source !== "canonical" && input.source !== "legacy") ||
- (input.phase !== undefined && input.phase !== "lease_record" && input.phase !== "effective_lease") ||
- (input.source === "canonical" && input.phase === "lease_record") ||
- Object.keys(input).some(key => !["schema_version", "source", "goal_id", "todo_id", "runtime_root", "authority", "phase"].includes(key))) {
+ Object.keys(input).some(key => !["schema_version", "source", "goal_id", "todo_id", "runtime_root", "authority"].includes(key))) {
throw new TaskLeaseAcquireError("invalid task lease inspection request", "invalid_inspection_request");
}
goalId = normalizeGoalId(input.goal_id); todoId = normalizeTodoId(input.todo_id);
@@ -85,13 +84,15 @@ export async function inspectTaskLease(value: unknown,
}
}
const timeActive = leaseIsActive(lease, at);
- const needsProjection = !canonical && input.phase === "lease_record" && timeActive;
- const ownerConstraint = !timeActive || lease === null || needsProjection ? null
+ const ownerConstraint = !canonical || !timeActive || lease === null ? null
: authority.todo_projection_error !== null
? {effective: false, reason: authority.todo_projection_error.code}
: leaseOwnerConstraint(todo, typeof lease.owner === "string" ? lease.owner : null, authority.registered_agents);
const repositoryRejection = canonical && ownerConstraint?.effective === true ? leaseRepositoryRejection(todo, lease) : null;
- const constraint = repositoryRejection !== null ? {effective: false, reason: repositoryRejection}
+ const retired = retiredHandoffModeRejection(mode);
+ const constraint = !canonical ? {effective: false, reason: "canonical_authority_required"}
+ : retired !== null ? {effective: false, reason: retired}
+ : repositoryRejection !== null ? {effective: false, reason: repositoryRejection}
: dependency !== null && ownerConstraint?.effective === true
? {effective: false, reason: dependency.code} : ownerConstraint;
// Both registration and route must still describe the source we inspected.
@@ -101,11 +102,13 @@ export async function inspectTaskLease(value: unknown,
if (canonicalAuthoritySha256(afterFence) !== canonicalAuthoritySha256(beforeFence)) {
throw new TaskLeaseAcquireError("lease authority route changed; retry inspection", "authority_source_changed");
}
- if (needsProjection) return {ok: true, schema_version: TASK_LEASE_SCHEMA_VERSION,
- action: "inspect", todo_projection_required: true};
return {ok: true, schema_version: TASK_LEASE_SCHEMA_VERSION, action: "inspect",
goal_id: goalId, todo_id: todoId, active: timeActive && constraint?.effective === true,
lease, lease_path: leasePath, ...evidence,
+ execution_policy: {supported: canonical && retired === null, execution_authority_granted: false,
+ ...(!canonical ? {reason_code: "canonical_authority_required",
+ reason: "Source leases are historical only. Use reviewed canonical import before starting execution."}
+ : retired !== null ? {reason_code: retired, reason: HANDOFF_MODE_RETIRED_REASON} : {})},
...(mode === null ? {} : {handoff_mode: mode}),
...(constraint?.effective === false ? {executor_constraint: constraint} : {}),
...(dependency === null ? {} : {resume_condition: dependency.condition})};
diff --git a/loopx/control_plane/work_items/task_lease_lifecycle_decision.ts b/loopx/control_plane/work_items/task_lease_lifecycle_decision.ts
index c688eab6f6..8b82b9b07e 100644
--- a/loopx/control_plane/work_items/task_lease_lifecycle_decision.ts
+++ b/loopx/control_plane/work_items/task_lease_lifecycle_decision.ts
@@ -1,7 +1,9 @@
+import {HANDOFF_MODES} from "../coordination/handoff_mode_vocabulary.ts";
+import {retiredHandoffModeRejection} from "../coordination/handoff_mode_policy.ts";
import {leaseOwnerRejection as ownerRejection} from "./task_lease_eligibility.ts";
import { EffectRuntimeRequestError } from "../effect_runtime_errors.ts";
import { type JsonObject } from "../effect_program.ts";
-import { requireJsonObject } from "../runtime_decode.ts";
+import { requireJsonObject, requireStringLiteral } from "../runtime_decode.ts";
import {leaseEpoch, utcIsoformat, type LeaseRecord} from "./task_lease_acquire.ts";
import {normalizeTodoRepository} from "../todos/work_requirements.ts";
import {leaseWriteRepository, leaseRepositoryRejection} from "./task_lease_repository.ts";
@@ -164,7 +166,7 @@ function decodeInput(value: unknown): TaskLeaseLifecycleDecisionInput {
"task lease lifecycle decision command",
);
return {
- handoff_mode: stringValue(input.handoff_mode, "handoff_mode"),
+ handoff_mode: requireStringLiteral(input.handoff_mode, HANDOFF_MODES, "handoff_mode"),
registered_agents: stringArray(
input.registered_agents,
"registered_agents",
@@ -266,9 +268,8 @@ export function decideTaskLeaseLifecycle(
nextLease: { ...lease, active: false, status: "released" },
});
}
- if (input.handoff_mode === "legacy") {
- return result("rejected", "handoff_mode_retired");
- }
+ const retired = retiredHandoffModeRejection(input.handoff_mode);
+ if (retired !== null) return result("rejected", retired);
if (lease === null || !lease.present || !lease.active) {
return result("rejected", "lease_not_active");
}
diff --git a/tests/control_plane/test_canonical_handoff_mode.py b/tests/control_plane/test_canonical_handoff_mode.py
index 4a40ab162b..fe81fe0bed 100644
--- a/tests/control_plane/test_canonical_handoff_mode.py
+++ b/tests/control_plane/test_canonical_handoff_mode.py
@@ -228,3 +228,94 @@ def test_provider_failure_is_not_a_legacy_fallback(canonical_mode, monkeypatch):
provider_handoff_mode.set_canonical_handoff_mode(runtime_root=runtime, goal_id="mode-goal",
mode="hard_lease", operation_id="unavailable", dry_run=False)
assert state.read_bytes() == before
+
+
+def test_retired_policy_requires_release_and_reviewed_migration_before_execution(canonical_mode):
+ state, runtime, cli = canonical_mode
+ from datetime import datetime, timedelta, timezone
+ from loopx.todos import add_goal_todo
+ from loopx.control_plane.coordination.local_authority import read_canonical_todos_if_promoted
+
+ registry = runtime.parent / "registry.json"
+ todo = add_goal_todo(registry_path=registry, goal_id="mode-goal", role="agent",
+ text="Preserve imported ownership", claimed_by="agent-a",
+ task_class="advancement_task", note="Retain original metadata")
+ # Imported historical facts are installed directly in this isolated test
+ # provider. No current business writer or new legacy lease grant produces them.
+ at = datetime.now(timezone.utc).replace(microsecond=0)
+ lease = {"schema_version": "task_lease_v0", "goal_id": "mode-goal", "todo_id": todo["todo_id"],
+ "owner": "agent-a", "idempotency_key": "imported-execution", "version": 4, "lease_epoch": 2,
+ "status": "active", "write_scopes": [], "acquire_ttl_seconds": 600,
+ "acquired_at": (at - timedelta(seconds=30)).isoformat(), "updated_at": at.isoformat(),
+ "expires_at": (at + timedelta(seconds=600)).isoformat()}
+ coordination = Path(__file__).resolve().parents[2] / "loopx/control_plane/coordination"
+ script = f"""
+ import {{openLocalAuthorityStore}} from {json.dumps((coordination / 'local_authority_provider.ts').as_uri())};
+ const store = await openLocalAuthorityStore(process.argv[1], 'mode-goal');
+ const before = await store.loadAuthority();
+ if (before.status !== 'loaded') throw new Error(JSON.stringify(before));
+ const result = await store.commitAuthority({{operation_id: 'imported-policy-fixture',
+ expected_provider_revision: before.provider_revision, next_projection: {{...before.head,
+ handoff_mode: 'legacy', leases: [{json.dumps(lease)}]}}, events: [], receipts: []}});
+ if (result.status !== 'applied') throw new Error(JSON.stringify(result));
+ """
+ seeded = subprocess.run(["node", "--no-warnings", "--experimental-strip-types", "--input-type=module",
+ "-e", script, str(runtime)], capture_output=True, text=True, timeout=45)
+ assert seeded.returncode == 0, seeded.stderr
+
+ def read():
+ return read_canonical_todos_if_promoted(runtime_root=runtime, goal_id="mode-goal", include_leases=True)
+
+ def work(*args, expected_exit=0):
+ child = subprocess.run([sys.executable, "-m", "loopx.cli", "--registry", str(registry), "--format", "json",
+ *args, "--goal-id", "mode-goal", "--todo-id", todo["todo_id"]], capture_output=True, text=True, timeout=90)
+ assert child.returncode == expected_exit, child.stdout + child.stderr
+ return json.loads(child.stdout)
+
+ before = read()
+ inspected = work("task-lease", "inspect")
+ assert inspected["active"] is False
+ assert inspected["executor_constraint"]["reason"] == "handoff_mode_retired"
+ assert inspected["lease"] == lease
+ for args in [
+ ("todo", "claim", "--agent-id", "agent-a", "--claimed-by", "agent-a"),
+ ("todo", "update", "--agent-id", "agent-a", "--note", "Unreviewed work"),
+ ("todo", "complete", "--agent-id", "agent-a", "--no-follow-up", "--note", "Bounded completion"),
+ ("todo", "update", "--agent-id", "agent-a", "--note", "Old proof cannot authorize",
+ "--task-lease-idempotency-key", "imported-execution", "--task-lease-expected-version", "4"),
+ ]:
+ refused = work(*args, expected_exit=1)
+ assert (refused.get("error_code") or refused.get("reason_code")) == "handoff_mode_retired", refused
+ assert read() == before
+ released = work("task-lease", "release", "--owner", "agent-a", "--idempotency-key", "imported-execution",
+ "--expected-version", "4")
+ assert released["released"] is True
+ retained = read()
+ assert retained["handoff_mode"] == "legacy" and retained["todos"] == before["todos"]
+ assert retained["leases"][0]["status"] == "released"
+ assert retained["leases"][0]["version"] == 4 and retained["leases"][0]["lease_epoch"] == 2
+ plan = runtime.parent / "retired-policy-upgrade.json"
+ code, planned = cli("plan-migration", "--mode", "hard_lease", "--plan", str(plan))
+ assert code == 0 and planned["status"] == "planned", planned
+ assert read() == retained
+ digest = planned["plan_sha256"]
+ code, applied = cli("migrate", "--plan", str(plan), "--plan-sha256", digest, "--execute")
+ assert code == 0 and applied["status"] == "applied", applied
+ assert applied["execution_authority_granted"] is False
+ assert Path(applied["backup_path"]).is_file()
+ migrated = read()
+ assert migrated["handoff_mode"] == "hard_lease"
+ assert migrated["todos"] == retained["todos"] and migrated["leases"] == retained["leases"]
+ acquired = work("task-lease", "acquire", "--owner", "agent-a", "--idempotency-key", "new-execution",
+ "--expected-version", "4", "--ttl-seconds", "600")
+ assert acquired["acquired"] is True and acquired["lease"]["version"] == 5
+ assert acquired["lease"]["lease_epoch"] == 3
+ assert work("todo", "update", "--agent-id", "agent-a", "--note", "Reviewed current work",
+ "--task-lease-idempotency-key", "new-execution", "--task-lease-expected-version", "5")["ok"] is True
+ later = read()
+ code, replay = cli("migrate", "--plan", str(plan), "--plan-sha256", digest, "--execute")
+ assert code == 0 and replay["status"] == "replayed", replay
+ assert read() == later, "historical migration cannot overwrite later execution"
+ assert work("task-lease", "release", "--owner", "agent-a", "--idempotency-key", "new-execution",
+ "--expected-version", "5")["released"] is True
+ assert state.exists()
diff --git a/tests/control_plane/test_canonical_lease_inspection.py b/tests/control_plane/test_canonical_lease_inspection.py
index 88ca782337..212b604778 100644
--- a/tests/control_plane/test_canonical_lease_inspection.py
+++ b/tests/control_plane/test_canonical_lease_inspection.py
@@ -200,7 +200,7 @@ def forbidden_projection(**_):
assert expired["active"] is False and expired["handoff_mode"] == "hard_lease"
-def test_legacy_inspection_reloads_lease_after_demanding_todo_facts(tmp_path, monkeypatch):
+def test_retained_source_inspection_never_projects_current_execution(tmp_path, monkeypatch):
registry = tmp_path / "registry.json"
state = tmp_path / "state.md"
state.write_text("---\nhandoff_mode: hard_lease\n---\n# Synthetic state\n")
@@ -212,16 +212,17 @@ def test_legacy_inspection_reloads_lease_after_demanding_todo_facts(tmp_path, mo
lease = {"schema_version": "task_lease_v0", "status": "active", "owner": "agent-a", "expires_at": "2099-01-01T00:00:00Z"}
lease_path.write_text(json.dumps(lease))
from loopx.control_plane.todos import list_readback
- calls = []
- def release_during_projection(**_):
- calls.append(True)
- lease_path.write_text(json.dumps({**lease, "status": "released"}))
- return {"todos": [{"todo_id": TODO, "status": "open", "claimed_by": None, "excluded_agents": []}]}
- monkeypatch.setattr(list_readback, "list_goal_todos", release_during_projection)
+ original = lease_path.read_bytes()
+ def forbidden_projection(**_):
+ raise AssertionError("retained source inspection must not revive Todo admission")
+ monkeypatch.setattr(list_readback, "list_goal_todos", forbidden_projection)
result = _inspect(registry, runtime)
- assert len(calls) == 1
assert result["active"] is False
- assert result["lease"]["status"] == "released"
+ assert result["lease"] == lease
+ assert lease_path.read_bytes() == original
+ assert result["executor_constraint"]["reason"] == "canonical_authority_required"
+ assert result["execution_policy"]["supported"] is False
+ assert result["execution_policy"]["execution_authority_granted"] is False
assert "todo_projection_required" not in result
diff --git a/tests/control_plane/test_chat_goal_ownership.py b/tests/control_plane/test_chat_goal_ownership.py
index cc7de9dece..e9029865bc 100644
--- a/tests/control_plane/test_chat_goal_ownership.py
+++ b/tests/control_plane/test_chat_goal_ownership.py
@@ -229,6 +229,10 @@ def test_pending_lease_downgrade_rejected_even_after_clock_expiry(api):
claimed_by="agent-a",
task_class="advancement_task",
)
+ # Migrate the historical policy through the real App owner before execution.
+ upgrade = preview(call, "hard_lease")
+ code, migrated = apply(call, upgrade)
+ assert code == 200 and migrated["ok"], migrated
# Use the real acquire owner; an expired lease is not proof the host stopped.
acquired = acquire_task_lease(
registry_path=registry,
diff --git a/tests/control_plane_ts/authority_projection_fixture.ts b/tests/control_plane_ts/authority_projection_fixture.ts
index 38e1851cd6..23bc6fe558 100644
--- a/tests/control_plane_ts/authority_projection_fixture.ts
+++ b/tests/control_plane_ts/authority_projection_fixture.ts
@@ -76,6 +76,9 @@ export function authorityProjectionFixture(
const normalizedLeases = leases.map(lease => structuredClone(lease)).sort((left, right) =>
authorityUnicodeCompare(String(left.todo_id), String(right.todo_id)));
return canonicalAuthorityObject({
+ // Normal fixture work uses a current policy; legacy wire format is a
+ // separate axis. Migration/history tests must explicitly request legacy mode.
+ handoff_mode: "soft_claim",
...structuredClone(extras),
goal_id: goalId,
todos: normalizedTodos,
diff --git a/tests/control_plane_ts/authority_store_conformance.ts b/tests/control_plane_ts/authority_store_conformance.ts
index 036db55ce6..d3a0c64a1a 100644
--- a/tests/control_plane_ts/authority_store_conformance.ts
+++ b/tests/control_plane_ts/authority_store_conformance.ts
@@ -420,6 +420,51 @@ export function registerAuthorityStoreConformance(
assert.deepEqual(await store.loadAuthority(),released);
});
+ for (const operation of ["renew", "transfer"] as const) test(`${providerName} retired policy cannot replay acquisition or ${operation} as current proof`, async t => {
+ const {store} = await factory(t);
+ const todo = {todo_id: "todo_work", role: "agent", status: "open", done: false,
+ text: "Imported work", archive_state: "active", claimed_by: null, task_class: "advancement_task"};
+ assert.equal((await store.commitAuthority({expected_provider_revision: null, operation_id: "seed",
+ next_projection: authorityProjectionFixture("goal-a", [todo], [], "native", {handoff_mode: "hard_lease"}),
+ events: [], receipts: []})).status, "applied");
+ const command = {goal_id: "goal-a", todo_id: "todo_work", owner: "agent-a", idempotency_key: "original-execution",
+ expected_version: 0, ttl_seconds: 600, write_scopes: [], registered_agents: ["agent-a", "agent-b"],
+ now: new Date("2026-09-01T03:01:00Z")};
+ const accepted = await executeCanonicalTaskLeaseAcquire(store, command);
+ assert.equal(accepted.status, "applied", JSON.stringify(accepted));
+ const acquisitionOperation = String(accepted.operation_id);
+ const original = await store.readReceipt(acquisitionOperation);
+ const maintenance = {...command, operation, expected_version: 1,
+ ...(operation === "transfer" ? {new_owner: "agent-b", new_idempotency_key: "receiver-execution"} : {})};
+ const maintained = await executeCanonicalTaskLeaseLifecycle(store, maintenance);
+ assert.equal(maintained.status, "applied", JSON.stringify(maintained));
+ const originalMaintenance = await store.readReceipt(String(maintained.operation_id));
+ const current = await store.loadAuthority();
+ if (current.status !== "loaded") throw new Error("fixture unavailable");
+ assert.equal((await store.commitAuthority({expected_provider_revision: current.provider_revision,
+ operation_id: "imported-policy", next_projection: {...current.head, handoff_mode: "legacy"},
+ events: [], receipts: []})).status, "applied");
+ const before = await store.loadAuthority();
+ for (const replay of [command, {...command, idempotency_key: "fresh-execution", expected_version: 2}]) {
+ const refused = await executeCanonicalTaskLeaseAcquire(store, replay);
+ assert.equal(refused.status, "failed", JSON.stringify(refused));
+ assert.equal(refused.reason_code, "handoff_mode_retired");
+ assert.equal(refused.lease, undefined, "historical acceptance is not a current execution grant");
+ assert.deepEqual(await store.readReceipt(acquisitionOperation), original);
+ assert.deepEqual(await store.loadAuthority(), before);
+ }
+ const replayedMaintenance = await executeCanonicalTaskLeaseLifecycle(store, maintenance);
+ assert.equal(replayedMaintenance.reason_code, "handoff_mode_retired");
+ assert.equal(replayedMaintenance.lease, undefined);
+ assert.deepEqual(await store.readReceipt(String(maintained.operation_id)), originalMaintenance);
+ assert.deepEqual(await store.loadAuthority(), before);
+ const released = await executeCanonicalTaskLeaseLifecycle(store, {...command,
+ operation: "release", expected_version: 2, ttl_seconds: null,
+ ...(operation === "transfer" ? {owner: "agent-b", idempotency_key: "receiver-execution"} : {})});
+ assert.equal(released.status, "applied", JSON.stringify(released));
+ assert.deepEqual(await store.readReceipt(acquisitionOperation), original);
+ });
+
registerClaimContentionConformance(providerName, factory);
registerAuthorityScanConformance(providerName, factory);
registerOwnershipObservationConformance(providerName, factory);
@@ -478,14 +523,14 @@ export function registerAuthorityStoreConformance(
assert.equal((await executeCoordinationTodoArchiveCompleted(store, request)).status, "replayed");
});
- for (const native of [false, true]) test(`${providerName} conformance: lease-free legacy-mode Monitor observation and successor (${native ? "native" : "legacy"})`, async (t) => {
+ for (const native of [false, true]) test(`${providerName} conformance: lease-free soft-claim Monitor observation and successor (${native ? "native" : "legacy"})`, async (t) => {
const {store, contender} = await factory(t);
const goal = "goal-monitor";
const fixture = productionScaleCoordinationFixture(goal, native ? "native" : "legacy");
const projection = structuredClone(fixture.projection);
- // A lease-free observation is legal in legacy mode. Hard mode now requires
+ // A lease-free observation is legal in soft_claim. Hard mode requires
// current execution proof; its positive/negative cases have their own fixture.
- projection.handoff_mode = "legacy";
+ projection.handoff_mode = "soft_claim";
const records = projection.todos as Record[];
const monitor = records.find(todo => todo.task_class === "continuous_monitor" && todo.status !== "done" &&
!(projection.leases as Record[]).some(lease => lease.todo_id === todo.todo_id));
diff --git a/tests/control_plane_ts/command_observation_scenarios.ts b/tests/control_plane_ts/command_observation_scenarios.ts
index ffac588d5f..475722bfc8 100644
--- a/tests/control_plane_ts/command_observation_scenarios.ts
+++ b/tests/control_plane_ts/command_observation_scenarios.ts
@@ -29,7 +29,7 @@ export async function commandObservationScenario(kind: CommandObservationCase, s
task_class: "advancement_task", action_kind: "implement", status: "open", done: false,
archive_state: "active", required_write_scopes: ["command-observation/**"]};
let projection = authorityProjectionFixture(goal, [...f.projection.todos as JsonObject[], todo],
- f.projection.leases as JsonObject[], schema, {handoff_mode: "legacy"});
+ f.projection.leases as JsonObject[], schema, {handoff_mode: "soft_claim"});
const monitor = productionScaleLeasedMonitorFixture(goal, schema);
const acquire = productionScaleLeaseAcquisitionFixture(goal, schema);
const lifecycle = productionScaleLeaseLifecycleFixture(goal, schema);
diff --git a/tests/control_plane_ts/coordination_command_fixture.ts b/tests/control_plane_ts/coordination_command_fixture.ts
index 3dd3967c87..77322a59c4 100644
--- a/tests/control_plane_ts/coordination_command_fixture.ts
+++ b/tests/control_plane_ts/coordination_command_fixture.ts
@@ -21,7 +21,7 @@ export async function coordinationCommandFixture(store: AuthorityStore, command:
const projection = fixture.projection;
// This recovery arm deliberately has no Monitor execution lease. The leased
// arm separately proves current proof and historical replay in hard mode.
- if (command === "monitor") projection.handoff_mode = "legacy";
+ if (command === "monitor") projection.handoff_mode = "soft_claim";
const todos = projection.todos as JsonObject[];
const leased = new Set((projection.leases as JsonObject[]).map(row => row.todo_id));
const claimTodo = todos.find(row => row.role === "agent" && row.status === "open" &&
diff --git a/tests/control_plane_ts/postgresql_authority_store.integration.test.ts b/tests/control_plane_ts/postgresql_authority_store.integration.test.ts
index c2290049e9..4841898d76 100644
--- a/tests/control_plane_ts/postgresql_authority_store.integration.test.ts
+++ b/tests/control_plane_ts/postgresql_authority_store.integration.test.ts
@@ -181,7 +181,7 @@ if (database && installed) {
status: "open", done: false, archive_state: "active", text: "[P2] Existing work",
title: "Existing work", priority: "P2", task_class: "advancement_task"}];
await store.commitAuthority({operation_id: "seed-priority", expected_provider_revision: null,
- events: [], receipts: [], next_projection: {goal_id: options.goal_id, todos, leases: [],
+ events: [], receipts: [], next_projection: {goal_id: options.goal_id, handoff_mode: "soft_claim", todos, leases: [],
todo_read_model: {schema_version: TODO_DOMAIN_READ_RECORD_SCHEMA, todo_count: 1,
records_sha256: canonicalAuthoritySha256(todos), contract_fields: [...TODO_DOMAIN_RECORD_CONTRACT.fields]}}});
const request = {goal_id: options.goal_id, todo_id: "todo_priority", expected_role: "agent",
diff --git a/tests/control_plane_ts/production_scale_coordination_fixture.ts b/tests/control_plane_ts/production_scale_coordination_fixture.ts
index 41bf57a013..cc073a1df7 100644
--- a/tests/control_plane_ts/production_scale_coordination_fixture.ts
+++ b/tests/control_plane_ts/production_scale_coordination_fixture.ts
@@ -590,7 +590,7 @@ export function productionScaleCompletedMonitorFixture(goalId: string,
status: retainedLease === "released" ? "released" : "active",
...(retainedLease === "expired" ? {expires_at: "2026-09-01T00:20:00Z"} : {}),
...(retainedLease === "released" ? {released_at: "2026-09-01T00:30:00Z"} : {})}]),
- schema, {source_authority: "synthetic_production_scale_fixture", handoff_mode: retainedLease === "absent" ? "legacy" : "hard_lease"})};
+ schema, {source_authority: "synthetic_production_scale_fixture", handoff_mode: retainedLease === "absent" ? "soft_claim" : "hard_lease"})};
}
/** User decisions act on an exact dependent inside the full mixed graph.
@@ -661,7 +661,7 @@ export function productionScaleConsumerScopeFixture(goalId: string, schema: Auth
text: "Review a synthetic result", source_section: "User Todo", index: 2000 + index, ...item}));
return {...fixture, projection: authorityProjectionFixture(goalId,
[...fixture.projection.todos as Record[], ...extra],
- fixture.projection.leases as Record[], schema, {handoff_mode: "legacy"})};
+ fixture.projection.leases as Record[], schema, {handoff_mode: "soft_claim"})};
}
/** Capability-owned bucket inside a full mixed graph, including a historical
diff --git a/tests/control_plane_ts/task_lease_eligibility.test.ts b/tests/control_plane_ts/task_lease_eligibility.test.ts
index ba27947a2e..840d017d7f 100644
--- a/tests/control_plane_ts/task_lease_eligibility.test.ts
+++ b/tests/control_plane_ts/task_lease_eligibility.test.ts
@@ -1,7 +1,7 @@
import assert from "node:assert/strict";
import test from "node:test";
import {evaluateTaskLeaseAcquireDecision} from "../../loopx/control_plane/work_items/task_lease_acquire_decision.ts";
-import {decideTaskLeaseLifecycle} from "../../loopx/control_plane/work_items/task_lease_lifecycle_decision.ts";
+import {decideTaskLeaseLifecycle, evaluateTaskLeaseLifecycleDecision} from "../../loopx/control_plane/work_items/task_lease_lifecycle_decision.ts";
import {evaluateTaskLeaseOwnerEligibility, leaseOwnerRejection, type LeaseEligibilityTodo} from
"../../loopx/control_plane/work_items/task_lease_eligibility.ts";
import {productionScaleCoordinationFixture} from "./production_scale_coordination_fixture.ts";
@@ -100,3 +100,12 @@ for (const generation of ["version", "lease_epoch"] as const) {
assert.equal(result.code, "lease_generation_exhausted"); assert.equal(result.outcome, "rejected");
});
}
+
+test("lease decision wires accept only the registered historical/current policy vocabulary", () => {
+ for (const mode of ["future_policy", "hard_lease_extra", "Legacy", ""]) {
+ assert.throws(() => evaluateTaskLeaseAcquireDecision({...acquire(false), handoff_mode: mode}), /handoff_mode/);
+ assert.throws(() => evaluateTaskLeaseLifecycleDecision({handoff_mode: mode, registered_agents: ["agent-a"],
+ todo, lease, command: {operation: "renew", owner: "agent-a", idempotency_key: "first",
+ expected_version: 1, ttl_seconds: 120, new_owner: null, new_idempotency_key: null}}), /handoff_mode/);
+ }
+});
diff --git a/tests/control_plane_ts/task_lease_inspection.test.ts b/tests/control_plane_ts/task_lease_inspection.test.ts
index ea2242b27b..a8dfce3745 100644
--- a/tests/control_plane_ts/task_lease_inspection.test.ts
+++ b/tests/control_plane_ts/task_lease_inspection.test.ts
@@ -32,7 +32,7 @@ const installed = database ? installPostgreSqlAuthorityStoreSchema(database, `po
test.after(async () => {await pool?.end();});
type Provider = "legacy" | "file" | "sqlite" | "postgresql";
-async function fixture(t: test.TestContext, provider: Provider, schema: "native" | "legacy" = "native") {
+async function fixture(t: test.TestContext, provider: Provider, schema: "native" | "legacy" = "native", mode = "hard_lease") {
const root = await mkdtemp(join(tmpdir(), "lease-inspect-")), goal = "inspect-goal";
t.after(() => rm(root, {recursive: true, force: true}));
const registry = join(root, "registry.json"), registryText = "synthetic registration source";
@@ -77,7 +77,7 @@ async function fixture(t: test.TestContext, provider: Provider, schema: "native"
if (item.todo !== null) todos.push({...todo, todo_id: `todo_inspect_${item.name}`, ...item.todo});
if (item.lease !== null) leases.push({...lease, todo_id: `todo_inspect_${item.name}`, expires_at: "2026-09-13T10:10:00Z", ...item.lease});
}
- const projection = authorityProjectionFixture(goal, todos, leases, schema, {handoff_mode: "hard_lease"});
+ const projection = authorityProjectionFixture(goal, todos, leases, schema, {handoff_mode: mode});
const authority = {handoff_mode: "hard_lease", registered_agent_candidates: [fixture.registered_agents],
todos: todos.filter(row => row.archive_state === "active"), todo_projection_error: null,
source_receipts: [{source_id: "registry", path: registry, state: "file", sha256: createHash("sha256").update(registryText).digest("hex")}]};
@@ -132,8 +132,9 @@ for (const provider of ["legacy", "file", "sqlite", "postgresql"] as const) {
for (const item of cases) {
const result = await inspectTaskLease({...request, todo_id: `todo_inspect_${item.name}`}, {now: () => NOW, authorityProvider});
assert.equal(result.ok, true, JSON.stringify(result));
- assert.equal(result.active, item.active, item.name);
- assert.deepEqual(result.executor_constraint, item.constraint, item.name);
+ assert.equal(result.active, provider === "legacy" ? false : item.active, item.name);
+ assert.deepEqual(result.executor_constraint, provider === "legacy"
+ ? {effective: false, reason: "canonical_authority_required"} : item.constraint, item.name);
if (item.name === "repository-bound") assert.equal((result.lease as JsonObject).write_repository, "git:github.com/team/a");
if (store && before?.status === "loaded") {
assert.equal(result.source_authority, `${provider}_v0`);
@@ -170,13 +171,30 @@ for (const fault of ["registration", "route", "clock", "field"] as const) {
});
}
-test("legacy inspection preserves projection errors and rejects malformed active expiry", async t => {
+test("retained source inspection ignores retired Todo admission and validates the original lease", async t => {
const {request, root, goal} = await fixture(t, "legacy");
const unavailable = await inspectTaskLease({...request, authority: {...request.authority,
todo_projection_error: {code: "todo_projection_unavailable", message: "source cannot be projected"}}}, {now: () => NOW});
assert.equal(unavailable.active, false);
- assert.deepEqual(unavailable.executor_constraint, {effective: false, reason: "todo_projection_unavailable"});
+ assert.deepEqual(unavailable.executor_constraint, {effective: false, reason: "canonical_authority_required"});
const lease = {schema_version: "task_lease_v0", status: "active", owner: "agent-a", expires_at: "broken"};
await writeFile(join(root, "goals", goal, "task-leases", `${request.todo_id}.json`), JSON.stringify(lease));
assert.equal((await inspectTaskLease(request, {now: () => NOW})).error_code, "corrupt_lease");
});
+
+for (const provider of ["file", "sqlite", "postgresql"] as const) {
+ test(`${provider} retired legacy policy reports retained leases without effective execution`,
+ {skip: (provider === "postgresql" && !pool) || (provider === "sqlite" && !sqliteQualified)}, async t => {
+ const {request, store, authorityProvider} = await fixture(t, provider, "native", "legacy");
+ const before = await store!.loadAuthority();
+ const result = await inspectTaskLease(request, {now: () => NOW, authorityProvider});
+ assert.equal(result.ok, true, JSON.stringify(result));
+ assert.equal(result.handoff_mode, "legacy");
+ assert.equal(result.active, false);
+ assert.equal((result.execution_policy as JsonObject).supported, false);
+ assert.equal((result.execution_policy as JsonObject).execution_authority_granted, false);
+ assert.deepEqual(result.executor_constraint, {effective: false, reason: "handoff_mode_retired"});
+ assert.equal((result.lease as JsonObject).status, "active", "historical record is not rewritten");
+ assert.deepEqual(await store!.loadAuthority(), before);
+ });
+}
diff --git a/tests/control_plane_ts/todo_terminal_decision.test.ts b/tests/control_plane_ts/todo_terminal_decision.test.ts
index 88884424c5..553a066ef9 100644
--- a/tests/control_plane_ts/todo_terminal_decision.test.ts
+++ b/tests/control_plane_ts/todo_terminal_decision.test.ts
@@ -17,7 +17,7 @@ function request(overrides: Record = {}) {
return {
schema_version: COORDINATION_TODO_TERMINAL_DECISION_REQUEST_SCHEMA,
command: "complete",
- handoff_mode: "legacy",
+ handoff_mode: "soft_claim",
registered_agents: ["agent-a", "agent-b"],
lifecycle_grants: [],
todo: {
@@ -104,10 +104,10 @@ test("explicit missing Todo is a typed refusal; missing or malformed wire fields
}
});
-test("snapshot admission preserves inactive holders and claim-neutral mutation effects", () => {
+test("current-policy admission preserves inactive holders and claim-neutral mutation effects", () => {
const holder = {present: true, active: true, status: "active", owner: "agent-a",
idempotency_key: "holder", version: 3, lease_epoch: 1, write_scopes: []};
- for (const mode of ["legacy", "soft_claim", "hard_lease"]) {
+ for (const mode of ["soft_claim", "hard_lease"]) {
for (const lease of [null, holder, {...holder, active: false, status: "expired"},
{...holder, active: false, status: "released"}]) {
const result = evaluateCoordinationTodoMutationDecision(mutation({handoff_mode: mode, lease}));
@@ -151,9 +151,9 @@ test("bound User action closure is administrative, not a fabricated execution le
lease: {...lease, active: false, status: "expired"}}).next_lease?.status, "released");
});
-test("update admission shares actor rules without inventing terminal effects", () => {
+test("current-policy update admission shares actor rules without inventing terminal effects", () => {
const base = mutation({ todo: { ...request().todo as object, claimed_by: null } });
- for (const mode of ["legacy", "soft_claim", "hard_lease"]) {
+ for (const mode of ["soft_claim", "hard_lease"]) {
const edited = evaluateCoordinationTodoMutationDecision({ ...base, handoff_mode: mode });
assert.equal(edited.outcome, "apply");
assert.equal(edited.next_todo_status, "open");
@@ -459,3 +459,22 @@ test("unneeded malformed delegation cannot block ordinary actor authority", () =
]}), /requires_reason/);
}
});
+
+test("retired legacy policy cannot authorize fresh lifecycle or execution proof", () => {
+ const lease = {present: true, active: true, status: "active", owner: "agent-a",
+ idempotency_key: "retained", version: 3, lease_epoch: 1, write_scopes: []};
+ for (const proof of [null, lease]) {
+ for (const command of ["claim", "update", "complete", "supersede"]) {
+ const terminal = command === "complete" || command === "supersede";
+ const build = terminal ? request : mutation;
+ const evaluate = terminal ? evaluateCoordinationTodoTerminalDecision : evaluateCoordinationTodoMutationDecision;
+ const result = evaluate(build({command, authority_action: command, handoff_mode: "legacy",
+ lease: proof, lease_idempotency_key: proof === null ? null : "retained",
+ lease_expected_version: proof === null ? null : 3}));
+ assert.equal(result.outcome, "rejected");
+ assert.equal(result.code, "handoff_mode_retired");
+ assert.equal(result.next_lease, null);
+ assert.equal(result.next_todo_status, null);
+ }
+ }
+});
diff --git a/tests/test_delegation_lease_lifetime.py b/tests/test_delegation_lease_lifetime.py
index 850fc39950..32c58634a5 100644
--- a/tests/test_delegation_lease_lifetime.py
+++ b/tests/test_delegation_lease_lifetime.py
@@ -435,3 +435,39 @@ def test_real_stop_leaves_a_newer_generation_alone(service, monkeypatch):
assert receipt["stop"]["settled"]["lease"] == "not_owed"
held = inspect(runner)["lease"]
assert held["status"] == "active" and held["idempotency_key"] == "new-execution"
+
+
+@pytest.mark.parametrize("status", ["prepared", "running"])
+def test_retired_policy_blocks_original_delegation_before_host_launch(service, monkeypatch, status):
+ root, runner = service
+ monkeypatch.setattr(runner, "_spawn", lambda _: None)
+ operation_id = "retired-policy-" + status
+ runner.start("analysis", operation_id, brief())
+ path = runner.path(operation_id)
+ row = _read(path)
+ if status == "running":
+ row["status"] = status
+ row["task_lease"] = {"required": False, "handoff_mode": "legacy"}
+ runner._fenced_write(path, row)
+ # Original operation records may outlive a pre-cutoff ownership snapshot.
+ # Install those historical facts only in this disposable test provider.
+ program = """
+import {openLocalAuthorityStore} from "./loopx/control_plane/coordination/local_authority_provider.ts";
+const [root, goal] = process.argv.slice(1);
+const store = await openLocalAuthorityStore(root, goal);
+const state = await store.loadAuthority();
+const result = await store.commitAuthority({expected_provider_revision: state.provider_revision,
+ operation_id: "retained-policy-fixture", events: [], receipts: [],
+ next_projection: {...state.head, handoff_mode: "legacy"}});
+if (result.status !== "applied") throw new Error(JSON.stringify(result));
+"""
+ installed = subprocess.run(["node", "--no-warnings", "--experimental-sqlite", "--experimental-strip-types",
+ "--input-type=module", "-e", program, str(runner.root), runner.goal_id],
+ capture_output=True, text=True, timeout=30)
+ assert installed.returncode == 0, installed.stderr
+ before = read_canonical_todos_if_promoted(runtime_root=runner.root, goal_id=runner.goal_id, include_leases=True)
+ with pytest.raises(ValueError, match="historical only"):
+ runner._execute(path, row, runner.binding("analysis"))
+ assert not list(root.rglob("host-invocations")), "an old no-lease record must not start its model Host"
+ assert read_canonical_todos_if_promoted(runtime_root=runner.root, goal_id=runner.goal_id, include_leases=True) == before
+ assert _read(path)["status"] == status
From a4fed4f644d2e4c5af897abe41c6e606e47a753e Mon Sep 17 00:00:00 2001
From: LoopX Agent <337587101+loopx-agent@users.noreply.github.com>
Date: Thu, 15 Oct 2026 13:16:40 +0800
Subject: [PATCH 31/38] docs(authority): reconcile two-policy retirement with
the RFC
Signed-off-by: LoopX Agent <337587101+loopx-agent@users.noreply.github.com>
---
.../typescript-control-plane-migration-v0.md | 15 +++-
...script-control-plane-migration-v0.zh-CN.md | 13 +++-
docs/reference/handoff-mode.md | 73 ++++++++++++-------
.../local-authority-provider-selection.md | 2 +-
4 files changed, 73 insertions(+), 30 deletions(-)
diff --git a/docs/architecture/rfcs/typescript-control-plane-migration-v0.md b/docs/architecture/rfcs/typescript-control-plane-migration-v0.md
index 5a158be19e..7939be2667 100644
--- a/docs/architecture/rfcs/typescript-control-plane-migration-v0.md
+++ b/docs/architecture/rfcs/typescript-control-plane-migration-v0.md
@@ -4,7 +4,7 @@
- Supersedes / closes: none
- Proposed by: LoopX maintainers
- Date: 2026-08-15
-- Last revised: 2026-10-07
+- Last revised: 2026-10-15
- Scope: an incremental, replacement-first migration of the LoopX control-plane
core from Python to TypeScript without maintaining two semantic
implementations
@@ -135,6 +135,19 @@ The competing implementation stack is not an integration prerequisite; adopted
combined-head validation, core trial, real Host and D2/release exits stay open.
+The same C1/T4 boundary now retires the live `legacy` ownership policy rather
+than preserving a third execution strategy. The existing TS policy owner rejects
+fresh claim/update/terminal/Monitor admission and execution fences; acquire and
+lease maintenance share that rule. Current-proof readback and inspection cannot
+turn a retained lease or original receipt into execution. Delegation consumes the
+same typed support before launch/resume, including old no-lease-required records;
+source inspection no longer carries a second Todo-admission projection. Historical mode decoding,
+original-operation recovery, exact release and reviewed backup/plan/migrate remain.
+Normal fixtures use soft/hard independently of their legacy/native wire shape;
+explicit legacy-policy negatives and retained 1.x artifacts preserve that boundary.
+No live Goal is changed by this proposal. Adopted combined-head qualification,
+installed policy migration, real Host adoption and D2/release remain open.
+
Team-plan recovery prerequisite: an unpromoted plan that published its original
receipt can now recover through the Chat confirmation owner while shadow
bootstrap is interrupted. Registry/Todo/source locks still protect the read,
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 bfc60649c4..84a7d54110 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
@@ -4,7 +4,7 @@
- 替代 / 关闭:无
- Proposed by:LoopX maintainers
- Date:2026-08-15
-- Last revised:2026-10-07
+- Last revised:2026-10-15
- Scope:LoopX 控制面核心从 Python 到 TypeScript 的增量、replacement-first
迁移;不长期维护两份语义实现
- Tracking issue:[#3225](https://github.com/loopx-project/loopx/issues/3225)
@@ -71,6 +71,17 @@ actor/可运行状态/输入拒绝、精确 lineage 读回,以及当前输
隔离路径验证。新能力进入时持续守卫,不把历史库存当永久出口。最后受支持的
旧源 writer、历史恢复、安装 cohort 与发布验收各自保持开放。
+当前 C1/T4 退役提案由贡献者主导的 #6396 及 fork 补缺 #2 承接,#6366 已被替代;
+[吸收清单](../../reference/local-authority-provider-selection.md#retirement-proposal-reconciliation)
+保留 canonical ownership/终态不变量、固定 1.x 回执恢复、审核后的 App 创建及真实锁竞争 oracle。
+同一 TS 策略 owner 清理第三种 live legacy 执行策略:fresh claim/update/terminal、
+Monitor、执行 fence、acquire 及租约维护拒绝 legacy;当前 proof 读回和 inspect 也不能把
+保留租约或原回执变为执行资格。delegation 首次启动和恢复都消费同一 typed support,
+包括旧“无需租约”记录;source inspect 不再二次解析 Todo 准入。历史 mode 解码、原操作恢复、精确 release 与有备份的
+plan/migrate 保留。正常 fixture 使用 soft/hard;legacy/native wire shape 与策略分开,
+显式 legacy 拒绝和固定 1.x artifact 继续验证恢复边界。提案不自动修改活跃 Goal。
+采用后的 combined head 验证、安装态策略迁移、真实 Host 采用与 D2/发布保持开放。
+
保留的 import-compatible field writer 在既有 typed field plan 中统一等待条件
归一化及矛盾 clear 拒绝;保持 Python 空白输入省略和锁内 IO,只退役重复的
normalization crossing,不删除仍受支持的 writer 或迁移 reader。
diff --git a/docs/reference/handoff-mode.md b/docs/reference/handoff-mode.md
index e3ba872707..b58e75a0e7 100644
--- a/docs/reference/handoff-mode.md
+++ b/docs/reference/handoff-mode.md
@@ -3,8 +3,17 @@
`soft_claim` uses the Todo assignment; `hard_lease` also requires the existing
execution lease fences. New `set` and migration intents accept these two modes.
`legacy` remains a historical source for backed-up upgrades and original receipt
-recovery; new requests cannot select it, even as a no-op. Existing Goals are not
-silently changed, and creation/Host defaults remain unchanged in this stage.
+recovery; new requests cannot select it, even as a no-op. In the 2.0 candidate,
+fresh claim/update/terminal and Monitor admission, execution fences, acquisition
+and lease maintenance reject `legacy` with `handoff_mode_retired`. Inspection
+keeps the original lease record but reports it ineffective; an original acquire
+receipt cannot be presented as current execution proof. Exact release and original
+receipt recovery remain available. Delegation checks the typed inspection's
+`execution_policy.supported` before first launch and resuming a running operation,
+including retained records declaring that no lease was needed. This readback grants
+no execution authority; hard mode still needs its original claim/lease proof.
+Source-only lease inspection is historical: it cannot report an effective execution
+and no longer asks Python for a second full Todo projection. Existing Goals are not silently changed, and creation/Host defaults remain unchanged in this stage.
This is an ownership policy, not a capability grant or a storage provider.
The App's Task list and Task details use the same reviewed completion entry.
@@ -40,9 +49,9 @@ loopx --format json handoff-mode set --goal-id example-goal --mode soft_claim --
loopx --format json handoff-mode set --goal-id example-goal --mode soft_claim --operation-id mode-change-1
```
-Before promotion, `show`/`set` use the existing frontmatter adapter and locks.
-After promotion they use the selected canonical provider, even when Markdown
-is stale or absent. Provider failure rejects the operation without fallback;
+Before import, `show` can inspect historical frontmatter; normal `set` requires
+canonical authority. Canonical operations use the selected provider even when
+Markdown is stale or absent. Provider failure rejects the operation without fallback;
leftover lease files cannot override canonical records. Only frontmatter and
compact ownership facts cross the legacy TS planning boundary; unrelated body,
metadata, line endings and the final newline remain intact.
@@ -50,9 +59,9 @@ metadata, line endings and the final newline remain intact.
A changed `set` requires no unfinished claimed active Todo and no time-active
lease in the complete snapshot. Invalid expiry/schema cannot prove quiescence;
concurrent work invalidates its CAS. Identical soft/hard mode is a no-op even
-with active work. Malformed legacy frontmatter can be repaired only when
-quiescent; duplicate fields or missing frontmatter reject, and malformed
-canonical state is never repaired through the legacy adapter.
+with active work. Historical malformed frontmatter must be resolved through the reviewed import
+boundary; it cannot create a normal writer fallback. Malformed canonical state
+is never repaired through that adapter.
Canonical `--operation-id` binds Goal and target. Even an accepted no-op seals a
receipt. Retry with the same ID/target recovers the original decision and never
@@ -61,8 +70,8 @@ restores an old mode over later work; changed intent needs a fresh ID.
Malformed receipts reject. An original `set --mode legacy --operation-id `
request still reaches receipt recovery after upgrade, including after a later
policy change. New legacy writes and dry-runs return `handoff_mode_retired`;
-`plan-migration` also refuses legacy as a target. The unpromoted writer does not
-promise durable operation replay.
+`plan-migration` also refuses legacy as a target. An unpromoted normal write returns `canonical_authority_required`; use the
+reviewed import before changing current policy.
## Migrate an assigned canonical Goal with a verified backup
@@ -127,16 +136,18 @@ unretained preview.
CLI and App share registry validation and the same TS migration owner. Lark
ordinary Todo/lease actions consume that canonical state; policy migration has
no Lark editor in this stage. The capability editor does not own this policy. Migration does not
-qualify provider defaults, D1–D3, or PostgreSQL deployment. Complete Host lease
-lifecycles and migration of remaining legacy Goals precede legacy execution and
-last-caller Python retirement; historical import/receipt readers stay at their
-migration/recovery boundary.
+qualify provider defaults, D1–D3, or PostgreSQL deployment. Complete Host lease lifecycles, reviewed migration of remaining Goals and installed
+adoption remain release qualifications. The candidate's legacy execution cutoff
+is not evidence that those qualifications passed. Historical import/receipt
+readers stay at their migration/recovery boundary.
## 中文
新增 `set` 和迁移只接受 `soft_claim`、`hard_lease`。legacy 仍可作为升级来源、
-历史回执恢复输入,不能再作为新请求的目标;已有 Goal 和创建/Host 默认值不被
-静默改写。soft 用于明确的单执行者,hard 用于本机并行 Agent/automation 和云端
+历史回执恢复输入,不能再作为新请求的目标;已有 Goal 和创建/Host 默认值不被静默改写。2.0 候选的 fresh
+claim/update/terminal、Monitor、执行 fence 及租约执行准入明确拒绝 legacy;
+inspect 保留原记录但报告无有效执行,旧 acquire 回执也不能换成当前 proof。
+精确 CAS release、历史原操作恢复及有备份的审核迁移继续可用。soft 用于明确的单执行者,hard 用于本机并行 Agent/automation 和云端
共享;默认策略切换前还需验证 Host 的 acquire、renew、release 和阻塞结算。
App 的任务列表和详情共用审核完成入口。终态 owner 要求租约或当前执行证明时,
页面明确说明此调用方无法代为完成;负责 Agent 须从原执行提交当前证明,之后刷新
@@ -178,15 +189,17 @@ App 在 **Goal 设置 → 任务所有权** 复用同一操作:选择协作认
CLI 与 App 共用注册信息校验和 TS 迁移 owner;飞书普通操作消费相同 canonical 状态,
本阶段没有飞书策略编辑器,也不新建 capability 设置项。此批不关闭 provider 默认与
-D1–D3;先完成 Host 生命周期、
-逐 Goal 备份迁移,再删除 legacy 执行分支和无调用方的 Python 业务逻辑。旧格式解析
-只保留在迁移/历史回执恢复边界。
+D1–D3;候选已拒绝 legacy 正常执行,并不证明 Host 生命周期、逐 Goal 备份迁移
+及安装采用已经完成。旧格式解析只保留在迁移/历史回执恢复边界。
## Recover a canonical Todo edit with retained lease history
-A canonical Todo can retain a released or expired lease even in `legacy` mode.
-In `legacy` and `hard_lease`, that record preserves execution lineage: `todo update`
-still requires a current active owner proof. In `soft_claim`, an open agent Todo's
+A canonical Todo can retain a released or expired lease in historical `legacy`
+mode. That mode requires a reviewed migration before normal work, even with a
+retained active proof. Release the exact old execution first, then use the existing
+backup/plan/migrate flow; never acquire another legacy execution. In `hard_lease`,
+the retained record preserves execution lineage: `todo update` requires current
+active owner proof. In `soft_claim`, an open agent Todo's
current claim owner may edit text/notes or clear those fields without proof when
its retained lease is released and held by the same owner. The transaction keeps
the lease, epoch and historical receipts unchanged and still checks actor,
@@ -202,6 +215,7 @@ under a fresh operation and current revision. Other execution fences remain:
| Observation | Recovery |
| --- | --- |
+| Historical `legacy` policy, with or without a retained lease | Release any retained execution using its exact CAS, then review and back up a migration to `soft_claim` or `hard_lease`. Historical acceptance grants no current execution. |
| Active lease held by the current claim owner; missing/stale proof | Inspect the lease and retry using its current proof. Do not acquire a competing execution. |
| Released/expired lease; current owner is eligible and acquisition passes the current mode, acceptance and scope checks | Inspect the version, acquire a short lease with a fresh key, update using the returned proof, then release it. |
| Active foreign holder, divergent claim, or absent claim | Reconcile ownership through its lifecycle; never borrow another holder's proof. |
@@ -227,7 +241,8 @@ todo_done: --reason ''` 暂停。此显式 wait
The recovery descriptor uses the standalone `loopx task-lease acquire` command.
Combined `todo claim --task-lease-idempotency-key` is restricted to `hard_lease`
-and is not the recovery route for `legacy`. Acquire uses `--owner`, a **fresh**
+and is unavailable in historical `legacy` mode. After reviewed migration,
+acquire uses `--owner`, a **fresh**
`--idempotency-key`, `--expected-version` from `task-lease inspect`, a bounded
`--ttl-seconds`, and any projected `--write-scope` values. Retry the original
update with `--task-lease-idempotency-key` and `--task-lease-expected-version`
@@ -241,8 +256,12 @@ the configured mode, promote a provider, or waive acceptance.
### 保留租约历史时的更新恢复
-canonical Todo 在 `legacy` 模式下也可能保留 released/expired lease。legacy 与
-hard_lease 仍要求当前有效 owner proof。soft_claim 下,open Agent Todo 的当前
+历史 `legacy` policy 即使保留 active lease,也不能执行新的 Todo mutation 或借原回执
+恢复当前执行资格。先按原 key/version 精确 release,再通过既有 plan/backup/migrate
+审核升级到 soft_claim 或 hard_lease;不改写旧回执、不丢弃 lease history,也不自动迁移活跃 Goal。
+
+canonical Todo 在历史 `legacy` 模式下也可能保留 released/expired lease,
+须先审核迁移;hard_lease 仍要求当前有效 owner proof。soft_claim 下,open Agent Todo 的当前
claim owner 可不带 proof 编辑文本/备注或清空这些字段,前提是保留的 lease 已释放
且同属本人。事务保留 lease、epoch 和原回执,仍核验 actor、excluded、bound 和
provider CAS;这不授权 ownership、planning、工作要求或完成变更。未释放的 active
@@ -253,8 +272,8 @@ lease 即使自然过期,也不适用。显式旧 proof 或部分 proof 仍拒
有效的本主租约应 inspect 后使用当前 proof 重试;已释放或过期的租约,只有当前
claim owner 满足相同的 acquire 准入规则时,才提示“inspect version → 用新 key
-申请短 lease → 带新 proof 更新 → release”。legacy 必须用独立 `task-lease acquire`,
-不能用仅限 hard_lease 的合并式 claim+lease。
+申请短 lease → 带新 proof 更新 → release”。legacy 下独立 acquire 与合并式
+claim+lease 均不可执行,必须先审核迁移;合并式 claim+lease 仍仅限 hard_lease。
异主有效 lease、claim 不一致、不允许执行的 Todo、验收阻塞或 scope
冲突不会得到不可执行的 acquire 建议;修改已租用工作的要求或状态须走对应 lifecycle。
diff --git a/docs/reference/local-authority-provider-selection.md b/docs/reference/local-authority-provider-selection.md
index f8a34101d0..c88104c424 100644
--- a/docs/reference/local-authority-provider-selection.md
+++ b/docs/reference/local-authority-provider-selection.md
@@ -321,7 +321,7 @@ every Goal and not removal of every Python module:
| --- | --- |
| Unpromoted Markdown authority | Retain inspection and explicit upgrade/recovery; reject normal mutation before any write or new execution grant, with an actionable supported migration route. Do not silently initialize an empty canonical store. |
| Canonical File or SQLite | Keep the recorded provider and either supported `soft_claim` or `hard_lease` policy. New unconfigured Goals default to SQLite; existing File Goals do not have to switch. Missing providers remain errors. |
-| Canonical store with `legacy` ownership policy | Review a claim-preserving policy migration to `soft_claim` or `hard_lease` before normal execution. Changing policy does not require promoting the store again or switching its provider. |
+| Canonical store with `legacy` ownership policy | Normal ownership mutations and execution proofs return `handoff_mode_retired`; old receipt recovery and exact release remain available. Review a claim-preserving policy migration to `soft_claim` or `hard_lease`. This does not require another storage promotion or provider switch. |
| Target-only v0 or v1 creation disabled | Keep decoding for inspection and reviewed configuration upgrade. Require an explicit compatible creation choice before creating a writable Goal; do not erase the preference or reinterpret disabled as enabled. |
| Historical ownership inputs, source captures and backups | Recognize them in declared migration/recovery contracts. Historical leases, receipts and a restored source do not grant new execution authority. Preserve supported original-operation recovery and permanent Markdown display. |
From b9cc95424709f834b870f1a911edc86a3955c0ce Mon Sep 17 00:00:00 2001
From: LoopX Agent <337587101+loopx-agent@users.noreply.github.com>
Date: Thu, 15 Oct 2026 13:27:52 +0800
Subject: [PATCH 32/38] test(authority): use current policies in claim and
deferral fixtures
Signed-off-by: LoopX Agent <337587101+loopx-agent@users.noreply.github.com>
---
tests/control_plane/test_causal_blocked_closeout_cli.py | 2 +-
tests/control_plane/test_local_coordination_authority.py | 7 +++----
2 files changed, 4 insertions(+), 5 deletions(-)
diff --git a/tests/control_plane/test_causal_blocked_closeout_cli.py b/tests/control_plane/test_causal_blocked_closeout_cli.py
index a91d232323..6582deaf5a 100644
--- a/tests/control_plane/test_causal_blocked_closeout_cli.py
+++ b/tests/control_plane/test_causal_blocked_closeout_cli.py
@@ -78,7 +78,7 @@ def cli(*args: str, cwd: Path = project) -> tuple[int, dict]:
if defer:
next(todo for todo in listed["todos"] if todo["todo_id"] == TODO_ID)["claimed_by"] = AGENT_ID
initialize_canonical_authority(runtime, GOAL_ID, build_todo_runtime_shadow_projection(
- goal_id=GOAL_ID, todos=listed["todos"], handoff_mode="legacy" if defer else "soft_claim", leases=[],
+ goal_id=GOAL_ID, todos=listed["todos"], handoff_mode="hard_lease" if defer else "soft_claim", leases=[],
), state_path=state, provider=provider)
binding = ("--agent-id", AGENT_ID, "--todo-id", TODO_ID,
"--turn-instance-id", f"causal-blocked-{kind}-{provider}")
diff --git a/tests/control_plane/test_local_coordination_authority.py b/tests/control_plane/test_local_coordination_authority.py
index 2f419fd250..51992c4524 100644
--- a/tests/control_plane/test_local_coordination_authority.py
+++ b/tests/control_plane/test_local_coordination_authority.py
@@ -941,10 +941,9 @@ def _seed_promoted_store(
}
],
)
- # main's builder carries no handoff mode and the claim owner reads a missing
- # mode as legacy; this branch's builder defaults to hard_lease, so seed the
- # legacy mode explicitly unless a test asks for another one.
- projection["handoff_mode"] = str(handoff_mode) if handoff_mode is not None else "legacy"
+ # Normal claim tests need a current policy; historical wire shape does not
+ # imply the retired ownership strategy. Explicit modes stay caller-owned.
+ projection["handoff_mode"] = str(handoff_mode) if handoff_mode is not None else "soft_claim"
state_path = runtime_root / "ACTIVE_GOAL_STATE.md"
if not state_path.exists():
state_path.write_text(
From 8d7ba30e24736708df6ff82c50d1f77bccba4aab Mon Sep 17 00:00:00 2001
From: LoopX Agent <337587101+loopx-agent@users.noreply.github.com>
Date: Thu, 15 Oct 2026 13:52:52 +0800
Subject: [PATCH 33/38] refactor(authority): retire unused source lease writers
and admission bridges
Signed-off-by: LoopX Agent <337587101+loopx-agent@users.noreply.github.com>
---
.../work_items/local_lease_record.py | 180 +-----------------
loopx/control_plane/work_items/task_lease.py | 144 --------------
.../work_items/task_lease_acquire_adapter.py | 106 +----------
.../work_items/task_lease_lifecycle.ts | 90 ++-------
.../test_canonical_handoff_invariants.py | 29 +--
.../test_canonical_lease_inspection.py | 15 +-
...test_goal_channel_hard_lease_visibility.py | 85 +++++----
tests/control_plane/test_task_lease.py | 157 +++++----------
8 files changed, 142 insertions(+), 664 deletions(-)
diff --git a/loopx/control_plane/work_items/local_lease_record.py b/loopx/control_plane/work_items/local_lease_record.py
index 23887e829e..c7e575f0a9 100644
--- a/loopx/control_plane/work_items/local_lease_record.py
+++ b/loopx/control_plane/work_items/local_lease_record.py
@@ -1,18 +1,15 @@
-"""Local task-lease record codec and generation normalization.
+"""Read retained task-lease JSON and report transport errors.
-This module owns the JSON/tombstone shape at the existing file adapter edge.
-It does not decide coordination transitions or perform locking.
+Current lifecycle decisions, generation validation and writes belong to the
+TypeScript owner. Historical readers do not mint or rewrite lease records.
"""
from __future__ import annotations
import json
-from datetime import datetime
from pathlib import Path
from typing import Any
-from ..runtime.time import utc_isoformat
-
TASK_LEASE_SCHEMA_VERSION = "task_lease_v0"
@@ -48,174 +45,3 @@ def read_lease(path: Path) -> dict[str, Any] | None:
payload={"lease_path": str(path)},
)
return payload
-
-
-def write_lease(path: Path, payload: dict[str, Any]) -> None:
- path.parent.mkdir(parents=True, exist_ok=True)
- temp_path = path.with_name(f".{path.name}.{id(payload)}.tmp")
- temp_path.write_text(
- json.dumps(payload, ensure_ascii=False, indent=2) + "\n",
- encoding="utf-8",
- )
- temp_path.replace(path)
-
-
-def lease_epoch(lease: dict[str, Any] | None) -> int:
- """Return the authority-owned generation for one todo's lease lineage."""
-
- if not lease:
- return 0
- raw_epoch = lease.get("lease_epoch")
- if raw_epoch is None:
- # A pre-lease_epoch record is the first visible generation.
- return 1
- if isinstance(raw_epoch, bool):
- # bool is an int subclass: JSON true must not become epoch 1.
- raise TaskLeaseError(
- "lease epoch must be a positive integer",
- code="corrupt_lease",
- payload={"lease_epoch": raw_epoch},
- )
- try:
- epoch = int(raw_epoch)
- except (TypeError, ValueError) as exc:
- raise TaskLeaseError(
- "lease epoch must be a positive integer",
- code="corrupt_lease",
- payload={"lease_epoch": raw_epoch},
- ) from exc
- if epoch <= 0:
- raise TaskLeaseError(
- "lease epoch must be a positive integer",
- code="corrupt_lease",
- payload={"lease_epoch": raw_epoch},
- )
- return epoch
-
-
-def lease_version(lease: dict[str, Any] | None) -> int:
- """Normalize the local CAS version without leaking ``ValueError``."""
-
- raw_version = (lease or {}).get("version")
- if raw_version is None:
- return 0
- if isinstance(raw_version, bool):
- raise TaskLeaseError(
- "lease version must be a non-negative integer",
- code="corrupt_lease",
- payload={"version": raw_version},
- )
- try:
- version = int(raw_version)
- except (TypeError, ValueError) as exc:
- raise TaskLeaseError(
- "lease version must be a non-negative integer",
- code="corrupt_lease",
- payload={"version": raw_version},
- ) from exc
- if version < 0:
- raise TaskLeaseError(
- "lease version must be a non-negative integer",
- code="corrupt_lease",
- payload={"version": raw_version},
- )
- return version
-
-
-def lease_acquire_ttl_seconds(lease: dict[str, Any] | None) -> int | None:
- """Read the acquire fingerprint only for acquire replay decisions."""
-
- raw_ttl = (lease or {}).get("acquire_ttl_seconds")
- if raw_ttl is None:
- return None
- if isinstance(raw_ttl, bool):
- raise TaskLeaseError(
- "lease acquire_ttl_seconds must be an integer",
- code="corrupt_lease",
- payload={"acquire_ttl_seconds": raw_ttl},
- )
- try:
- return int(raw_ttl)
- except (TypeError, ValueError) as exc:
- raise TaskLeaseError(
- "lease acquire_ttl_seconds must be an integer",
- code="corrupt_lease",
- payload={"acquire_ttl_seconds": raw_ttl},
- ) from exc
-
-
-def released_lease_payload(
- lease: dict[str, Any],
- *,
- at: datetime,
-) -> dict[str, Any]:
- released = dict(lease)
- released["lease_epoch"] = lease_epoch(lease)
- released["status"] = "released"
- released["released_at"] = utc_isoformat(at)
- released["updated_at"] = utc_isoformat(at)
- return released
-
-
-def assert_expected_version(
- lease: dict[str, Any] | None,
- expected_version: int | None,
-) -> None:
- if expected_version is None:
- return
- actual = lease_version(lease)
- if actual != expected_version:
- raise TaskLeaseError(
- f"lease version mismatch: expected {expected_version}, got {actual}",
- code="version_mismatch",
- payload={"expected_version": expected_version, "actual_version": actual},
- )
-
-
-def require_expected_version(expected_version: int | None, *, action: str) -> int:
- if expected_version is None:
- raise TaskLeaseError(
- f"task lease {action} requires the current lease version",
- code="version_required",
- payload={"action": action},
- )
- if isinstance(expected_version, bool):
- # bool is an int subclass; JSON true from a tool argument must not
- # silently become expected version 1.
- raise TaskLeaseError(
- f"task lease {action} requires an integer lease version",
- code="version_required",
- payload={"action": action, "expected_version": expected_version},
- )
- return int(expected_version)
-
-
-def build_lease(
- *,
- goal_id: str,
- todo_id: str,
- owner: str,
- idempotency_key: str,
- write_scopes: list[str],
- acquire_ttl_seconds: int,
- version: int,
- lease_epoch: int,
- acquired_at: str,
- updated_at: str,
- expires_at: str,
-) -> dict[str, Any]:
- return {
- "schema_version": TASK_LEASE_SCHEMA_VERSION,
- "goal_id": goal_id,
- "todo_id": todo_id,
- "owner": owner,
- "idempotency_key": idempotency_key,
- "write_scopes": write_scopes,
- "acquire_ttl_seconds": acquire_ttl_seconds,
- "version": version,
- "lease_epoch": lease_epoch,
- "acquired_at": acquired_at,
- "updated_at": updated_at,
- "expires_at": expires_at,
- "status": "active",
- }
diff --git a/loopx/control_plane/work_items/task_lease.py b/loopx/control_plane/work_items/task_lease.py
index e7e1abd857..e468774f10 100644
--- a/loopx/control_plane/work_items/task_lease.py
+++ b/loopx/control_plane/work_items/task_lease.py
@@ -5,10 +5,6 @@
from pathlib import Path
from typing import Any
-from ...agent_registry import (
- registered_agent_ids_from_registry,
- require_registered_agent_id,
-)
from ...history import load_registry
from ...paths import resolve_runtime_root
from ..runtime.time import now_utc as runtime_now_utc
@@ -19,20 +15,12 @@
from ..todos.contract import (
normalize_required_write_scopes,
normalize_todo_claimed_by,
- normalize_todo_excluded_agents,
normalize_todo_id,
)
from .local_lease_record import (
TASK_LEASE_SCHEMA_VERSION,
TaskLeaseError,
- assert_expected_version as assert_expected_version,
- build_lease as build_lease,
- lease_acquire_ttl_seconds as lease_acquire_ttl_seconds,
- lease_epoch as lease_epoch,
- lease_version as lease_version,
read_lease as read_lease,
- require_expected_version as require_expected_version,
- write_lease as write_lease,
)
DEFAULT_TASK_LEASE_TTL_SECONDS = 45 * 60
@@ -138,138 +126,6 @@ def runtime_root_from_registry(
)
-def task_lease_todo_projection(
- *,
- registry_path: Path,
- goal_id: str,
- todo_id: str,
-) -> dict[str, Any] | None:
- """Resolve the canonical Markdown/event todo projection for lease guards."""
-
- from ...control_plane.todos.list_readback import list_goal_todos
-
- try:
- payload = list_goal_todos(
- registry_path=registry_path,
- goal_id=goal_id,
- todo_id=todo_id,
- )
- except (OSError, ValueError) as exc:
- raise TaskLeaseError(
- "cannot resolve todo projection for task lease",
- code="todo_projection_unavailable",
- payload={"goal_id": goal_id, "todo_id": todo_id, "error": str(exc)},
- ) from exc
- todo = payload.get("todo")
- return dict(todo) if isinstance(todo, dict) else None
-
-
-def task_lease_owner_constraint(
- todo: dict[str, Any] | None,
- *,
- owner: Any,
- registered_agents: list[str] | None = None,
-) -> dict[str, Any]:
- from ..effect_runtime import effect_runtime_result
-
- normalized_owner = normalize_todo_claimed_by(owner)
- registered = registered_agents if registered_agents is not None else ([normalized_owner] if normalized_owner else [])
- result = effect_runtime_result("task_lease.owner_eligibility", {
- "todo": None if todo is None else {
- "status": str(todo.get("status") or "").strip().lower(),
- "claimed_by": normalize_todo_claimed_by(todo.get("claimed_by")),
- "excluded_agents": normalize_todo_excluded_agents(todo.get("excluded_agents")),
- },
- "owner": normalized_owner, "registered_agents": registered,
- })
- if not isinstance(result, dict) or result.get("schema_version") != "task_lease_owner_eligibility_v0":
- raise RuntimeError("typed lease owner constraint is missing; update the runtime")
- constraint = result.get("constraint")
- if not isinstance(constraint, dict) or not isinstance(constraint.get("effective"), bool):
- raise RuntimeError("typed lease owner constraint shape mismatch")
- if constraint["effective"] is False and not isinstance(constraint.get("reason"), str):
- raise RuntimeError("typed lease owner rejection omitted its reason")
- return constraint
-
-
-def require_registered_task_lease_owner(
- *,
- registry_path: Path,
- goal_id: str,
- todo_id: str,
- owner: str,
-) -> str:
- try:
- return require_registered_agent_id(
- registry_path=registry_path,
- goal_id=goal_id,
- agent_id=owner,
- field="task lease owner",
- )
- except ValueError as exc:
- raise TaskLeaseError(
- str(exc),
- code="owner_not_registered",
- payload={"goal_id": goal_id, "todo_id": todo_id, "owner": owner},
- ) from exc
-
-
-def require_task_lease_owner_allowed(
- *,
- registry_path: Path,
- goal_id: str,
- todo_id: str,
- owner: str,
-) -> dict[str, Any]:
- owner = require_registered_task_lease_owner(
- registry_path=registry_path,
- goal_id=goal_id,
- todo_id=todo_id,
- owner=owner,
- )
- todo = task_lease_todo_projection(
- registry_path=registry_path,
- goal_id=goal_id,
- todo_id=todo_id,
- )
- constraint = task_lease_owner_constraint(
- todo,
- owner=owner,
- registered_agents=registered_agent_ids_from_registry(registry_path, goal_id),
- )
- if constraint.get("effective") is not True:
- reason = str(constraint.get("reason") or "owner_not_allowed")
- if reason == "todo_not_found":
- message = "todo is missing from the canonical projection"
- elif reason == "todo_not_open":
- message = (
- f"task lease requires an open todo; "
- f"{todo_id!r} is {constraint.get('todo_status')!r}"
- )
- elif reason == "owner_excluded_from_todo":
- message = f"task lease owner {owner!r} is excluded from todo {todo_id!r}"
- elif reason == "owner_conflicts_with_claim":
- message = (
- f"task lease owner {owner!r} conflicts with todo claim "
- f"{constraint.get('claimed_by')!r}"
- )
- elif reason == "owner_not_registered":
- message = f"task lease owner {owner!r} is not registered for goal {goal_id!r}"
- else:
- message = f"task lease owner {owner!r} is not eligible for todo {todo_id!r}"
- raise TaskLeaseError(
- message,
- code=reason,
- payload={
- "goal_id": goal_id,
- "todo_id": todo_id,
- "owner": owner,
- **constraint,
- },
- )
- return todo
-
-
def lease_expires_at(lease: dict[str, Any] | None) -> datetime | None:
return parse_timestamp((lease or {}).get("expires_at"))
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 11326a02f7..7feef5a81c 100644
--- a/loopx/control_plane/work_items/task_lease_acquire_adapter.py
+++ b/loopx/control_plane/work_items/task_lease_acquire_adapter.py
@@ -18,7 +18,6 @@
from ..goals.legacy_event_source import (
state_event_log_candidates as _state_event_log_candidates,
)
-from ..todos.contract import normalize_todo_id
from ..todos.handoff_mode import HANDOFF_MODE_LEGACY
from .local_lease_record import TASK_LEASE_SCHEMA_VERSION, TaskLeaseError
@@ -123,82 +122,22 @@ def _raw_registered_agent_candidates(goal: dict[str, Any]) -> list[Any]:
]
-def _compact_task_lease_todo_fact(todo: dict[str, Any]) -> dict[str, Any] | None:
- todo_id = normalize_todo_id(todo.get("todo_id"))
- if not todo_id:
- return None
- compacted = {
- "todo_id": todo_id,
- "status": todo.get("status"),
- "claimed_by": todo.get("claimed_by"),
- "excluded_agents": todo.get("excluded_agents"),
- }
- for field in ("role", "task_class", "bound_agent", "blocks_agent"):
- if field in todo:
- compacted[field] = todo.get(field)
- return compacted
-
-
-def _compact_task_lease_todos(projection: dict[str, Any]) -> list[dict[str, Any]]:
- todos: list[dict[str, Any]] = []
- for raw_todo in projection.get("todos") or []:
- if not isinstance(raw_todo, dict):
- continue
- compact_todo = _compact_task_lease_todo_fact(raw_todo)
- if compact_todo is not None:
- todos.append(compact_todo)
- return todos
-
-
def _task_lease_authority_projection(
*,
- registry_path: Path,
- goal_id: str,
- todo_id: str,
goal: dict[str, Any] | None,
state_file: Path | None,
- project_todos: bool = True,
-) -> tuple[Any, list[Any], list[dict[str, Any]], dict[str, Any] | None]:
- from ..todos.list_readback import list_goal_todos
+) -> tuple[Any, list[Any]]:
from ..goals.active_state_metadata import parse_state_frontmatter
if goal is None:
- return HANDOFF_MODE_LEGACY, [], [], None
+ return HANDOFF_MODE_LEGACY, []
handoff_mode: Any = HANDOFF_MODE_LEGACY
if state_file is not None and state_file.exists():
handoff_mode = parse_state_frontmatter(
state_file.read_text(encoding="utf-8")
).get("handoff_mode")
- if not project_todos:
- return handoff_mode, _raw_registered_agent_candidates(goal), [], None
- try:
- projection = list_goal_todos(
- registry_path=registry_path,
- goal_id=goal_id,
- )
- except (OSError, ValueError) as exc:
- projection_error = {
- "code": "todo_projection_unavailable",
- "message": "cannot resolve todo projection for task lease",
- "payload": {
- "goal_id": goal_id,
- "todo_id": todo_id,
- "error": str(exc),
- },
- }
- return (
- handoff_mode,
- _raw_registered_agent_candidates(goal),
- [],
- projection_error,
- )
- return (
- handoff_mode,
- _raw_registered_agent_candidates(goal),
- _compact_task_lease_todos(projection),
- None,
- )
+ return handoff_mode, _raw_registered_agent_candidates(goal)
def _authority_source_identity(
@@ -215,7 +154,6 @@ def _task_lease_authority_snapshot_attempt(
registry_path: Path,
goal_id: str,
todo_id: str,
- project_todos: bool = True,
) -> dict[str, Any] | None:
registry_receipt_before = _authority_source_receipt("registry", registry_path)
registry = load_registry(registry_path)
@@ -230,15 +168,8 @@ def _task_lease_authority_snapshot_attempt(
if receipts_before[0] != registry_receipt_before:
return None
- handoff_mode, registered_candidates, todos, projection_error = (
- _task_lease_authority_projection(
- registry_path=registry_path,
- goal_id=goal_id,
- todo_id=todo_id,
- goal=goal,
- state_file=state_file,
- project_todos=project_todos,
- )
+ handoff_mode, registered_candidates = _task_lease_authority_projection(
+ goal=goal, state_file=state_file,
)
registry_after = load_registry(registry_path)
@@ -257,8 +188,8 @@ def _task_lease_authority_snapshot_attempt(
return {
"handoff_mode": handoff_mode,
"registered_agent_candidates": registered_candidates,
- "todos": todos,
- "todo_projection_error": projection_error,
+ "todos": [],
+ "todo_projection_error": None,
"source_receipts": receipts_after,
}
@@ -268,16 +199,14 @@ def task_lease_acquire_authority_facts(
registry_path: Path,
goal_id: str,
todo_id: str,
- project_todos: bool = True,
) -> dict[str, Any]:
- """Project source-stable facts; inspection can defer Todo parsing to TS demand."""
+ """Read source-stable history metadata without reviving Todo admission."""
for _attempt in range(TASK_LEASE_AUTHORITY_SNAPSHOT_ATTEMPTS):
facts = _task_lease_authority_snapshot_attempt(
registry_path=registry_path,
goal_id=goal_id,
todo_id=todo_id,
- project_todos=project_todos,
)
if facts is not None:
return facts
@@ -551,22 +480,7 @@ def execute_native_task_lease_lifecycle(
code="claim_transfer_requires_canonical_authority",
)
if registry_path is not None and needs_authority:
- try:
- authority = _canonical_lease_authority_facts(registry_path, goal_id)
- except FileNotFoundError:
- # Release is a cleanup operation and may run without its
- # optional authority projection. Every operation that can
- # authorize a lease mutation remains fail-closed when the
- # canonical registry is unavailable.
- if needs_authority:
- raise
- authority = None
- except TaskLeaseError:
- if needs_authority:
- raise
- # Release is intentionally usable for cleanup even when its
- # optional source projection is unavailable.
- authority = None
+ authority = _canonical_lease_authority_facts(registry_path, goal_id)
request: dict[str, Any] = {
"schema_version": (
TASK_LEASE_CANONICAL_LIFECYCLE_REQUEST_SCHEMA
@@ -680,7 +594,7 @@ def inspect_native_task_lease(
# Source records are retained history, never a current execution policy.
# No retired Todo projection is needed to report them read-only.
authority = _canonical_lease_authority_facts(registry_path, goal_id) if canonical else task_lease_acquire_authority_facts(
- registry_path=registry_path, goal_id=goal_id, todo_id=todo_id, project_todos=False,
+ registry_path=registry_path, goal_id=goal_id, todo_id=todo_id,
)
request = {
"schema_version": "loopx_task_lease_inspect_request_v0",
diff --git a/loopx/control_plane/work_items/task_lease_lifecycle.ts b/loopx/control_plane/work_items/task_lease_lifecycle.ts
index 785cbe9688..26e69600cc 100644
--- a/loopx/control_plane/work_items/task_lease_lifecycle.ts
+++ b/loopx/control_plane/work_items/task_lease_lifecycle.ts
@@ -49,13 +49,11 @@ import {
taskLeasePath,
type AuthorityFacts,
type LeaseRecord,
- type TodoFact,
} from "./task_lease_acquire.ts";
import {
decideTaskLeaseLifecycle,
releasedTaskLeaseRecord as releasedLease,
type TaskLeaseLifecycleDecision,
- type TaskLeaseLifecycleDecisionInput,
} from "./task_lease_lifecycle_decision.ts";
import {decodeTaskLeaseLifecycleRequest, TaskLeaseLifecycleError, TASK_LEASE_LIFECYCLE_OPERATIONS,
type LifecycleRequest, type CanonicalLifecycleRequest, type LifecycleErrorInfo, type LifecycleStage,
@@ -837,35 +835,6 @@ function errorInfo(error: unknown, fallbackStage: LifecycleStage = "durable_writ
-function ownerError(
- request: LifecycleRequest,
- code: string,
- todo: TodoFact | null,
- owner = request.owner,
-): TaskLeaseLifecycleError {
- let message: string;
- const payload: JsonObject = {
- goal_id: request.goal_id,
- todo_id: request.todo_id,
- owner,
- reason: code,
- };
- if (code === "todo_not_found") message = "todo is missing from the canonical projection";
- else if (code === "todo_not_open") {
- payload.todo_status = todo?.status || "unknown";
- message = `task lease requires an open todo; '${request.todo_id}' is '${payload.todo_status}'`;
- } else if (code === "owner_excluded_from_todo") {
- payload.excluded_agents = [...(todo?.excluded_agents ?? [])];
- message = `task lease owner '${owner}' is excluded from todo '${request.todo_id}'`;
- } else if (code === "owner_conflicts_with_claim") {
- payload.claimed_by = todo?.claimed_by ?? null;
- message = `task lease owner '${owner}' conflicts with todo claim '${payload.claimed_by}'`;
- } else {
- message = `task lease owner '${owner}' is not registered for goal '${request.goal_id}'`;
- }
- return new TaskLeaseLifecycleError(message, code, payload, "validation");
-}
-
function transitionError(
request: LifecycleRequest,
code: string,
@@ -958,23 +927,17 @@ async function persistOperationReceipt(
});
}
-function ordinaryDecision(
+function retainedReleaseDecision(
request: LifecycleRequest,
lease: LeaseRecord | null,
- todo: TodoFact | null,
at: Date,
): TaskLeaseLifecycleDecision {
- const input: TaskLeaseLifecycleDecisionInput = {
+ return decideTaskLeaseLifecycle({
+ // Release preserves historical cleanup in every decoded mode. It neither
+ // reads Todo admission nor authorizes a fresh execution lease.
handoff_mode: request.authority?.handoff_mode ?? "legacy",
- registered_agents: request.authority?.registered_agents ?? [],
- todo: todo === null
- ? null
- : {
- todo_id: todo.todo_id,
- status: todo.status,
- claimed_by: todo.claimed_by,
- excluded_agents: todo.excluded_agents,
- },
+ registered_agents: [],
+ todo: null,
lease: lease === null
? null
: {
@@ -985,33 +948,19 @@ function ordinaryDecision(
idempotency_key: persistedKey(lease),
version: leaseVersion(lease),
lease_epoch: leaseEpoch(lease),
- write_scopes: Array.isArray(lease.write_scopes)
- ? lease.write_scopes.filter((scope): scope is string =>
- typeof scope === "string"
- )
- : [],
- // Lifecycle decisions do not depend on the original acquire TTL.
- // Legacy records may carry an unrelated malformed value here; keep
- // that compatibility field out of the decision instead of turning
- // renew/release into a new validation surface.
- acquire_ttl_seconds:
- typeof lease.acquire_ttl_seconds === "number" &&
- Number.isSafeInteger(lease.acquire_ttl_seconds) &&
- lease.acquire_ttl_seconds >= 0
- ? lease.acquire_ttl_seconds
- : null,
+ write_scopes: [],
+ acquire_ttl_seconds: null,
},
command: {
- operation: request.operation as "renew" | "transfer" | "release",
+ operation: "release",
owner: request.owner!,
idempotency_key: request.idempotency_key!,
expected_version: request.expected_version,
- ttl_seconds: request.ttl_seconds,
- new_owner: request.new_owner,
- new_idempotency_key: request.new_idempotency_key,
+ ttl_seconds: null,
+ new_owner: null,
+ new_idempotency_key: null,
},
- };
- return decideTaskLeaseLifecycle(input);
+ });
}
function rejectFreshSourceLeaseOperation(request: LifecycleRequest): never {
@@ -1104,19 +1053,8 @@ async function ordinaryOperation(
}
await requireLegacyCoordinationPrimaryWriteAllowed(request.runtime_root, request.goal_id);
- const todo: TodoFact | null = null;
- const decision = ordinaryDecision(request, existing, todo, at);
+ const decision = retainedReleaseDecision(request, existing, at);
if (decision.outcome === "conflict" || decision.outcome === "rejected") {
- if (
- decision.code === "todo_not_found" ||
- decision.code === "todo_not_open" ||
- decision.code === "owner_not_registered" ||
- decision.code === "owner_excluded_from_todo" ||
- decision.code === "owner_conflicts_with_claim"
- ) {
- const rejectedOwner = request.owner;
- throw ownerError(request, decision.code, todo, rejectedOwner);
- }
throw transitionError(request, decision.code, existing, leasePath);
}
diff --git a/tests/control_plane/test_canonical_handoff_invariants.py b/tests/control_plane/test_canonical_handoff_invariants.py
index 486eacb56c..0f1d9602f1 100644
--- a/tests/control_plane/test_canonical_handoff_invariants.py
+++ b/tests/control_plane/test_canonical_handoff_invariants.py
@@ -29,7 +29,6 @@
from loopx.control_plane.work_items.task_lease import (
TaskLeaseError,
acquire_task_lease,
- build_lease,
inspect_task_lease,
release_task_lease,
renew_task_lease,
@@ -263,19 +262,21 @@ def create(root, *, handoff_mode="soft_claim", retained=None, **kwargs):
now = task_lease.now_utc()
start = now - timedelta(hours=2) if retained.get("expired") else now
leases = [
- build_lease(
- goal_id=GOAL_ID,
- todo_id="todo_retained",
- owner=AGENT_A,
- idempotency_key="turn-lease-1",
- write_scopes=[],
- acquire_ttl_seconds=600,
- version=1,
- lease_epoch=1,
- acquired_at=task_lease.isoformat(start),
- updated_at=task_lease.isoformat(start),
- expires_at=task_lease.isoformat(start + timedelta(seconds=600)),
- )
+ {
+ "schema_version": "task_lease_v0",
+ "status": "active",
+ "goal_id": GOAL_ID,
+ "todo_id": "todo_retained",
+ "owner": AGENT_A,
+ "idempotency_key": "turn-lease-1",
+ "write_scopes": [],
+ "acquire_ttl_seconds": 600,
+ "version": 1,
+ "lease_epoch": 1,
+ "acquired_at": task_lease.isoformat(start),
+ "updated_at": task_lease.isoformat(start),
+ "expires_at": task_lease.isoformat(start + timedelta(seconds=600)),
+ }
]
projection = build_todo_runtime_shadow_projection(
goal_id=GOAL_ID, todos=todos, leases=leases, handoff_mode=handoff_mode
diff --git a/tests/control_plane/test_canonical_lease_inspection.py b/tests/control_plane/test_canonical_lease_inspection.py
index 212b604778..b00f5767bf 100644
--- a/tests/control_plane/test_canonical_lease_inspection.py
+++ b/tests/control_plane/test_canonical_lease_inspection.py
@@ -171,16 +171,6 @@ def change_source(method, payload, **kwargs):
assert len(calls) == 3
-def test_owner_constraint_requires_the_native_diagnostic_contract(monkeypatch):
- from loopx.control_plane import effect_runtime
- from loopx.control_plane.work_items.task_lease import task_lease_owner_constraint
- monkeypatch.setattr(effect_runtime, "effect_runtime_result", lambda *_: {
- "schema_version": "task_lease_owner_eligibility_v0", "outcome": "apply", "code": "lease_owner_allowed",
- })
- with pytest.raises(RuntimeError, match="constraint shape mismatch"):
- task_lease_owner_constraint({"status": "open"}, owner="agent-a")
-
-
def test_inactive_legacy_inspection_does_not_project_todos(tmp_path, monkeypatch):
registry = tmp_path / "registry.json"
state = tmp_path / "state.md"
@@ -200,10 +190,11 @@ def forbidden_projection(**_):
assert expired["active"] is False and expired["handoff_mode"] == "hard_lease"
-def test_retained_source_inspection_never_projects_current_execution(tmp_path, monkeypatch):
+@pytest.mark.parametrize("mode", ["legacy", "soft_claim", "hard_lease"])
+def test_retained_source_inspection_never_projects_current_execution(tmp_path, monkeypatch, mode):
registry = tmp_path / "registry.json"
state = tmp_path / "state.md"
- state.write_text("---\nhandoff_mode: hard_lease\n---\n# Synthetic state\n")
+ state.write_text(f"---\nhandoff_mode: {mode}\n---\n# Synthetic state\n")
registry.write_text(json.dumps({"goals": [{"id": GOAL, "repo": str(tmp_path), "state_file": str(state),
"coordination": {"registered_agents": ["agent-a"]}}]}))
runtime = tmp_path / "runtime"
diff --git a/tests/control_plane/test_goal_channel_hard_lease_visibility.py b/tests/control_plane/test_goal_channel_hard_lease_visibility.py
index 2f6189d3ff..c5738365d3 100644
--- a/tests/control_plane/test_goal_channel_hard_lease_visibility.py
+++ b/tests/control_plane/test_goal_channel_hard_lease_visibility.py
@@ -5,12 +5,15 @@
from pathlib import Path
from typing import Any
+import pytest
+
+from canonical_authority_fixture import initialize_canonical_authority, isolate_sqlite_runtime
+
from loopx.control_plane.goals.goal_channel import attach_goal_channel_projection
from loopx.control_plane.goals.goal_channel_projection import (
build_goal_channel_projection,
)
from loopx.control_plane.work_items.task_lease import (
- build_lease,
isoformat,
now_utc,
task_lease_dir,
@@ -48,19 +51,21 @@ def _write_active_lease(
ttl_seconds: int = 600,
) -> dict[str, Any]:
at = now_utc()
- lease = build_lease(
- goal_id=GOAL_ID,
- todo_id=todo_id,
- owner=owner,
- idempotency_key=f"{owner}-instance-1",
- write_scopes=["src/"],
- acquire_ttl_seconds=ttl_seconds,
- version=version,
- lease_epoch=1,
- acquired_at=isoformat(at),
- updated_at=isoformat(at),
- expires_at=isoformat(at + timedelta(seconds=ttl_seconds)),
- )
+ lease = {
+ "schema_version": "task_lease_v0",
+ "status": "active",
+ "goal_id": GOAL_ID,
+ "todo_id": todo_id,
+ "owner": owner,
+ "idempotency_key": f"{owner}-instance-1",
+ "write_scopes": ["src/"],
+ "acquire_ttl_seconds": ttl_seconds,
+ "version": version,
+ "lease_epoch": 1,
+ "acquired_at": isoformat(at),
+ "updated_at": isoformat(at),
+ "expires_at": isoformat(at + timedelta(seconds=ttl_seconds)),
+ }
path = task_lease_path(runtime_root=runtime_root, goal_id=GOAL_ID, todo_id=todo_id)
path.parent.mkdir(parents=True, exist_ok=True)
path.write_text(json.dumps(lease), encoding="utf-8")
@@ -152,19 +157,21 @@ def test_missing_runtime_root_preserves_previous_projection_shape() -> None:
def test_expired_lease_is_not_surfaced(tmp_path: Path) -> None:
at = now_utc()
- lease = build_lease(
- goal_id=GOAL_ID,
- todo_id=TODO_ID,
- owner="agent-a",
- idempotency_key="agent-a-instance-1",
- write_scopes=[],
- acquire_ttl_seconds=60,
- version=1,
- lease_epoch=1,
- acquired_at=isoformat(at - timedelta(seconds=120)),
- updated_at=isoformat(at - timedelta(seconds=120)),
- expires_at=isoformat(at - timedelta(seconds=60)),
- )
+ lease = {
+ "schema_version": "task_lease_v0",
+ "status": "active",
+ "goal_id": GOAL_ID,
+ "todo_id": TODO_ID,
+ "owner": "agent-a",
+ "idempotency_key": "agent-a-instance-1",
+ "write_scopes": [],
+ "acquire_ttl_seconds": 60,
+ "version": 1,
+ "lease_epoch": 1,
+ "acquired_at": isoformat(at - timedelta(seconds=120)),
+ "updated_at": isoformat(at - timedelta(seconds=120)),
+ "expires_at": isoformat(at - timedelta(seconds=60)),
+ }
path = task_lease_path(runtime_root=tmp_path, goal_id=GOAL_ID, todo_id=TODO_ID)
path.parent.mkdir(parents=True, exist_ok=True)
path.write_text(json.dumps(lease), encoding="utf-8")
@@ -309,8 +316,9 @@ def _write_status_workspace(tmp_path: Path) -> tuple[Path, Path, Path]:
return registry, repo, runtime_root
+@pytest.mark.parametrize("provider", ["file", "sqlite"])
def test_status_attention_queue_surfaces_hard_lease_end_to_end(
- tmp_path: Path,
+ tmp_path: Path, monkeypatch: pytest.MonkeyPatch, provider: str,
) -> None:
"""Regression pin for the real delivery surface.
@@ -322,19 +330,20 @@ def test_status_attention_queue_surfaces_hard_lease_end_to_end(
"""
from loopx.status import collect_status
- from loopx.todos import add_goal_todo
+ from loopx.control_plane.coordination.runtime_shadow import build_todo_runtime_shadow_projection
+ isolate_sqlite_runtime(tmp_path, monkeypatch)
registry, repo, runtime_root = _write_status_workspace(tmp_path)
- added = add_goal_todo(
- registry_path=registry,
- goal_id=GOAL_ID,
- role="agent",
- text="Deliver one bounded control-plane change.",
- task_class="advancement_task",
- claimed_by="agent-b",
- )
- todo_id = added["todo_id"]
+ todo_id = TODO_ID
lease = _write_active_lease(runtime_root, owner="agent-a", todo_id=todo_id)
+ # Import an inconsistent retained claim/lease pair into the real provider.
+ # The observation must expose it; current mutation must never create it.
+ todo = {"schema_version": "todo_item_v0", "todo_id": todo_id, "role": "agent", "status": "open", "done": False,
+ "text": "Deliver one bounded control-plane change.", "task_class": "advancement_task",
+ "claimed_by": "agent-b", "archive_state": "active", "source_section": "Agent Todo", "index": 1}
+ initialize_canonical_authority(runtime_root, GOAL_ID,
+ build_todo_runtime_shadow_projection(goal_id=GOAL_ID, todos=[todo], leases=[lease], handoff_mode="hard_lease"),
+ state_path=repo / "ACTIVE_GOAL_STATE.md", provider=provider)
payload = collect_status(
registry_path=registry,
diff --git a/tests/control_plane/test_task_lease.py b/tests/control_plane/test_task_lease.py
index 4125e8aa87..06fbb99ff1 100644
--- a/tests/control_plane/test_task_lease.py
+++ b/tests/control_plane/test_task_lease.py
@@ -1,5 +1,6 @@
from __future__ import annotations
+import json
from datetime import datetime, timedelta, timezone
from pathlib import Path
from typing import Any
@@ -11,12 +12,10 @@
from loopx.control_plane.work_items.task_lease import (
MAX_TASK_LEASE_TTL_SECONDS,
TaskLeaseError,
- assert_expected_version,
normalize_idempotency_key,
normalize_ttl_seconds,
release_task_lease,
renew_task_lease,
- task_lease_owner_constraint,
write_scopes_overlap,
)
@@ -63,57 +62,6 @@ def test_write_scopes_overlap(
assert write_scopes_overlap(left, right) is expected
-@pytest.mark.parametrize(
- ("todo", "owner", "registered_agents", "reason"),
- [
- (None, "agent-a", ["agent-a"], "todo_not_found"),
- ({"status": "done"}, "agent-a", ["agent-a"], "todo_not_open"),
- ({"status": "open"}, "", ["agent-a"], "invalid_owner"),
- ({"status": "open"}, "agent-b", ["agent-a"], "owner_not_registered"),
- (
- {"status": "open", "excluded_agents": ["agent-a"]},
- "agent-a",
- ["agent-a"],
- "owner_excluded_from_todo",
- ),
- (
- {"status": "open", "claimed_by": "agent-b"},
- "agent-a",
- ["agent-a", "agent-b"],
- "owner_conflicts_with_claim",
- ),
- ],
-)
-def test_task_lease_owner_constraint_rejects_ineligible_owner(
- todo: dict[str, Any] | None,
- owner: str,
- registered_agents: list[str],
- reason: str,
-) -> None:
- constraint = task_lease_owner_constraint(
- todo,
- owner=owner,
- registered_agents=registered_agents,
- )
-
- assert constraint["effective"] is False
- assert constraint["reason"] == reason
-
-
-def test_task_lease_owner_constraint_accepts_matching_claim() -> None:
- constraint = task_lease_owner_constraint(
- {
- "status": "open",
- "claimed_by": "agent-a",
- "excluded_agents": ["agent-b"],
- },
- owner="agent-a",
- registered_agents=["agent-a", "agent-b"],
- )
-
- assert constraint == {"effective": True}
-
-
@pytest.mark.parametrize("ttl", [0, -1, MAX_TASK_LEASE_TTL_SECONDS + 1])
def test_normalize_ttl_seconds_rejects_out_of_range(ttl: int) -> None:
with pytest.raises(TaskLeaseError, match="ttl seconds") as error:
@@ -130,43 +78,40 @@ def test_normalize_idempotency_key_rejects_non_token(key: str) -> None:
assert error.value.code == "invalid_idempotency_key"
-def test_expected_version_is_compare_and_swap_guard() -> None:
- with pytest.raises(TaskLeaseError) as error:
- assert_expected_version({"version": 3}, 2)
+@pytest.fixture
+def retained_lease(tmp_path: Path):
+ # Fixed original 1.x bytes; the current runtime must read/release these
+ # without relying on a surviving Python lease producer.
+ original = json.loads((Path(__file__).parent / "fixtures" / "retained_lease_receipts_1x.json").read_text())["scenarios"]["original"]
+ relative, lease = next(iter(original["files"].items()))
+ runtime = tmp_path / "runtime"
+ path = runtime / relative
+ path.parent.mkdir(parents=True)
+ path.write_text(json.dumps(lease))
+ request = {key: original["request"][key] for key in ("goal_id", "todo_id", "owner", "idempotency_key")}
+ return path, lease, {**request, "runtime_root": runtime, "expected_version": lease["version"]}
- assert error.value.code == "version_mismatch"
- assert error.value.payload == {"expected_version": 2, "actual_version": 3}
-
-
-def test_lease_epoch_migrates_legacy_records_and_rejects_corruption() -> None:
- assert task_lease.lease_epoch(None) == 0
- assert (
- task_lease.lease_epoch({"schema_version": "task_lease_v0", "version": 7}) == 1
- )
-
- for raw_epoch in (0, -1, "not-an-epoch"):
- with pytest.raises(TaskLeaseError) as error:
- task_lease.lease_epoch({"lease_epoch": raw_epoch})
- assert error.value.code == "corrupt_lease"
+def test_retained_release_enforces_cas_without_rewriting_history(retained_lease) -> None:
+ path, lease, request = retained_lease
+ before = path.read_bytes()
+ with pytest.raises(TaskLeaseError) as error:
+ release_task_lease(**{**request, "expected_version": lease["version"] + 1})
+ assert error.value.code == "version_mismatch"
+ assert path.read_bytes() == before
-def test_bool_disguised_lease_integers_fail_closed() -> None:
- """bool is an int subclass: a JSON ``true`` must not become epoch/version/
- TTL ``1`` silently. The shared Stage 2 head codec already rejects bool
- (head._is_count); the local record codec is the migration source for
- ``last_lease_epoch`` seeding, so the same typed corrupt_lease applies
- before any local-to-shared migration."""
- for raw in (True, False):
- with pytest.raises(TaskLeaseError) as error:
- task_lease.lease_epoch({"lease_epoch": raw})
- assert error.value.code == "corrupt_lease"
- with pytest.raises(TaskLeaseError) as error:
- task_lease.lease_version({"version": raw})
- assert error.value.code == "corrupt_lease"
- with pytest.raises(TaskLeaseError) as error:
- task_lease.lease_acquire_ttl_seconds({"acquire_ttl_seconds": raw})
- assert error.value.code == "corrupt_lease"
+@pytest.mark.parametrize(("field", "raw"), [
+ ("lease_epoch", value) for value in [0, -1, "not-an-epoch", True, False]
+] + [("version", value) for value in [-1, "not-a-version", True, False]])
+def test_retained_release_rejects_corrupt_generation_without_writes(retained_lease, field, raw) -> None:
+ path, lease, request = retained_lease
+ path.write_text(json.dumps({**lease, field: raw}))
+ before = path.read_bytes()
+ with pytest.raises(TaskLeaseError) as error:
+ release_task_lease(**request)
+ assert error.value.code == "corrupt_lease"
+ assert path.read_bytes() == before
def test_lifecycle_mutation_fails_closed_without_registry(
@@ -181,24 +126,24 @@ def test_lifecycle_mutation_fails_closed_without_registry(
goal_id="goal-a",
todo_id="todo_missing_registry",
)
- task_lease.write_lease(
- lease_path,
- task_lease.build_lease(
- goal_id="goal-a",
- todo_id="todo_missing_registry",
- owner="agent-a",
- idempotency_key="turn-1",
- write_scopes=[],
- acquire_ttl_seconds=120,
- version=1,
- lease_epoch=1,
- acquired_at=now.isoformat().replace("+00:00", "Z"),
- updated_at=now.isoformat().replace("+00:00", "Z"),
- expires_at=(now + timedelta(seconds=120))
+ lease_path.parent.mkdir(parents=True)
+ lease_path.write_text(json.dumps({
+ "schema_version": "task_lease_v0",
+ "status": "active",
+ "goal_id": "goal-a",
+ "todo_id": "todo_missing_registry",
+ "owner": "agent-a",
+ "idempotency_key": "turn-1",
+ "write_scopes": [],
+ "acquire_ttl_seconds": 120,
+ "version": 1,
+ "lease_epoch": 1,
+ "acquired_at": now.isoformat().replace("+00:00", "Z"),
+ "updated_at": now.isoformat().replace("+00:00", "Z"),
+ "expires_at": (now + timedelta(seconds=120))
.isoformat()
.replace("+00:00", "Z"),
- ),
- )
+ }))
before = task_lease.read_lease(lease_path)
with pytest.raises(TaskLeaseError) as error:
@@ -228,9 +173,8 @@ def test_legacy_generation_and_unrelated_ttl_do_not_block_release(
goal_id="goal-a",
todo_id="todo_legacy",
)
- task_lease.write_lease(
- lease_path,
- {
+ lease_path.parent.mkdir(parents=True)
+ lease_path.write_text(json.dumps({
"schema_version": "task_lease_v0",
"goal_id": "goal-a",
"todo_id": "todo_legacy",
@@ -245,8 +189,7 @@ def test_legacy_generation_and_unrelated_ttl_do_not_block_release(
"expires_at": (now + timedelta(seconds=120))
.isoformat()
.replace("+00:00", "Z"),
- },
- )
+ }))
released = release_task_lease(
runtime_root=runtime_root,
From d3f758be64f9126acf2160e73c25bb7382e89685 Mon Sep 17 00:00:00 2001
From: LoopX Agent <337587101+loopx-agent@users.noreply.github.com>
Date: Thu, 15 Oct 2026 13:52:52 +0800
Subject: [PATCH 34/38] docs(authority): record current policy and retired
helper boundaries
Signed-off-by: LoopX Agent <337587101+loopx-agent@users.noreply.github.com>
---
docs/reference/local-authority-provider-selection.md | 11 +++++++++++
1 file changed, 11 insertions(+)
diff --git a/docs/reference/local-authority-provider-selection.md b/docs/reference/local-authority-provider-selection.md
index c88104c424..ff8b7d06d2 100644
--- a/docs/reference/local-authority-provider-selection.md
+++ b/docs/reference/local-authority-provider-selection.md
@@ -545,6 +545,8 @@ module, response alias or test filename:
| Imported completion, succession and policy repair | Typed terminal/update owners; shared provider conformance and actual CLI consumer smokes in the supplement. |
| Claim/lease ownership and atomic completion | `test_canonical_handoff_invariants.py`, canonical mutation/claim-transfer tests and provider conformance. Metadata cannot implement the retired two-write handoff. |
| Original-operation recovery | Fixed 1.x lease and team-plan artifacts guard exact replay, prepared matching effects, original release, lost close response and preservation of later state. Fresh legacy verification/grants remain retired. |
+| Current ownership policy | Only `soft_claim` and `hard_lease` admit current work. Imported `legacy` records and original receipts cannot grant execution; reviewed backup/plan/migrate and exact release remain available. |
+| Retired lease helpers | Delete the unused Python lease writer/factory, generation/CAS helpers, owner-validation chain, and source Todo projection. The typed transaction remains the decision owner; retained JSON reading stays read-only. Release corruption/CAS assertions run through the actual native adapter with fixed 1.x records. |
| Reviewed App creation and configuration | Chat binds the accepted operation to the reviewed provider revision and Todo identity; stale preview rejects, corrected preview can retry. Settings preserve a disabled preference and explain the required upgrade. |
| Mutation-oracle strength and packaging | The fence oracle observes actual exclusive-open contention, including delayed startup. Existing prerelease bundle and installed-package checks retain their independent scope. |
@@ -554,6 +556,15 @@ candidate retain their own acceptance. Historical output aliases and tests
whose only subject was a deleted producer are superseded by the typed current
contract and its supported recovery tests.
+Retirement removes internal Python helper exports with no shipped caller or
+registered import contract; it does not remove CLI/API lease operations, the
+historical wire vocabulary or receipt decoding. Test fixtures describe source
+records directly instead of keeping a production writer solely to construct
+fixtures. Owner eligibility remains covered at its typed owner and by real
+canonical claim/lease transactions. Historical generation checks belong to
+native recovery/release, including corrupt boolean generations and unchanged
+bytes after rejection.
+
### 2.0 退役边界
正常写入现在只接受 canonical authority;旧 writer、producer 和无调用内部桥接
From 7867467d68bb43cf4761264a2b033d592842d976 Mon Sep 17 00:00:00 2001
From: LoopX Agent <337587101+loopx-agent@users.noreply.github.com>
Date: Thu, 15 Oct 2026 14:16:15 +0800
Subject: [PATCH 35/38] test(authority): target retained release fence in
mutation oracle
Signed-off-by: LoopX Agent <337587101+loopx-agent@users.noreply.github.com>
---
examples/shared-goal-authority-e2e/mutants.py | 5 +++--
1 file changed, 3 insertions(+), 2 deletions(-)
diff --git a/examples/shared-goal-authority-e2e/mutants.py b/examples/shared-goal-authority-e2e/mutants.py
index d8c9586845..b6b63402bc 100644
--- a/examples/shared-goal-authority-e2e/mutants.py
+++ b/examples/shared-goal-authority-e2e/mutants.py
@@ -183,8 +183,9 @@ def remove_native_update_maintenance(source: str) -> str:
pattern="^exact legacy release honors an engaged promotion fence without writes$"))
CASES.append(Case("lifecycle_fence_guard_skipped", (
("loopx/control_plane/work_items/task_lease_lifecycle.ts", replacement(
- " await requireLegacyCoordinationPrimaryWriteAllowed(request!.runtime_root, request!.goal_id);\n",
- "")),
+ " await requireLegacyCoordinationPrimaryWriteAllowed(request.runtime_root, request.goal_id);\n"
+ " const decision = retainedReleaseDecision(request, existing, at);",
+ " const decision = retainedReleaseDecision(request, existing, at);")),
), "tests/control_plane_ts/task_lease_lifecycle.test.ts",
pattern="^exact legacy release honors an engaged promotion fence without writes$"))
From 8cad0cb2ac5443c1790b12989cc9641db14065d1 Mon Sep 17 00:00:00 2001
From: LoopX Agent <337587101+loopx-agent@users.noreply.github.com>
Date: Thu, 15 Oct 2026 14:59:52 +0800
Subject: [PATCH 36/38] test(authority): migrate remaining execution fixtures
off legacy
Signed-off-by: LoopX Agent <337587101+loopx-agent@users.noreply.github.com>
---
.../deferred_hard_lease_lifecycle.test.ts | 11 +++++++-
.../goal_acceptance_runtime.test.ts | 20 ++++++++++-----
.../local_authority_runtime.test.ts | 2 +-
.../todo_monitor_poll.test.ts | 2 +-
tests/control_plane_ts/todo_update.test.ts | 25 +++++++++++++------
5 files changed, 43 insertions(+), 17 deletions(-)
diff --git a/tests/control_plane_ts/deferred_hard_lease_lifecycle.test.ts b/tests/control_plane_ts/deferred_hard_lease_lifecycle.test.ts
index 7393a5773b..0af34ead17 100644
--- a/tests/control_plane_ts/deferred_hard_lease_lifecycle.test.ts
+++ b/tests/control_plane_ts/deferred_hard_lease_lifecycle.test.ts
@@ -163,7 +163,7 @@ for (const provider of ["file", "sqlite"] as const) {
for (const provider of ["file", "sqlite"] as const) {
for (const mode of ["legacy", "hard_lease"] as const) {
- test(`${provider}/${mode}: owner deferral releases its grant atomically and can resume`,
+ test(`${provider}/${mode}: ${mode === "legacy" ? "retired policy rejects deferral with retained proof" : "owner deferral releases its grant atomically and can resume"}`,
{skip: provider === "sqlite" && sqliteSkip}, async () => {
const store = await seeded(provider, {status: "open", done: false},
oldLease("active", "2026-09-06T00:00:00Z"));
@@ -173,6 +173,15 @@ for (const provider of ["file", "sqlite"] as const) {
const input = {...resume("defer-owner"), lease_idempotency_key: "old-execution", lease_expected_version: 3,
planning_intent: {status: "deferred", resume_when: "todo_done:todo_dependency", reason: "Dependency is pending"}};
const before = await read(store);
+ if (mode === "legacy") {
+ for (const dry_run of [true, false]) {
+ const rejected = await executeCoordinationTodoUpdate(store, {...input, dry_run});
+ assert.equal(rejected.reason_code, "handoff_mode_retired");
+ assert.deepEqual(await read(store), before);
+ assert.equal((await store.readReceipt(input.operation_id)).status, "missing");
+ }
+ return;
+ }
for (const change of [{lease_expected_version: 2}, {lease_idempotency_key: "foreign"},
{actor_agent_id: "agent-b"}, {lease_idempotency_key: null, lease_expected_version: null},
{patch: {text: "Changed work"}}, {planning_intent: {...input.planning_intent, completion_criteria: "New contract"}}]) {
diff --git a/tests/control_plane_ts/goal_acceptance_runtime.test.ts b/tests/control_plane_ts/goal_acceptance_runtime.test.ts
index eefa2d3a09..a49dab559a 100644
--- a/tests/control_plane_ts/goal_acceptance_runtime.test.ts
+++ b/tests/control_plane_ts/goal_acceptance_runtime.test.ts
@@ -6,6 +6,7 @@ import {join} from "node:path";
import test, {type TestContext} from "node:test";
import {Pool} from "pg";
import type {JsonObject} from "../../loopx/control_plane/effect_program.ts";
+import type {HandoffMode} from "../../loopx/control_plane/coordination/handoff_mode_vocabulary.ts";
import type {AuthorityStore} from "../../loopx/control_plane/coordination/authority_store.ts";
import {FileAuthorityStore} from "../../loopx/control_plane/coordination/file_authority_store.ts";
import {PostgreSqlAuthorityStore, installPostgreSqlAuthorityStoreSchema} from "../../loopx/control_plane/coordination/postgresql_authority_store.ts";
@@ -35,7 +36,7 @@ function acceptance(records: JsonObject[], bound = true): JsonObject {
}
function projection(records: JsonObject[], state?: JsonObject): JsonObject {
records = [...records].sort((a, b) => String(a.todo_id).localeCompare(String(b.todo_id)));
- return {goal_id: "goal-a", handoff_mode: "legacy", todos: records, leases: [],
+ return {goal_id: "goal-a", handoff_mode: "soft_claim", todos: records, leases: [],
todo_read_model: coordinationTodoReadModel(records, "loopx_todo_domain_read_record_v0"),
unrelated_extension: {value: "retained"}, ...(state ? {goal_acceptance: state} : {})};
}
@@ -64,7 +65,7 @@ async function replace(store: AuthorityStore, head: JsonObject, operation_id: st
assert.equal((await store.commitAuthority({operation_id, expected_provider_revision: current.provider_revision,
next_projection: head, events: [], receipts: []})).status, "applied");
}
-async function seeded(t: TestContext, provider: string, state: "bound" | "unbound" | "off", extra: JsonObject = {}) {
+async function seeded(t: TestContext, provider: string, state: "bound" | "unbound" | "off", extra: JsonObject = {}, mode: HandoffMode = "soft_claim") {
const root = await mkdtemp(join(tmpdir(), "loopx-acceptance-runtime-"));
t.after(() => rm(root, {recursive: true, force: true}));
let store: AuthorityStore;
@@ -80,7 +81,8 @@ async function seeded(t: TestContext, provider: string, state: "bound" | "unboun
store = new PostgreSqlAuthorityStore(database, {tenant_id: `acceptance-${randomUUID()}`, goal_id: "goal-a"});
} else store = new FileAuthorityStore(join(root, "authority", "file-v0"), "goal-a");
const records = [todo(extra)];
- const head = projection(records, state === "off" ? undefined : acceptance(records, state === "bound"));
+ const head = {...projection(records, state === "off" ? undefined : acceptance(records, state === "bound")),
+ handoff_mode: mode};
assert.equal((await store.commitAuthority({operation_id: "seed", expected_provider_revision: null,
next_projection: head, events: [], receipts: []})).status, "applied");
return {store, root, head};
@@ -104,6 +106,9 @@ for (const provider of ["file", ...(process.env.LOOPX_TEST_POSTGRES_URL ? ["post
assert.equal(plan.status, "execute_validation");
assert.equal((plan.validation_effect as JsonObject).validation_label, "ordinary-check");
assert.deepEqual(plan.goal_acceptance_validation_effects ?? [], []);
+ // Lease admission is exercised with hard_lease; soft_claim intentionally has no lease grant.
+ const current = await loaded(store);
+ await replace(store, {...current.head, handoff_mode: "hard_lease"}, "require-leased-execution");
assert.equal((await executeCanonicalTaskLeaseAcquire(store, acquire)).status, "applied");
assert.equal(acceptanceWorkGuard((await loaded(store)).head, "goal-a", "todo_probe")?.allowed, false);
});
@@ -123,8 +128,11 @@ for (const provider of ["file", ...(process.env.LOOPX_TEST_POSTGRES_URL ? ["post
const before = await loaded(store);
assert.equal((await executeCoordinationTodoClaim(store, {...claim, actor_agent_id: "intruder"})).reason_code, "actor_not_registered");
assert.equal((await executeCoordinationTodoClaim(store, claim)).reason_code, "goal_acceptance_unbound");
- assert.equal((await executeCanonicalTaskLeaseAcquire(store, {...acquire, owner: "intruder"})).reason_code, "owner_not_registered");
- assert.equal((await executeCanonicalTaskLeaseAcquire(store, acquire)).reason_code, "goal_acceptance_unbound");
+ const leased = await seeded(t, provider, "unbound", {}, "hard_lease");
+ const leaseBefore = await loaded(leased.store);
+ assert.equal((await executeCanonicalTaskLeaseAcquire(leased.store, {...acquire, owner: "intruder"})).reason_code, "owner_not_registered");
+ assert.equal((await executeCanonicalTaskLeaseAcquire(leased.store, acquire)).reason_code, "goal_acceptance_unbound");
+ assert.deepEqual(await loaded(leased.store), leaseBefore);
assert.equal((await executeCoordinationTodoTerminalLifecycle(store, terminal)).reason_code, "goal_acceptance_unbound");
assert.deepEqual(await loaded(store), before);
assert.equal((await store.readReceipt("complete")).status, "missing");
@@ -164,7 +172,7 @@ for (const provider of ["file", ...(process.env.LOOPX_TEST_POSTGRES_URL ? ["post
});
test(`${provider}: acquisition replay checks current acceptance after current lease proof`, async t => {
- const {store} = await seeded(t, provider, "bound");
+ const {store} = await seeded(t, provider, "bound", {}, "hard_lease");
assert.equal((await executeCanonicalTaskLeaseAcquire(store, acquire)).status, "applied");
const current = await loaded(store);
await replace(store, {...current.head, goal_acceptance: acceptance(current.head.todos as JsonObject[], false)}, "unbind");
diff --git a/tests/control_plane_ts/local_authority_runtime.test.ts b/tests/control_plane_ts/local_authority_runtime.test.ts
index 1d93705b66..32f8d8c0ef 100644
--- a/tests/control_plane_ts/local_authority_runtime.test.ts
+++ b/tests/control_plane_ts/local_authority_runtime.test.ts
@@ -1328,7 +1328,7 @@ test("receipt-only claims revalidate CAS contention and recover a lost commit re
await engageCanonicalRoute(root);
const directory = join(root, "authority", "file-v0");
const store = new FileAuthorityStore(directory, "goal-a");
- const projection = withTodoReadModel({goal_id: "goal-a", todos: [
+ const projection = withTodoReadModel({goal_id: "goal-a", handoff_mode: "soft_claim", todos: [
todoRecord({claimed_by: "agent-a"}),
], leases: []});
await store.commitAuthority({operation_id: "seed", expected_provider_revision: null,
diff --git a/tests/control_plane_ts/todo_monitor_poll.test.ts b/tests/control_plane_ts/todo_monitor_poll.test.ts
index c2416ee31b..0a5b474a43 100644
--- a/tests/control_plane_ts/todo_monitor_poll.test.ts
+++ b/tests/control_plane_ts/todo_monitor_poll.test.ts
@@ -16,7 +16,7 @@ async function seeded(overrides: JsonObject = {}, extras: JsonObject[] = []) {
status: "open", done: false, text: "Observe external progress", archive_state: "active",
task_class: "continuous_monitor", target_key: "watch-a", cadence: "1h", ...overrides}, ...extras];
await store.commitAuthority({operation_id: "seed", expected_provider_revision: null,
- events: [], receipts: [], next_projection: {goal_id: "goal-a", todos: records, leases: [],
+ events: [], receipts: [], next_projection: {goal_id: "goal-a", handoff_mode: "soft_claim", todos: records, leases: [],
todo_read_model: {schema_version: TODO_DOMAIN_READ_RECORD_SCHEMA, todo_count: records.length,
records_sha256: canonicalAuthoritySha256(records), contract_fields: [...TODO_DOMAIN_RECORD_CONTRACT.fields]}}});
const request = {goal_id: "goal-a", operation_id: "poll-a", actor_agent_id: "agent-a",
diff --git a/tests/control_plane_ts/todo_update.test.ts b/tests/control_plane_ts/todo_update.test.ts
index 52b5220089..5390a2afa3 100644
--- a/tests/control_plane_ts/todo_update.test.ts
+++ b/tests/control_plane_ts/todo_update.test.ts
@@ -242,7 +242,7 @@ async function seeded(overrides: Record = {}) {
const records = [todo(overrides)];
if (records[0]!.claimed_by === null) Reflect.deleteProperty(records[0]!, "claimed_by");
await store.commitAuthority({operation_id: "seed", expected_provider_revision: null,
- events: [], receipts: [], next_projection: {goal_id: "goal-a", todos: records, leases: [],
+ events: [], receipts: [], next_projection: {goal_id: "goal-a", handoff_mode: "soft_claim", todos: records, leases: [],
todo_read_model: {schema_version: TODO_DOMAIN_READ_RECORD_SCHEMA, todo_count: 1,
records_sha256: canonicalAuthoritySha256(records),
contract_fields: [...TODO_DOMAIN_RECORD_CONTRACT.fields]}}});
@@ -952,6 +952,15 @@ test("retained execution fences distinguish recovery; soft released owner copy n
expires_at: state === "expired" ? "2026-09-04T00:00:00Z" : "2026-09-06T00:00:00Z", write_scopes: []}]}});
const before = await store.loadAuthority();
const result = await executeCoordinationTodoUpdate(store, {...request, dry_run: true});
+ if (mode === "legacy") {
+ assert.equal(result.reason_code, "handoff_mode_retired");
+ assert.match(String(result.reason), /plan-migration\/migrate/);
+ assert.equal(result.recovery, undefined, "retired policy cannot offer executable proof or acquisition");
+ assert.equal(JSON.stringify(result).includes("private-execution-key"), false);
+ assert.deepEqual(await store.loadAuthority(), before);
+ assert.equal((await store.readReceipt(request.operation_id)).status, "missing");
+ continue;
+ }
if (mode === "soft_claim" && state === "released" && owner === "agent-a") {
assert.equal(result.status, "planned");
assert.deepEqual(await store.loadAuthority(), before);
@@ -1043,7 +1052,7 @@ test("recovery preserves stale-proof rejection and never offers foreign proof",
const head = await store.loadAuthority();
if (head.status !== "loaded") assert.fail("missing fixture");
await store.commitAuthority({operation_id: "lease", expected_provider_revision: head.provider_revision,
- events: [], receipts: [], next_projection: {...head.head, handoff_mode: "legacy",
+ events: [], receipts: [], next_projection: {...head.head, handoff_mode: "hard_lease",
leases: [{todo_id: "todo_a", owner: "agent-a", idempotency_key: "current-key",
version: 4, lease_epoch: 1, status: "active", expires_at: "2026-09-06T00:00:00Z", write_scopes: []}]}});
const before = await store.loadAuthority();
@@ -1057,7 +1066,7 @@ test("recovery preserves stale-proof rejection and never offers foreign proof",
const head = await store.loadAuthority();
if (head.status !== "loaded") assert.fail("missing fixture");
await store.commitAuthority({operation_id: "foreign-lease", expected_provider_revision: head.provider_revision,
- events: [], receipts: [], next_projection: {...head.head, handoff_mode: "legacy",
+ events: [], receipts: [], next_projection: {...head.head, handoff_mode: "hard_lease",
leases: [{todo_id: "todo_a", owner: "agent-b", idempotency_key: "foreign-key",
version: 3, lease_epoch: 1, status: "active", expires_at: "2026-09-06T00:00:00Z", write_scopes: []}]}});
const before = await store.loadAuthority();
@@ -1073,10 +1082,10 @@ test("recovery preserves stale-proof rejection and never offers foreign proof",
test("inactive history does not invent an executable lease acquisition", async () => {
for (const [overrides, intent, mode, expected] of [
- [{status: "blocked"}, {}, "legacy", "resolve_acquire_rejection"],
- [{claimed_by: null}, {}, "legacy", "reconcile_lease_owner"],
- [{}, {required_capabilities: ["code_review"]}, "legacy", "resolve_lifecycle_edit"],
- [{}, {status: "blocked"}, "legacy", "resolve_lifecycle_edit"],
+ [{status: "blocked"}, {}, "hard_lease", "resolve_acquire_rejection"],
+ [{claimed_by: null}, {}, "hard_lease", "reconcile_lease_owner"],
+ [{}, {required_capabilities: ["code_review"]}, "hard_lease", "resolve_lifecycle_edit"],
+ [{}, {status: "blocked"}, "hard_lease", "resolve_lifecycle_edit"],
[{status: "blocked"}, {}, "soft_claim", "resolve_acquire_rejection"],
] as const) {
const {store, request} = await seeded(overrides);
@@ -1102,7 +1111,7 @@ for (const blocker of ["scope", "acceptance"]) test(`recovery respects ${blocker
const other = todo({todo_id: "todo_other", claimed_by: "agent-b"});
const todos = [...head.head.todos as Record[], other];
await store.commitAuthority({operation_id: "scope-conflict", expected_provider_revision: head.provider_revision,
- events: [], receipts: [], next_projection: {...head.head, handoff_mode: "legacy", todos,
+ events: [], receipts: [], next_projection: {...head.head, handoff_mode: "hard_lease", todos,
todo_read_model: {schema_version: TODO_DOMAIN_READ_RECORD_SCHEMA, todo_count: 2,
records_sha256: canonicalAuthoritySha256(todos), contract_fields: [...TODO_DOMAIN_RECORD_CONTRACT.fields]},
leases: [
From dcdb7305a44234436036584e6f4133d7a81ce6ba Mon Sep 17 00:00:00 2001
From: LoopX Agent <337587101+loopx-agent@users.noreply.github.com>
Date: Thu, 15 Oct 2026 15:36:03 +0800
Subject: [PATCH 37/38] test(authority): compare current-policy monitor
recovery with full source records
Signed-off-by: LoopX Agent <337587101+loopx-agent@users.noreply.github.com>
---
.../quota-monitor-observation-receipt-v0.md | 15 +++++++-
.../authority-monitor-poll-rehearsal.py | 37 +++++++++----------
2 files changed, 30 insertions(+), 22 deletions(-)
diff --git a/docs/reference/protocols/quota-monitor-observation-receipt-v0.md b/docs/reference/protocols/quota-monitor-observation-receipt-v0.md
index c93d5f0443..395f10e124 100644
--- a/docs/reference/protocols/quota-monitor-observation-receipt-v0.md
+++ b/docs/reference/protocols/quota-monitor-observation-receipt-v0.md
@@ -217,8 +217,19 @@ lease proof. Before downgrading to a version without this protocol, finish v1
pending settlements; older binaries cannot interpret them. Never disable a
writer fence to recover by writing an older Markdown projection.
-The bounded real-source rehearsal clones read-only source state and adds only
-synthetic records in disposable runtimes. It requires an isolated PostgreSQL URL:
+The bounded source rehearsal reads a stable Markdown snapshot, including
+unreferenced archives, and adds synthetic records only in disposable runtimes.
+Use a baseline that supports the same authority format and Monitor reactivation;
+all four arms must preserve the full source records and reach identical heads.
+The disposable cases use `soft_claim` for observation and `hard_lease` for
+execution. These fixture transitions do not qualify policy migration, whole-Goal
+recovery, or the contents of a selected canonical store. It requires an isolated
+PostgreSQL URL:
+
+有界演练读取稳定的 Markdown 快照,保留无依赖关系的完整归档;仅在临时运行目录添加
+合成记录。基线须支持相同的权威存储格式及 Monitor 重启语义,四组最终状态必须一致。
+观察使用 `soft_claim`,执行使用 `hard_lease`;样本内切换不证明实际策略迁移、完整
+Goal 恢复或已选 canonical 存储的内容验收。
```bash
LOOPX_TEST_POSTGRES_URL="$DISPOSABLE_POSTGRES_URL" \
diff --git a/examples/control_plane/authority-monitor-poll-rehearsal.py b/examples/control_plane/authority-monitor-poll-rehearsal.py
index 7f21b47867..9de8a3896d 100644
--- a/examples/control_plane/authority-monitor-poll-rehearsal.py
+++ b/examples/control_plane/authority-monitor-poll-rehearsal.py
@@ -1,8 +1,10 @@
#!/usr/bin/env python3
-"""Compare Monitor polling and observation updates on a read-only Goal snapshot.
+"""Compare current-policy Monitor recovery on a read-only Goal snapshot.
Only disposable copies receive synthetic Monitor/lease records. The report is
bounded and excludes source text, identifiers, paths and connection strings.
+The baseline must support the same Monitor recovery and authority format;
+this is a parity rehearsal, not an assertion that an older bug still exists.
"""
from __future__ import annotations
@@ -43,7 +45,7 @@
const {selectLocalSqliteAuthority}=await moduleAt(input.repo,'coordination/local_authority_provider.ts');
const {executeCanonicalTaskLeaseAcquire}=await moduleAt(input.repo,'coordination/task_lease_acquire.ts');
const {coordinationTodoReadModel}=await moduleAt(input.repo,'coordination/coordination_projection.ts');
-const {canonicalAuthoritySha256: digest}=await moduleAt(input.repo,'coordination/authority_store_codec.ts');
+const {canonicalAuthoritySha256: digest,authorityUnicodeCompare}=await moduleAt(input.repo,'coordination/authority_store_codec.ts');
const {engageLegacyCoordinationWriterFence}=await moduleAt(input.repo,'coordination/legacy_writer_fence.ts');
const goal=input.goal_id, target='todo_monitor_rehearsal';
const initial=structuredClone(input.projection);
@@ -52,9 +54,9 @@
text:'Observe isolated public changes',archive_state:'active',source_section:'Agent Todo',
claimed_by:'agent-a',excluded_agents:[],task_class:'continuous_monitor',target_key:'isolated-watch',
cadence:'1h',watch_only:'true',next_due_at:'2026-09-01T00:00:00Z'});
-initial.todos.sort((a,b)=>a.todo_idb.todo_id?1:0);
+initial.todos.sort((a,b)=>authorityUnicodeCompare(a.todo_id,b.todo_id));
initial.todo_read_model=coordinationTodoReadModel(initial.todos,initial.todo_read_model.schema_version);
-initial.handoff_mode='legacy';
+initial.handoff_mode='soft_claim';
const proof={idempotency_key:'monitor-rehearsal-execution',expected_version:3};
const lease={schema_version:'task_lease_v0',goal_id:goal,todo_id:target,owner:'agent-a',
idempotency_key:proof.idempotency_key,version:3,lease_epoch:2,status:'active',write_scopes:[],
@@ -98,6 +100,7 @@
expected_shadow_provider_revision:seed.provider_revision}})).status,'applied');
await rm(display);
const poll=arm==='baseline'?baseline:current;
+ const updateTodo=arm==='baseline'?baselineUpdate:update;
const request={schema_version:'loopx_coordination_monitor_poll_request_v0',runtime_root:runtime,goal_id:goal,
operation_id:'unchanged',actor_agent_id:'agent-a',registered_agents:['agent-a','agent-b'],dry_run:false,
observation:{todo_id:target,generated_at:'2026-09-01T00:00:00Z',result_hash:'first',material_change:false},intent:{}};
@@ -105,7 +108,7 @@
assert.equal(firstPoll.status,'applied',JSON.stringify(firstPoll));
const first=await store.loadAuthority(); assert.equal(first.status,'loaded'); compatible[arm]=first.head;
assert.equal((await poll(request,dependencies)).status,'replayed');
- const leased={...first.head,handoff_mode:'hard_lease',leases:[...first.head.leases,lease].sort((a,b)=>a.todo_idb.todo_id?1:0)};
+ const leased={...first.head,handoff_mode:'hard_lease',leases:[...first.head.leases,lease].sort((a,b)=>authorityUnicodeCompare(a.todo_id,b.todo_id))};
assert.equal((await store.commitAuthority({operation_id:'seed-execution',expected_provider_revision:first.provider_revision,
events:[],receipts:[],next_projection:leased})).status,'applied');
const before=await store.loadAuthority();
@@ -152,17 +155,11 @@
planning_intent:{status:'open',no_followup:false},monitor_observation:{generated_at:'2030-01-01T00:00:00Z',
result_hash:'second',material_change:true,monitor_effect_id:'reactivate',cadence:'1h'}};
const closed=await store.loadAuthority();
- if(arm==='baseline') {
- const unsupported=await baselineUpdate(observation,dependencies);
- assert.equal(unsupported.status,'failed'); assert.equal(unsupported.reason_code,'monitor_reactivation_lease_transition_required');
- assert.deepEqual(await store.loadAuthority(),closed);
- report[arm]={poll_parity:true,retained_lease_reactivation:'rejected_no_write'}; continue;
- }
- assert.equal((await update({...observation,dry_run:true},dependencies)).status,'planned');
+ assert.equal((await updateTodo({...observation,dry_run:true},dependencies)).status,'planned');
assert.deepEqual(await store.loadAuthority(),closed);
- assert.equal((await update({...observation,patch:{text:'mixed edit'}},dependencies)).status,'failed');
+ assert.equal((await updateTodo({...observation,patch:{text:'mixed edit'}},dependencies)).status,'failed');
assert.deepEqual(await store.loadAuthority(),closed);
- const resumed=await update(observation,dependencies); assert.equal(resumed.status,'applied',JSON.stringify(resumed));
+ const resumed=await updateTodo(observation,dependencies); assert.equal(resumed.status,'applied',JSON.stringify(resumed));
assert.equal(resumed.monitor_poll_transition.material_change_generation,2);
const afterResume=await store.loadAuthority(); assert.equal(afterResume.status,'loaded');
const resumedTodo=afterResume.head.todos.find(t=>t.todo_id===target);
@@ -177,7 +174,7 @@
assert.equal(fresh.status,'applied',JSON.stringify(fresh));
assert.equal(fresh.lease.version,4); assert.equal(fresh.lease.lease_epoch,3);
const acquired=await store.loadAuthority();
- assert.equal((await update(observation,dependencies)).status,'replayed');
+ assert.equal((await updateTodo(observation,dependencies)).status,'replayed');
assert.deepEqual(await store.loadAuthority(),acquired,'historical replay cannot release the fresh execution');
assert.deepEqual(afterResume.head.todos.filter(t=>input.projection.todos.some(old=>old.todo_id===t.todo_id)),input.projection.todos);
const closedAgain=afterResume.head.todos.map(t=>t.todo_id===target?{...t,status:'done',done:true,completed_at:'2030-01-01T01:00:00Z'}:t);
@@ -185,15 +182,15 @@
events:[],receipts:[],next_projection:{...acquired.head,todos:closedAgain,
todo_read_model:coordinationTodoReadModel(closedAgain,afterResume.head.todo_read_model.schema_version)}});
const final=await store.loadAuthority();
- assert.equal((await update(observation,dependencies)).status,'replayed');
- assert.equal((await update({...observation,operation_id:'stale-new-id'},dependencies)).status,'failed');
+ assert.equal((await updateTodo(observation,dependencies)).status,'replayed');
+ assert.equal((await updateTodo({...observation,operation_id:'stale-new-id'},dependencies)).status,'failed');
assert.deepEqual(await store.loadAuthority(),final);
heads[arm]=final.head;
report[arm]={poll_parity:true,retained_lease_reactivation:'applied',fresh_execution_epoch:3,same_hash_reactivation_generation:2,
historical_replay_keeps_completion:true,stale_observation:'rejected',non_target_unchanged:true};
}
for(const arm of ['file','sqlite','postgresql']) assert.deepEqual(compatible[arm],compatible.baseline);
- assert.deepEqual(heads.file,heads.sqlite); assert.deepEqual(heads.file,heads.postgresql);
+ for(const arm of ['file','sqlite','postgresql']) assert.deepEqual(heads[arm],heads.baseline);
process.stdout.write(JSON.stringify({schema_version:'authority_monitor_rehearsal_v0',
source_todos:input.projection.todos.length,source_leases:input.projection.leases.length,
compatible_head_sha256:digest(compatible.baseline),final_head_sha256:digest(heads.file),
@@ -227,7 +224,7 @@ def git(*command):
goal = next(g for g in registry["goals"] if g["id"] == args.goal_id)
runtime = resolve_runtime_root(registry, None, registry_path=registry_path)
_, _, state = resolve_goal_state(registry=registry, goal_id=args.goal_id, project_override=None, state_file_override=None)
- projection, snapshot = build_runtime_shadow_source_snapshot(goal=goal, runtime_root=runtime, state_path=state, registry_path=registry_path)
+ projection, snapshot = build_runtime_shadow_source_snapshot(goal=goal, runtime_root=runtime, state_path=state, registry_path=registry_path, include_all_archived_todos=True)
source_digest = hashlib.sha256(json.dumps(snapshot, sort_keys=True).encode()).hexdigest()
child = subprocess.run(["node", "--no-warnings", "--experimental-sqlite", "--experimental-strip-types", "--input-type=module", "-e", NODE_REHEARSAL],
input=json.dumps({"repo": str(REPOSITORY), "baseline_repo": str(baseline), "goal_id": args.goal_id, "projection": projection}),
@@ -239,7 +236,7 @@ def git(*command):
with os.fdopen(descriptor, "w") as stream:
stream.write(child.stderr)
raise SystemExit(f"isolated Monitor rehearsal failed (exit {child.returncode}); no qualification; diagnostics stay private")
- after_projection, after_snapshot = build_runtime_shadow_source_snapshot(goal=goal, runtime_root=runtime, state_path=state, registry_path=registry_path)
+ after_projection, after_snapshot = build_runtime_shadow_source_snapshot(goal=goal, runtime_root=runtime, state_path=state, registry_path=registry_path, include_all_archived_todos=True)
if projection != after_projection or snapshot != after_snapshot or registry_path.read_bytes() != registry_bytes:
raise SystemExit("live source changed during rehearsal; rerun from a stable snapshot")
result = json.loads(child.stdout)
From 83b63aed3bc823418ea20bc6bffcf55341cbe014 Mon Sep 17 00:00:00 2001
From: LoopX Agent <337587101+loopx-agent@users.noreply.github.com>
Date: Thu, 15 Oct 2026 18:19:06 +0800
Subject: [PATCH 38/38] fix(ci): align full smoke qualification with current
contracts
Signed-off-by: LoopX Agent <337587101+loopx-agent@users.noreply.github.com>
---
.github/workflows/full-public-smokes.yml | 7 +++----
docs/product/release-readiness.md | 9 +++++++++
examples/blocker-push-runtime-smoke.py | 2 +-
examples/full-public-smokes-workflow-smoke.py | 2 +-
examples/install-local-smoke.py | 2 +-
loopx/help_surface.py | 2 --
6 files changed, 15 insertions(+), 9 deletions(-)
diff --git a/.github/workflows/full-public-smokes.yml b/.github/workflows/full-public-smokes.yml
index 0f5d34e08c..8b2e68dd92 100644
--- a/.github/workflows/full-public-smokes.yml
+++ b/.github/workflows/full-public-smokes.yml
@@ -95,10 +95,9 @@ jobs:
run: sudo apt-get update && sudo apt-get install -y zsh
- name: Install smoke runtime dependencies
- # Budget probes import pytest. LaunchAgent qualification resolves the
- # selected distribution outside the checkout, so install this source
- # explicitly rather than relying on an unrelated runner installation.
- run: python -m pip install --disable-pip-version-check "jsonschema>=4.23,<5" "pytest>=9.0.3,<10" --editable .
+ # Smokes use the declared test extra (including YAML workflow readers).
+ # Install this source for qualification outside the checkout as well.
+ run: python -m pip install --disable-pip-version-check --editable '.[test]'
- name: Install locked TypeScript parser for semantic production checks
run: npm ci --ignore-scripts
diff --git a/docs/product/release-readiness.md b/docs/product/release-readiness.md
index f4b803e45c..33768adf7d 100644
--- a/docs/product/release-readiness.md
+++ b/docs/product/release-readiness.md
@@ -862,6 +862,15 @@ path, and canary route rather than as a user-facing release baseline.
SQLite qualification remain unqualified. No paired-version benchmark uplift
or isolated Astra causal effect is claimed.
+- `v1.4.1` on 2026-10-15 16:50 +08:00: clearer Goal workspace and result
+ adoption, explicit research decisions, and original-operation recovery at
+ frozen commit `4f27801c4e26fd51246d352eada72c98f8e7e657`. The
+ [published release](https://github.com/loopx-project/loopx/releases/tag/v1.4.1)
+ records bilingual operating instructions, community contributions and
+ source/artifact validation limits. Existing Goals retain their recorded
+ policy and require explicit migration; canonical-only retirement remains
+ a separate 2.0 candidate.
+
When a new public release is promoted, add it here only after the matching tag,
release note, stable ref, update path, and focused release canary agree.
diff --git a/examples/blocker-push-runtime-smoke.py b/examples/blocker-push-runtime-smoke.py
index f847f6af4a..05d5718973 100644
--- a/examples/blocker-push-runtime-smoke.py
+++ b/examples/blocker-push-runtime-smoke.py
@@ -217,7 +217,7 @@ def main() -> int:
# The bootstrap rule is shared from heartbeat.rules after #4201; assert the
# current compact sentence instead of the retired per-shell phrasing.
assert "reuse the value on retries" in compact_prompt, prompt
- assert "guard; 2 stalls->replan" in compact_prompt, prompt
+ assert "obey projected replan/stop, never wake counts" in compact_prompt, prompt
assert "Exact monitor settlement=no refresh/spend" in compact_prompt, prompt
assert "Admitted work: settlement_plan even if artifacts unchanged" in compact_prompt, prompt
# The shipped writeback sentence moved to the mixed-language form in the
diff --git a/examples/full-public-smokes-workflow-smoke.py b/examples/full-public-smokes-workflow-smoke.py
index 3f874bd781..ca3c576675 100644
--- a/examples/full-public-smokes-workflow-smoke.py
+++ b/examples/full-public-smokes-workflow-smoke.py
@@ -49,7 +49,7 @@ def main() -> int:
"--jobs \"${SMOKE_JOBS}\"",
"--no-execute",
"--json",
- 'python -m pip install --disable-pip-version-check "jsonschema>=4.23,<5" "pytest>=9.0.3,<10" --editable .',
+ "python -m pip install --disable-pip-version-check --editable '.[test]'",
"actions/upload-artifact@",
"$RUNNER_TEMP/loopx-smoke-results/full-public-shard-${{ matrix.shard }}.json",
"${{ runner.temp }}/loopx-smoke-results/full-public-shard-${{ matrix.shard }}.json",
diff --git a/examples/install-local-smoke.py b/examples/install-local-smoke.py
index 34699eb2ef..1120ba7f43 100644
--- a/examples/install-local-smoke.py
+++ b/examples/install-local-smoke.py
@@ -833,7 +833,7 @@ def main() -> int:
)
assert "```sh\nLOOPX_TURN=\n" in payload["task_body"], payload
assert "not a command-prefix assignment" in payload["task_body"], payload
- assert "guard; 2 stalls->replan" in payload["task_body"], payload
+ assert "obey projected replan/stop, never wake counts" in payload["task_body"], payload
assert "Exact monitor settlement=no refresh/spend" in payload["task_body"], payload
assert "Admitted work: settlement_plan even if artifacts unchanged" in payload["task_body"], payload
assert "no-change=surface_only/no spend" not in payload["task_body"], payload
diff --git a/loopx/help_surface.py b/loopx/help_surface.py
index 1846495159..55abccdf52 100644
--- a/loopx/help_surface.py
+++ b/loopx/help_surface.py
@@ -395,7 +395,6 @@ def register_command_reference(subparsers: argparse._SubParsersAction) -> None:
"global-todos",
"goal-actions",
"goal-alignment",
- "goal-direction",
"amendment-proposal",
"goal-amendment-proposal",
"handoff-mode",
@@ -423,7 +422,6 @@ def register_command_reference(subparsers: argparse._SubParsersAction) -> None:
"value-connectors",
"version",
"worker-bridge",
- "zcode-goal",
}
)