Skip to content
Merged
Original file line number Diff line number Diff line change
Expand Up @@ -535,10 +535,13 @@ Unknown command types fail closed. Transfer or delegated assignment,
arbitrary todo/gate mutation, quota reservation, and external effects still
require later runtime contracts and qualification. Non-empty write scopes and
cross-todo scope-overlap rejection likewise require a later command contract
and qualification. The recoverable-execution verbs below are the Stage 3
slice; steps 1 through 4 and 7 through 10 of Section 5 (identity, digest,
replay, CAS, reload, rebase, budget) apply to every verb unchanged, and only
the per-verb preconditions and transition (steps 5 and 6) differ.
and qualification. The recoverable-execution verbs below were called Stage 3
in the historical #3669 implementation sequence. Under the current delivery
sequence in Section 11, that merged work is part of the Stage 0 reference foundation,
not the Stage 3 remote-shadow phase in Section 11. Steps 1 through
4 and 7 through 10 of Section 5 (identity, digest, replay, CAS, reload, rebase,
budget) apply to every verb unchanged, and only the per-verb preconditions and
transition (steps 5 and 6) differ.

### 5.2 `renew_work`

Expand Down Expand Up @@ -1079,6 +1082,41 @@ The sequence is:
cache, offline projection, and diagnostic material. Never keep a long-lived
dual-write or dual-master mode.

#### Stage 2C observation foundation: local post-commit capture

The first half of Stage 2C is an explicit, default-off product path. Preview
and enable it with:

```bash
loopx configure-goal --goal-id GOAL --local-authority-shadow-file
loopx configure-goal --goal-id GOAL --local-authority-shadow-file --execute
```

Todo, handoff-mode, follow-up, and task-lease facades sample the full current
local projection after their primary write returns committed, then ask
`FileAuthorityStore` to retain that snapshot. `observation_trigger` records
why sampling began; it is not the primary transaction identity. A concurrent
primary commit may therefore appear in the sampled snapshot. A `captured` or
`replayed` result proves only the candidate-side observation commit. It does
not compare the source and candidate and carries `parity_verdict=not_evaluated`.

Candidate bytes live under
`authority-shadow/file/` outside the legacy per-Goal runtime tree, so state
migration never copies a store identity or revision; an executed migration
seeds a new target lineage from the migrated local state. Candidate failure is
reported as an observation result but never reverses the completed local write.

Disable the observer in one command with
`loopx configure-goal --goal-id GOAL --clear-local-authority-shadow --execute`.
This is rollback of observation only: the local Markdown and task-lease files
remain canonical throughout. The slice does not read the candidate for a
decision, fence a legacy writer, qualify a remote provider, or complete the
second Stage 2C promotion. A process crash after the local commit but before
the observer call may miss that individual observation; a later committed
write or migration seed refreshes the full current projection, but no durable
shadow outbox or transaction-correlated receipt is claimed here. This plumbing
is not parity evidence and cannot by itself support Stage 2C promotion.

### Implementation prerequisite: put local file mode behind the same coordination contract

Before wiring a live NoKV or another remote provider, the runtime should first
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -461,9 +461,11 @@ todo 已提交就要求调用方重新发一条 operation。
未知 command type fail closed。transfer 或 delegated assignment、任意 todo/gate
mutation、quota reservation 与 external effect 仍需要后续 runtime 合同与
qualification;非空 write scope 与跨 todo scope-overlap 拒绝同样需要后续 command
contract 与 qualification。下面的可恢复执行动词是 Stage 3 切片;第 5 节的步骤
1-4 与 7-10(identity、digest、replay、CAS、reload、rebase、budget)对每个动词
原样适用,只有每动词的前置条件与迁移(步骤 5-6)不同。
contract 与 qualification。下面的可恢复执行动词在 #3669 历史实施序列中曾称为
Stage 3;按照第 11 节当前的交付编号,已合入的这部分属于 Stage 0 reference foundation,
不是第 11 节的 Stage 3 远端 shadow 阶段。第 5 节的步骤 1-4 与 7-10
(identity、digest、replay、CAS、reload、rebase、budget)对每个动词原样适用,只有
每动词的前置条件与迁移(步骤 5-6)不同。

### 5.2 `renew_work`

Expand Down Expand Up @@ -867,6 +869,36 @@ Stage 3/4 qualification 必须保持以下 ownership 与 proof 边界:
LoopX service 成为唯一 writer。本地 `.loopx` 退为 cache、offline projection 与
诊断材料。绝不长期维持 dual-write 或 dual-master。

#### Stage 2C 观察基础:本地提交后 capture

Stage 2C 的前半段是一个显式开启、默认关闭的产品路径。先预览,再开启:

```bash
loopx configure-goal --goal-id GOAL --local-authority-shadow-file
loopx configure-goal --goal-id GOAL --local-authority-shadow-file --execute
```

Todo、handoff-mode、follow-up 与 task-lease facade 会在本地主写返回成功后,采样
完整当前本地投影,再让 `FileAuthorityStore` 保存该 snapshot。
`observation_trigger` 只记录为何开始采样,不是主写 transaction identity;并发主写
因此可能出现在该次 snapshot 中。`captured` 或 `replayed` 只证明候选侧 observation
commit,不表示已经对比 source 与 candidate;结果明确携带
`parity_verdict=not_evaluated`。

候选数据位于 legacy 单 Goal runtime tree
之外的 `authority-shadow/file/`,因此 state migration 不会复制 store identity 或
revision;真正执行迁移时,会从迁移后的本地主状态为目标端建立一条新 lineage。
候选失败只形成 observation result,不会推翻已经完成的本地写入。

用
`loopx configure-goal --goal-id GOAL --clear-local-authority-shadow --execute`
即可关闭 observer。这里回退的只是观察路径:Markdown 与 task-lease 文件始终是
canonical。本切片不会读取候选来决策,不会 fence legacy writer,不会资格化远端
provider,也没有完成 Stage 2C 后半段的本地 canonical promotion。若进程恰好在本地
提交后、observer 调用前崩溃,该次 observation 可能丢失;后续成功写入或 migration
seed 会刷新完整当前投影,但这里不宣称已有 durable shadow outbox 或与主写 transaction
关联的 receipt。这套 plumbing 不是 parity evidence,不能单独支持 Stage 2C promotion。

### 实施前置条件:先让本地文件模式经过同一协调合同

在接入 live NoKV 或其他远端 provider 之前,runtime 应先把当前 todo/lease 写路径中的
Expand Down
1 change: 1 addition & 0 deletions loopx/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -807,6 +807,7 @@ def main(argv: list[str] | None = None) -> int:
registry_path=registry_path,
output_format=output_format,
print_payload=print_payload,
runtime_root_arg=args.runtime_root,
)
if handoff_mode_result is not None:
return handoff_mode_result
Expand Down
2 changes: 2 additions & 0 deletions loopx/cli_commands/handoff_mode.py
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,7 @@ def handle_handoff_mode_command(
registry_path: Path,
output_format: Callable[..., str],
print_payload: PrintPayload,
runtime_root_arg: str | None = None,
) -> int | None:
if args.command != "handoff-mode":
return None
Expand All @@ -124,6 +125,7 @@ def handle_handoff_mode_command(
registry_path=registry_path,
goal_id=args.goal_id,
mode=args.mode,
runtime_root_arg=runtime_root_arg,
**path_args,
)
except HandoffModeError as exc:
Expand Down
6 changes: 6 additions & 0 deletions loopx/cli_commands/registry_admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -494,6 +494,12 @@ def handle_registry_admin_command(
write_scope=args.write_scope,
replace_write_scope=bool(args.replace_write_scope),
clear_write_scope=bool(args.clear_write_scope),
local_authority_shadow_file=bool(
args.local_authority_shadow_file
),
clear_local_authority_shadow=bool(
args.clear_local_authority_shadow
),
waiting_on=args.waiting_on,
clear_waiting_on=bool(args.clear_waiting_on),
boundary_authority_scopes=args.boundary_authority_scope,
Expand Down
16 changes: 16 additions & 0 deletions loopx/cli_commands/registry_admin_configure.py
Original file line number Diff line number Diff line change
Expand Up @@ -251,6 +251,22 @@ def register_configure_goal_command(subparsers: argparse._SubParsersAction) -> N
action="store_true",
help="Clear coordination.write_scope.",
)
configure_goal_parser.add_argument(
"--local-authority-shadow-file",
action="store_true",
help=(
"Enable default-off, one-way capture of post-commit local snapshots "
"in FileAuthorityStore. This does not compare source and candidate."
),
)
configure_goal_parser.add_argument(
"--clear-local-authority-shadow",
action="store_true",
help=(
"Disable the local authority shadow. This does not delete retained "
"candidate observations."
),
)
configure_goal_parser.add_argument(
"--waiting-on",
choices=["codex", "user_or_controller", "controller", "external_evidence"],
Expand Down
7 changes: 7 additions & 0 deletions loopx/cli_commands/todo.py
Original file line number Diff line number Diff line change
Expand Up @@ -779,6 +779,7 @@ def handle_todo_command(
)
payload = add_goal_todo(
registry_path=registry_path,
runtime_root_arg=runtime_root_arg,
goal_id=args.goal_id,
role=args.role,
text=args.text,
Expand Down Expand Up @@ -839,6 +840,7 @@ def handle_todo_command(
validate_todo_claim_options(args)
payload = update_goal_todo(
registry_path=registry_path,
runtime_root_arg=runtime_root_arg,
goal_id=args.goal_id,
todo_id=args.todo_id,
role=args.role,
Expand All @@ -852,6 +854,7 @@ def handle_todo_command(
validate_todo_update_options(args)
payload = update_goal_todo(
registry_path=registry_path,
runtime_root_arg=runtime_root_arg,
goal_id=args.goal_id,
todo_id=args.todo_id,
text=args.text,
Expand Down Expand Up @@ -993,6 +996,7 @@ def handle_todo_command(
if completion_error is None:
payload = complete_goal_todo(
registry_path=registry_path,
runtime_root_arg=runtime_root_arg,
goal_id=args.goal_id,
todo_id=args.todo_id,
role=args.role,
Expand Down Expand Up @@ -1032,6 +1036,7 @@ def handle_todo_command(
validate_todo_supersede_options(args)
payload = supersede_goal_todo(
registry_path=registry_path,
runtime_root_arg=runtime_root_arg,
goal_id=args.goal_id,
todo_id=args.todo_id,
role=args.role,
Expand All @@ -1057,6 +1062,7 @@ def handle_todo_command(
validate_todo_archive_completed_options(args)
payload = archive_completed_todos(
registry_path=registry_path,
runtime_root_arg=runtime_root_arg,
goal_id=args.goal_id,
role=args.role or "agent",
max_active_done=args.max_active_done,
Expand All @@ -1081,6 +1087,7 @@ def handle_todo_command(
followups.append(args.text)
payload = capture_followup_todos(
registry_path=registry_path,
runtime_root_arg=runtime_root_arg,
goal_id=args.goal_id,
followups=followups,
evidence=args.evidence or "",
Expand Down
54 changes: 54 additions & 0 deletions loopx/configuration_catalog.py
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,11 @@ def build_goal_configuration_catalog(
if isinstance(feature_summary.get("peer_task_coordination"), Mapping)
else {}
)
local_authority_shadow = (
feature_summary.get("local_authority_shadow")
if isinstance(feature_summary.get("local_authority_shadow"), Mapping)
else {}
)
graph_enable_args = ("--explore-graph-enabled",)
harness_enable_args = (
"--explore-harness-enabled",
Expand All @@ -100,6 +105,55 @@ def build_goal_configuration_catalog(
),
},
"features": [
{
"feature_id": "local_authority_shadow",
"display_name": "Local post-commit authority observation",
"availability": "experimental_opt_in",
"default": {"enabled": False},
"current": {
"enabled": local_authority_shadow.get("enabled") is True,
"mode": local_authority_shadow.get("mode"),
"status": local_authority_shadow.get("status", "disabled"),
},
"consider_when": (
"A Goal needs to exercise the first Stage 2C observation "
"plumbing while legacy local writers remain authoritative."
),
"effect": (
"Captures a best-effort post-commit snapshot of Todo and "
"task-lease state through the FileAuthorityStore contract."
),
"does_not": [
"read the candidate for lifecycle decisions",
"write candidate state back into Markdown or task-lease files",
"promote shared authority or fence legacy writers",
"bind the snapshot to the exact primary transaction",
"guarantee delivery through a durable outbox",
"compare source and candidate or issue a parity verdict",
],
"commands": {
"preview_enable": _configure_command(
goal_id, "--local-authority-shadow-file"
),
"apply_enable": _configure_command(
goal_id, "--local-authority-shadow-file", execute=True
),
"preview_disable": _configure_command(
goal_id, "--clear-local-authority-shadow"
),
"apply_disable": _configure_command(
goal_id, "--clear-local-authority-shadow", execute=True
),
"verify": [inspect_command],
},
"documentation": {
"path": "docs/architecture/rfcs/shared-goal-authority-state-provider-v0.md",
"url": (
"https://github.com/huangruiteng/loopx/blob/main/"
"docs/architecture/rfcs/shared-goal-authority-state-provider-v0.md"
),
},
},
{
"feature_id": "multi_subagent",
"display_name": "Adaptive child capacity",
Expand Down
35 changes: 19 additions & 16 deletions loopx/configure_goal.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,8 @@
)
from .control_plane.agents.supervisor import normalize_peer_supervisor
from .control_plane.agents.work_mode import normalize_agent_work_modes
from .control_plane.coordination import local_authority_shadow_adapter as shadow
from .control_plane.coordination.configuration import normalize_goal_write_scope
from .control_plane.operator_inbox_binding import local_private_config_digest
from .control_plane.reward_memory import (
reward_memory_goal_policy,
Expand Down Expand Up @@ -221,18 +223,6 @@ def _clean_registered_agents(values: list[str] | None) -> list[str] | None:
return agents


def _clean_write_scope(values: list[str] | None) -> list[str] | None:
if values is None:
return None
scopes: list[str] = []
for value in values:
for part in str(value).split(","):
scope = part.strip()
if scope and scope not in scopes:
scopes.append(scope)
return scopes


def _settings_summary(goal: dict[str, Any]) -> dict[str, Any]:
quota = goal_quota_config(goal)
control_plane = compact_control_plane_policy(goal.get("control_plane"))
Expand All @@ -259,7 +249,9 @@ def _settings_summary(goal: dict[str, Any]) -> dict[str, Any]:
"explore_graph": compact_explore_graph_policy(goal.get("explore_graph")),
"orchestration": orchestration,
"waiting_on": goal.get("waiting_on"),
"write_scope": _clean_write_scope(coordination.get("write_scope") or []) or [],
"write_scope": normalize_goal_write_scope(coordination.get("write_scope") or [])
or [],
"local_authority_shadow": shadow.local_authority_shadow_summary(goal),
"checkpointed_boundary_authority": checkpointed_boundary_authority_summary(
coordination
),
Expand Down Expand Up @@ -461,6 +453,8 @@ def configure_goal(
write_scope: list[str] | None = None,
replace_write_scope: bool = False,
clear_write_scope: bool = False,
local_authority_shadow_file: bool = False,
clear_local_authority_shadow: bool = False,
waiting_on: str | None = None,
clear_waiting_on: bool = False,
boundary_authority_scopes: list[str] | None = None,
Expand Down Expand Up @@ -535,6 +529,9 @@ def configure_goal(
raise ValueError(
"--clear-write-scope cannot be combined with --replace-write-scope"
)
shadow.validate_local_authority_shadow_change(
local_authority_shadow_file, clear_local_authority_shadow
)
if clear_waiting_on and waiting_on:
raise ValueError("--clear-waiting-on cannot be combined with --waiting-on")
adding_boundary_authority = any(
Expand Down Expand Up @@ -634,7 +631,7 @@ def configure_goal(
clear_todo_lifecycle_authority
)
supervised_agents = _clean_registered_agents(supervised_agents)
write_scope = _clean_write_scope(write_scope)
write_scope = normalize_goal_write_scope(write_scope)
issue_fix_reviewer_notification_config = _local_private_config_path(
issue_fix_reviewer_notification_config,
label="reviewer notification config",
Expand Down Expand Up @@ -1218,10 +1215,12 @@ def configure_goal(
coordination["write_scope"] = write_scope
else:
existing_write_scope = (
_clean_write_scope(coordination.get("write_scope") or []) or []
normalize_goal_write_scope(coordination.get("write_scope") or [])
or []
)
coordination["write_scope"] = (
_clean_write_scope([*existing_write_scope, *write_scope]) or []
normalize_goal_write_scope([*existing_write_scope, *write_scope])
or []
)
if clear_boundary_authority:
coordination.pop("checkpointed_boundary_authority", None)
Expand All @@ -1241,6 +1240,9 @@ def configure_goal(
coordination["checkpointed_boundary_authority"] = [*entries, entry]
goal["coordination"] = coordination

shadow.apply_local_authority_shadow_change(
goal, local_authority_shadow_file, clear_local_authority_shadow
)
after = _settings_summary(goal)
changed_fields = _changed_fields(before, after)
if goal != before_goal and not changed_fields:
Expand Down Expand Up @@ -1279,6 +1281,7 @@ def configure_goal(
"peer_task_coordination": deepcopy(
after.get("peer_task_coordination") or {"enabled": False}
),
"local_authority_shadow": deepcopy(after["local_authority_shadow"]),
"lark_event_inbox": _lark_event_inbox_config_summary(goal),
"lark_kanban_heartbeat_sync": _lark_kanban_heartbeat_config_summary(goal),
"reward_memory": reward_memory_goal_policy_summary(goal),
Expand Down
Loading
Loading