Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -2500,6 +2500,12 @@ The next complete stage packages are:
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
legacy summary before it. Real CLI coverage includes capacity changes and
missing promoted display without writing it. This does not close all quota
source paths, authorize monitor writeback, or change provider/promotion holds.
2. **Permanent projection closure.** Reuse `provider_projection.py`, the
Todo-section renderer and existing journal/outbox. Preserve non-owned human
narrative; render owned sections from a known canonical revision, with
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1982,6 +1982,11 @@ backend、实时双向同步或按命令拆开的权威;晋升后不支持的
这不是完整 native 字段编辑:在 update 的字段、ownership、validation 和 monitor/resume
effect 一起闭合前,保留严格 text/note 事务边界。准入结果和 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
缺失且不写回的场景。这不代表所有 quota source 路径已闭合,不授予 monitor 写回
权限,也不改变 provider 默认与 promotion hold。
2. **永久投影闭合。** 复用 `provider_projection.py`、Todo-section renderer 和既有
journal/outbox。保留非托管的人工叙述,从已知 canonical revision 渲染托管 section,
提供幂等修复与 freshness/readback 证据。投影 pending 独立于业务 commit/replay。
Expand Down
34 changes: 34 additions & 0 deletions docs/architecture/rfcs/typescript-control-plane-migration-v0.md
Original file line number Diff line number Diff line change
Expand Up @@ -218,6 +218,40 @@ this slice reduces semantic owners, not crossing count. Native transactions stay
in-process. Fold the remaining crossings into that complete transaction rather
than extending these adapters field by field.

The waiting/resume planning slice now uses `todos/resume_planning.ts` for the
complete deferred, resume-blocked, monitor-repair and blocked-successor selection.
Quota composes capacity evaluation with these lanes in one request per source summary, reusing the
existing TS resume evaluator in-process; vision-wait, agent-scope, frontier and
replan consumers use the same projection. The old `deferred_resume.py` rule owner
is removed, not retained behind a second implementation. The Python adapter keeps
the reader compatibility boundary, not claim/exclusion selection or wait routing.
Resume, route-continuation and succession-warning share `compact_projection.py`
for field omission and scope normalization; caller-specific text inference and
succession-only fields remain explicit. Priority rank normalization stays in the
resume adapter. This retires
one read-policy family, not the whole quota reducer or the monitor/lease writers.
Equal public sort keys retain source order; full counts precede display limits;
`monitor_changed` is not the legacy `todo_done:<monitor>` repair path. This
read-only result grants neither execution authority nor a lifecycle receipt.
The adapter exits when its callers consume typed Todo records in-process.

Resume condition diagnosis is now shared by the evaluator and planning owner;
agent-scope consumes the selected repair lane rather than reinterpreting target
type/status. Old compact inputs may recover omitted kind/class from typed
`resume_when` and the same snapshot's monitor records, never from narrative.
This refinement includes explicit behavior corrections: self-dependencies and
`todo_done` dependencies on unfinished monitors are `resume_condition_invalid`,
not ordinary pending waits. Completed historical monitor dependencies remain
satisfied; missing completion targets remain pending because absence in a
partial snapshot is not proof of an invalid dependency. Valid generation fences,
claim/exclusion, capacity and PR waits retain their existing semantics. Invalid
conditions cannot become exact blocked-successor waits. Monitor completion
repair stays visible and selectable only in the permitted executor scope.
No automatic conversion to `monitor_changed`, baseline reset, persisted-state
rewrite or new writer admission is implied. General add/update admission and a
generic repair action for every invalid condition remain separate scopes; this
is not a claim of zero behavior change or full Todo writer closure.

1. **Close the actual command and consumer inventory.** Build on the merged
create/claim/update and #4053 terminal/successor/archive transactions; do not
recreate them. Inventory remaining field-edit, monitor, lease, and event
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -169,6 +169,30 @@ legacy update writer**。字段 patch、省略/清空、monitor/resume effect
语义 owner,不宣称减少 crossings,native transaction 仍进程内调用。下一步将这些
crossing 一起折叠进完整事务,不能沿着 adapter 逐字段继续加桥。

等待/恢复规划现由 `todos/resume_planning.ts` 一次完成 deferred、resume-blocked、
monitor-repair 和 blocked-successor 选择。Quota 为每个 source summary 将容量条件与这些 lane 合为一个请求,
在 TS 进程内复用既有 resume evaluator;vision-wait、agent-scope、frontier、replan
共用此投影。删除旧 `deferred_resume.py` 规则 owner,不保留第二份实现。Python 适配层
只保留 reader 兼容边界,不再决定 claim/exclusion 选择或等待路由。Resume、
route-continuation、succession-warning 共用 `compact_projection.py` 的字段省略与 scope
归一化;各 caller 的文本推断差异及 succession 独有字段显式保留,priority rank
归一化仍在 resume adapter。这闭合一个读取策略族,不是整个 quota reducer,也未
迁移 monitor/lease writer。相同公开排序键保持 source 顺序;完整计数先于展示截断;
`monitor_changed` 不进入旧 `todo_done:<monitor>` 修复路径。只读结果不授予执行权限,
也不是生命周期 receipt;caller 在进程内消费 typed Todo record 后可删除此适配层。

条件 evaluator 与规划 owner 现在共用恢复条件诊断;agent-scope 消费已选好的修复 lane,
不再重新解释 target 类型/状态。旧 compact 输入缺少 kind/class 时,只从 typed
`resume_when` 和同一快照的 Monitor 记录补足,不从叙述猜测。本次 refinement 包含
明确行为修正:自依赖,以及对未完成 Monitor 的 `todo_done` 依赖,被诊断为
`resume_condition_invalid`,不再当作普通 pending wait。历史已完成 Monitor 依赖仍可
满足;完成依赖的目标缺失仍为 pending,因局部快照中的缺失不能证明依赖非法。合法的
generation fence、claim/exclusion、capacity 和 PR 等待语义保持。非法条件不进入
精确 blocked-successor 等待;Monitor 完成依赖的修复仍可见,且仅在合法执行者范围内
可选。此诊断不自动改写为 `monitor_changed`、重置 baseline、重写持久状态或增加写入
准入。普通 add/update 准入及覆盖全部非法条件的通用修复动作仍是独立范围;不能宣称
全量零行为变化或全部 Todo writer 已闭合。

1. **闭合实际命令与 consumer 清单。** 基于已合入的 create/claim/update 和 #4053
terminal/successor/archive transaction 推进,不重复建设。按真实合同盘点剩余
字段编辑、monitor、lease、event caller,把规则迁入既有 TS owner,并在同一切片
Expand Down
32 changes: 8 additions & 24 deletions examples/control_plane/todo-deferred-resume-lanes-smoke.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,14 +11,7 @@
if str(REPO_ROOT) not in sys.path:
sys.path.insert(0, str(REPO_ROOT))

from loopx.control_plane.todos.deferred_resume import ( # noqa: E402
TODO_DEFERRED_RESUME_SELECTION_POLICY,
TODO_MONITOR_BLOCKED_RESUME_SELECTION_POLICY,
build_todo_deferred_visibility_lanes,
build_todo_resume_blocked_visibility_lanes,
todo_summary_monitor_blocked_resume_items,
todo_summary_resume_blocked_items,
)
from loopx.control_plane.todos.resume_planning import project_todo_resume_planning # noqa: E402


CURRENT_AGENT = "codex-product-capability"
Expand Down Expand Up @@ -116,11 +109,11 @@ def assert_deferred_resume_lanes_filter_current_unclaimed_and_other_agents() ->
],
}

lanes = build_todo_deferred_visibility_lanes(
lanes = project_todo_resume_planning(
summary,
agent_identity={"agent_id": CURRENT_AGENT},
agent_id=CURRENT_AGENT,
item_limit=10,
)
)["deferred_lanes"]
assert lanes["deferred_count"] == 1, lanes
assert lanes["deferred_visibility_limit"] == 10, lanes
assert lanes["deferred_items"][0]["todo_id"] == "todo_deferred_backlog", lanes
Expand All @@ -140,9 +133,6 @@ def assert_deferred_resume_lanes_filter_current_unclaimed_and_other_agents() ->
current = lanes["current_agent_deferred_resume_candidates"][0]
assert current["required_write_scopes"] == ["loopx/**"], current
assert current["decision_scope"]["scope_key"] == "resume", current
assert lanes["deferred_resume_selection_policy"] == (
TODO_DEFERRED_RESUME_SELECTION_POLICY
), lanes


def assert_monitor_blocked_resume_lanes_filter_by_claim_and_monitor_target() -> None:
Expand Down Expand Up @@ -181,15 +171,17 @@ def assert_monitor_blocked_resume_lanes_filter_by_claim_and_monitor_target() ->
"backlog_items": [current],
}

resume_blocked = todo_summary_resume_blocked_items(summary)
projection = project_todo_resume_planning(summary, agent_id=CURRENT_AGENT, item_limit=10)
lanes = projection["resume_blocked_lanes"]
resume_blocked = lanes["resume_blocked_items"]
assert [item["todo_id"] for item in resume_blocked] == [
"todo_current_blocked",
"todo_unclaimed_blocked",
"todo_other_blocked",
"todo_non_monitor_blocked",
"todo_excluded_blocked",
], resume_blocked
monitor_blocked = todo_summary_monitor_blocked_resume_items(summary)
monitor_blocked = projection["monitor_blocked_items"]
assert [item["todo_id"] for item in monitor_blocked] == [
"todo_current_blocked",
"todo_unclaimed_blocked",
Expand All @@ -201,20 +193,12 @@ def assert_monitor_blocked_resume_lanes_filter_by_claim_and_monitor_target() ->
for item in monitor_blocked
), monitor_blocked

lanes = build_todo_resume_blocked_visibility_lanes(
summary,
agent_identity={"agent_id": CURRENT_AGENT},
item_limit=10,
)
assert lanes["resume_blocked_count"] == 5, lanes
assert lanes["monitor_blocked_resume_count"] == 4, lanes
assert lanes["current_agent_monitor_blocked_resume_count"] == 1, lanes
assert lanes["unclaimed_monitor_blocked_resume_count"] == 1, lanes
assert lanes["other_agent_monitor_blocked_resume_count"] == 1, lanes
assert lanes["executor_excluded_self_monitor_blocked_resume_count"] == 1, lanes
assert lanes["monitor_blocked_resume_selection_policy"] == (
TODO_MONITOR_BLOCKED_RESUME_SELECTION_POLICY
), lanes


def main() -> int:
Expand Down
31 changes: 8 additions & 23 deletions loopx/control_plane/agents/agent_scope.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@
work_lane_contract_requires_current_agent_attempt,
)
from ..todos.contract import (
TODO_STATUS_OPEN,
TODO_TASK_CLASS_ADVANCEMENT,
TODO_TASK_CLASS_MONITOR,
normalize_todo_blocks_agent,
Expand All @@ -26,11 +25,9 @@
normalize_todo_excluded_agents,
normalize_todo_global_gate,
normalize_todo_id,
normalize_todo_status,
normalize_todo_task_class,
)
from ..todos.handoff_gate import HandoffGateState
from ..todos.deferred_resume import todo_summary_blocked_successor_items
from ..todos.resume_planning import project_todo_resume_planning
from ..todos.projection import (
todo_item_claimed_by_agent_or_unclaimed,
todo_item_excludes_agent,
Expand Down Expand Up @@ -936,28 +933,16 @@ def _agent_scope_monitor_blocked_resume_candidates(
continue
if item.get("resume_ready") is not False:
continue
raw_condition = item.get("resume_condition")
condition = raw_condition if isinstance(raw_condition, dict) else {}
if normalize_todo_status(condition.get("target_status")) != TODO_STATUS_OPEN:
continue
target_todo_id = normalize_todo_id(
item.get("blocking_monitor_todo_id")
or condition.get("target_todo_id")
or condition.get("target")
)
target_task_class = normalize_todo_task_class(
condition.get("target_task_class"),
text="",
)
if target_task_class != TODO_TASK_CLASS_MONITOR and not target_todo_id:
continue
# The typed resume-planning owner has already diagnosed and selected
# this repair lane. This consumer keeps executor scope and presentation,
# not a second interpretation of condition target/class/status.
identity = str(item.get("todo_id") or item.get("index") or item.get("text") or "")
if identity in seen:
continue
seen.add(identity)
compact = compact_todo_summary_item(item, text=str(item.get("text") or "").strip())
if target_todo_id:
compact["blocking_monitor_todo_id"] = target_todo_id
if item.get("blocking_monitor_todo_id"):
compact["blocking_monitor_todo_id"] = item["blocking_monitor_todo_id"]
unique.append(compact)
return sorted(unique, key=_todo_projection_sort_key)

Expand Down Expand Up @@ -1261,10 +1246,10 @@ def _deferred_resume_frontier(
def _blocked_successor_wait_frontier(
context: _AgentScopeNoCandidateContext,
) -> dict[str, Any] | None:
candidates = todo_summary_blocked_successor_items(
candidates = project_todo_resume_planning(
context.summary,
agent_id=context.agent_id,
)
)["blocked_successor_items"]
if not candidates:
return None
first = candidates[0]
Expand Down
2 changes: 2 additions & 0 deletions loopx/control_plane/effect_runtime_handlers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@ import {
planTodoExternalWaitTransition,
} from "./todos/resume_condition.ts";
import { evaluateSchedulerStateTransition } from "./scheduler/state_transition_rules.ts";
import { projectTodoResumePlanning } from "./todos/resume_planning.ts";
import {
evaluateSchedulerStateOperation,
loadSchedulerState,
Expand Down Expand Up @@ -394,6 +395,7 @@ export function createEffectRuntimeHandlers(
["todo.next_action.transition", transitionTodoNextAction],
["todo.resume_condition.normalize", normalizeTodoResumeWhen],
["todo.resume_condition.evaluate", evaluateTodoResumeConditions],
["todo.resume_planning.project", projectTodoResumePlanning],
["todo.external_wait.plan", planTodoExternalWaitTransition],
["scheduler.state_transition.evaluate", evaluateSchedulerStateTransition],
["scheduler.state.evaluate", evaluateSchedulerStateOperation],
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
from ...todos.contract import (
normalize_todo_id,
)
from ...todos.deferred_resume import todo_summary_blocked_successor_items
from ...todos.resume_planning import project_todo_resume_planning
from ...todos.projection import (
agent_scoped_selectable_advancement_todo_ids,
)
Expand Down Expand Up @@ -125,10 +125,10 @@ def _blocked_successor_todo_ids(
todo_id
for todo_id in (
normalize_todo_id(item.get("todo_id"))
for item in todo_summary_blocked_successor_items(
for item in project_todo_resume_planning(
agent_todo_summary,
agent_id=agent_id,
)
)["blocked_successor_items"]
if isinstance(item, dict)
)
if todo_id
Expand All @@ -148,10 +148,10 @@ def _blocked_primary_waiting(
if isinstance(blocker_items, list) and blocker_items:
return True
return bool(
todo_summary_blocked_successor_items(
project_todo_resume_planning(
agent_todo_summary,
agent_id=agent_id,
)
)["blocked_successor_items"]
)


Expand Down
6 changes: 3 additions & 3 deletions loopx/control_plane/goals/goal_vision_wait.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
normalize_todo_id,
normalize_todo_status,
)
from ..todos.deferred_resume import todo_summary_blocked_successor_items
from ..todos.resume_planning import project_todo_resume_planning

GOAL_VISION_WAIT_STATE_SCHEMA_VERSION = "goal_vision_wait_state_v0"
VISION_ACCEPTANCE_GAP_KIND = "vision_acceptance_gap"
Expand Down Expand Up @@ -170,9 +170,9 @@ def _covered_wait_items(
and normalize_todo_claimed_by(item.get("claimed_by")) == safe_agent_id
and not todo_item_excludes_agent(item, agent_id=safe_agent_id)
]
candidates = todo_summary_blocked_successor_items(
candidates = project_todo_resume_planning(
agent_todo_summary or {}, agent_id=agent_id
)
)["blocked_successor_items"]
coverage = effect_runtime_result(
"goal.vision_wait.coverage",
{
Expand Down
Loading