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
2 changes: 1 addition & 1 deletion docs/architecture/rfcs/STATUS.md
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ appendix may keep dated history, but no dated log heading may precede it.
| [RFC: Shared Goal Alignment and Governed Amendment Protocol (v0)](shared-goal-alignment-and-governed-amendment-v0.md) | Accepted | none | [2 entries](ledger/shared-goal-alignment-and-governed-amendment-v0/) |
| [RFC: LoopX Shared Control-Plane Authority and Pluggable State Providers (v0)](shared-goal-authority-state-provider-v0.md) | Accepted | none | [23 entries](ledger/shared-goal-authority-state-provider-v0/) |
| [RFC: Single-Owner Local Daemon (v0)](single-owner-local-daemon-v0.md) | Accepted | none | — |
| [RFC: TypeScript Control-Plane Migration Direction v0](typescript-control-plane-migration-v0.md) | Accepted | none | [13 entries](ledger/typescript-control-plane-migration-v0/) |
| [RFC: TypeScript Control-Plane Migration Direction v0](typescript-control-plane-migration-v0.md) | Accepted | none | [14 entries](ledger/typescript-control-plane-migration-v0/) |

## Superseded (0)

Expand Down
2 changes: 1 addition & 1 deletion docs/architecture/rfcs/STATUS.zh-CN.md
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@
| [RFC:共享 Goal 对齐与受治理 Amendment 协议(v0)](shared-goal-alignment-and-governed-amendment-v0.zh-CN.md) | 已接受 | 无 | [2 条](ledger/shared-goal-alignment-and-governed-amendment-v0/) |
| [RFC:LoopX 共享控制面权威与可插拔状态 Provider(v0)](shared-goal-authority-state-provider-v0.zh-CN.md) | 已接受 | 无 | [23 条](ledger/shared-goal-authority-state-provider-v0/) |
| [RFC: Single-Owner Local Daemon (v0)](single-owner-local-daemon-v0.md) | 已接受 | none | — |
| [RFC:LoopX 控制面 TypeScript 渐进迁移方向 v0](typescript-control-plane-migration-v0.zh-CN.md) | 已接受 | 无 | [13 条](ledger/typescript-control-plane-migration-v0/) |
| [RFC:LoopX 控制面 TypeScript 渐进迁移方向 v0](typescript-control-plane-migration-v0.zh-CN.md) | 已接受 | 无 | [14 条](ledger/typescript-control-plane-migration-v0/) |

## 已被替代 (0)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -206,6 +206,9 @@ profile's promotion status.
Use existing domain tasks and roadmap checkpoints for execution; this plan does
not require unrelated migrations before a bounded repair can ship.

The [quiet-to-due Monitor checkpoint](ledger/typescript-control-plane-migration-v0/2026-10-02-monitor-quiet-due-recovery.md)
records bounded CLI replay/successor evidence and its explicit M2/M3 limits.

## 12. Open decisions

The first M2 implementation must choose its smallest sufficient exploration
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -167,6 +167,9 @@ provider 会使相应前提不成立;必须暴露这个事实,不能判定

执行沿用领域任务与总路线 checkpoint,不要求无关迁移先于有界修复完成。

[Monitor 静默到到期 checkpoint](ledger/typescript-control-plane-migration-v0/2026-10-02-monitor-quiet-due-recovery.zh-CN.md)
记录有界 CLI 重放/successor 证据,并明确 M2/M3 尚未覆盖的部分。

## 12. 未决事项

首个 M2 实现由测试与领域维护者选择足够小的探索方法及边界。优先复用 fixture
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
# Quiet-to-due Monitor recovery qualification

[中文镜像](2026-10-02-monitor-quiet-due-recovery.zh-CN.md)

This checkpoint adds composition evidence for T2 and the
[recovery verification RFC](../../composable-state-machines-recovery-verification-v0.md).
Base `e38b057b3` already
contains the effect-identity repair from [#4335](https://github.com/loopx-project/loopx/pull/4335).
An older installed runtime can still exhibit that repaired defect.
The remaining repair handles an unpolled, bound Monitor that becomes blocked:
its original Turn now exposes conditional lifecycle recovery instead of ordinary
execution or conflicting replan selection. No new RPC or persisted schema is added.

## Missing counterexample

A quiet heartbeat automatically commits an observation without a Todo. If a
Monitor becomes due during that same Turn, its explicit binding and actual
observation must remain possible. The first observation cannot settle the later
Todo or occupy its effect identity. Testing a newly due Monitor after an ordinary
unbound guard misses this prerequisite: the quiet poll has already committed.

```mermaid
flowchart LR
Q[Quiet guard: no Todo] --> O[Commit quiet observation]
O --> D[Monitor becomes due]
D --> B[Bind original Turn to Monitor]
B --> P[Commit exact poll once]
P --> L[Discard caller response]
L --> R[Read back and retry original poll]
R --> S[Turn settled; no quota debit]
S --> N[Material successor uses a new Turn]
```

## Executable boundary

`tests/control_plane/test_monitor_quiet_due_recovery.py` enumerates twelve journeys:
legacy Markdown, canonical File and canonical SQLite, each with unchanged and
material observations, with and without a peer-scoped user gate and reminder. Each journey uses real CLI subprocesses, real TS effects
and isolated provider state. It performs two exact retries and one conflicting
result retry. The response is deliberately discarded after successful command
completion; this models acknowledgement loss, not a process crash inside commit.

Independent assertions require two distinct polls (quiet and Todo-bound),
unchanged original observation, no refresh/spend records, no mutation on replay
or conflict, one material generation and successor only in the material case,
and settled readback that cannot execute further work in the original Turn.
The Monitor remains open; material successor selection uses a fresh Turn.

Five additional journeys block an already-bound Monitor before its poll: legacy,
File and SQLite soft claims, plus File and SQLite hard leases. The
unmodified base incorrectly returns `normal_run` in this fixture; other frontier
states can attempt a conflicting replan binding. Head returns the existing
`unsettled_host_turn_recovery` mode with the original identity and no delivery
authority. Two readbacks preserve blocked state. Only after the fixture's blocker
is resolved does the test execute the projected restore command, re-enter the
original guard, poll and settle without spending quota. The projected restore
carries the independently verified reason and `--clear-resume-when`, as required
by the existing lifecycle owner. Hard-lease restoration grants no execution
authority: polling without a lease remains rejected, and a fresh lease is acquired
before the exact poll. Two additional File/SQLite cases execute the same projected
restore against an active holder and verify rejection with Todo, lease and Goal
state unchanged.

Sensitivity was checked in a disposable checkout of the same base: restoring
Turn-only effect allocation makes the unchanged/legacy journey fail at its first
bound poll with `heartbeat_receipt_identity_conflict`. Unmodified base passes
all six journeys. This is a deliberate historical-rule mutation, not a claim
that the current base fails. The temporary mutant is not a shipped fixture.

An additional counterexample supplies an incomplete Todo frontier with no aggregate
work lane and an unrelated peer gate. The exact settled Monitor still has to
return `heartbeat_settled_skip`. Base instead allows ordinary execution because
the adapter drops its typed phase when the aggregate lane is absent. The adapter
now preserves the bound Monitor projection independently of aggregate coverage;
TS still owns poll verification and phase classification. This regression is red
before the adapter change and green after it.

A further composition case settles an advancement Turn, then moves its primary
Todo to `blocked` before an independent due Monitor is observed.
The emitted poll previously failed because admission accepted only open/done
primaries. The existing TS transaction owner now accepts this later hold only
when exact settlement readback is `settled`. Unsettled holds still fail closed;
the Monitor must independently pass its due, owner, gate and lease checks.
No held Todo is reopened, no new advancement is admitted and no extra debit is
created. Synthetic TS regressions cover the accepted hold and identity/gate
rejections (including deferred primaries); one CLI journey exercise in-flight writeback, spend, hold, poll and
exact replay. The accepted blocked-primary TS case fails before the change.

## Ownership and limits

| Boundary | Existing owner retained |
| --- | --- |
| Selection arbitration | `work_items/action_portfolio.ts` |
| Monitor transaction and immutable replay | `quota/monitor_poll_commit.ts` |
| Observation and successors in canonical authority | `coordination/todo_monitor_poll.ts` |
| Turn closeout readback | `quota/settlement_readback.ts`, `quota/settlement_phase.ts` |
| Bound Monitor lifecycle recovery | `quota/blocked_wait.ts` |
| Bound phase projection into an optional aggregate lane | `work_items/work_lane.py` |
| Legacy effect-id compatibility and transport | `quota/monitor_poll.py` |

The related refactor shares the current-Turn recovery envelope between causal
waits and blocked Monitor recovery in `blocked_wait.ts`. Python passes the already
verified Monitor phase through the existing request and renders the typed repair;
it owns no second lifecycle rule. The added phase field is optional: earlier
requests retain their causal-wait behavior. Only active, correctly owned blocked
Monitors with `poll_due` qualify; missing, duplicate, foreign, archived and already
polled inputs do not enter this restoration route. Tests assert these exclusions.

Default behavior changes for blocked, unpolled replay, for bound Monitor
projection when an incomplete frontier has no aggregate lane, and for independent
auxiliary observation after an exact settled advancement is held. The emitted
restore command is conditional on a verified resolved blocker; the projection
neither reopens the Todo nor establishes a poll/closeout receipt. Retain the
blocker when unresolved. The existing Todo writer still enforces mutation authority.
Runtime request count is unchanged; no Python rule deletion or performance gain
is claimed. Moving the retained Python effect-id compatibility resolver needs
separate pending-receipt/caller characterization and is deferred.

This qualifies a bounded Monitor closeout and CLI successor-selection sequence,
not full M2/M3: lease transfer, mid-commit crashes, PostgreSQL, scheduler dispatch
and original-context App/Lark delivery are outside this test. Existing pending-
wait recovery tests separately cover original binding retention on File/SQLite.
No model, external provider or benchmark job runs. There is no frontend change.
Reverting restores the previous projection without rewriting persisted data,
but also restores the blocked-Monitor and missing-lane recovery gaps.
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
# Monitor 从静默到到期的恢复验证

[English mirror](2026-10-02-monitor-quiet-due-recovery.md)

本 checkpoint 为 T2 和[恢复验证 RFC](../../composable-state-machines-recovery-verification-v0.zh-CN.md)
补充组合证据。
基线 `e38b057b3` 已包含 [#4335](https://github.com/loopx-project/loopx/pull/4335)
的 effect identity 修复;较旧的已安装 runtime 仍可能出现该缺陷。
剩余修复处理已绑定但尚未 poll 的 Monitor 变为 blocked:原 Turn 现在暴露有条件的
生命周期恢复,不再进入普通执行或冲突的 replan 选择。不新增 RPC 或持久化 schema。

## 缺失的反例

静默 heartbeat 会自动提交一条未绑定 Todo 的 observation。若 Monitor 在同一 Turn
内变为到期,仍必须能显式绑定并提交实际观察。前一条 observation 不能完成后续
Todo 的结算,也不能占用它的 effect identity。仅从普通未绑定 guard 测试新到期
Monitor 会遗漏这个前提:静默 poll 已经提交。

```mermaid
flowchart LR
Q[静默 guard:未绑定 Todo] --> O[提交静默观察]
O --> D[Monitor 到期]
D --> B[原 Turn 绑定 Monitor]
B --> P[准确 poll 只提交一次]
P --> L[丢弃调用方回执]
L --> R[回读并重试原 poll]
R --> S[Turn 已结算;不扣配额]
S --> N[有变化时 successor 使用新 Turn]
```

## 可执行边界

`tests/control_plane/test_monitor_quiet_due_recovery.py` 确定性枚举十二条旅程:
legacy Markdown、canonical File、canonical SQLite,分别观察无变化与有变化。
每组分别覆盖有、无其他 peer 的 user gate 及普通用户提醒。
各旅程使用真实 CLI 子进程、TS effects 和隔离 provider 状态,执行两次原请求重试
和一次结果冲突重试。第一次命令成功后主动丢弃响应,模拟调用方确认丢失,
不等于模拟提交过程中的进程崩溃。

独立断言要求:静默和 Todo-bound 两条 poll 身份不同;原观察不变;没有 refresh/
spend 记录;重放和冲突不改状态;仅有变化时生成一次 material generation 与一个
successor;已结算回读不允许原 Turn 再执行工作。Monitor 仍为 open;有变化时,
后继选择通过新的 Turn 完成。

另五条旅程在绑定之后、poll 之前将 Monitor 标为 blocked:legacy、File、SQLite
的 soft claim,以及 File、SQLite 的 hard lease。未修改基线在这个
fixture 中错误返回 `normal_run`;其他 frontier 状态还可能尝试冲突的 replan
绑定。修复后使用既有 `unsettled_host_turn_recovery`,保留原身份且不给交付权限。
两次回读都保留 blocked 状态;仅在 fixture 的 blocker 已解除后,测试才执行
投影的恢复命令、重入原 guard、poll 并无配额扣减地结束 Turn。恢复命令向现有
生命周期 owner 提交独立核实的原因与 `--clear-resume-when`。hard lease 下恢复
不授予执行权限:无 lease 的 poll 仍拒绝,取得新 lease 后才执行精确 poll。
另外两条 File/SQLite 用例将相同投影命令用于存在活跃 holder 的状态,验证拒绝
且 Todo、lease 与 Goal 状态均不变。

敏感性验证使用同一基线的临时 checkout:恢复仅按 Turn 分配 effect 的历史规则后,
无变化/legacy 旅程在首次绑定后的 poll 以 `heartbeat_receipt_identity_conflict`
失败。未修改基线的六条旅程全部通过。这是刻意注入历史规则的 mutation,
不声称当前基线仍有该缺陷。临时 mutant 不作为发布 fixture 保留。

另一个反例给出不完整的 Todo frontier:聚合 work lane 缺失,且存在其他 peer 的
user gate。精确的已结算 Monitor 仍必须返回 `heartbeat_settled_skip`。基线 adapter
在聚合 lane 缺失时丢弃已验证 phase,错误恢复普通执行。修复后,绑定 Monitor 的
投影不再依赖聚合覆盖完整性;poll 验证与 phase 判断仍归 TS。此反例在 adapter
修复前失败、修复后通过。

另一个组合场景先结算 advancement Turn,再将主 Todo 置为 `blocked`,
随后观察独立到期的 Monitor。此前命令已经投影,但准入只接受 open/done 主 Todo,
导致执行失败。现在由现有 TS 事务 owner 检查精确结算回读:仅 `settled` 才允许
这些后续 hold。尚未结算的 hold 仍拒绝,Monitor 仍须独立通过到期、owner、gate 和
lease 检查。不重开被 hold 的 Todo,不授予新交付,不增加扣费。合成 TS 回归覆盖
该 hold 及身份/gate 拒绝(含 deferred 主 Todo);一条 CLI 旅程覆盖 in-flight 写回、扣费、hold、poll
和精确重放。blocked 主 Todo 的 TS 正例在修改前失败。

## 归属与限制

| 边界 | 保留的现有 owner |
| --- | --- |
| selection 仲裁 | `work_items/action_portfolio.ts` |
| Monitor 事务与不可变重放 | `quota/monitor_poll_commit.ts` |
| canonical authority 中的观察与 successor | `coordination/todo_monitor_poll.ts` |
| Turn 结算回读 | `quota/settlement_readback.ts`、`quota/settlement_phase.ts` |
| 绑定 Monitor 的生命周期恢复 | `quota/blocked_wait.ts` |
| 已绑定 phase 到可选聚合 lane 的投影 | `work_items/work_lane.py` |
| legacy effect-id 兼容与 transport | `quota/monitor_poll.py` |

伴随重构在 `blocked_wait.ts` 内共享 causal wait 与 blocked Monitor 的 current-Turn
恢复 envelope。Python 通过现有请求传递已验证的 Monitor phase,并渲染 typed repair,
不另建生命周期判断。新增 phase 字段为可选,旧请求保留 causal-wait 行为。
只有 active、owner 合法、状态 blocked 且 phase 为 `poll_due` 的 Monitor 进入此
恢复路线;缺失、重复、其他 owner、已归档或已 poll 的输入均排除,测试覆盖这些边界。

默认行为改变上述 blocked、尚未 poll 的重入,不完整 frontier 缺少聚合 lane
时已绑定 Monitor 的投影,以及精确结算后的 advancement 被 hold 时的独立辅助观察。
恢复命令以 blocker 已核实解除为
前提;投影不会自行重开 Todo,也不构成 poll/closeout receipt。原因未解除时
继续保留 blocked。既有 Todo writer 仍执行变更权限检查。runtime 请求数不变,
不计 Python 规则删除量或性能收益。保留的 Python effect-id 兼容 resolver 迁移
需要另行刻画 pending receipt/caller,本次延后。

本次仅验证有界 Monitor 结算与 CLI successor selection,不完成整个 M2/M3:
lease transfer、提交中断、PostgreSQL、scheduler dispatch、App/Lark 原上下文投递
均不在覆盖范围。现有 pending-wait 回归另行验证 File/SQLite 保留原绑定的恢复。
不调用模型、外部 provider 或 benchmark Job,无前端变更。
撤销修复可恢复旧投影且无需改写持久数据,但也会恢复 blocked Monitor 与聚合 lane 缺失的恢复缺口。
Loading
Loading