Skip to content
Original file line number Diff line number Diff line change
Expand Up @@ -3016,6 +3016,16 @@ independent legacy three-arm comparison or D2 soak.
move. Exit with deterministic freshness/readback and an actionable repair
path; a successful render once is insufficient.

Continuation and closure readback now uses the same TS relation evidence for
completed-work gaps and handoff states before filtering/capping. Capture v1
retains reachable archived successors and original deferred status; derived
summary evaluations never enter provider records. Completion retries also
recover the matching receipt when a peer commits between receipt and head reads,
without accepting state-only replay with fresh validation evidence. Real CLI and complete-graph
provider conformance cover the consumer family. See [operation and semantic
changes](../../reference/todo-continuation-readback.md). This closes a bounded
L5/L7 gap; permanent projection delivery/recovery, D2 and D3 are still open.

**D2 — qualify exactly one local profile; independent of PostgreSQL deployment.**

- Reconcile the SQLite candidate #4121 with Section 7.2 before adding code.
Expand Down Expand Up @@ -3073,8 +3083,9 @@ The reconciled baseline includes #4286 (command receipts/archive), #4289
#4316 (Goal Channel observation), #4317 (provider opening), #4348 (renew),
#4328 (first SQLite D2 batch) and #4334 (PostgreSQL service admission): all are
merged at the 2026-09-20 checkpoint. Their existence does not qualify the full
cards. #4732 remains the open Monitor observation/reactivation slice; #4224
retains contributor ownership of SQLite D2. Re-read actual heads before work.
cards. Monitor observation/reactivation #4732 and linked User completion #4754
are also merged. #4224 retains contributor ownership of SQLite D2. Re-read
actual heads before work.

The identifiers below are **planned PR packages**, not reserved GitHub numbers.
A package may split at a real effect/compatibility boundary; changing languages
Expand All @@ -3096,12 +3107,11 @@ or moving a helper is not by itself a package exit.
packages as complete operations while L6/L7 progress independently. B integrates
those contracts into complete user flows; C has one reproducible qualification
checkpoint; D changes the default in its own reviewable PR. After the linked
User completion slice, the 2026-09-20 planning estimate is **6–9 further cohesive
User completion slice, the 2026-09-20 planning estimate is **5–8 further cohesive
PRs**, conditional on the caller audit finding no additional missing effects:

| Remaining work package | Estimated PRs | Exit |
| --- | --- | --- |
| Monitor observation/reactivation | 1, existing #4732 | Real caller and complete graph acceptance; avoid a duplicate implementation. |
| Remaining L2/L3 caller and executor-effect fences | 1–2 | Actual CLI/Turn/Chat command inventory and external-effect boundary closure. |
| L5 / D1 consumer and projection closure | 1 | Full consumer parity, lag/recovery and packaged client readback. |
| L6 / SQLite D2 | 1–2, contributor-owned #4224 | Capacity, crash/restore and separately authorized elapsed-soak evidence on one profile. |
Expand All @@ -3113,8 +3123,8 @@ Scope may split only where a real effect/compatibility boundary warrants it.
Small Python business-rule deletions can ship with each TS owner; rendering,
private command execution and import/export keep their active adapters.

截至 2026-09-20,关联 User 完成链路补齐后,按以上六类完整交付边界估算还需 **6–9 个 PR**。
Monitor 复用 #4732,SQLite D2 仍归 #4224 contributor;其余顺序是调用方/执行围栏、
截至 2026-09-20,关联 User 完成链路补齐后,按以上五类完整交付边界估算还需 **5–8 个 PR**。
Monitor #4732 已合并,SQLite D2 仍归 #4224 contributor;其余顺序是调用方/执行围栏、
消费与投影、capture 与整 Goal 演练,最后独立切换默认值。该估算以未发现更多缺失
effect 为前提,不是合并数承诺,也不要求先删完 Python。TS owner 每收敛一块即可
删除对应旧规则;仍有真实调用方的渲染、私有命令执行和导入导出适配器继续保留。
Expand Down
13 changes: 13 additions & 0 deletions docs/architecture/rfcs/typescript-control-plane-migration-v0.md
Original file line number Diff line number Diff line change
Expand Up @@ -1017,6 +1017,19 @@ history before that checkpoint. Successful schemas, File/NoKV persisted bytes,
request identity and revision algorithms remain compatible. This supports T3/D1
readers but does not finish Todo writers, retention/compaction or promotion.

Continuation readback now shares one typed succession resolver, handoff state
machine and summary closure decision. The legacy adapter no longer owns those
rules. Full-source evaluations survive display selection; nonexistent/self
successors cannot certify closure and archived continuation evidence survives
capture. The existing archive-capture request advances to v1 so older runtimes
cannot silently omit the expanded graph. Query subsets do not emit whole-source
closure proofs, and bounded handoff views preserve their state and exclusions.
See [continuation readback](../../reference/todo-continuation-readback.md).
This closes that T3/L5 consumer family and its bounded L7 dependency, not D1–D3
or every T3 consumer. Python retains codecs, IO and the documented legacy route
prose hint until its remaining writers emit explicit replan flags; no new
capability/provider or parallel business authority is introduced.

**T4 — collect full-writer retirement after durability cutover.**

- The 2026-09-19 command audit retires two already-typed but unconsumed
Expand Down
138 changes: 138 additions & 0 deletions docs/reference/todo-continuation-readback.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
# Todo continuation and closure readback

Todo list, status and quota distinguish a completed record from a closed work
slice. A completed tracked advancement Todo still needs an existing successor
or an explicit `no_followup=true`. This read policy lives in
`control_plane/todos/succession.ts`; Python normalizes legacy input and renders
its decisions. It belongs to the existing Todo control plane, uses the selected
AuthorityStore, and adds no capability or extension provider.

## Relationship evidence

The policy evaluates the complete available Todo graph before role, status,
Agent, ID or display-limit selection. It recognizes explicit
`successor_todo_ids`, `superseded_by`, and advancement records pointing back
through `unblocks_todo_id` or `resume_when=todo_done:<source>`.

- A declared successor must exist and differ from the source. A dangling or
self reference does not close work. A retained archived record remains
relationship evidence; it does not become active work.
- Explicit links retain their existing role-neutral meaning. Inferred
successors require an advancement task. `monitor_changed` is a resume
condition, not an inferred work successor.
- An existing successor records continuation lineage. It does not prove that
the successor has executed, been accepted or acquired a lease. This is not a
transitive Goal acceptance proof or a cycle-freedom certificate.
- Basic historical checkboxes without structured execution context keep their
compatibility behavior. Explicit no-follow-up remains an independent closeout
choice. Deferred work is never classified as a completed advancement gap.

Both completed-work warnings and handoff gates use that same graph. Previously
handoff ignored explicit successor lists, while completed-work warnings accepted
nonexistent/self links. Filtering or archiving a valid inferred successor could
also manufacture a warning that was absent on the full source.

| Handoff facts, in precedence order | State |
| --- | --- |
| Existing, non-self supersession target | `superseded` |
| Deferred source | `deferred` |
| Source has not completed | `blocking` |
| Completed with explicit no-follow-up | `cleared_no_followup` |
| Completed with a resolved successor | `cleared_with_successor` |
| Completed without either | `cleared_without_successor` |

Only active dependency-linked executor exclusions are handoff gates. These
states describe the gate; none changes claims, grants, leases or stored Todos.
The existing legacy stale-closeout prose hint remains a compatibility adapter
until route-closeout writers supply the explicit replan flag. Its substring
matching can overmatch narrative and is not used for successor resolution,
handoff state or permission. An explicit boolean replan flag takes precedence.

## Selection, proofs and transport

A fresh full-source evaluation accompanies each internal summary row as an
ephemeral Python attribute, outside dictionary fields and JSON serialization. Its fact
digest prevents reuse after relevant item edits; it is a consistency check,
not authentication. Fresh parsing/canonical reads always recompute it rather
than trusting stored evaluations. Shadow capture discards this derived field;
canonical records and durable source digests do not gain a second authority.
Public parser rows keep their existing dictionary schema. Final list/status
responses copy plain dictionaries, retaining decision fields without exposing
the internal evaluation or expanding the hot-path payload.

Legacy archive/recreate can retain one archived and one active record with the
same logical Todo ID. The active record owns that ID's inferred edges regardless
of source order; archived metadata cannot supply stale edges for the replacement.
Two active or two archived records with the same ID remain ambiguous and reject.
This read precedence does not relax canonical capture's unique-identity contract.

A status/ID/Agent-filtered list describes that selection but emits no Goal-source
or terminal-closure proof. A display limit alone does not change the source:
counts, warning decisions and proof eligibility are computed first. Handoff
state, successor count and executor exclusions survive the bounded list view.

Terminal closure additionally requires no deferred/convergent work, unresolved
handoff, successor gap or route-replan obligation. Watch-only monitors retain
the existing convergent-work exception. It remains separate from Goal acceptance.

Field-presence sets are interned inside a succession RPC request so long archive
histories do not repeat identical metadata shapes. The full graph is retained;
no record sampling, per-page rule evaluation or RPC limit increase is used.

## Migration and operation

The shared archive-capture owner retains the reachable continuation graph as
well as resume dependencies and standing decisions. It preserves real record
status, including deferred history: capturing a deferred record does **not**
satisfy `todo_done`. Duplicate identities, invalid archive state and incompatible
role/authority combinations still reject capture. Unrelated archive records
remain outside the bounded canonical capture.

The internal request is `todo_archive_dependency_capture_request_v1`. Python
and the bundled TS runtime must be upgraded together; an older runtime rejects
the new request instead of silently omitting continuation edges. Existing
historical capture/promotion receipts are not rewritten or upgraded in place.
Requalify capture on this runtime before a future promotion.

Use existing read commands; no activation or new option is needed:

```bash
loopx --registry registry.json todo list --goal-id example-goal --format json
loopx --registry registry.json todo list --goal-id example-goal --todo-id todo_source --limit 1 --format json
```

Completion retries also close a receipt/head read race: if the first receipt
lookup misses a peer commit but the head already shows completion, recheck the
matching operation receipt before interpreting a supplied validation receipt.
This returns the committed result without repeating effects; no matching
receipt still follows the existing validation and identity guards.

Reads do not repair Markdown, mutate Todo/lease state or replay a business
operation. Missing promoted Markdown is acceptable; an unavailable provider is
not an empty Goal. No frontend configuration changes are needed: CLI, manager
Chat details and existing status/quota consumers retain their current entry
points. To reverse a business decision, use its ordinary mutation, not a read
model or restored Markdown. Code rollback retains provider state and fences;
old read policies can again misclassify these cases.

## 中文

“这个 Todo 已完成”与“这一段工作已闭环”不同。结构化推进任务完成后,要有真实
存在的后继,或明确声明 `no_followup=true`。TS 现在统一解析显式后继、替代关系和
反向交接关系;Python 保留旧输入规范化与展示。不存在的 ID、自指 ID 不再遮住
未闭环工作,归档与筛选也不再凭空制造后继缺口。

关系在完整可用源上判定,然后才筛选、分页。按状态、ID 或 Agent 筛出的列表不能
为整个源出具闭环证明;仅限制显示条数不会改变完整源上的计数与判断。handoff 的
状态与排除执行者信息不会在压缩展示时丢失。派生判断带相关事实摘要以防陈旧复用,
但不是授权凭据,也不写回 provider。旧 prose replan 提示仍仅用于兼容;显式
布尔标记优先,不能靠标题里的几个词推导后继存在或授予权限。

归档捕获现在保留与当前工作有关的后继图和原有依赖、standing decision。延后历史
可以被保留,但状态仍是 deferred,绝不会因此满足 `todo_done`。新的内部 v1 请求
要求 Python 与 TS 配套升级;旧回执不被重新解释。长历史重复字段集合采用无损共享,
没有放宽 RPC 上限或丢弃历史节点。

本阶段关闭一组 T3/L5 读语义及其 L7 捕获依赖,不代表 D1 投影投递、D2 耐久性、
D3 整 Goal 切换完成,也不修改默认 provider。PostgreSQL 使用相同规则,服务部署与
资格仍独立。复杂 fixture 和只读快照演练不是长期 soak 或生产晋升许可。
2 changes: 2 additions & 0 deletions examples/control_plane/hot-path-interface-budget-smoke.py
Original file line number Diff line number Diff line change
Expand Up @@ -388,6 +388,8 @@ def main() -> int:
assert "presentation_surfaces" not in status_payload, status_payload
status_items = status_payload["attention_queue"]["items"]
assert status_items, status_payload
# Internal graph evaluations must not expand public status payloads.
assert "succession_evaluation" not in json.dumps(status_items)
assert "task_graph_projection" not in status_items[0], status_items[0]
route_health = status_payload["runtime_projection_routes"]
assert route_health["healthy"] is True, route_health
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -456,12 +456,14 @@ def assert_completed_successor_keeps_old_gate_cleared() -> None:
assert summary["current_agent_cleared_without_successor_handoff_count"] == 0, payload


def assert_superseded_completed_blocker_does_not_wake_agent() -> None:
def assert_resolved_supersession_does_not_wake_agent() -> None:
payload = build_quota_should_run(
status_payload(
[
primary_owned_todo(),
handoff_review(status="done", superseded_by="todo_value_successor"),
todo_item(todo_id="todo_value_successor", text="Verified replacement",
status="done", no_followup=True),
],
recommended_action="Wait for main-control after superseded handoff.",
),
Expand Down Expand Up @@ -503,7 +505,7 @@ def main() -> int:
assert_archived_completed_blocker_does_not_wake_agent()
assert_existing_successor_runs_normally()
assert_completed_successor_keeps_old_gate_cleared()
assert_superseded_completed_blocker_does_not_wake_agent()
assert_resolved_supersession_does_not_wake_agent()
assert_no_followup_completed_blocker_does_not_wake_agent()
print("quota-cleared-blocker-successor-gate-smoke ok")
return 0
Expand Down
7 changes: 4 additions & 3 deletions loopx/control_plane/coordination/runtime_shadow.py
Original file line number Diff line number Diff line change
Expand Up @@ -159,10 +159,11 @@ def capture_todo_archive_dependencies(todos: list[dict[str, Any]], state_text: s
_, archived, _ = parse_todo_source(state_text)
# No prose or wide diagnostics cross the selection transport budget.
capture_fields = ("todo_id", "role", "task_class", "status", "done", "archive_state", "resume_when",
"decision_scope", "decision_outcome", "global_gate", "blocks_agent", "bound_agent", "goal_bound")
"decision_scope", "decision_outcome", "global_gate", "blocks_agent", "bound_agent", "goal_bound",
"successor_todo_ids", "superseded_by", "unblocks_todo_id")
capture = effect_runtime_result("todo.archive.capture_dependencies", {
"schema_version": "todo_archive_dependency_capture_request_v0",
"active": [{"todo_id": item["todo_id"], "resume_when": item.get("resume_when")} for item in todos],
"schema_version": "todo_archive_dependency_capture_request_v1",
"active": [{key: item[key] for key in capture_fields if key in item} for item in todos],
"archived": [{key: item[key] for key in capture_fields if key in item} for item in archived],
})
if not isinstance(capture, dict) or capture.get("schema_version") != "todo_archive_dependency_capture_result_v0":
Expand Down
7 changes: 7 additions & 0 deletions loopx/control_plane/coordination/todo_terminal_lifecycle.ts
Original file line number Diff line number Diff line change
Expand Up @@ -805,6 +805,13 @@ export async function executeCoordinationTodoTerminalLifecycle(
todo_id: input.todo_id,
}, "decision_rejection");
}
// The first receipt read can precede a peer commit while this head already
// observes it. Recover only the matching operation receipt; never discard a
// validation receipt to manufacture a terminal replay from Todo state alone.
if (input.command === "complete" && todo.status === "done" && input.validation_receipt !== null) {
const committedReplay = await terminalReceipt(input, requestSha).read(store);
if (committedReplay !== null) return committedReplay;
}
if (input.expected_role !== null && todo.role !== input.expected_role) {
return terminalFailure(
"todo_role_mismatch",
Expand Down
3 changes: 3 additions & 0 deletions loopx/control_plane/effect_runtime_handlers.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import {evaluateUserCompletion} from "./todos/user_completion.ts";
import {projectTodoSuccession, projectTodoClosure} from "./todos/succession.ts";
import {projectTodoSummaryLanes, projectLegacyTodoWorkCounts} from "./todos/summary_lanes.ts";
import {recordDelegationAdoption, delegationInventoryItem, delegationInventoryQuery, delegationPreflight, delegationTurnPlanDecision, selectDelegationBinding, transitionDelegationObservation} from "./collaboration/delegation.ts";
import {planChatMode} from "./collaboration/chat_mode.ts";
Expand Down Expand Up @@ -412,6 +413,8 @@ export function createEffectRuntimeHandlers(
["todo.public_update.plan", planPublicTodoUpdate],
["todo.standing_decision.project", evaluateStandingDecisionProjection],
["todo.summary_lanes.project", projectTodoSummaryLanes],
["todo.succession.project", projectTodoSuccession],
["todo.succession.closure", projectTodoClosure],
["todo.work_counts.project", projectLegacyTodoWorkCounts],
["todo.decision_scope.evaluate", evaluateDecisionScope],
["todo.user_completion.plan", evaluateUserCompletion],
Expand Down
4 changes: 3 additions & 1 deletion loopx/control_plane/todos/active_state_todos.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@
read_canonical_todos_if_promoted,
)

from .succession_warning import public_todo_summary

MONITOR_WRITEBACK_CONTRACT_SCHEMA_VERSION = "monitor_writeback_contract_v0"


Expand Down Expand Up @@ -46,7 +48,7 @@ def _redacted_status_todo_fields(fields: dict[str, Any]) -> dict[str, Any]:
group = redacted.get(key)
if not isinstance(group, dict):
continue
group_copy = dict(group)
group_copy = public_todo_summary(group)
items: list[Any] = []
for item in group_copy.get("items") or []:
if not isinstance(item, dict):
Expand Down
Loading
Loading