diff --git a/docs/architecture/rfcs/loopx-overall-roadmap-v0.md b/docs/architecture/rfcs/loopx-overall-roadmap-v0.md index 02899a4dbc..a9b195c1cd 100644 --- a/docs/architecture/rfcs/loopx-overall-roadmap-v0.md +++ b/docs/architecture/rfcs/loopx-overall-roadmap-v0.md @@ -909,7 +909,7 @@ This narrows a local recovery gap, not the full R1/R2 coordination acceptance. **R1 transaction checkpoint.** Team-plan admission and whole-batch planning now live in `work_items/team_plan.ts`. Confirmation assigns all admitted lanes in one write with a durable operation receipt; identity is proposal + lane, never Todo text. File/SQLite authority uses the existing CAS and receipt owner; legacy Markdown writes the records and immutable receipt together under its existing fence and lock. Exact replay reads historical results even after a receiver changes, completes or deletes work. A precommit failure creates no lane prefix, and pending canonical display delivery requires recovery before Chat reports verified success. The card names partial assignments and gaps; quota/stop remain advisory and an explicit enforcement claim is rejected. Agent-originated settlement binds the same state basis at its journal's first write and re-reads it at settlement; a plan whose basis is missing or moved, or whose every lane is a gap, is a typed failed receipt that creates no Todo and replays unchanged. -This closes the local assignment/retry portion of F4, not R1's collaboration acceptance. Registered receivers are assigned without being impersonated as authors; agent-originated settlement cannot assign another peer without owner confirmation. Assignment does not attest receiver adoption, a lease, execution, dependency consumption or independent acceptance. Do not add a second confirmation to ordinary already-authorized work. Gap resolution requires new explicit intent; replay must not silently extend the confirmed subset. The fingerprint binds current local state and canonical revision, not a full shared Goal-intent transaction. R2/R3/R4 still own executor qualification, receiver adoption/result return and shared intent/authorization; the cross-host Turn lease is not a plan barrier. +This closes the local assignment/retry portion of F4, not R1's collaboration acceptance. Registered receivers are assigned without being impersonated as authors; agent-originated settlement cannot assign another peer without owner confirmation. Assignment does not attest receiver adoption, a lease, execution, dependency consumption or independent acceptance. Do not add a second confirmation to ordinary already-authorized work. Gap resolution requires new explicit intent; replay must not silently extend the confirmed subset. The fingerprint binds current local state and canonical revision, not a full shared Goal-intent transaction. Local registry and active-state guards now remain held through the typed commit: native token claims survive caller exit, changed source bytes reject new assignments, and expired handoffs can only replay an exact committed receipt. Registry contention returns a retryable failure without creating work; canonical display recovery runs after the source guards release. R2/R3/R4 still own executor qualification, receiver adoption/result return and shared intent/authorization; the cross-host Turn lease is not a plan barrier. Validation: production Chat apply and real file projection recovery; packaged confirmation card; typed batch cases on FileAuthorityStore and an isolated PostgreSQL server, including equal-text identities, concurrent commit, invalid final lane, lost response and receiver changes. These fixtures do not qualify Lark transport or cross-host worker execution. diff --git a/docs/architecture/rfcs/loopx-overall-roadmap-v0.zh-CN.md b/docs/architecture/rfcs/loopx-overall-roadmap-v0.zh-CN.md index dde2c195e4..94338b621c 100644 --- a/docs/architecture/rfcs/loopx-overall-roadmap-v0.zh-CN.md +++ b/docs/architecture/rfcs/loopx-overall-roadmap-v0.zh-CN.md @@ -686,7 +686,7 @@ lifecycle。 **R1 事务检查点。** 团队计划准入与整批规划现在归 `work_items/team_plan.ts`。确认后,全部已准入 lane 与持久操作回执一次提交;身份由 proposal + lane 决定,不再由 Todo 文本决定。File/SQLite 权威复用现有 CAS 与回执 owner;legacy Markdown 在原有 fence 和锁内同时写入任务和不可变回执。同一操作重试只读历史结果,接收者后来修改、完成或删除任务也不会触发重建。提交前失败不会留下部分 lane;canonical 展示投递仍 pending 时,Chat 必须恢复后才能报告验证成功。卡片列出部分分配及缺口;quota/stop 是参考,显式强制声明会被拒绝。Agent 发起的结算在 journal 首次写入时绑定同一状态基线并在结算时重读;基线缺失或已变动、或全部 lane 均为缺口的计划,记录为类型化的失败回执,不创建 Todo,重放结果不变。 -这完成 F4 的本地分配/重试部分,不等于 R1 协同验收。注册接收者可以被分配任务,但不会被冒充为作者;Agent 发起的结算未经业主确认不能给另一 peer 分配任务。分配不证明接收者采纳、lease、执行、依赖消费或独立验收。普通已授权工作不应普遍增加第二次确认。解决缺口需要明确的新意图;重放不能静默扩展原确认子集。fingerprint 绑定当前本地状态与 canonical revision,不是完整共享 Goal 意图事务。R2/R3/R4 仍负责执行器资格、接收者采纳/结果返回及共享意图/授权;跨主机 Turn lease 不是计划屏障。 +这完成 F4 的本地分配/重试部分,不等于 R1 协同验收。注册接收者可以被分配任务,但不会被冒充为作者;Agent 发起的结算未经业主确认不能给另一 peer 分配任务。分配不证明接收者采纳、lease、执行、依赖消费或独立验收。普通已授权工作不应普遍增加第二次确认。解决缺口需要明确的新意图;重放不能静默扩展原确认子集。fingerprint 绑定当前本地状态与 canonical revision,不是完整共享 Goal 意图事务。本地 registry 与活动状态锁现在持续保护到类型化提交结束:native token claim 在调用进程退出后仍有效;源字节变化拒绝新分配;失效交接只能重放精确匹配的已提交回执。registry 竞争返回可重试失败,不创建任务;canonical 展示恢复在源锁释放后执行。R2/R3/R4 仍负责执行器资格、接收者采纳/结果返回及共享意图/授权;跨主机 Turn lease 不是计划屏障。 验证覆盖真实 Chat apply、文件权威投递恢复、打包确认卡片,以及 FileAuthorityStore 和隔离 PostgreSQL 上的同文不同身份、并发提交、末条 lane 非法、响应丢失和接收者变更。这些 fixture 不验收 Lark 传输或跨主机 worker 执行。 diff --git a/loopx/control_plane/work_items/team_plan_adapter.py b/loopx/control_plane/work_items/team_plan_adapter.py index 0b13b3fba1..d580b165d2 100644 --- a/loopx/control_plane/work_items/team_plan_adapter.py +++ b/loopx/control_plane/work_items/team_plan_adapter.py @@ -11,18 +11,25 @@ import json import re from collections.abc import Callable, Mapping +from contextlib import ExitStack from pathlib import Path from typing import Any from ...agent_registry import registered_agent_ids_for_goal from ...history import load_registry +from ...file_lock import ( + LockAcquireTimeoutError, cross_runtime_lock_witness, exclusive_cross_runtime_file_lock, +) from ...registry import find_registry_goal from ...state_refresh import now_local from ..effect_runtime import EffectRuntimeRejected, effect_runtime_result from ..runtime.public_safety import validate_public_safe_value from ..coordination.local_authority import read_canonical_todos_if_promoted from ..coordination.local_authority_shadow_adapter import effective_runtime_root -from ..coordination.legacy_writer_fence import legacy_todo_write_transaction +from ..coordination.legacy_writer_fence import ( + legacy_coordination_todo_lock_path, legacy_todo_write_transaction, +) +from ..coordination.shadow_management import shadow_maintenance_lock_target from ..coordination.runtime_shadow_writer_adapter import ( begin_todo_runtime_shadow_capture, write_captured_todo_state, settle_todo_runtime_shadow_capture, @@ -136,20 +143,34 @@ def apply_team_plan( validate_public_safe_value(dict(proposal), path="team_plan") registry_path = Path(registry_path).expanduser() runtime = effective_runtime_root(registry_path, None) - goal = find_registry_goal(load_registry(registry_path), goal_id) - if goal is None: + if find_registry_goal(load_registry(registry_path), goal_id) is None: raise ValueError("steward team plan proposal names an unknown Goal") project, state = resolve_todo_state_path(registry_path=registry_path, goal_id=goal_id) request = { "goal_id": goal_id, "plan": dict(proposal), "actor_agent_id": agent_id, - "registered_agents": registered_agent_ids_for_goal(goal), "supported_action_kinds": sorted(TODO_ACTION_KIND_ADVANCEMENT_VALUES), "observed_at": now_local(), "expected_state_fingerprint": expected_state_fingerprint, - "intent_basis": steward_team_plan_intent_basis( - goal_id=goal_id, goal=goal, registry_path=registry_path, plan=proposal), } + + def read_source_request() -> None: + # Called under the registry and state locks, including on the legacy + # path. A registry rebind while acquiring the guards must not redirect + # the operation to an unguarded source or provider. + goal = find_registry_goal(load_registry(registry_path), goal_id) + if goal is None: + raise ValueError("steward team plan proposal names an unknown Goal") + _, current_state = resolve_todo_state_path(registry_path=registry_path, goal_id=goal_id) + if (effective_runtime_root(registry_path, None).resolve() != runtime.resolve() + or current_state.resolve() != state.resolve()): + raise TeamPlanCommitError("team_plan_preview_stale", "team plan source binding changed after preview") + request.update( + registered_agents=registered_agent_ids_for_goal(goal), + intent_basis=steward_team_plan_intent_basis( + goal_id=goal_id, goal=goal, registry_path=registry_path, plan=proposal), + ) + identity = effect_runtime_result("work_items.team_plan.identity", request) marker = f"