fix(todos): compose public update planning and preserve retained waits - #4165
Conversation
Signed-off-by: huangruiteng <huangrt01@163.com>
huangruiteng
left a comment
There was a problem hiding this comment.
Approval conclusion (author-owned PR; GitHub blocks formal self-approval)
本次审阅绑定 base f75b8ee56e310f1b04be440078d8cc9c248dfd2e 与 exact head fe5ef4f831170f9b6f734c3fd806c9ba8a806109。结论:未发现阻塞性代码问题,代码与语义可批准;远端必需检查仍需重新跑绿后再合并。
动机
这个 PR 修的是 public Todo writer 中一个可复现的状态契约缺口。基线只在当前命令显式传入 resume_when 时调用 external-wait planner;因此一个已经带有 monitor_changed、generation fence 和独立 successor 的 Todo,如果后续只传 --status blocked、--task-class continuous_monitor 或空 successor 列表,旧 writer 会保留原 wait 却写入与它矛盾的拓扑。最小的 Python 条件判断虽能挡住三个例子,但会继续让 authoring scope、wait topology、Monitor 和 field update 分属多个决策点。把现有 typed owners 在一次纯计划中组合,才消除了同一更新快照上的规则分叉。
改动思路
权限、锁、completion proof、shadow capture、持久化和 provider fence 仍由 update_goal_todo 的既有 Python transaction 负责;它在锁内把当前 Todo、raw intent、有效 actor scope,以及需要时的完整 active/archive compact snapshot 一次性交给 todo.public_update.plan。TypeScript 依次复用 planTodoAuthoringScope、planTodoExternalWaitTransition、planTodoFieldUpdate 和 planMonitorMetadata:先算有效 scope/resume,再对实际变化的 status/task class/successor topology 验证 retained wait,最后生成字段计划。普通 text/note/evidence 修改不重新武装已满足的 wait,也不捕获新的 Monitor generation;显式 clear 仍是离开该契约的修复动作。
我也检查了保留路径:completion/supersede/import 等内部调用未带 public_context,继续走 field-only planner;这不是第二套 public policy,而是不同生命周期边界的现有调用者。新 update_source.py 只搬运旧 adapter 已使用的 resume-relevant facts,并继续使用同一个 compactor,不引入新的持久化真相或 UI 截断。
具体改动
完整 diff 为 15 个文件、+390/-184;生产代码 +173/-179(净减少 6 行),其余主要是回归测试和英中双语契约更新。
关键代码讲解
public_update.ts:16的externalWait把“新 condition”和“retained condition + topology change”统一送入既有 typed transition;更新后的 waiting row 会先替换 role/status/task class,再检查 dependency、successor 和 generation,不再根据请求中是否出现resume_when决定是否验证。public_update.ts:39的planPublicTodoUpdate是新的组合 owner,但注释与返回契约明确它不是 admission、provider transaction 或 receipt。retained Monitor wait 重新验证时使用 transition 返回的原始 baseline,避免静默捕获更新后的 generation。line_update.py:217的 public 分支从同一锁定行集构造完整 snapshot,调用一次 effect runtime,并把external_wait_*code 适配回原有TodoExternalWaitAuthoringError/authoring contract;拒绝发生在 Markdown 变更之前。update_source.py:8收集 archive、user、agent 三段完整事实且不经过 display cap;Todo prose 不进入 plan。旧external_wait_writeback.py的 route selection 和 baseline 提取被删除。todos.py:1293删除 Python 对 scope/wait/field 三段结果的手工拼接,仍保留 authority、handoff gate、completion proof、write capture 与 settlement,因此没有扩大 TypeScript 的写入权力。
对主干的风险
最强反例不是作者已有的三个非法输入,而是 --note 同类的“省略字段是否偷偷改变旧状态”:我在 base/head 两个 detached worktree 上用同一真实 update_goal_todo fixture 跑了 10 个 case。note、evidence、text、显式相同 status/class/successor 和 clear 共七条合法路径保持相同持久化语义;尤其 copy-only 更新不返回 wait transition,generation 保持 2。三个非法 partial edit 在 base 都成功写入矛盾状态,在 head 分别以 external_wait_todo_status_must_remain_open、external_wait_todo_task_class_invalid、external_wait_successor_required 失败,且 state bytes 不变。显式相同 successor 会返回 already_waiting transition 但 changed=false,这是拓扑重验的已披露结果。
验证还包括相关 Python 七个文件 148 passed;本地完整 control-plane TypeScript 为 940 passed、1 个 PostgreSQL 环境 skip、0 failed;typecheck、Ruff、diff check 与 DCO 均通过。真实 CLI 的拒绝/no-write 和 clear dry-run 也已覆盖。PostgreSQL skip 不阻塞本 diff:这里新增的是纯 planner,实际受影响 writer 使用锁定文件 fixture,provider/promotion 没有变化。
远端当前仍为红色:一个 untouched change-window Git pull 测试偶发失败,本地单独重跑通过;Node 26 的六个 EISDIR error-text parity 失败在 base 与 head 可完全同样复现,亦不属于本 diff。pytest 和 merge-gate 只是继承前者失败。它们不是 #4165 的代码 blocker,但必需门禁应重新跑或修复后再合并,不能用本审阅代替绿色 gate。
我的整体评价
这是合理且可回滚的同一 change-reason 重构:它修复真实 public writer 缺口,同时减少 Python 决策所有权和生产行数;没有借机扩到 native update、lease、provider promotion 或原子 successor commit。typed state、domain-neutral diagnostics、machine-enforced obligation 与非权限性 plan 的边界都清楚,双语文档也披露了旧/新默认。future-facing pass 已落在最有价值的边界上,未发现需要继续扩张的 companion refactor。
English verdict: APPROVE — exact head fe5ef4f831170f9b6f734c3fd806c9ba8a806109 correctly composes the existing typed Todo owners, rejects retained Monitor-wait topology corruption without writing, and preserves copy-only --note/text/evidence semantics and the original generation fence. The current unrelated hosted failures reproduce at the base or pass on focused rerun; required checks must still be green before merge.
Summary
Compose public Todo authoring scope, external-wait topology, Monitor observation and field planning in one TypeScript call over the locked snapshot. Retire Python leaf-RPC orchestration while retaining its permission/lock/persistence responsibilities.
Fix a public writer gap: omitting
resume_whenfrom a partial edit previously bypassed external-wait validation. Clearing a retained Monitor wait's successors, changing its status to blocked, or changing its task class to Monitor could persist an invalid waiting state. These transitions now fail without writing. Explicitly clearing the condition in the same edit remains the repair path. Copy-only edits neither re-arm a satisfied wait nor reset its generation.Issue Or Task
Maintainer-requested next cohesive stage of the TS migration and shared Goal Authority RFCs, based on merged #4143/#4149. This is a T1 public planning prerequisite, not full native update, provider promotion, or T2 atomic Monitor/successor closure. The independent lease-edit PR #4152 remains separate.
Ownership and scope
todos/public_update.tscomposes existing typed owners, rather than duplicating their state rules.update_source.pytransports complete compact active/archive facts, without UI caps or Todo prose.external_wait_writeback.py's Python wait-route selection and baseline extraction. Keep the internal field codec for actual terminal/import callers; it must not inherit public-update restrictions.Validation
fe5ef4f831170f9b6f734c3fd806c9ba8a806109, based onf75b8ee56e310f1b04be440078d8cc9c248dfd2e.Coverage/gaps: production writer, normal and malformed edits, claim/lifecycle fencing, waiting topology, Monitor generation and retry are covered. Hosted CI has not started before publication. This does not qualify native metadata mutation, atomic successor writes, or whole-Goal promotion.
Type of Change
Includes a bounded refactor with deliberate behavior changes disclosed above, not a zero-behavior-change claim.
LoopX Area
Technical Direction
Shared-authority RFC fixture impact
loopx_coordination_production_scale_fixture_v0.Boundary Checklist
Future-facing pass: reuse existing typed scope/resume/field owners, delete Python composition decisions, and keep only adapters with active callers. No new capability/provider, new storage truth or automatic promotion. Review requested; no self-merge or local installation requested for this PR.