Skip to content

refactor(coordination): unify handoff transition checks and recovery - #4894

Merged
huangruiteng merged 4 commits into
mainfrom
codex/handoff-transition-0922
Sep 22, 2026
Merged

huangruiteng merged 4 commits into
mainfrom
codex/handoff-transition-0922

Conversation

@huangruiteng

Copy link
Copy Markdown
Collaborator

A legacy handoff-mode set could miss an unfinished claim that existed only in the event projection. The canonical path had a separate blocker scan and receipt replay implementation. This PR gives both paths one typed ownership-fact/transition owner and makes the legacy scan use the complete locked event overlay.

The same change removes Python's artificial previous-mode substitution and whole-document rewrite. Invalid legacy modes remain explicitly repairable only when quiescent; duplicate fields and unreadable event sources reject. Only frontmatter and compact facts cross the TS boundary, preserving large bodies, CRLF and Unicode metadata. Canonical mode changes reuse durable command receipt recovery and reject malformed historical decisions.

This advances the T1/T2 and L2/L3 boundary in #4574 and both migration RFCs. It does not change provider defaults or replace the open projection/recovery work. The RFC checkpoint retains the conditional 5–8 delivery-package estimate. The administrative mutation entrypoint is the existing CLI; Dashboard/Lark have no mode-setting editor or new configuration field to update. Ordinary ownership gates, canonical CAS, writer fencing and capture remain in force.

Validation:

  • 662 File/SQLite/real PostgreSQL/store-service regression tests; final 24 typed mode-plan tests.
  • 97 Python CLI/mode/capture cases; final 11 source cases rerun after the Unicode scanner repair.
  • The same four public claim/CRLF regressions fail on base c4314d9b0 and pass on this branch.
  • Consistent read-only local source snapshot: 1,023 Todos and 10 leases. Legacy and three real providers reject changed modes with claims; separate quiescent twins pass preview/apply/retry/readback and retain every Todo/lease record. No active Goal was migrated or rewritten; private evidence remains local.
  • TypeScript typecheck, configured mypy, changed-file Ruff, public-boundary scan and diff check pass.
  • Premerge canary: 19 selected checks plus 5 direct checks pass; zero failures, warnings or manual holds. Exact-scope change-quality receipt verifies valid.

The future-facing pass removes duplicated decision/recovery knowledge while retaining the existing Python source-lock/capture adapter. Frontmatter repair is a compatibility operation; malformed canonical authority is not silently repaired. Maintainer review and merge are required.

Signed-off-by: huangruiteng <14976749+huangruiteng@users.noreply.github.com>
…edgements

Signed-off-by: huangruiteng <14976749+huangruiteng@users.noreply.github.com>
Signed-off-by: huangruiteng <14976749+huangruiteng@users.noreply.github.com>
@hahahahahayesyeseys

Copy link
Copy Markdown
Contributor

The conflict is additive-only, and it is the sole in-tree blocker

mergeStateStatus=DIRTY against main c41e09473. The only conflicted files are the migration RFC and
its Chinese mirror, and in both cases each side added one paragraph at the top of
## Current implementation checkpoint / ## 当前实现检查点:

  • docs/architecture/rfcs/typescript-control-plane-migration-v0.md
  • docs/architecture/rfcs/typescript-control-plane-migration-v0.zh-CN.md

Of the 11 commits that reached main since this PR's base a6b3dc5fc, exactly one code file overlaps:
loopx/control_plane/effect_runtime_handlers.ts. Both sides add one import and one handler-table row at
different positions, so the textual auto-merge is also semantically clean (tsc --noEmit passes on the
combined tree). No other file was touched by both sides.

Resolution: keep both paragraphs, this PR's entry first. That section is prepended in landing order, so
the entry that merges later goes on top. I traced the current top-to-bottom order of the first four entries
to commits dated 2026-09-22 23:40 (#4886), 09-21 17:14, 09-20 19:25, 09-18 16:51 — strictly newest first,
which is also where #4886 inserted itself (git show 4b4031c9e hunk at line 22).

Final shape of the English block (the Chinese block resolves identically — same two paragraphs, same order):

## Current implementation checkpoint

Handoff-mode transition now shares typed ownership facts and an explicit
valid/invalid previous-mode state across legacy and canonical paths. Python's
blocker classification, artificial previous mode and whole-text rewrite are
removed; its retained boundary is source projection/locking and capture IO.
The legacy scan includes event-only claims, and canonical mode receipts reuse
command recovery with strict historical decisions. Full-source snapshot and
real-provider validation guard this T1/T2 replacement. This closes a rule and
caller discrepancy, not a whole default-cutover package; the conditional 5–8
package estimate remains. [Changed behavior and recovery](../../reference/handoff-mode.md).

Canonical command observation now has one typed receipt/head boundary. Team,
Todo creation/edit/claim/terminal/archive, Monitor, lease maintenance and Goal
acceptance recheck receipts after the head read before interpreting new state.
This repairs same-operation races without provider API changes, write retries
or a Python copy of the decision. Standalone and atomic claim acquisition share
current lease proof; renewed proof is returned without rewriting history, while
retired execution and unavailable current authority cannot return stale success.
This closes a concurrency/current-proof slice of L2/L3, not whole-Goal migration,
default onboarding, contributor-owned SQLite D2 or T4 Python retirement. The
[operator contract](../../reference/canonical-lease-renew.md#commit-retry-and-readback)
distinguishes historical results from present execution.

Terminal review and validation now converge in the existing TS terminal owner.

Nothing should be dropped here: each paragraph is the disclosure that its own PR owes the checkpoint, and
a "take ours" / "take theirs" resolution would erase one of them.

The three red checks are one timeout, not three failures

On head 3ec1e1f08: kernel-static-checks ran 10:01:10 → 10:31:26 = 30m16s and was cancelled by its own
timeout-minutes: 30
. The three red jobs — checks ("Require kernel and Dashboard qualification", 5s),
pytest ("Require every upstream check", 3s), merge-gate ("Reject incomplete or unsuccessful
qualification", 9s) — are downstream fail-closed aggregators, and the job named pytest in this workflow is
a collector, not a runner. Every actual test job passed on the exact head: 4/4 test-shard, stage2c
(e2e 1–2, installed, mutants), stage2c-correctness-e2e, node-minimum-compatibility,
node-forward-compatibility, windows-powershell, dashboard-acceptance, postgresql-authority, build,
changes, Sign-off. For scale, the last four runs of kernel-static-checks that completed successfully
took 23m / 23m / 26m / 23m against that same 30-minute ceiling, so a re-run after the merge is the first
thing to try; if the same head hits the ceiling twice, the ceiling itself becomes the question.

Measured on a local trial merge (main c41e094733ec1e1f08, with the resolution above)

  • pytest tests/control_plane/test_goal_handoff_mode.py tests/control_plane/test_handoff_mode_source.py72 passed
  • node --no-warnings --experimental-sqlite --experimental-strip-types --test tests/control_plane_ts/handoff_mode_plan.test.ts24 passed / 0 failed
  • tsc --project tsconfig.control-plane.json --noEmit → clean
  • examples/docs-governance-smoke.py, examples/docs-asset-integrity-smoke.py,
    examples/repository-hygiene-smoke.py, examples/visible-governance-slice-smoke.py → all pass
  • no conflict markers left repo-wide, git diff --check clean

Environment: Node 22.23.2, CPython 3.12 with pip install -e ".[test]", node_modules deps identical to the
branch's package.json.

One unrelated thing I noticed in the same file

typescript-control-plane-migration-v0.md lines 66–69 hold a Chinese paragraph inside the English file
(关联 User 完成的决策消解…), while this RFC's own header says the two files are semantic mirrors and a
difference between them is a defect. It came in with 5ef53b314 (2026-09-20), not with this PR — flagging it
only because it sits in the section the conflict touches.

Posting this as a prepared resolution only: the head branch is in the org repository and
maintainer_can_modify is false, so I cannot push it onto the PR myself.

@huangruiteng huangruiteng left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Approval conclusion (author-owned PR; GitHub blocks formal self-approval)

Exact head: 3ec1e1f08c70756f9e5ce588e2ecdd3400713beb

动机

这个 PR 解决的不是“把 Python 改写成 TypeScript”本身,而是 handoff-mode 迁移前存在的三处 authority 风险:

  1. legacy Python 与 canonical TypeScript 各自拥有一套 mode-transition eligibility,下一次规则变化必须双改;
  2. legacy 路径借用了面向展示/普通 Todo 读取的有界投影,可能漏掉 event-only 或第 521 个 unfinished claim,从而把非静默状态误判为可切换;
  3. legacy frontmatter 修改和 canonical post-commit recovery 分散在不同语义边界,格式保真、receipt replay 与失败归属不够集中。

我对照了 base c4314d9b06a52073e12ec64fb76ff42e486bdc7e、当前 exact head、现有 canonical transaction、legacy setter、event projection、lease store、runtime handler 和历史 receipt 实现。单纯把 limit=500 调大只能推迟同一缺陷,也保留第二个 policy owner;本 PR 选择了更小的长期边界:TypeScript 只拥有 facts/policy/plan,Python 只保留 legacy source locking 和文件 effect。

改动思路

整体设计符合当前 control-plane owner 方向:

  • decideHandoffMode 成为 legacy/canonical 共用的纯 transition rule;
  • planLegacyHandoffMode 接收完整 snapshot,执行同一规则并产生只修改 handoff_mode scalar 的 lossless patch;
  • handoff_mode_source.py 在有序锁内读取并验证所有候选 event source 与 lease source,再构造完整 facts,不把 display cap 当 authority completeness;
  • canonical transaction 复用同一 policy/facts,并继续让 durable operation receipt 负责 replay/recovery;
  • no-op 请求仍可在无需 ownership snapshot 的阶段返回,避免为了“不变”扩大锁和读取成本。

最强反对意见是 660+/389- 的改动是否对问题过重,以及更严格的 receipt decoder 是否破坏历史兼容。我逐项核验后认为可接受:最大的生产改动是删除 281 行左右的 Python 重复规则;新模块按 facts、pure policy、legacy patch plan、source acquisition、transaction effect 分解,且都有活跃 caller。历史 previous_mode_valid 也不是本 PR 新加的隐含前置,canonical transaction 从原始实现起就已把它写入 schema v0 receipt,因此这里没有制造已知旧 receipt 的断裂。

具体改动

关键文件与符号:

  • handoff_mode_policy.ts::decideHandoffMode:统一 valid/invalid persisted mode、same-mode no-op 和 claim/lease blockers 的 typed decision。
  • handoff_mode_legacy_plan.ts::planLegacyHandoffMode:把 legacy 决策放进 TypeScript,并且以 scalar patch 保留 CRLF、Unicode separator、正文和 final-newline;缺失/重复字段不能形成 partial repair。
  • handoff_mode_source.py:锁住 state/event/lease 相关源,验证每个 candidate,通过完整 Todo projection 获取 event-only 和尾部 claims;source 不完整时不把“没看见”当“没有”。
  • handoff_mode_transaction.ts::setHandoffModeTransaction:canonical 路径复用统一 rule,严格校验 operation/hash/decision receipt 并进行 applied/replayed/recovered readback。
  • handoff_mode.py:删除重复 Python policy/frontmatter rewrite,只协调 phase-one no-op、complete facts snapshot、typed plan、locked write/readback。
  • reference/RFC 文档:明确披露 event-only claim 现在会阻止切换、candidate corruption 会 fail closed,以及 owner 边界变化。

正向 walkthrough:用户请求改变 legacy mode → phase one 验证 snapshot 且确认需要 facts → source adapter 在锁内读取全部 event/lease facts → typed planner 调用统一 policy → 只替换 frontmatter scalar → Python 在原锁内写入并回读;正文与换行格式保持不变。

负向 walkthrough:unfinished claim 只存在于 event journal 的第 521 项,或某个 configured/fallback candidate 无法解析 → source 层仍能看到 blocker,或在 facts 不完整时直接失败 → planner/write phase 不会把 mode 改掉。canonical 路径若 commit 返回不确定状态,则按 exact operation receipt 恢复,而不是二次盲写。

对主干的风险

未发现阻塞实现问题,但合并风险需要明确分开:

  • 语义风险:event-only/late claim 与 corrupt source 现在更严格地阻止切换。这是有意的 fail-closed 修复,已经在 reference/RFC 和测试名称中披露;不是静默 default change。
  • 数据保真风险:legacy patch 覆盖 CRLF、quoted Unicode separator、large body、duplicate field、missing frontmatter 和 final newline;只修改目标 scalar。
  • authority/recovery 风险:local file/shadow real paths 已验证,exact-head 的 postgresql-authority (real server) 也为绿;storage shape 与 receipt schema version 未改变。
  • 当前 merge risk:PR 为 DIRTY/CONFLICTING,必须先解决与 main 的文档冲突;kernel-static-checks 同样在 30 分钟被取消,使 aggregate checks 红。冲突解决会产生新 head,所以当前 review 不能替代 rebase 后的 exact-head required-check rerun。

独立 detached worktree 验证结果:

  • handoff_mode_plan.test.ts:24 passed;
  • test_goal_handoff_mode.pytest_handoff_mode_source.py、CLI、local-authority shadow 与 writer-boundary suites:128 passed;
  • npx tsc --project tsconfig.control-plane.json --noEmit:通过;
  • git diff --check:通过;
  • GitHub exact-head postgresql-authority (real server):通过;所有实际运行的 test shards/e2e/mutants/installed/Windows/Node/dashboard 也为绿。

我的整体评价

APPROVE(实现结论),但当前不能合并。这个 PR 把 handoff transition 的决策权收回到一个 typed TypeScript owner,同时保留 Python 作为必要的 legacy storage/effect bridge;complete-source counterexample、文件保真和 post-commit recovery 都有实质负向覆盖。它没有新增 mode、CLI 或人工同步状态,改动规模与 authority 风险相称。

合并前必须:解决 main 冲突、重新检查冲突解决后的完整 diff、在新 exact head 上完成 required checks。未来导向检查已在本 PR 内完成:重复 Python policy 已删除,下一次规则变化只应修改 decideHandoffMode;不建议再加平行 compatibility rule。

English verdict: APPROVE - The exact-head refactor correctly centralizes transition authority and validates complete legacy facts; merge remains blocked on conflict resolution and a full required-check rerun for the rebased head.

…ion-0922

Signed-off-by: huangruiteng <14976749+huangruiteng@users.noreply.github.com>

# Conflicts:
#	docs/architecture/rfcs/typescript-control-plane-migration-v0.md
#	docs/architecture/rfcs/typescript-control-plane-migration-v0.zh-CN.md
@huangruiteng
huangruiteng merged commit 912ffc3 into main Sep 22, 2026
35 of 44 checks passed
@huangruiteng
huangruiteng deleted the codex/handoff-transition-0922 branch September 22, 2026 17:45
huangruiteng added a commit that referenced this pull request Sep 22, 2026
…point-paragraph-break

docs(rfc): restore the checkpoint paragraph break lost in the #4894 merge
pull Bot pushed a commit to codingwatching/loopx that referenced this pull request Sep 22, 2026
…roject#4894 merge

The loopx-project#4894 merge resolved an additive conflict in
`typescript-control-plane-migration-v0.md` by keeping both checkpoint
paragraphs, but the blank line between them was staged only in the zh-CN
mirror, so the English section renders the two entries as one paragraph
while the mirror renders two. Restore the missing paragraph break; no text
is added, removed or reordered.

Signed-off-by: huangruiteng <14976749+huangruiteng@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants