Skip to content

Refactor/event driven control plane - #3200

Open
hufeide wants to merge 1 commit into
huangruiteng:frontend-control-plane-im-prototype-rfcfrom
hufeide:refactor/event-driven-control-plane
Open

Refactor/event driven control plane#3200
hufeide wants to merge 1 commit into
huangruiteng:frontend-control-plane-im-prototype-rfcfrom
hufeide:refactor/event-driven-control-plane

Conversation

@hufeide

@hufeide hufeide commented Aug 14, 2026

Copy link
Copy Markdown

refactor: migrate control plane to event-driven task scheduling

Summary

This PR refactors the LoopX Control Plane from the legacy heartbeat-driven execution model to an event-driven task scheduling model.

The core change is:

Legacy: heartbeat polling → multi-layer checks → runtime execution → scattered projections
New: events → state projection → unified policy → task queue → worker → events

The new architecture treats Task, Event, Scheduler, Lease, and Worker as first-class concepts, while keeping Goal Acceptance and Goal Closure as explicit control-plane stages.

Why

The legacy Control Plane relies heavily on heartbeat-driven polling.

Each heartbeat wakes the system and re-evaluates multiple independent layers:

heartbeat
  ↓
quota / should_run
  ↓
capability gate
  ↓
agent scope
  ↓
execution context
  ↓
turn driver
  ↓
runtime
  ↓
projections / ledgers
  ↓
next heartbeat

This creates several structural problems:

  • execution is driven by periodic polling rather than state changes

  • quota, capability, scope, and scheduler decisions are distributed across multiple layers

  • decision values have accumulated into many different states/actions

  • state is spread across multiple projections and ledgers

  • scheduler has limited task lifecycle management

  • there is no unified claim/lease/retry lifecycle

  • crash recovery and zombie execution recovery are difficult to reason about

  • observability is distributed across multiple sources

The refactor moves the control plane toward an event-driven Task OS model.

New Runtime Model

USER
  ↓
GOAL
  ↓
Task Graph
  ↓
Events
  ↓
State Projection
  ↓
PolicyEngine
  ↓
TaskReady
  ↓
Task Queue
  ↓
Worker claim + lease
  ↓
Execution
  ↓
Result / Event
  ↓
State Projection
  ↓
Next Task

The heartbeat is no longer the primary execution driver. It is reduced to an event source/timer mechanism where periodic observation is actually required.

Key Changes

1. Event-driven task dispatch

Introduce:

  • scheduler/event_driven_dispatch.py

  • scheduler/resident.py

Task readiness produces events which are placed into the Task Queue.

The scheduler consumes ready work incrementally instead of repeatedly scanning the entire control plane on every heartbeat.

TaskReady
   ↓
Task Queue
   ↓
Worker claim
   ↓
Execution

Idle systems can remain idle instead of repeatedly waking and recomputing the entire execution state.

2. Task lifecycle with lease and retry

Introduce:

  • scheduler/task_lifecycle.py

Tasks now have an explicit lifecycle:

pending
  ↓
claimed
  ↓
done

Failure and recovery paths are explicitly modeled:

running
  ↓ lease expired
pending

failed
↓ transient
retry_wait

pending

failed
↓ retry exhausted
dead_letter

cancelled

Workers claim tasks using:

  • lease_until

  • attempt

  • claimed_by

  • capability requirements

  • idempotent task identity

This provides a foundation for crash recovery, retry, duplicate prevention, and multi-worker execution.

3. Unified Policy Engine

Introduce:

  • policy/decision.py

  • policy/engine.py

  • policy/decision_events.py

The legacy quota/capability/scope decisions are consolidated behind:

PolicyEngine.decide()

The primary decision space becomes:

run
wait
deny

while richer action semantics are represented separately:

ALLOW
DENY
DEFER
RETRY
BLOCK
CANCEL
ESCALATE

Decisions are also serializable through Decision.to_dict() / from_dict() for event/audit integration.

4. Event Store as source of truth

The new architecture treats append-only events as the authoritative record.

State is derived through projections rather than maintained as multiple independent sources of truth.

Relevant changes include:

  • append-only rollout events

  • state projection

  • checkpoint/replay support

  • generation-aware task identity

Task identities use generation information such as:

todo_id:generation:N

to reduce replay and duplicate-execution collisions.

5. Explicit worker execution safety gates

Resident workers do not automatically execute arbitrary claimed work.

Execution requires the expected control-plane gates:

worker_command
+
allowed prefix
+
guard_checked

If the required execution gates are not satisfied, the task is not automatically executed.

This makes the execution boundary explicit rather than relying on implicit routing through heartbeat/runtime layers.

6. Resident scheduler reconciliation

The resident scheduler performs reconciliation before normal dispatch.

This covers:

  • expired leases

  • zombie task recovery

  • retry advancement

  • queue state reconciliation

The scheduler therefore has a deterministic recovery path rather than depending on another heartbeat cycle to eventually rediscover stale work.

7. Goal Acceptance and Closure

Introduce:

  • goals/goal_acceptance.py

  • goals/goal_closure.py

Task completion is intentionally kept separate from Goal completion.

The control plane can therefore distinguish:

Task completed

Goal completed

Goal closure is evaluated after acceptance and remaining-work checks rather than being inferred from an individual Todo completion.

8. Control-plane observability

Introduce:

  • status/control_plane_observability.py

A unified read-only snapshot exposes six major categories:

scheduler
worker
queue
task
decision
event

This is also exposed through:

--control-plane-status

so the runtime state can be inspected without modifying control-plane state.

Architecture Comparison

Area Legacy New
Trigger Heartbeat polling Event-driven
Scheduling Repeated full-state evaluation Ready-task dispatch
Decision Distributed quota/capability/scope Unified PolicyEngine
Task lifecycle Minimal Claim / lease / retry / expiry / cancellation
Queue Implicit Explicit Task Queue
Worker ownership Implicit Lease-based claim
Failure recovery Distributed recovery paths Resident reconciliation
Source of truth Multiple projections/ledgers Append-only event store
State Distributed projections Reconstructable projections
Idempotency Limited Generation-aware task identity
Execution safety Implicit/distributed Explicit worker gates
Goal closure Distributed Acceptance → Closure
Observability Scattered Unified read-only snapshot

Conceptual Simplification

The refactor also reduces the number of concepts directly exposed to the scheduling layer.

The legacy model contains many independent concepts around:

Goal / Vision / Frontier / Dreaming
Quota / Slot / Settlement / Budget
Capability Gate
Agent Scope
Execution Context
Wake
Heartbeat
Supervisor
Handoff
Multiple Projections

The new model consolidates these around:

Goal
Task
Policy
Scheduler / Task Queue
Event Store
State Projection
Worker
AgentHandoff Event

The goal is not to remove domain semantics, but to establish clearer ownership boundaries between:

  • state

  • policy

  • scheduling

  • execution

  • acceptance

  • closure

Tests

New and updated tests cover:

  • task lifecycle

  • lease expiration

  • retry behavior

  • event-driven dispatch

  • resident scheduler reconciliation

  • policy decisions

  • policy integration

  • rich decision serialization

  • goal acceptance

  • goal closure

  • heartbeat event source

  • checkpoint/replay

  • cost projection

  • capability bridge

  • control-plane observability

  • CLI integration

Core validation:

cd /home/fei/workspace/loopx

/home/fei/.workbuddy/binaries/python/versions/3.14.3/bin/python3 -m pytest
tests/control_plane/test_task_lifecycle.py
tests/control_plane/test_rich_decision.py
tests/control_plane/test_control_plane_observability.py
tests/control_plane/test_scheduler_resident_merge.py
tests/control_plane/test_event_driven_dispatch.py
tests/control_plane/test_policy_engine.py -q

Scope

This PR focuses on:

loopx/loopx/control_plane

and the integration points required to move the existing runtime toward the new control-plane model.

The intent is to establish the new scheduling/control-plane primitives while preserving the existing Goal/Agent integration points where migration is still required.

Review Focus

The main areas for review are:

  1. Event Store / projection ownership

  2. Task lifecycle and lease semantics

  3. PolicyEngine decision semantics

  4. Resident scheduler reconciliation

  5. Worker execution safety gates

  6. Goal acceptance / closure semantics

  7. Compatibility between the new event-driven path and existing runtime integrations

Design Principle

The architectural direction can be summarized as:

Event = fact
Projection = current state
Task Graph = work relationships
Policy = constraints
Scheduler = scheduling authority
Lease = execution authority
Worker = execution
Acceptance = validation
Closure = termination

The intended end state is:

LoopX moves from an Agent Operating System centered around heartbeat-driven turns toward an Agent Runtime + Task OS centered around event-driven work execution.

@huangruiteng huangruiteng left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

这个 PR 似乎应该合往前端那个分支

@hufeide
hufeide changed the base branch from main to frontend-control-plane-im-prototype-rfc August 14, 2026 13:44

@huangruiteng huangruiteng left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

又看了下,没改前端,应该合往main

@huangruiteng

Copy link
Copy Markdown
Owner

感谢把方向和实现一次性展开。事件事实、显式 Task lifecycle、lease/retry、Goal acceptance 与 closure 分离,这些方向值得保留;但当前提交更像一个并行控制面原型,不是可直接合入的迁移 PR(+12,685/-97,旧路径基本未删除)。我建议先不要合并或直接 retarget,而是从最新 main 重建分支并分层搬运。

我本地做了以下验证:

  • changed Python tests:305 passed;相关 Node tests:56 passed;
  • changed Python files 上 Ruff:25 errors;
  • PR 当前没有 status checks;
  • 两个独立进程同时 claim 同一个 pending task,首轮就复现两个 worker 都返回 claim 成功;
  • 允许前缀为 printf 时,printf ...; <second command> 会通过检查并由 shell=True 执行第二条命令。

合并前至少有这些阻断项:

  1. 默认行为与文档相反。 new_architecture.master_switch_enabled() 在环境变量未设置时返回 True;event dispatch、heartbeat event source、PolicyEngine 和 policy event recording 都继承这个默认值,但 CLI/help/docstring 多处写的是 opt-in / disabled by default。这个 PR 会静默改变默认路径并新增持久化写入,必须改为默认关闭、披露行为,并做主线 parity/canary 后再逐步放量。

  2. Task claim 没有原子性。 enqueue_tasksclaim_next_task 以及 task_lifecycle 的 mutation 都是无锁的 read-modify-write JSONL。WorkerPool/resident scheduler 已经提供多 worker 入口,因此这里必须有跨进程 lock 或带 generation/lease epoch 的 CAS;同时加入 race、lease-expiry-vs-complete、retry-vs-reclaim fault matrix。现在的 generation-aware task_id 并不能防止两个 worker 同时领取同一 generation。

  3. worker exec allowlist 可被 shell chaining 绕过。 _command_matches_worker_prefix 只判断首命令/字符串前缀,而 _run_worker_shell_command 使用 shell=True。应改为结构化 argv + shell=False,或使用现有 executor 的完整命令合同;不能把“首 token 合法”视为整个 shell program 合法。

  4. 新 closure 会绕过现有 frontier/vision/replan authority。 build_event_driven_dispatch() 构造 closure state 时把 replan_required=Falseexternal_followup_required=False 写死;没有消费主线现有的 goal-frontier/vision acceptance obligations。空 queue 因而可以被判为 goal_closed,即使当前仍有 replan/acceptance obligation。Goal acceptance/closure 不能另建一套判定真相,必须适配当前权威 read model。

  5. “Event Store is source of truth”尚未实现。 实际上 task queue 是会被原地重写的独立 JSONL,task events 在 rollout log,checkpoint/replay 又从 run index 读取;write_checkpoint/recover_task_state 没有生产调用方。task event 还按 (goal_id,event_kind,todo_id) 去重,retry/re-dispatch 同一 todo 的后续事实会被折叠。应先选定权威 transition event + projector,再谈 checkpoint/replay;当前 generic replay scaffold 建议暂缓。

  6. resident scheduler 仍然是 polling。 run_resident_scheduler_loop()while True -> tick -> sleep,每 tick 重读并扫描 todo projection;而且把每个完整 tick payload 永久 append 到内存列表,长驻进程会无界增长。它还不能支撑 PR 中“idle systems remain idle / event-driven”的结论。

  7. 若干测试验证的是手造形状,不是模块间真实合同。 例如 policy recorder 把 outcome/source 放在 rollout event 的 status/classification/details,observability test 却手造顶层 outcome/source,所以真实 decision history 会统计成 unknown。需要跨 adapter conformance 和真实 writer→reader tests,而不是各模块各自通过。

建议拆分与合并节奏:

  1. PR-A:characterization / parity only:从最新 main 固化 heartbeat/quota/capability/scope/replan/terminal 的现有决策矩阵,不改默认行为。
  2. PR-B:typed Policy contract(shadow-only):只做纯 normalization/composition;使用 enum/validated action,保留全部原因与 obligation,不重跑另一遍 quota,不写事件,不接 host。先证明与主线最终决策 parity。
  3. PR-C:Task transition + projector + atomic claim:选择唯一 authority,完成 lock/CAS、lease epoch、retry/dead-letter、幂等 identity,以及 fault/replay/race matrix。不要同时带 resident worker。
  4. PR-D:read-only surfaces:cost projection 可单独较早合入;queue/status observability 在 writer→reader conformance 修正后合入。
  5. PR-E:Goal acceptance/closure adapter:复用主线 goal-frontier/vision/replan obligations,仅新增 typed closure projection;不得从“空 queue”自行推导终局。
  6. PR-F:resident scheduler pilot:在 C/E 稳定后,默认关闭,单一 host/canary 接入;证明不会双执行、不会漏执行、不会绕过 quota/settlement/terminal authority,再逐步接 OpenCode/Pi/Claude。
  7. 单独延期:checkpoint/replay 要等真实 reducer/生产恢复调用方;CapabilityEventHub/HookRegistry 等没有第二个真实调用方的抽象先不合。CLI discovery、capability binding 若确有独立价值,也各自单开 PR。

另外,当前 head 基于 frontend-control-plane-im-prototype-rfc,与最新 main 已明显分叉;直接把本 PR base 改成 main 会把原型分支历史混入。建议保留 #3200 作为设计/原型参考,在最新 main 上按上述顺序重新开 PR。

@huangruiteng

Copy link
Copy Markdown
Owner

补充一个更明确的取舍,尤其是 Policy 与现有 Effect Program 的关系。

先按当前真实价值拆分

  1. characterization / parity tests:优先级最高。 先把现有 quota、frontier、capability、handoff、settlement、host-action 的输入输出和非法转换固定下来;这是后续移动决策代码时的安全网,也能独立成 PR。
  2. cost projection:有条件保留。 只有当它能区分 protocol/control-plane cost 与实际 work cost,并有 quota/status/benchmark 的真实 reader 时才有产品价值;仅新增一组估算字段或演示输出,不值得先进入核心路径。
  3. observability writer→reader conformance:方法有价值,但应跟随真实 writer。 先证明现有执行路径会写出稳定、可重放、可消费的 receipt/projection,再补 reader conformance;不应先建立另一套 event authority,然后让生产路径以后迁入。

硬约束:Policy 不得成为第二套 Effect/Settlement 引擎

主线已经有通用 Effect Program 与 settlement algebra:有 ordered steps、failure short-circuit、receipt accumulation、effect identity,以及 scheduler-outside-settlement。新 Policy 层应成为它的纯决策前端/编译阶段,而不是并列的执行系统:

domain authorities
  quota / frontier / capability / scope / handoff / host
        ↓  typed PolicyFragment(各域仍保有规则权威)
pure combine
        ↓  PolicyVerdict(允许/拒绝、obligations、selected work、terminal/retry/host constraints、provenance、snapshot id)
compile
        ↓  existing EffectTurn / EffectProgram
interpret + settle
        ↓  existing SettlementPlan / receipts;scheduler 仍是 host handoff

这里值得形成一个通用抽象,但抽象点应是 “typed verdict → effect program”,不是 PolicyEngine.run()

  • Policy 只组合各 bounded context 已算出的决定;不能重新计算 quota/frontier,避免出现第二份 truth。
  • Policy 不做 I/O,不 claim lease,不写 event,不 spend quota,不直接调 scheduler。
  • obligationsexecution disposition 必须正交;不能把 wait/run/deny 压成一个字符串后丢掉尚未满足的义务。
  • verdict 与 effect plan 必须绑定同一 evaluated_snapshot_id,防止组合阶段和执行阶段看到不同状态。
  • 审计事件默认从实际 interpretation/settlement receipt 投影;Policy 不另写一份“已执行”的事实。
  • mutation 最终仍必须经过现有 settlement identity、短路和 receipt 校验。

第一版无需做庞大的通用 policy framework。建议只增加最小 typed contracts,例如 PolicyFragmentPolicyVerdictcompile_policy_effects(verdict),并让一个真实入口(建议 quota should-run)以 shadow mode 生成 verdict/effect program,与当前结果做 parity 对比;没有 parity 证据前不切默认、不接 resident worker。

建议把 #3200 停在设计/试验参考,不继续叠代码;从最新 main 重开以下批次:

  1. characterization/parity oracle;
  2. typed fragments/verdict + 编译到现有 Effect Program(pure + shadow-only);
  3. 一个真实 host adapter canary,证明 obligation 不丢、terminal 不被错误放宽、失败仍短路;
  4. 最后才接真实 receipt/projection 的 observability reader。

这样 Policy 的收益是统一决策形状与组合律,Effect Program 的收益是统一执行与结算语义;两者互补,同时避免多一套 queue/event/checkpoint/worker truth。

@huangruiteng huangruiteng left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

PR #3200 全量双语评审 — Refactor/event driven control plane

精确评审头(Exact Head): 3200@f6549a9e9f1f21cb1600de2765b461840c26a196
Base: frontend-control-plane-im-prototype-rfc(owner 已两次要求改为 main,见下)
规模: 67 files, +12685/-97;production 41、test/example 22、build config 1、public docs 1、other 2;无 CI checks rollup


详细中文评审

动机

PR 将 LoopX 控制平面从「心跳轮询 → 多层检查 → 运行时 → 分散投影」重构为「事件 → 状态投影 → 统一策略 → 任务队列 → worker 租约执行 → 事件」的事件驱动 Task OS 模型。动机是真实且可复现的:quota/capability/scope/scheduler 决策分散在多层,状态同时存在于多个投影与 ledger,任务缺少统一的 claim/lease/retry/崩溃恢复生命周期,goal 闭包依赖单个 todo 的 no_followup 而缺少验收证据。PR 明确引用了 website1 颜色会话的具体故障(已闭 goal 被复用污染、完成 todo 不写事件导致 ready=6 永远无法闭包),这些是真实的自动化卡死问题,而不是演示性的重构理由。就近小修不足以解决:这些是横切架构问题,继续在心跳层打补丁只会增加更多层;因此「事件驱动 + 显式生命周期」的整体方向成立。

改动思路

架构主线清晰:Event=事实、Projection=当前状态、Policy=约束、Scheduler=调度权威、Lease=执行权威、Worker=执行、Acceptance=验证、Closure=终止。正路径设计为:todo add/complete 桥接 rollout 事件 → dispatch 从 handoff gate 重算 READY 后继 → 入队(幂等)→ worker claim(带 lease)→ resident 执行门(command + prefix + guard)→ finalize 把任务标记完成 → Goal Acceptance 验证 → goal_closure_ready + goal_closed 原子闭包 → registry 状态同步 closed。负路径也有明确设计:终态 todo 永不重新入队;lease 过期 → pending(僵尸恢复);retry_wait 到期 → pending;goal_acceptance_pending 阻止无 criteria 的 tick 静默闭包;执行门不满足绝不自动执行。接入面包括:quota live_decision 附加统一 policy_decision、Claude/OpenCode/Pi 的 run gate 优先读 policy_decision、bootstrap/start-goal 增加 closed-goal 拦截、todos.py 增加事件桥、registry 增加 sync_registry_goal_closed、前端投影展示调度/策略摘要。

设计整体自洽,但有两处「设计说得很安全、实现没有兑现」的关键矛盾:worker 前缀允许列表在实现中退化为首 token 匹配;能力绑定任务的 capability 字段在入队时被丢弃,导致 fail-closed 只对测试中手工构造的队列条目生效。此外「opt-in / 默认不变」的文档与默认开启的实现互相矛盾(详见风险 2/4)。

具体改动

按面分类:

  • 调度核心scheduler/event_driven_dispatch.py(768)、scheduler/resident.py(811)、scheduler/task_lifecycle.py(677)、scheduler/merge.py(327)。
  • 统一策略policy/decision.pydecision_events.pyengine.py(run/wait/deny + ALLOW/DENY/DEFER/RETRY/BLOCK/CANCEL/ESCALATE,序列化 Decision,transition-only 决策事件)。
  • Goal 生命周期goals/goal_acceptance.py(grep 独立验证 + absence 语义)、goals/goal_closure.py(RUN/WAIT/CLOSE 三态)、goals/goal_channel_projection.py
  • 运行时runtime/checkpoint.pyruntime/replay.pyquota/cost_projection.pyquota/live_decision.py
  • 能力桥接capabilities_bridge.py(504)(token 归一化、P1 资格、P2 registry 驱动 CLI 注册、P3 事件/钩子 hub)。
  • 可观测status/control_plane_observability.py(295) + status_markdown 渲染。
  • CLI/集成starter_scheduler 新增 dispatch/resident/merge 三命令、project_lifecycle 新增 goal-closurecli.py 改为 registry 驱动注册;chat_agent、Claude goal-mode、OpenCode/Pi goal loops、todos.pybootstrap_command_pack.pyregistry.pyrollout_event_log.py 均有桥接改动。
  • 测试tests/control_plane/ 新增 22 个文件约 5,200 行,含调度、策略、检查点/重放、验收/闭包、能力桥、可观测、todo 变更权威。

关键代码讲解

1. loopx/control_plane/scheduler/resident.py::_command_matches_worker_prefix(P1 安全回归)

该函数声称「Mirrors the original scheduler executor gate」,但与原实现不同:它只比较命令的第一个 token(first == prefix.split(None, 1)[0]),而不是完整前缀 token 序列。实测:

  • _command_matches_worker_prefix("git push --force origin main", ["git status"])True
  • _command_matches_worker_prefix("python3 -c 'import os; os.system(\"id\")'", ["python3 examples/x.py"])True
  • codex_cli_scheduler._command_matches_allowed_prefix 使用 command_parts[: len(prefix_parts)] == prefix_parts,多 token 前缀是完整匹配的。

后果:--worker-command-prefix "git status" 会授权任意 git … 命令(包括 push/reset/checkout --force),python3 examples/foo.py 会授权任意 python3 -c …。这是本 PR 核心卖点「显式 worker 执行安全门」的实质性削弱。现有测试只用单 token 前缀(sed),未覆盖多 token 场景。

2. loopx/control_plane/scheduler/event_driven_dispatch.py::enqueue_tasks(P1 能力门失效)

队列条目只写入 schema_version/goal_id/todo_id/status/enqueued_at/enqueued_by丢弃 required_capabilitiescapability_binding_refclaim_next_eligible_task 对无能力字段的条目返回任意 worker 可 claim。实测:经 build_event_driven_dispatch 入队的能力绑定任务(required_capabilities=["issue_fix"] + capability_binding_ref="issue-fix:feasibility_v0"),一个无任何能力声明的 worker 也能成功 claim。测试 test_event_driven_dispatch.py:629/666/747 是入队后手工修改 entries 再断言,绕过了真实入队路径,因此迁移笔记中「带 binding 的任务 fail-closed」在端到端 CLI 路径上不成立。

3. loopx/control_plane/new_architecture.py::master_switch_enabled + scheduler/event_driven_dispatch.py::event_driven_dispatch_enabled(P1 默认行为与文档矛盾)

env 未设置时 master_switch_enabled() 返回 True,dispatch/event-source/merge 全部默认开启;live_decision._attach_unified_policy_decision 默认把 policy_decision 附加到每个 quota should-run 并写 policy_decision 审计事件;OpenCode/Pi goal loop 默认 fire-and-forget 调用 dispatch(claim 任务、写 task_ready/enqueued/dispatched)。而 CLI help、模块 docstring 与 PR 正文反复声称「Opt-in … disabled by default / default unchanged」。test_policy_pilot_wiring.py 的 docstring 写「default behavior is unchanged (no policy_decision key…)」,测试断言却要求默认附加 policy_decision——同一文件自相矛盾,说明披露面没有收敛。

4. loopx/control_plane/quota/live_decision.py::_attach_unified_policy_decision(P1 行为变更未披露)

默认开启后,每次 loopx quota should-run / turn 都会:(a) 在 payload 增加 policy_decision(JSON schema 变化);(b) 向目标 goal 的 rollout event log 写 policy_decision 事件(持久化写);(c) 下游 goal-mode/statusline/opencode/pi 的 run gate 改为优先消费该字段。这属于会改变现有自动化与状态量的默认行为变更,未在 PR 正文/help/迁移笔记中统一披露,也未说明旧/新默认与关闭方式的一致性。

5. loopx/todos.py::add_goal_todo / complete_goal_todo(P2,并入披露项)

两个核心命令现在无条件追加 todo_add/todo_complete rollout 事件(只要 runtime root 可解析且非 dry-run),即使 LOOPX_NEW_ARCHITECTURE=0 也写。这与「master switch 关闭即恢复 legacy 路径」的说法不符;同时 update_goal_todo/supersede_goal_todo 未桥接事件,rollout 投影对 reopen/unblock 等更新会过期(只有 add/complete 两态)。

正向路径

以「新 goal 首个任务」为例:todo add 写 markdown + todo_add 事件 → codex-cli-local-scheduler-dispatch --completed-todo-id …todo_complete 事件并同步内存状态 → advance_ready_todo_ids 从 handoff gate/自由 advancement todo 算出 READY → 去重入队 → claim_next_task 加 lease 后 claim → resident 模式经 execute_claimed_task(command+prefix+guard 全过)执行 → finalize_resident_executiontask_completedevaluate_goal_acceptance 验证 → maybe_close_goal 原子发 goal_closure_ready+goal_closedsync_registry_goal_closed 同步 registry。该路径的单元/集成测试覆盖充分(1204 个 control-plane 测试通过)。

负向路径

  • 能力不匹配:设计意图是 eligible_bridged fail-closed;实际端到端因入队丢字段而失效(风险 3)。
  • 多 token 前缀:设计意图是白名单精确匹配;实际首 token 即放行(风险 2)。
  • 验收未满足:goal_acceptance_pending 会阻止无 criteria 的后续 tick 静默闭包,这是实现正确的亮点;但 resident 模式 grep 证据无 base_dir/pattern,自报 ok=True 即可满足(风险 6)。
  • 执行门跳过:gate-skip 的任务在 finalize 被 transient=False 标记为 failed,任务永久卡住(风险 7)。

对主干的风险

阻断项(P1):

  1. 基线分支不匹配:PR 仍指向 frontend-control-plane-im-prototype-rfc,但改动是生产 runtime/CLI/quota/todos,owner 已两次 CHANGES_REQUESTED(「这个 PR 似乎应该合往前端那个分支」→「又看了下,没改前端,应该合往 main」)。合入前必须重定向到 main(或作者明确说明该分支作为新架构主线)。
  2. worker 前缀允许列表安全回归(见关键代码 1):多 token 前缀退化为首 token,任意同首 token 命令可执行。最小修复:移植原 command_parts[:len(prefix_parts)] == prefix_parts 逻辑,并加负例测试(git push --forcegit status 前缀必须拒绝;python3 -c …python3 examples/x.py 必须拒绝)。
  3. 能力匹配未端到端接线(见关键代码 2):入队丢失能力字段,能力绑定任务可被任意 worker claim,capability 边界形同虚设。最小修复:enqueue_tasks/队列条目携带 required_capabilities + capability_binding_ref(含 task_ready/task_enqueued 事件的可审计字段),并新增「经 build_event_driven_dispatch 入队 → 无能力 worker 必须被拒绝」的端到端测试,替换手工改 entries 的测试。
  4. 默认开启与文档/披露矛盾(见关键代码 3/4/5):要么把默认改为真正 opt-in(与 help/docstring 一致),要么在 PR 正文、CLI help、迁移笔记统一披露默认开启、旧/新默认、关闭方式(LOOPX_NEW_ARCHITECTURE=0),并让 todos.py 事件桥与决策记录跟随 master switch;同时修正 test_policy_pilot_wiring.py 自相矛盾的 docstring。

非阻断(P2):

  1. M6 maintainability ratchet 仍为红:head 上 test_m6_maintainability_ratchet_has_no_unreviewed_debt 失败,新增 unreviewed 债务 dependency_debt: event_driven_dispatch -> capabilities.catalog(control_plane 外向依赖)。base 分支本就红(chat_actions.py 1590 行超 1500 上限,PR 已通过 baseline 修复),但 PR 同时引入了新的未审依赖。需要添加 reviewed exception(稳定 finding id + reason + retirement plan),或把 build_capability_registry() 改为注入式 seam,消除 control_plane 对 capabilities 包的 import。
  2. resident 验收证据不对称:dispatch 路径传 --project 做独立 grep 验证;resident CLI 不传 project/base_dir,且 evidence 解析 split("=", 2)ok=True 无条件,无 regex/absence 语义。目标可以在 resident 模式靠自报证据闭包。应与 dispatch 共用同一套验收解析与验证路径。
  3. 安全跳过被永久失败finalize_resident_execution 对 gate-skip(未执行)任务调用 fail_task(transient=False),任务进入 failed 且不会自动重试,只能手工 requeue;安全门的「跳过」不应消耗任务失败预算。应保持 pending/重新入队,仅对真正尝试且失败的任务标记失败。
  4. 次要契约/披露漂移verify_criterion docstring 承诺「无明确证据要求时有 manual/snapshot 兜底」但代码未实现;replay_audit_record 声称「no task contents」却内嵌完整 checkpoint state snapshot;frontstage 投影 goal_channel_projection._compact_quota 暴露 scheduler_reset_token(控制 token,建议仅保留 rrule/cadence 标量);--dry-run 参数在 goal-closure 中只回显不参与逻辑。

其余残留风险:12,685 行新增且无 status-check rollup,依赖本地 pytest 自证;队列 claim 为 FIFO,Decision.priority 未接入队列排序;OpenCode/Pi 循环 fire-and-forget dispatch 只 claim 不 complete,lease 过期后会产生重复 claim/执行风险(在原型分支可接受,合 main 前需明确 worker 归属);迁移笔记自认 90+ 处硬编码钩子未迁移,属阶段边界。

验证矩阵(exact head 实测)

场景 命令 状态 结果
control-plane 全量 pytest tests/control_plane PASS 1204 passed
M6 门 pytest tests/control_plane/test_m6_quality_gates.py FAIL 1 failed(unreviewed dependency_debt)
Claude policy pytest tests/test_claude_goal_policy.py PASS 3 passed
OpenCode/Pi runtime node --test tests/opencode_goal_bridge_runtime.test.mjs tests/pi_goal_loop_runtime.test.mjs PASS 56 passed
CLI 注册 smoke loopx codex-cli-local-scheduler-{dispatch,resident,merge} --help PASS 三命令可注册
前缀门负例 聚焦探针 FAIL(期望拒绝) git push --force 通过 git status 前缀
能力传播端到端 聚焦探针 FAIL(期望拒绝) 无能力 worker 可 claim 绑定任务
默认开关 聚焦探针 与文档矛盾 env 未设时 dispatch/event_source/merge=True
base 对照 M6 on ece816a2 FAIL(pre-existing) chat_actions 1590>1500;head 变为新 dependency 债务

我的整体评价

这是一次方向正确、测试投入很大的架构重构:typed lifecycle/Decision、幂等事件、原子 checkpoint、闭环 acceptance→closure 的设计都明显优于现状,1204 个 control-plane 测试 + 56 个 JS 测试通过,代码组织也遵守了 bounded context。但本 PR 的核心卖点是「显式 worker 安全门 + capability fail-closed」,而这两条链路在实现层面都失效(前缀首 token 匹配、入队丢能力字段),且「opt-in」文档与默认开启的实现矛盾、基线仍指向 owner 明确要求改掉的原型分支。code_volume 判定:necessary(重构规模与面匹配),但需要补丁级修整并补齐 M6 未审债务。建议:先重定向 base 到 main,修复 3 个 P1(前缀门、能力传播、默认/披露),再补 5-8 的 P2,重新在 exact head 上跑一遍上述验证矩阵后再合并。当前结论:REQUEST_CHANGES


English Verdict

Verdict: REQUEST_CHANGES

Exact head: 3200@f6549a9e9f1f21cb1600de2765b461840c26a196

Key findings:

  1. The PR still targets the frontend-control-plane-im-prototype-rfc branch although the owner twice requested a retarget to main; the diff contains production runtime/CLI/quota/todos changes.
  2. Worker execution prefix allow-list regression: multi-token prefixes match on the first token only, so --worker-command-prefix "git status" permits git push --force, and python3 examples/x.py permits python3 -c .... The original matcher compares the full prefix token sequence.
  3. Capability matching is not wired end-to-end: enqueue_tasks drops required_capabilities/capability_binding_ref, so capability-bound tasks are claimable by any worker via the real CLI path; tests mutate entries manually after enqueue and do not cover the real path.
  4. The new architecture is ON by default (master switch) while the PR body, CLI help and docstrings claim opt-in / disabled by default / unchanged: quota should-run gains and records policy_decision by default, OpenCode/Pi loops dispatch by default, and todo add/todo complete write rollout events even with the master switch off.

Validation: At the exact head, pytest tests/control_plane = 1204 passed / 1 failed (M6 maintainability ratchet: new unreviewed event_driven_dispatch -> capabilities.catalog dependency debt; base was already red on chat_actions), tests/test_claude_goal_policy.py = 3 passed, JS runtime tests = 56 passed, CLI registration smoke passed; focused probes reproduced the prefix-gate bypass and the capability-drop claim; default flags confirmed ON with no env.

Minimum repair: retarget to main; port the original full-token prefix matcher and add negative tests; carry capability fields on queue entries and add an end-to-end claim-rejection test; align default-on behavior with documentation (or make it truly opt-in) and update the contradictory pilot-wiring test docstring; then re-run the validation matrix on the new exact head.

@huangruiteng huangruiteng added the direction/operator-surface-im Operator surfaces, frontend control plane, and bounded IM integration. label Aug 16, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

direction/operator-surface-im Operator surfaces, frontend control plane, and bounded IM integration.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants