Skip to content

Commit 1e6ca0c

Browse files
authored
refactor(todos): cut over completion transaction to TypeScript (#3530)
* refactor(todos): cut over completion transaction Signed-off-by: huangruiteng <huangrt01@163.com> * docs(rfc): record Todo completion cutover Signed-off-by: huangruiteng <huangrt01@163.com> * docs(todos): clarify completion transaction envelope Signed-off-by: huangruiteng <huangrt01@163.com> * refactor(todos): keep completion facade bounded Signed-off-by: huangruiteng <huangrt01@163.com> --------- Signed-off-by: huangruiteng <huangrt01@163.com>
1 parent acf2db5 commit 1e6ca0c

19 files changed

Lines changed: 1720 additions & 649 deletions

‎docs/architecture/rfcs/typescript-control-plane-migration-v0.md‎

Lines changed: 35 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
- Status: Accepted, transaction-payoff phase in progress
44
- Proposed by: LoopX maintainers
55
- Date: 2026-08-15
6-
- Last revised: 2026-08-22
6+
- Last revised: 2026-08-23
77
- Scope: an incremental, replacement-first migration of the LoopX control-plane
88
core from Python to TypeScript without maintaining two semantic
99
implementations
@@ -158,6 +158,7 @@ choice is now implemented rather than hypothetical.
158158
| Todo, quota, and scheduler proof slices ([#3431](https://github.com/huangruiteng/loopx/pull/3431)–[#3434](https://github.com/huangruiteng/loopx/pull/3434)) | Completion fence/state, workspace causality, and scheduler transitions each have one TS rule owner | The cuts are mostly leaf-shaped; Python still composes several product transactions |
159159
| Scheduler durable state ([#3440](https://github.com/huangruiteng/loopx/pull/3440)) | State normalization, persistence, replay, and one coarse transition are TS-owned | The Python compatibility path still pays a cross-runtime transport tax |
160160
| Runtime decoders ([#3443](https://github.com/huangruiteng/loopx/pull/3443)) | Stable primitive decoding has one small shared module; domain decoders remain local | No larger schema framework is justified |
161+
| Transaction payoff ([#3464](https://github.com/huangruiteng/loopx/pull/3464), [#3481](https://github.com/huangruiteng/loopx/pull/3481), and Todo completion) | Turn settlement, quota delivery routing, and Todo completion each cross one coarse TS boundary; the Todo transaction owns identity, replay fencing, validation planning/result reduction, continuation/recovery, and completion metadata | Python still executes explicitly external providers and materializes legacy Markdown/event results; other domains still need their own bounded cutovers |
161162

162163
These slices proved correctness, packaging, Windows lifecycle, crash recovery,
163164
real TS-owned writes, and acceptable warm primitive-call latency. They also
@@ -223,40 +224,46 @@ domains would now increase total complexity.
223224
### Stage 2B — Complete transaction cutovers (active)
224225

225226
Select by deletion leverage and runtime traffic, not by ease of translation.
226-
The first candidate is the complete Turn settlement/commit transaction because
227-
the current Python settlement surface composes several TS calls and retains
228-
duplicate settlement types. Subsequent candidates are complete Todo completion,
229-
quota spend/settlement, and scheduler heartbeat/state transactions, chosen only
230-
when each PR can retire an existing facade or materially shrink it.
227+
The shipped Turn settlement, quota delivery-routing, and Todo-completion
228+
cutovers establish the pattern. Subsequent candidates are quota
229+
spend/settlement and scheduler heartbeat/state transactions, chosen only when
230+
each PR can retire an existing facade or materially shrink it.
231231

232232
For each completed transaction, replace migration-only characterization workers
233233
and Python implementation fixtures with native TS semantic/invariant tests plus
234234
one durable end-to-end adapter contract. Retain a characterization corpus only
235235
while an old authority remains executable or a versioned compatibility window
236236
requires differential proof; record its deletion trigger when introduced.
237237

238-
Current implementation status: Stage 1 and the bounded Stage 2A proofs are
239-
shipped. The first Stage 2B cutover is the complete Turn settlement/commit
240-
transaction. TypeScript owns preflight authorization, ordered-prefix and replay
241-
validation, provider failure classification, receipt construction, terminal
242-
closeout joining, and the canonical result. Python is a mechanical adapter for
243-
the still-external writeback, spend, and terminal providers. New work therefore
244-
uses two coarse reductions and a completed replay uses one, replacing the prior
245-
multi-helper bridge. The remaining fine-grained settlement facade can be
246-
deleted after quota, host-adapter, and task-lease callers move to their own
247-
coarse transactions.
248-
249-
Quota delivery routing is the next bounded payoff cutover. TypeScript now owns
250-
continuity-versus-fallback selection and the selected Todo's settlement boundary
251-
behind one request. Python still prepares the normal fallback candidate and
252-
projects the typed result into the legacy quota packet, but it no longer composes
253-
two leaf decisions. The in-flight path moves from two cross-runtime calls to one;
254-
no-anchor paths with a fallback candidate and preempted paths remain at one,
255-
while an empty no-anchor candidate set remains at zero through a projection-layer
256-
short circuit. The Python facade exits when the
257-
quota route and CLI caller move into the native TypeScript transaction. Vision
258-
checkpointing remains a separate refresh/writeback transaction because it does
259-
not share the delivery-selection lifecycle phase.
238+
Current implementation status: Stage 1, the bounded Stage 2A proofs, and three
239+
Stage 2B cutovers are shipped:
240+
241+
- Turn settlement/commit: TypeScript owns preflight authorization,
242+
ordered-prefix and replay validation, provider failure classification,
243+
receipt construction, terminal closeout joining, and the canonical result.
244+
A real Python provider uses two coarse reductions; completed replay uses one.
245+
- Quota delivery routing: TypeScript owns continuity-versus-fallback selection
246+
and the selected Todo's settlement boundary. The in-flight path moved from
247+
two cross-runtime calls to one; the empty candidate short circuit remains
248+
zero.
249+
- Todo completion: TypeScript owns completion identity, terminal replay fence,
250+
validation declaration/effect planning, validation-receipt reduction,
251+
continuation/recovery, and completion metadata in one transaction. A Todo
252+
without declared validation, including a replay, uses one reduction. A real
253+
caller-approved validation command remains an explicit Python provider
254+
between two reductions. A source snapshot is compared after the mutation
255+
lock so a receipt for one declaration cannot authorize a changed Todo.
256+
Materialized and event-projected writes consume the same typed result.
257+
258+
The Todo cutover removes the Python state-evaluation dataclass, local identity
259+
projection, replay helper, and public runtime handlers for those implementation
260+
leaves. The remaining Python Todo facade owns transport, external command
261+
execution, source compare-and-swap, legacy response projection, and the actual
262+
Markdown/event write. It exits when those writers and the CLI move into the
263+
native TS transaction. The remaining fine-grained Turn facade exits after
264+
quota, host-adapter, and task-lease callers move to their own coarse
265+
transactions. Vision checkpointing remains a separate refresh/writeback
266+
transaction because it does not share the delivery-selection lifecycle phase.
260267

261268
### Stage 3 — CLI and App convergence
262269

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

Lines changed: 30 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
- Status:Accepted,transaction-payoff 阶段进行中
44
- Proposed by:LoopX maintainers
55
- Date:2026-08-15
6-
- Last revised:2026-08-22
6+
- Last revised:2026-08-23
77
- Scope:LoopX 控制面核心从 Python 到 TypeScript 的增量、replacement-first
88
迁移;不长期维护两份语义实现
99
- Tracking issue:[#3225](https://github.com/huangruiteng/loopx/issues/3225)
@@ -137,6 +137,7 @@ replay、receipt 与 settlement。这个架构选择已经落地,不再是假
137137
| Todo、quota 与 scheduler 证明切片([#3431](https://github.com/huangruiteng/loopx/pull/3431)–[#3434](https://github.com/huangruiteng/loopx/pull/3434)) | Completion fence/state、workspace causality 与 scheduler transition 各有一个 TS rule owner | 切口大多仍是 leaf-shaped;Python 继续组合多个产品 transaction |
138138
| Scheduler durable state([#3440](https://github.com/huangruiteng/loopx/pull/3440)) | State normalization、persistence、replay 与一笔粗粒度 transition 由 TS 拥有 | Python compatibility path 仍承担跨 runtime transport 税 |
139139
| Runtime decoder([#3443](https://github.com/huangruiteng/loopx/pull/3443)) | 稳定 primitive decoding 进入一个很小的共享模块;domain decoder 仍留在本地 | 没有理由建设更大的 schema framework |
140+
| Transaction 兑现([#3464](https://github.com/huangruiteng/loopx/pull/3464)、[#3481](https://github.com/huangruiteng/loopx/pull/3481) 与 Todo completion) | Turn settlement、quota delivery routing 与 Todo completion 均只跨一个粗粒度 TS boundary;Todo transaction 拥有 identity、replay fence、validation planning/result reduction、continuation/recovery 与 completion metadata | Python 仍执行显式 external provider,并物化 legacy Markdown/event result;其他 domain 仍需各自的 bounded cutover |
140141

141142
这些切片已经证明 correctness、packaging、Windows lifecycle、crash recovery、真实
142143
TS-owned write 和可接受的 warm primitive-call latency。它们也暴露了迁移边界:
@@ -193,35 +194,41 @@ leaf pattern 会增加总复杂度。
193194

194195
### Stage 2B — 完整 transaction cutover(进行中)
195196

196-
按删除杠杆与 runtime traffic 选切口,而不是按翻译难度选。首个候选是完整的 Turn
197-
settlement/commit transaction,因为当前 Python settlement surface 会组合多次 TS
198-
调用,并保留重复 settlement type。后续候选是完整 Todo completion、quota
199-
spend/settlement 与 scheduler heartbeat/state transaction;只有当每个 PR 能删除
200-
既有 facade,或使它物质变薄时才选择。
197+
按删除杠杆与 runtime traffic 选切口,而不是按翻译难度选。已经交付的 Turn
198+
settlement、quota delivery routing 与 Todo completion cutover 建立了这一模式。后续
199+
候选是 quota spend/settlement 与 scheduler heartbeat/state transaction;只有当每个
200+
PR 能删除既有 facade,或使它物质变薄时才选择。
201201

202202
每完成一笔 transaction,就用 native TS semantic/invariant test 加一个持久的
203203
end-to-end adapter contract,替换 migration-only characterization worker 与 Python
204204
implementation fixture。只有旧 authority 仍可执行,或 versioned compatibility
205205
window 仍需 differential proof 时才保留 characterization corpus;引入时必须记录
206206
删除触发条件。
207207

208-
当前实现状态:Stage 1 与 bounded Stage 2A proof 已交付。首个 Stage 2B cutover 是
209-
完整的 Turn settlement/commit transaction。TypeScript 拥有 preflight authorization、
210-
ordered-prefix 与 replay validation、provider failure classification、receipt
211-
construction、terminal closeout joining 和 canonical result。Python 只是仍属外部
212-
authority 的 writeback、spend 与 terminal provider 的机械 adapter。因此新工作使用
213-
两次 coarse reduction,完成态 replay 使用一次,替换此前的 multi-helper bridge。
214-
Quota、host-adapter 与 task-lease caller 迁到各自的 coarse transaction 后,即可删除
215-
剩余细粒度 settlement facade。
216-
217-
Quota delivery routing 是下一笔 bounded payoff cutover。TypeScript 通过一次请求拥有
218-
continuity 与 fallback 的选择,以及 selected Todo 的 settlement boundary。Python 仍
219-
准备正常 fallback candidate,并把 typed result 投影成 legacy quota packet,但不再
220-
组合两条 leaf decision。In-flight 路径的跨 runtime 调用从两次降为一次;存在 fallback
221-
candidate 的无 anchor 路径与 preempted 路径保持一次,而 anchor 和 candidate 均为空时
222-
由 projection 层短路并保持零次调用。Quota route 与 CLI caller 进入 native TypeScript
223-
transaction 后,Python facade 即可退出。Vision checkpointing 属于不同的
224-
refresh/writeback 生命周期阶段,因此继续作为独立 transaction。
208+
当前实现状态:Stage 1、bounded Stage 2A proof 与三笔 Stage 2B cutover 已交付:
209+
210+
- Turn settlement/commit:TypeScript 拥有 preflight authorization、ordered-prefix
211+
与 replay validation、provider failure classification、receipt construction、
212+
terminal closeout joining 和 canonical result。真实 Python provider 使用两次
213+
coarse reduction;完成态 replay 使用一次。
214+
- Quota delivery routing:TypeScript 拥有 continuity 与 fallback 的选择,以及
215+
selected Todo 的 settlement boundary。In-flight 路径从两次跨 runtime 调用降到
216+
一次;空 candidate 的 short circuit 仍为零次。
217+
- Todo completion:TypeScript 在一笔 transaction 中拥有 completion identity、
218+
terminal replay fence、validation declaration/effect planning、validation receipt
219+
reduction、continuation/recovery 与 completion metadata。没有声明 validation 的
220+
Todo(包括 replay)使用一次 reduction;真实 caller-approved validation command
221+
作为显式 Python provider,位于两次 reduction 之间。取得 mutation lock 后会比较
222+
source snapshot,确保一份 declaration 的 receipt 不能授权已经变化的 Todo。
223+
Materialized 与 event-projected 写入消费同一 typed result。
224+
225+
Todo cutover 删除了 Python state-evaluation dataclass、local identity projection、
226+
replay helper,以及这些 implementation leaf 的 public runtime handler。剩余 Python
227+
Todo facade 只拥有 transport、external command execution、source compare-and-swap、
228+
legacy response projection 与实际 Markdown/event write;当 writer 与 CLI 进入 native
229+
TS transaction 后即可退出。剩余细粒度 Turn facade 则在 quota、host-adapter 与
230+
task-lease caller 进入各自 coarse transaction 后退出。Vision checkpointing 属于
231+
不同的 refresh/writeback 生命周期阶段,因此继续作为独立 transaction。
225232

226233
### Stage 3 — CLI 与 App 汇合
227234

‎loopx/control_plane/effect_runtime.py‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@
4141
"scheduler/state_transition_rules.ts",
4242
"todos/completion_fence.ts",
4343
"todos/completion_state.ts",
44+
"todos/completion_transaction.ts",
4445
"todos/completion_validation_plan.ts",
4546
"todos/next_action.ts",
4647
"turn_driver/turn_journal.ts",

‎loopx/control_plane/effect_runtime_handlers.ts‎

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -44,16 +44,14 @@ import {
4444
import { commitTurnJournal } from "./turn_driver/turn_journal_effects.ts";
4545
import {
4646
evaluateTodoCompletionFence,
47-
projectTodoCompletionIdentity,
4847
} from "./todos/completion_fence.ts";
4948
import {
5049
buildTodoCompletionMetadataUpdates,
5150
normalizeTodoCompletionValue,
5251
requireTodoCompletionMetadataValue,
5352
selectTodoCompletionContinuation,
54-
selectTodoCompletionState,
5553
} from "./todos/completion_state.ts";
56-
import { evaluateTodoCompletionValidationPlan } from "./todos/completion_validation_plan.ts";
54+
import { reduceTodoCompletionTransaction } from "./todos/completion_transaction.ts";
5755
import { transitionTodoNextAction } from "./todos/next_action.ts";
5856
import { evaluateSchedulerStateTransition } from "./scheduler/state_transition_rules.ts";
5957
import {
@@ -260,9 +258,8 @@ export function createEffectRuntimeHandlers(
260258
["todo.completion_state.normalize", normalizeTodoCompletionValue],
261259
["todo.completion_state.require_metadata", requireTodoCompletionMetadataValue],
262260
["todo.completion_state.continuation_for_write", selectTodoCompletionContinuation],
263-
["todo.completion_state.evaluate", selectTodoCompletionState],
264261
["todo.completion_state.metadata_updates", buildTodoCompletionMetadataUpdates],
265-
["todo.completion_validation.plan", evaluateTodoCompletionValidationPlan],
262+
["todo.completion.reduce", reduceTodoCompletionTransaction],
266263
["todo.next_action.transition", transitionTodoNextAction],
267264
["scheduler.state_transition.evaluate", evaluateSchedulerStateTransition],
268265
["scheduler.state.evaluate", evaluateSchedulerStateOperation],
@@ -392,7 +389,6 @@ export function createEffectRuntimeHandlers(
392389
),
393390
],
394391
["turn.settlement.reduce", reduceTurnSettlementTransaction],
395-
["todo.completion_identity.project", projectTodoCompletionIdentity],
396392
["work_item.replan_settlement.project", projectReplanSettlementContract],
397393
[
398394
"work_item.replan_settlement.reentry",

‎loopx/control_plane/todos/completion_fence.py‎

Lines changed: 0 additions & 106 deletions
Original file line numberDiff line numberDiff line change
@@ -8,51 +8,6 @@
88

99
TODO_COMPLETION_FENCE_REQUEST_SCHEMA = "loopx_todo_completion_fence_request_v0"
1010
TODO_COMPLETION_FENCE_RESULT_SCHEMA = "loopx_todo_completion_fence_result_v0"
11-
TODO_COMPLETION_IDENTITY_REQUEST_SCHEMA = "loopx_todo_completion_identity_request_v0"
12-
TODO_COMPLETION_IDENTITY_RESULT_SCHEMA = "loopx_todo_completion_identity_v0"
13-
14-
15-
def local_todo_completion_identity(*, goal_id: str, todo_id: str) -> str:
16-
"""Return the TS-owned stable identity for an unscoped completion."""
17-
18-
try:
19-
result = effect_runtime_result(
20-
"todo.completion_identity.project",
21-
{
22-
"schema_version": TODO_COMPLETION_IDENTITY_REQUEST_SCHEMA,
23-
"goal_id": goal_id,
24-
"todo_id": todo_id,
25-
},
26-
)
27-
except EffectRuntimeRejected as exc:
28-
raise ValueError(str(exc)) from None
29-
if not (
30-
isinstance(result, Mapping)
31-
and result.get("schema_version") == TODO_COMPLETION_IDENTITY_RESULT_SCHEMA
32-
and result.get("identity_source") == "unscoped_completion"
33-
and isinstance(result.get("completion_identity_key"), str)
34-
and str(result["completion_identity_key"]).startswith("local_completion_")
35-
):
36-
raise RuntimeError("TypeScript Todo completion identity result shape mismatch")
37-
return str(result["completion_identity_key"])
38-
39-
40-
def resolve_todo_completion_identity(
41-
*,
42-
todo: Mapping[str, Any],
43-
goal_id: str,
44-
todo_id: str,
45-
completion_turn_key: str | None,
46-
completion_identity_source: str | None,
47-
) -> tuple[str | None, str | None]:
48-
"""Bind an unscoped open completion to its stable local identity."""
49-
50-
if completion_turn_key is not None or str(todo.get("status") or "") == "done":
51-
return completion_turn_key, completion_identity_source
52-
return (
53-
local_todo_completion_identity(goal_id=goal_id, todo_id=todo_id),
54-
"unscoped_completion",
55-
)
5611

5712

5813
def _json_successor_value(value: Any) -> Any:
@@ -121,64 +76,3 @@ def evaluate_todo_completion_fence(
12176
):
12277
raise RuntimeError("TypeScript Todo completion fence result shape mismatch")
12378
return dict(result)
124-
125-
126-
def completed_todo_replay(
127-
*,
128-
todo: Mapping[str, Any],
129-
goal_id: str,
130-
todo_id: str,
131-
completion_turn_key: str | None,
132-
completion_identity_source: str | None,
133-
no_followup: bool,
134-
handoff_mode: str,
135-
mutation_authority: dict[str, Any],
136-
state_file: str,
137-
project: str | None,
138-
dry_run: bool,
139-
) -> dict[str, Any] | None:
140-
"""Project a typed TS replay decision into the legacy Python response.
141-
142-
``handoff_mode`` is the currently resolved goal mode, not a persisted copy
143-
of the original completion result. This local replay therefore is not a
144-
durable operation receipt and must not be presented as Stage 2 proof.
145-
Original effect markers such as a lease fence or handoff-gate override are
146-
intentionally absent because this branch performs neither effect. A
147-
same-turn request may return ``None`` only to append the missing terminal
148-
no-follow-up state; it cannot replace a successor or cross a turn fence.
149-
"""
150-
151-
# This fast path runs before the caller selects the event writer. Treat it
152-
# as a materialized projection; an event-only deferred Todo falls through
153-
# and is evaluated with ``projection_source=event_log`` by that writer.
154-
fence = evaluate_todo_completion_fence(
155-
todo=todo,
156-
projection_source="materialized",
157-
completion_turn_key=completion_turn_key,
158-
no_followup=no_followup,
159-
goal_id=goal_id,
160-
todo_id=todo_id,
161-
completion_identity_source=completion_identity_source,
162-
)
163-
164-
# Event-projected deferred rows must pass through the event writer so the
165-
# existing lease-fence receipt remains visible. Done rows preserve the
166-
# earlier fast replay path for both projection sources.
167-
if fence.get("outcome") != "replay" or fence.get("status") != "done":
168-
return None
169-
return {
170-
"ok": True,
171-
"dry_run": dry_run,
172-
"completed": True,
173-
"idempotent_replay": True,
174-
"changed": False,
175-
"goal_id": goal_id,
176-
"todo_id": todo_id,
177-
"status": "done",
178-
"completion_continuation": fence.get("completion_continuation"),
179-
"completion_recovery": todo.get("completion_recovery"),
180-
"handoff_mode": handoff_mode,
181-
"mutation_authority": mutation_authority,
182-
"state_file": state_file,
183-
"project": project,
184-
}

0 commit comments

Comments
 (0)