Make event Todo replay typed, bounded and identity-preserving - #5014
Conversation
|
Validation evidence for exact head
GitHub CI is still running. Maintainer review and merge are required; this branch has not been self-merged. |
huangruiteng
left a comment
There was a problem hiding this comment.
Request changes conclusion (author-owned PR; GitHub blocks formal self-review)
动机
审查对象:#5014 exact head 552654d0e5948215dd3adfd8558b2cfcc2b90ec1,对照 #4574 的 R1/R5 与本 PR 新增的事件回放 ledger。旧 build_state_projection 用 Python 独立维护 Todo 生命周期;不同 event ID 的第二次创建可覆盖已完成 Todo,优先级改动可留下旧显示文本,角色改动可留下旧段归属,planner order 0 可被当作缺失。这些都会使长程承诺与真实状态读回不一致。把同一读路径的状态决策归于 typed owner 是有用且可回滚的阶段,但它不等于事件写入绑定、整 Goal 迁移或 provider 默认切换。
改动思路
现有调用 event_sourced_state.build_state_projection 先由 Python 保持历史 event 解码、去重、内容和 checksum 顺序,抽出无敏感正文的 facts;每 256 个 event 通过 goal.state_event.plan_replay 送给纯 TS fold。TS 判定 identity、生命周期、priority/role、不可变 binding、寻址互斥和 continuation 修复,返回字段来源序号与排序键;Python 再用序号恢复原内容并构建现有投影格式。这样没有新增持久权威或执行权限,也不更改未选择 event source 的 File/SQLite 入口。其正向路径在定向用例里可恢复大证据文本与跨批次 Todo;负向路径应拒绝重复创建、孤儿更新和非法排序事实,不能只在 TS 直调用时拒绝。
具体改动
关键代码讲解
loopx/control_plane/goals/state_event_replay.ts::planStateEventReplay是新纯 fold:解码 event/continuation、按序处理所有 event kind,返回 Todo 字段序号、状态、来源段和排序键;effect_runtime_handlers.ts注册唯一新 handler。loopx/event_sourced_state.py::build_state_projection从 Python 状态分支改为事实适配与 256-event 分批,_decode_added_todo_content、_decode_todo_event_content只保留历史内容规范化。checksum、timeline 内容与现有公开投影仍由 Python 输出。local_authority_shadow_projection.py的投影类型收窄不改运行语义;当前 main 已有等价但写法不同的检查,是这次唯一合并冲突。- Python/TS 两组新测试分别覆盖四个基线反例、重复 event、混合事件、长内容、17 批次、非法原始 TS facts 与 continuation;六处 RFC/ledger 文档把本 reader 切片、#5003 写入器、后续 executor fence 和整体迁移分开计数,明确没有默认升级或 PostgreSQL 资格化。
独立在 exact head 执行:事件源/回放 17 Python passed;Todo 完成、变更、File/SQLite CLI 路径 122 Python passed;TS fold 10 passed,TS typecheck、改动 Python 路径 Ruff 和 diff whitespace 均通过。当前 Goal 配置不要求等待远端 CI;未将作者的 900-event 私有只读演练或预 rebase 144-case 结果冒充本轮独立验证。
对主干的风险
[P1,Python 入口绕过 typed 数值拒绝] event_sourced_state.py:859-866 把 planner_order 先做 int(order)。因此真实 make_state_event → build_state_projection 对 1.5 并不报错:两个新建 Todo 依次带 1.5 和 1 时,实际输出顺序是 [(todo_fractional, 1.5), (todo_integer, 1)]。适配器把前者当 1 送给 TS 排序,却仍在结果保留 1.5,与 ledger 的“非法数值事实拒绝”、TS 直调用负向测试及可解释排序相矛盾。最小修复是在 Python 事实边界明确验证整数/允许的历史形式,不要截断小数;增加经过 build_state_projection 的小数拒绝回归,并说明若旧日志存在此类值如何处理。这里不是声称旧主干已正确拒绝,而是指出新 typed 保障没有贯通真实入口。
[P1,当前主干不可合并] git merge-tree --write-tree origin/main 552654d0e... 在 local_authority_shadow_projection.py 报内容冲突。main 已把 projection = result.get("projection") 分两步检查并返回,提供本 PR 相同的类型收窄;这段非事件回放的重复 hunk 可在 rebase 时去掉,随后重跑回放、File/SQLite 真实入口、类型检查与无冲突检查。现在的通过测试仅证明隔离 head,尚无可交付的集成 head。
其余边界与成本。 本 PR 明确未绑定 event writer/outbox、未 fence 外部效果、未迁移活跃 Goal,也未验证 PostgreSQL 部署;这些是 RFC 的后继,不应在本 PR 被算作已交付。作者披露的 900-event 暖态中位延迟约从 9 ms 到 66 ms,属于显著但已说明的 RPC 成本;本轮没有独立复现吞吐/冷启动或扩大预算。TS fold 的 175 行与 Python 分支删除服务同一读路径,未发现需要另起抽象层的理由;但 rebase 后必须确认共用 handler 没有改变 feature-off 的 File/SQLite 输出。
语义与 CI 对齐
机器承诺是“非法回放事实在真实入口 fail closed、Todo 顺序与显示同源”;TS 单测通过并不足以证明 Python 适配后的同一语义。需要把小数反例落到 Python 集成测试,并在新 head 上重跑现有 139 个定向 Python 用例、10 个 TS 用例、类型检查和当前 main 合并检查。CI 按 Goal 配置未轮询,不记为通过。
我的整体评价
结论 REQUEST_CHANGES。架构归属选择合理,代码与文档把 reader 切片和整体权威迁移分清了;对长期恢复与用户读回有明确收益,且已有真实 File/SQLite 验证。当前两个阻塞分别是入口数值规则失配和与 main 的重复类型 hunk 冲突;它们都可在同一 reviewable 切片内修复,而不必扩大为 TS 全面迁移。修复后以新 exact head 重新核对正反例及主干集成,才能判断这项阶段交付是否完成;性能与 D1–D3 资格仍是明示的剩余风险。
English verdict: REQUEST_CHANGES - exact head 552654d passes 139 focused Python cases, 10 TS cases, typecheck and lint, but the real Python replay entrypoint silently truncates planner_order=1.5 before typed validation, and the head conflicts with current main in an already-fixed projection typing hunk. Reject fractional order at the adapter, add an end-to-end negative test, rebase away the duplicate hunk and revalidate.
552654d to
9df62c7
Compare
huangruiteng
left a comment
There was a problem hiding this comment.
Approval conclusion (author-owned PR; GitHub blocks formal self-approval). Reviewed exact head 9df62c7ae00015d896ef91177679bcd206c35808, rebased onto origin/main bd24092af976c914fff74dcf818d9f4a72f6aed2, which now contains the atomic event-append prerequisite #5003.
动机
已复核基线行为:legacy 事件回放把跨事件决策留在 Python,于是四个反例都成立——同一 Todo 的第二个 todo_added(不同 event 身份)会把已完成的承诺覆盖回新建;只有 priority 的更新会留下旧文本;role 更新后 source section 仍是旧的;planner_order 为零被当成缺失值排到末尾。集成 head 上这四类分别被拒绝或产出连贯状态,而它们在基线 bd24092af(含 #5003)上失败。
对长期推进而言,事件投影是 Goal 状态的读侧源头:修复前每多一种事件类型就多一处 Python 分支,读侧语义漂移只能靠测试逐个补;修复后一次 fold 覆盖生命周期、身份、binding 不可变、寻址、删除策略修复与显示排序,后续事件类型的处置是显式的 assertNever,读侧语义不再散落。对用户旅程而言,受影响入口是任何读取事件投影的路径(loopx 状态读、build_state_projection、以及依赖它的 CLI 与投影),可观察结果是同一批事件在 base 与 head 上给出完全相同的投影摘要(我在真实本机源上做了逐字节摘要比对),以及历史字节与 source_checksum 不变。
改动思路
新 owner 是 TypeScript 的 goal.state_event.plan_replay:Python 负责 legacy 解码与内容,把每个事件压成有界事实(身份、时间、event_type、todo_id、role/priority/planner_order、内容字段名、binding/continuation/删除策略/排除/归属布尔、以及"文本是否变化"),并按最多 256 个事件一批调用 TS,只随批携带被触及 Todo 的续算行;TS 返回最终状态、field_sources(字段 → 内容序号)、timeline_indices 与排序键,Python 再用这些序号从自己的内容表重建 Todo 字段。这样证据正文、校验命令和任意 payload 都不过 RPC,只有字段名与序号跨界;历史校验和顺序仍由 legacy codec 决定。
语义与集成
todo_updated 的语义现在是显式的:binding 一旦设置不可变更;removed_continuation_policy 保留到显式的 independent handoff 修复为止;设置 goal_bound 会清掉 bound_agent,反之亦然;只有真正发生文本变化或 priority 变化才重渲染带 priority 前缀的 text。initial_todos 的 field_sources 不允许指向本批(否则拒绝),确保续算只描述先前的批次。与 #5003 的集成是自然的:追加侧决定事件身份与序号,回放侧决定这些事件的语义,两侧都不再各自拥有一份 Python 状态机。
具体改动
loopx/control_plane/goals/state_event_replay.ts(新增 175 行):planStateEventReplay解码事实、拒绝重复身份/混合 Goal/未知 todo 引用/非法 role 与 priority/不安全的整数,按append_sequence→ Unicode 时间 → Unicode id 稳定排序,fold 全部历史事件类型,输出state_event_replay_plan_v0。loopx/event_sourced_state.py:build_state_projection改为分批调用 TS 并按序号重建内容;_todo_from_added_event收敛为_decode_added_todo_content、_update_todo_from_event收敛为_decode_todo_event_content(只做值归一化,不读先前状态),删除了重复的状态迁移分支。loopx/control_plane/effect_runtime_handlers.ts:注册goal.state_event.plan_replay。- 测试:
tests/control_plane/test_event_replay_integrity.py(8 例,含 2 MiB 级内容仍以小于 4 KiB 事实跨界、4,100 事件跨 17 次调用、混合历史与依赖边)与tests/control_plane_ts/state_event_replay.test.ts(10 例,含零 order、用户泳道、Unicode 标量序、非法事实、续算不可指向本批)。 - 文档:新增双语 ledger
2026-09-25-event-replay.md,并在两本 RFC 的 checkpoint 中登记本条边界与仍待推进的 5–8 个包,明确这是计划而非承诺。
对主干的风险
最明确的代价是性能:真实 6.1 MB 本机源(958 个 backfill 事件)上,投影中位数从 base 的 12.77 ms 升到 head 的 86.93 ms(约 6.8 倍,与作者披露的 9.28 → 66.12 ms 同量级),换来的是单一决策 owner 与真实数据上的投影等价——我在同一份事件上比较了 base 与 head 的投影摘要,sha256=47bbd00e441aae030d23c2d243e132de5ae3ab4101b2ee50728c92338a8f0f0e 完全一致,source_checksum 与源文件摘要均未变化。第二类风险是 RPC 边界:单批 256 事件、单条事实只带字段名与序号,因此超大 evidence 正文不出主机,但单条事实与续算行仍受既有 RPC 上限约束,这一点已在 ledger 中写清而不是承诺无限。第三类风险是门禁现状:canary premerge --goal-id loopx-meta 选中 19 项检查,18 项通过,唯一的 semantic-vocabulary-drift-smoke 失败是模块对预算 44 > 43,我在干净的 origin/main 上复现同一失败,属于继承的基线红灯;本目标不等待远端 CI,PostgreSQL 与打包前端不在本切片内。
我的整体评价
没有发现阻断项。这个增量把事件回放从「散落的 Python 分支」收敛成一个类型化、有界、可续算的纯 fold,四个基线反例与两个规模反例都有对应测试,并且在真实本机 Goal 源上与 base 逐字节等价。验证覆盖正例、负例与真实路径:10 个 TS replay 用例、110 个事件/授权 focused Python 用例、3086 个 control-plane TS 用例(0 失败)、严格 mypy、ruff、类型检查与真实源投影摘要比对全部通过;变更质量回执 cqr_3716d61650b0e47d669c 对精确指纹有效。需要维护者知道的两点:约 7 倍的投影开销是被接受并披露的取舍,以及继承的词汇预算红灯需要单独决策。建议按维护者流程合并。
English verdict: APPROVE - exact head 9df62c7ae00015d896ef91177679bcd206c35808; one typed, bounded replay fold replaces scattered Python state branches, the four baseline counterexamples plus large-history batching are covered, and the real local source projects to a byte-identical digest at base and head, at a disclosed ~7x projection cost. The only failing check is the inherited module-pair vocabulary budget, reproduced on a clean origin/main.
Signed-off-by: huangruiteng <14976749+huangruiteng@users.noreply.github.com>
Signed-off-by: huangruiteng <14976749+huangruiteng@users.noreply.github.com>
Signed-off-by: huangruiteng <14976749+huangruiteng@users.noreply.github.com>
9df62c7 to
ae84cfc
Compare
huangruiteng
left a comment
There was a problem hiding this comment.
Approval conclusion (author-owned PR; GitHub blocks formal self-approval). Reviewed exact head ae84cfc8b33d72b457ab69a6eb1ba18b2ded1e6c, rebased twice: first onto origin/main bd24092af976c914fff74dcf818d9f4a72f6aed2 (which contains the atomic event-append prerequisite #5003) and then, after main advanced with the chat control PR, onto e5e1a6a9e2c85ef62b9f35b91dbeede09c6b7486. Both rebases were conflict-free; every check below was re-run on this final head.
动机
已复核基线行为:legacy 事件回放把跨事件决策留在 Python,于是四个反例都成立——同一 Todo 的第二个 todo_added(不同 event 身份)会把已完成的承诺覆盖回新建;只有 priority 的更新会留下旧文本;role 更新后 source section 仍是旧的;planner_order 为零被当成缺失值排到末尾。集成 head 上这四类分别被拒绝或产出连贯状态,而它们在基线 bd24092af(含 #5003)上失败。
对长期推进而言,事件投影是 Goal 状态的读侧源头:修复前每多一种事件类型就多一处 Python 分支,读侧语义漂移只能靠测试逐个补;修复后一次 fold 覆盖生命周期、身份、binding 不可变、寻址、删除策略修复与显示排序,后续事件类型的处置是显式的 assertNever,读侧语义不再散落。对用户旅程而言,受影响入口是任何读取事件投影的路径(loopx 状态读、build_state_projection、以及依赖它的 CLI 与投影),可观察结果是同一批事件在 base 与 head 上给出完全相同的投影摘要(我在真实本机源上做了逐字节摘要比对),以及历史字节与 source_checksum 不变。
改动思路
新 owner 是 TypeScript 的 goal.state_event.plan_replay:Python 负责 legacy 解码与内容,把每个事件压成有界事实(身份、时间、event_type、todo_id、role/priority/planner_order、内容字段名、binding/continuation/删除策略/排除/归属布尔、以及"文本是否变化"),并按最多 256 个事件一批调用 TS,只随批携带被触及 Todo 的续算行;TS 返回最终状态、field_sources(字段 → 内容序号)、timeline_indices 与排序键,Python 再用这些序号从自己的内容表重建 Todo 字段。这样证据正文、校验命令和任意 payload 都不过 RPC,只有字段名与序号跨界;历史校验和顺序仍由 legacy codec 决定。
语义与集成
todo_updated 的语义现在是显式的:binding 一旦设置不可变更;removed_continuation_policy 保留到显式的 independent handoff 修复为止;设置 goal_bound 会清掉 bound_agent,反之亦然;只有真正发生文本变化或 priority 变化才重渲染带 priority 前缀的 text。initial_todos 的 field_sources 不允许指向本批(否则拒绝),确保续算只描述先前的批次。与 #5003 的集成是自然的:追加侧决定事件身份与序号,回放侧决定这些事件的语义,两侧都不再各自拥有一份 Python 状态机。
具体改动
loopx/control_plane/goals/state_event_replay.ts(新增 175 行):planStateEventReplay解码事实、拒绝重复身份/混合 Goal/未知 todo 引用/非法 role 与 priority/不安全的整数,按append_sequence→ Unicode 时间 → Unicode id 稳定排序,fold 全部历史事件类型,输出state_event_replay_plan_v0。loopx/event_sourced_state.py:build_state_projection改为分批调用 TS 并按序号重建内容;_todo_from_added_event收敛为_decode_added_todo_content、_update_todo_from_event收敛为_decode_todo_event_content(只做值归一化,不读先前状态),删除了重复的状态迁移分支。loopx/control_plane/effect_runtime_handlers.ts:注册goal.state_event.plan_replay。- 测试:
tests/control_plane/test_event_replay_integrity.py(8 例,含 2 MiB 级内容仍以小于 4 KiB 事实跨界、4,100 事件跨 17 次调用、混合历史与依赖边)与tests/control_plane_ts/state_event_replay.test.ts(10 例,含零 order、用户泳道、Unicode 标量序、非法事实、续算不可指向本批)。 - 文档:新增双语 ledger
2026-09-25-event-replay.md,并在两本 RFC 的 checkpoint 中登记本条边界与仍待推进的 5–8 个包,明确这是计划而非承诺。
对主干的风险
最明确的代价是性能:真实 6.1 MB 本机源(958 个 backfill 事件)上,投影中位数从 base 的 12.77 ms 升到 head 的 86.93 ms(约 6.8 倍,与作者披露的 9.28 → 66.12 ms 同量级),换来的是单一决策 owner 与真实数据上的投影等价——我在同一份事件上比较了 base 与 head 的投影摘要,sha256=47bbd00e441aae030d23c2d243e132de5ae3ab4101b2ee50728c92338a8f0f0e 完全一致,source_checksum 与源文件摘要均未变化。第二类风险是 RPC 边界:单批 256 事件、单条事实只带字段名与序号,因此超大 evidence 正文不出主机,但单条事实与续算行仍受既有 RPC 上限约束,这一点已在 ledger 中写清而不是承诺无限。第三类风险是门禁现状:canary premerge --goal-id loopx-meta 选中 19 项检查,18 项通过,唯一的 semantic-vocabulary-drift-smoke 失败是模块对预算 44 > 43,我在干净的 origin/main 上复现同一失败,属于继承的基线红灯;本目标不等待远端 CI,PostgreSQL 与打包前端不在本切片内。
我的整体评价
没有发现阻断项。这个增量把事件回放从「散落的 Python 分支」收敛成一个类型化、有界、可续算的纯 fold,四个基线反例与两个规模反例都有对应测试,并且在真实本机 Goal 源上与 base 逐字节等价。验证覆盖正例、负例与真实路径:10 个 TS replay 用例、110 个事件/授权 focused Python 用例、3086 个 control-plane TS 用例(0 失败)、严格 mypy、ruff、类型检查与真实源投影摘要比对全部通过;变更质量回执 cqr_3716d61650b0e47d669c 对精确指纹有效。需要维护者知道的两点:约 7 倍的投影开销是被接受并披露的取舍,以及继承的词汇预算红灯需要单独决策。建议按维护者流程合并。
English verdict: APPROVE - exact head ae84cfc8b33d72b457ab69a6eb1ba18b2ded1e6c; one typed, bounded replay fold replaces scattered Python state branches, the four baseline counterexamples plus large-history batching are covered, and the real local source projects to a byte-identical digest at base and head, at a disclosed ~7x projection cost. The only failing check is the inherited module-pair vocabulary budget, reproduced on a clean origin/main.
Merge note — #5014 merged on the reviewed exact headMerged via admin bypass as Changed surfaces: the new TypeScript replay fold and its handler registration, the Python projection adapter (bounded facts plus ordinal-based content re-assembly, stateful replay branches deleted), one Python integrity suite, one TypeScript replay suite, and the bilingual RFC ledger/checkpoints. Rebase and review restart: the branch was first rebased onto Checks on the reviewed head
Failures, skips and holds: the goal-scoped premerge gate selected 19 checks and 18 passed. Residual risk: the projection now costs roughly 7x because it folds through bounded cross-runtime batches; this is disclosed in the ledger and accepted for a single typed decision owner. The reader remains a legacy adapter expected to be replaced by the eventual provider. |
Goal And Delivered Outcome
Related to #4574 (R1/R5), shared-authority L7/L8 and TS migration T3. Legacy event replay could replace a completed Todo with a second create event, retain stale priority text or source sections, and misorder planner order zero. Four independent baseline counterexamples now reject or produce coherent state.
One pure TypeScript fold owns cross-event Todo decisions. Python retains historical event decoding, checksum bytes, content and filesystem IO. The adapter sends at most 256 event facts and the touched Todo continuations per call; returned field ordinals preserve large content without sending it through RPC. No provider default or active-Goal migration is enabled.
Scope And Continuation
event_log_writer_not_boundremains. This PR does not bind outbox writes or certify whole-Goal cutover. Carry complete Goal sources through typed coordination and reconcile cutover work #5006 source transfer and [Task]: Qualify SQLite D2 capacity and crash/replay recovery on the merged provider #4224 D2 ownership are reused, not recreated.Validation
2e1e63260.The changed backend is the existing local event log reader, exercised through real files and the CLI. PostgreSQL authority transactions/service and packaged frontend code are unchanged; this is not PostgreSQL deployment or D2/D3 qualification. CI remains the full-suite oracle.
Measured tradeoff: seven alternating warm runs of the same detached input moved median projection time from 9.28 ms to 66.12 ms. This adds RPC/typed-owner overhead; no speedup, cold-throughput qualification or budget increase is claimed. Event count is batched, but individual fact/continuation rows still obey the existing RPC bound.
Frontend / Visual Evidence
No component, setting, asset or command shape changes. Existing event-source consumers receive the same projection schema with corrected priority text and role/source metadata; real CLI and existing API/rendering smokes cover that readback. No new frontend/Lark companion flow is required. Private source content and raw evidence are excluded.
Maintainer review and merge required; no self-merge.