diff --git a/docs/architecture/rfcs/shared-goal-authority-state-provider-v0.md b/docs/architecture/rfcs/shared-goal-authority-state-provider-v0.md index 5029d9066c..1fc2bb3396 100644 --- a/docs/architecture/rfcs/shared-goal-authority-state-provider-v0.md +++ b/docs/architecture/rfcs/shared-goal-authority-state-provider-v0.md @@ -2560,14 +2560,15 @@ for command inventory, update/monitor transactions and consumer deletion. Do not repeat that plan in a second implementation or treat a merged read-policy PR as storage readiness. Its T0 checkpoint is the entry condition for these cards. -Lifecycle admission and the preauthorized terminal fence now share the TS -owner across legacy writers and native terminal transactions; the replaced -Python rules are removed without changing provider defaults or promotion. -This is not full native field-edit support: retain the strict text/note -transaction boundary until update's fields, ownership, validation and -monitor/resume effects close together. Neither an admission result nor a -lease-fence result is a commit receipt. Keep provider CAS/replay and existing -writer lock lifetimes unchanged while collecting this deletion payoff. +Lifecycle admission shares the TS owner across legacy writers and native +transactions. Native text/note edits and terminal transitions reuse the +preauthorized lease fence in-process; the replaced Python rules and the +callerless standalone effect-runtime wire are removed without changing provider +defaults or promotion. This remains a strict text/note transaction boundary, +not general native metadata support, until update's fields, ownership, +validation and monitor/resume effects close together. Neither an admission +result nor a lease-fence result is a commit receipt. Keep provider CAS/replay +and existing writer lock lifetimes unchanged while collecting this deletion payoff. Waiting/resume lane selection is now one TS read-policy owner shared by quota, vision-wait, agent-scope and replan. The obsolete Python selector module is deleted; the adapter accepts the same canonical summary after promotion and diff --git a/docs/architecture/rfcs/shared-goal-authority-state-provider-v0.zh-CN.md b/docs/architecture/rfcs/shared-goal-authority-state-provider-v0.zh-CN.md index e4e3dea5dc..2ffcff31a5 100644 --- a/docs/architecture/rfcs/shared-goal-authority-state-provider-v0.zh-CN.md +++ b/docs/architecture/rfcs/shared-goal-authority-state-provider-v0.zh-CN.md @@ -2033,11 +2033,13 @@ D1–D3 资格化和 T1/T2 条件保持不变。 推进,不在这里复制第二套实现路线,也不把 read-policy PR 合并视为存储就绪。 进入本节前先完成 T0 基线核对。 -Lifecycle 准入及预授权 terminal fence 现由 legacy writer 与 native terminal -transaction 共用 TS owner;删除对应 Python 规则,不改变 provider 默认或 promotion。 -这不是完整 native 字段编辑:在 update 的字段、ownership、validation 和 monitor/resume -effect 一起闭合前,保留严格 text/note 事务边界。准入结果和 lease-fence 结果都不是 -commit receipt;兑现删除收益时,provider CAS/replay 与既有 writer 持锁生命周期不变。 +Lifecycle 准入由 legacy writer 与 native transaction 共用 TS owner。Native +text/note 编辑与 terminal transition 在进程内复用预授权 lease fence;删除对应 +Python 规则和无调用方的独立 effect-runtime wire,不改变 provider 默认或 promotion。 +在 update 的字段、ownership、validation 和 monitor/resume effect 一起闭合前, +这仍是严格 text/note 事务边界,不是通用 native metadata 支持。准入结果和 +lease-fence 结果都不是 commit receipt;兑现删除收益时,provider CAS/replay 与既有 +writer 持锁生命周期不变。 等待/恢复 lane 选择现由 quota、vision-wait、agent-scope、replan 共用一个 TS 读取 策略 owner,删除旧 Python selector 模块。适配层在 promotion 后消费同一 canonical summary,之前消费 legacy summary;真实 CLI 覆盖容量变化和 promoted display diff --git a/docs/architecture/rfcs/typescript-control-plane-migration-v0.md b/docs/architecture/rfcs/typescript-control-plane-migration-v0.md index b863bdbf59..42105def95 100644 --- a/docs/architecture/rfcs/typescript-control-plane-migration-v0.md +++ b/docs/architecture/rfcs/typescript-control-plane-migration-v0.md @@ -308,10 +308,11 @@ An input adapter or external-effect executor may remain Python. The lifecycle-admission slice now uses `todo_lifecycle_decision.ts` for legacy claim/update admission, delegated action/reason checks, ownership-holder routing, -and the preauthorized terminal fence, alongside native complete/supersede. -`authority_core.py` projects results rather than retaining those decisions. -The terminal wire contract stays terminal-only; mutation admission cannot complete -a Todo, and a standalone fence neither grants actor authority nor completes it. +and native complete/supersede. Native text/note edits and terminal transitions +reuse the preauthorized lease fence in-process. `authority_core.py` projects only +the live admission and terminal decisions; there is no standalone Python command +or effect-runtime handler for the fence. Mutation admission cannot complete a +Todo, and the in-process fence cannot grant actor authority or commit a change. This deletes duplicate rules now, **not** the complete legacy update writer. Field patches, omission/clear semantics, monitor/resume effects and validation still need one complete update transaction before the writer can retire. Legacy 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 53a157ec34..71ef1ba21a 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 @@ -240,11 +240,12 @@ Markdown renderer 长期保留。 全部改成 TypeScript 或 `loopxd` 落地为前提;输入适配和外部 effect 执行可以保留 Python。 本次 lifecycle-admission 切片将 legacy claim/update 准入、委托 action/reason 检查、 -ownership-holder 路由及预授权 terminal fence 统一到 `todo_lifecycle_decision.ts`, -与 native complete/supersede 共用规则;`authority_core.py` 只投影这些决策结果。 -Terminal wire 合同仍只接受 terminal 命令;mutation admission 不能完成 Todo, -独立 fence 不能授予 actor 权限或完成 Todo。这立即删除重复规则,**不等于删除完整 -legacy update writer**。字段 patch、省略/清空、monitor/resume effect 和 validation +ownership-holder 路由及 native complete/supersede 统一到 +`todo_lifecycle_decision.ts`。Native text/note 编辑与 terminal transition 在进程内 +复用预授权 lease fence;`authority_core.py` 只投影仍有真实调用方的准入和 terminal +决策,不再暴露独立 Python command 或 effect-runtime handler。Mutation admission +不能完成 Todo,进程内 fence 不能授予 actor 权限或提交变更。这立即删除重复规则, +**不等于删除完整 legacy update writer**。字段 patch、省略/清空、monitor/resume effect 和 validation 仍需收口为完整 update transaction。Legacy 准入及持锁 gate 仍跨 runtime;本次减少 语义 owner,不宣称减少 crossings,native transaction 仍进程内调用。下一步将这些 crossing 一起折叠进完整事务,不能沿着 adapter 逐字段继续加桥。 diff --git a/loopx/control_plane/coordination/authority_core.py b/loopx/control_plane/coordination/authority_core.py index 874ae80429..aa9dd1c7c3 100644 --- a/loopx/control_plane/coordination/authority_core.py +++ b/loopx/control_plane/coordination/authority_core.py @@ -176,18 +176,6 @@ class LeaseModeGateCommand: action: LeaseAction -@dataclass(frozen=True) -class TerminalFenceCommand: - """Verify the lease side of an already-authorized terminal mutation.""" - - actor_agent_id: str | None - lease_idempotency_key: str | None = None - lease_expected_version: int | None = None - delegated_authority: bool = False - allow_user_gate_auto_acquire: bool = False - require_active_when_fence_supplied: bool = True - - @dataclass(frozen=True) class HandoffModeTransitionCommand: requested_mode: HandoffMode @@ -201,7 +189,6 @@ class HandoffModeTransitionCommand: | LeaseReleaseCommand | LeaseOwnerEligibilityCommand | LeaseModeGateCommand - | TerminalFenceCommand | HandoffModeTransitionCommand ) @@ -489,51 +476,6 @@ def ownership_gate_requirement( return OwnershipGate(payload["ownership_gate"]) -def _typescript_terminal_fence( - snapshot: CoordinationSnapshot, - command: TerminalFenceCommand, -) -> TransitionPlan: - if snapshot.todo is None: - return _result(DecisionOutcome.REJECTED, "todo_not_found") - payload = effect_runtime_result( - "task_lease.terminal_fence.decide", - { - "schema_version": "loopx_coordination_terminal_fence_request_v0", - "todo": _todo_fact_payload(snapshot.todo), - "lease": _lease_fact_payload(snapshot.lease), - "registered_agents": list(snapshot.registered_agents), - "handoff_mode": snapshot.handoff_mode.value, - "actor_agent_id": command.actor_agent_id, - "lease_idempotency_key": command.lease_idempotency_key, - "lease_expected_version": command.lease_expected_version, - "delegated_authority": command.delegated_authority, - "allow_user_gate_auto_acquire": command.allow_user_gate_auto_acquire, - "require_active_when_fence_supplied": command.require_active_when_fence_supplied, - }, - ) - if not isinstance(payload, dict) or payload.get("schema_version") != ( - "loopx_coordination_terminal_fence_result_v0" - ): - raise RuntimeError("TypeScript terminal fence result shape mismatch") - outcome = DecisionOutcome(payload["outcome"]) - next_snapshot = None - if outcome is DecisionOutcome.APPLY: - next_snapshot = replace( - snapshot, - lease=( - snapshot.lease - if payload["next_lease"] is None - else _lease_fact_from_payload(payload["next_lease"]) - ), - ) - return TransitionPlan( - outcome=outcome, - code=payload["code"], - next_snapshot=next_snapshot, - lease_fence=LeaseFence(payload["lease_fence"]), - ) - - def _lease_handoff_rejection(snapshot: CoordinationSnapshot) -> str | None: if snapshot.handoff_mode is HandoffMode.SOFT_CLAIM: return "handoff_mode_forbids_lease" @@ -902,8 +844,6 @@ def decide( return _decide_lease_owner_eligibility(snapshot, command) if isinstance(command, LeaseModeGateCommand): return _decide_lease_mode_gate(snapshot, command) - if isinstance(command, TerminalFenceCommand): - return _typescript_terminal_fence(snapshot, command) if isinstance(command, HandoffModeTransitionCommand): return _decide_handoff_transition(snapshot, command) raise TypeError(f"unsupported coordination command: {type(command).__name__}") diff --git a/loopx/control_plane/effect_runtime_handlers.ts b/loopx/control_plane/effect_runtime_handlers.ts index dfad9328b2..c245749d7f 100644 --- a/loopx/control_plane/effect_runtime_handlers.ts +++ b/loopx/control_plane/effect_runtime_handlers.ts @@ -138,7 +138,6 @@ import { evaluateCoordinationTodoClaimDecision } from "./coordination/todo_claim import { evaluateCoordinationTodoTerminalDecision, evaluateCoordinationTodoMutationDecision, - evaluateCoordinationTerminalFence, evaluateTodoOwnershipGate, } from "./coordination/todo_lifecycle_decision.ts"; import { evaluateCoordinationTodoArchiveSelection } from "./coordination/todo_archive_selection.ts"; @@ -397,7 +396,6 @@ export function createEffectRuntimeHandlers( ], ["todo.terminal.decide", evaluateCoordinationTodoTerminalDecision], ["todo.mutation.decide", evaluateCoordinationTodoMutationDecision], - ["task_lease.terminal_fence.decide", evaluateCoordinationTerminalFence], ["todo.ownership_gate.decide", evaluateTodoOwnershipGate], ["todo.archive.select", evaluateCoordinationTodoArchiveSelection], ["todo.successor.derive", evaluateCoordinationTodoSuccessorDerivation], diff --git a/tests/control_plane/test_coordination_authority_core.py b/tests/control_plane/test_coordination_authority_core.py index 4ff5e03bfe..a7e15011c2 100644 --- a/tests/control_plane/test_coordination_authority_core.py +++ b/tests/control_plane/test_coordination_authority_core.py @@ -21,7 +21,6 @@ LifecycleGrant, OtherLeaseSnapshot, OwnershipGate, - TerminalFenceCommand, TodoAction, TodoMutationCommand, TodoSnapshot, @@ -118,26 +117,6 @@ def test_update_authority_keeps_claim_neutral_edits_separate_from_ownership( assert result.authority_mode == "registered_peer_actor" -@pytest.mark.parametrize("strict", [False, True]) -def test_standalone_fence_does_not_complete_todo_or_reauthorize_the_actor(strict): - state = snapshot(todo=todo(claimed_by=AGENT_B)) - result = decide( - state, - TerminalFenceCommand( - actor_agent_id=None, - lease_idempotency_key="stale-key", - require_active_when_fence_supplied=strict, - ), - ) - assert result.authority_mode is None - if strict: - assert result.code == "lease_not_active" - assert result.next_snapshot is None - else: - assert result.code == "terminal_fence_not_required" - assert result.next_snapshot == state - - @pytest.mark.parametrize("clear", [False, True]) def test_delegated_update_requires_the_actual_action_and_never_releases_holder(clear): state = snapshot( @@ -402,75 +381,6 @@ def test_exact_user_gate_can_plan_auto_acquire_but_never_displaces_a_live_lease( assert foreign_live.code == "lease_fence_required" -def test_terminal_entrypoints_share_user_gate_auto_acquire_policy() -> None: - state = snapshot( - handoff_mode=HandoffMode.HARD_LEASE, - registered_agents=(AGENT_A,), - todo=todo(role="user", task_class="user_gate"), - ) - full = decide( - state, - terminal( - lease_idempotency_key="auto-turn-key", - allow_user_gate_auto_acquire=True, - ), - ) - fence = decide( - state, - TerminalFenceCommand( - actor_agent_id=AGENT_A, - lease_idempotency_key="auto-turn-key", - allow_user_gate_auto_acquire=True, - require_active_when_fence_supplied=False, - ), - ) - - assert full.outcome is fence.outcome is DecisionOutcome.APPLY - assert full.lease_fence is fence.lease_fence is LeaseFence.AUTO_ACQUIRE - assert full.next_snapshot is not None and fence.next_snapshot is not None - assert full.next_snapshot.lease == fence.next_snapshot.lease - - -def test_preauthorized_terminal_fence_preserves_mode_and_delegation_rules() -> None: - legacy = decide( - snapshot(), - TerminalFenceCommand(actor_agent_id=AGENT_A), - ) - assert legacy.outcome is DecisionOutcome.APPLY - assert legacy.lease_fence is LeaseFence.NOT_REQUIRED - - hard_missing = decide( - snapshot(handoff_mode=HandoffMode.HARD_LEASE), - TerminalFenceCommand(actor_agent_id=AGENT_A), - ) - assert hard_missing.outcome is DecisionOutcome.REJECTED - assert hard_missing.code == "handoff_mode_requires_lease" - - delegated = decide( - snapshot(handoff_mode=HandoffMode.HARD_LEASE), - TerminalFenceCommand( - actor_agent_id=ORCHESTRATOR, - delegated_authority=True, - ), - ) - assert delegated.outcome is DecisionOutcome.APPLY - assert delegated.lease_fence is LeaseFence.DELEGATED_OVERRIDE - - verified = decide( - snapshot(handoff_mode=HandoffMode.HARD_LEASE, lease=lease()), - TerminalFenceCommand( - actor_agent_id=AGENT_A, - lease_idempotency_key="execution-a", - lease_expected_version=3, - ), - ) - assert verified.outcome is DecisionOutcome.APPLY - assert verified.lease_fence is LeaseFence.REQUIRED - assert verified.next_snapshot is not None - assert verified.next_snapshot.lease is not None - assert verified.next_snapshot.lease.status == "released" - - @pytest.mark.parametrize( ("target", "owner", "code"), [ @@ -734,18 +644,6 @@ def test_contradictory_normalized_lease_state_fails_closed( assert plan.code == "invalid_lease_snapshot" -def test_active_terminal_fence_requires_current_version_after_key_match() -> None: - plan = decide( - snapshot(handoff_mode=HandoffMode.HARD_LEASE, lease=lease()), - TerminalFenceCommand( - actor_agent_id=AGENT_A, - lease_idempotency_key="execution-a", - ), - ) - assert plan.outcome is DecisionOutcome.REJECTED - assert plan.code == "version_required" - - def test_soft_claim_forbids_lease_mutation_but_allows_release() -> None: state = snapshot(handoff_mode=HandoffMode.SOFT_CLAIM, lease=lease()) acquire = decide( diff --git a/tests/control_plane_ts/effect_runtime_handlers.test.ts b/tests/control_plane_ts/effect_runtime_handlers.test.ts index d5088afad6..00b5063d82 100644 --- a/tests/control_plane_ts/effect_runtime_handlers.test.ts +++ b/tests/control_plane_ts/effect_runtime_handlers.test.ts @@ -156,3 +156,14 @@ test("completion policy has no standalone runtime handler", async () => { /unsupported Effect runtime method/, ); }); + +test("preauthorized lease fence has no standalone runtime handler", async () => { + await assert.rejects( + dispatchEffectRuntimeMethod( + handlers, + "task_lease.terminal_fence.decide", + {}, + ), + /unsupported Effect runtime method/, + ); +}); diff --git a/tests/control_plane_ts/todo_terminal_decision.test.ts b/tests/control_plane_ts/todo_terminal_decision.test.ts index 20f667030d..aedd1cc2ec 100644 --- a/tests/control_plane_ts/todo_terminal_decision.test.ts +++ b/tests/control_plane_ts/todo_terminal_decision.test.ts @@ -144,7 +144,7 @@ test("executor reclaim remains internal and preserves actor rejection precedence }))); }); -test("standalone fence is preauthorized and never completes or attributes a Todo", () => { +test("in-process preauthorized fence never completes or attributes a Todo", () => { const base = request({ schema_version: COORDINATION_TERMINAL_FENCE_REQUEST_SCHEMA, actor_agent_id: null, delegated_authority: false, require_active_when_fence_supplied: false, lease_idempotency_key: "old-key" });