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
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
- Status: Accepted, transaction-payoff phase in progress
- Proposed by: LoopX maintainers
- Date: 2026-08-15
- Last revised: 2026-09-13
- Last revised: 2026-09-26
- Scope: an incremental, replacement-first migration of the LoopX control-plane
core from Python to TypeScript without maintaining two semantic
implementations
Expand Down Expand Up @@ -1407,6 +1407,52 @@ while public, persisted, RPC, or extension input still reaches its semantic
core through an unvalidated assertion. TypeScript complements runtime
validation; it does not replace it.

### 2.6 Projection envelope is a kernel read contract

The kernel already binds writes to receipts, fences and CAS. Reads need a
matching contract. A read model such as status, a global summary or a context
packet combines several sources read at different times, and it is often
consumed later from a cache, a saved file or a pasted packet. Without a
machine-checkable statement of what it observed, consumers, agents above all,
treat an old or partial projection as the current whole state. `ok: true`, a
passing check or a healthy host says nothing about that.

Every operator- or agent-facing projection therefore carries one
`projection_envelope` (`loopx_projection_envelope_v0`):

- `observed_at` and `served_at`: when the sources were read and when this copy
was emitted. A cache hit or replay keeps the first and restamps the second.
- One row per source with `last_read_at`, `read_status`, window, staleness and
alert reasons. A derived projection inherits its upstream rows, so it cannot
look fresher than the oldest read it depends on.
- `coverage` of the requested scope: expected and included counts, and named
omissions. Display truncation is disclosed separately and is not an
incompleteness alert.

Ownership follows this RFC instead of creating new migration debt.
`projection_envelope.ts` alone decodes the facts and decides staleness, alerts
and completeness, through runtime method `projection.envelope.seal`.
Python-owned projections only pass compact read facts. That is one request per
projection, on paths that already pin a runtime revision and make dozens of TS
calls. It is not a leaf migration: it keeps a new cross-cutting rule from
being born in Python and migrated later. The Python facts adapter exits with
its projection: when status projection moves into the kernel (already a
facade-exit condition in §4), TS gathers the facts directly and the adapter is
deleted.

Rollout. `status` (including `--goal-id` and projection-cache hits),
`global-summary` and `global-gates` now carry the envelope. Every other
`collect_status` caller receives the status envelope in its payload but does
not yet emit its own. Next, in order: `global-todos` and `global-risks` (the
same composition, one call each), `quota should-run`, `review-packet`, and
Decision Context packets. A read model added to or migrated into TypeScript
emits the envelope in the same PR; §6 makes this a promotion gate.

Consumers treat a missing envelope as unknown freshness, and disclose an
alerting one before stating any conclusion that depends on it. Field
semantics and the consumer rule are in the
[projection envelope contract](../../reference/contracts/projection-envelope-contract.md).

## 3. Current baseline and phase transition

Effect Program moved first because it joins ordered steps, identity,
Expand All @@ -1426,6 +1472,7 @@ choice is now implemented rather than hypothetical.
| Quota monitor-poll commit transaction | TypeScript owns monitor admission revalidation, target/event/result construction, effect replay/index CAS, provider intent, and repairable JSON/Markdown/index persistence | Python projects compact `should-run` facts, invokes the real Todo provider between at most two reductions, reloads legacy status, and holds the cross-writer index lock |
| 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 |
| 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 |
| Projection envelope | TypeScript owns decoding of `loopx_projection_envelope_v0` and every freshness, alert, completeness and replay decision | Python gathers read facts for `status`, `global-summary` and `global-gates` until those projections migrate |
| Promoted-authority Todo claim | TypeScript owns the provider-head read, lifecycle validation, complete-record update, hard-lease check, CAS, receipt, and readback-safe result for claims after authority promotion | Default local Markdown mode remains on the legacy writer; other Todo mutations and Markdown regeneration remain bounded follow-ups |

The scheduler facade exit now includes its first bounded Stage 3 route. A
Expand Down Expand Up @@ -1794,6 +1841,9 @@ not authorize a generic schema framework.
concurrent same-key mutations are serialized or use a tested CAS contract,
and retry identity distinguishes successive checkpoints within one Turn.
- Process crash and retry cannot duplicate a committed internal effect.
- An operator- or agent-facing read model that is added or migrated emits
`projection_envelope` through `projection.envelope.seal`. Its tests cover a
stale source, an unreadable source, an incomplete scope and a replayed copy.
- Wheel and sdist are installed into fresh environments and execute deep
semantic probes from packaged files.

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
- Status:Accepted,transaction-payoff 阶段进行中
- Proposed by:LoopX maintainers
- Date:2026-08-15
- Last revised:2026-09-13
- Last revised:2026-09-26
- Scope:LoopX 控制面核心从 Python 到 TypeScript 的增量、replacement-first
迁移;不长期维护两份语义实现
- Tracking issue:[#3225](https://github.com/huangruiteng/loopx/issues/3225)
Expand Down Expand Up @@ -1049,6 +1049,43 @@ validator、负向边界覆盖和移除 owner。只要 public、持久化、RPC
输入仍通过未经验证的断言进入已迁 domain 的 semantic core,该 domain 就不能
通过 promotion gate。TypeScript 补充运行时验证,而不是替代它。

### 2.6 Projection envelope 是 kernel 级读合同

kernel 已经用 receipt、fence 与 CAS 约束写入,读取也需要对应的合同。status、
全局摘要、context packet 这类读模型由多个在不同时间读取的来源组合而成,而且常常
在事后通过 cache、保存的文件或粘贴的 packet 被消费。如果没有机器可检查的"它看到
了什么",消费者(尤其是 agent)会把旧的或不完整的投影当作当前的全貌。`ok: true`、
检查通过或 host 健康都不说明这一点。

因此每个面向 operator 或 agent 的投影都携带一个 `projection_envelope`
(`loopx_projection_envelope_v0`):

- `observed_at` 与 `served_at`:来源何时被读取、这份副本何时被输出。cache 命中或
重放保留前者、重盖后者。
- 每个来源一行,含 `last_read_at`、`read_status`、窗口、staleness 与告警原因。
派生投影继承上游的来源行,因此不可能显得比它依赖的最旧读取更新。
- 对所请求范围的 `coverage`:期望数与已包含数,以及具名的缺漏。显示截断单独披露,
不算不完整告警。

归属遵循本 RFC,而不是制造新的迁移债务。只有 `projection_envelope.ts` 解码这些
facts,并决定 staleness、告警与完整性,runtime 方法为 `projection.envelope.seal`。
Python 拥有的投影只传入紧凑的读取 facts。每个投影一次请求,而这些路径本来就固定
了 runtime revision、每次要发起几十次 TS 调用。这不是 leaf 迁移:它避免一条新的
横切规则先在 Python 里诞生、之后再迁移。Python facts adapter 随其投影退出:当
status projection 迁入 kernel(§4 已将其列为 facade 退出条件),由 TS 直接收集
facts,adapter 随之删除。

推广顺序。`status`(含 `--goal-id` 与 projection cache 命中)、`global-summary`
与 `global-gates` 现已携带 envelope。其他 `collect_status` 调用方会在 payload 里
收到 status envelope,但尚未输出自己的 envelope。下一步依次为:`global-todos` 与
`global-risks`(同样的组合,各一次调用)、`quota should-run`、`review-packet`,
以及 Decision Context packet。新加入或迁入 TypeScript 的读模型在同一个 PR 里输出
envelope;§6 把这一条定为 promotion 门禁。

消费者把缺失 envelope 视为新鲜度未知;envelope 告警时,必须先披露,再陈述依赖它的
结论。字段语义与消费者规则见
[projection envelope 合同](../../reference/contracts/projection-envelope-contract.md)(仅英文)。

## 3. 当前基线与阶段转换

Effect Program 先迁,是因为它连接 ordered step、identity、short-circuit failure、
Expand All @@ -1067,6 +1104,7 @@ replay、receipt 与 settlement。这个架构选择已经落地,不再是假
| Quota monitor-poll commit transaction | TypeScript 拥有 monitor admission 复核、target/event/result 构造、effect replay/index CAS、provider intent,以及可修复的 JSON/Markdown/index persistence | Python 投影 compact `should-run` facts,在最多两次 reduction 之间调用真实 Todo provider,刷新 legacy status,并持有 cross-writer index lock |
| Runtime decoder([#3443](https://github.com/huangruiteng/loopx/pull/3443)) | 稳定 primitive decoding 进入一个很小的共享模块;domain decoder 仍留在本地 | 没有理由建设更大的 schema framework |
| 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 |
| Projection envelope | TypeScript 拥有 `loopx_projection_envelope_v0` 的解码,以及全部 freshness、告警、完整性与重放判定 | 在 `status`、`global-summary`、`global-gates` 迁移前,Python 仍为它们收集读取 facts |

Scheduler facade exit 已交付第一段有边界的 Stage 3 路径。带版本的
`heartbeat_followup_cli.ts` 从生成的 ACK/failure hint 接收有大小上限的 compact host
Expand Down Expand Up @@ -1382,6 +1420,9 @@ happy path 及其 retry/recovery path 上实测,不能由 handler 数量推断
并发 mutation 必须串行化或使用经过测试的 CAS 合同,retry identity 必须区分同一
Turn 内连续发生的 checkpoint。
- 进程 crash 与 retry 不得重复已经提交的内部 effect。
- 新增或迁移的、面向 operator 或 agent 的读模型通过 `projection.envelope.seal`
输出 `projection_envelope`;其测试覆盖 stale 来源、不可读来源、不完整范围与
重放副本。
- wheel 与 sdist 安装到全新环境后,从打包文件执行 deep semantic probe。

#### Caller 可观测语义是 promotion 门禁
Expand Down
1 change: 1 addition & 0 deletions docs/reference/contracts/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ implementation modules.
- [Dashboard budget governance](dashboard-budget-governance-contract.md)
- [Dashboard reward write boundary](dashboard-reward-write-boundary.md)
- [Reward gate direct-write contract](reward-gate-direct-write-contract.md)
- [Projection envelope contract](projection-envelope-contract.md)
- [Status data contract](../../status-data-contract.md)
- [Quota allocation](../../quota-allocation.md)
- [Project agent todo contract](../../project-agent-todo-contract.md)
Expand Down
39 changes: 38 additions & 1 deletion docs/reference/contracts/interface-budget-contract.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ and size/count budgets.
| `heartbeat_prompt_json` | heartbeat automation | wake and route one bounded turn | `quota should-run`, `status`, or `review-packet --handoff-only` | `json_chars <= 5400` plus `interface_budget.within_budget=true` | `nested_keys <= 40` | `top_level_keys <= 30` |
| `review_packet_handoff_only_json` | project-agent handoff | forward the smallest sufficient task packet | full `review-packet` or run-history artifact | `json_chars <= 3000` plus `handoff_interface_budget.within_budget=true` | `nested_keys <= 40` | `top_level_keys <= 18` |
| `quota_should_run_json` | quota guard | decide whether the selected goal may spend compute | `status`, `history`, or active state | `json_chars <= 14500` | `nested_keys <= 360` | `top_level_keys <= 52` |
| `dashboard_status_json` | operator dashboard | render first-screen operator state | `history`, run artifacts, or project-local adapter output | `json_chars <= 19500` | `nested_keys <= 260` | `top_level_keys <= 25` |
| `dashboard_status_json` | operator dashboard | render first-screen operator state | `history`, run artifacts, or project-local adapter output | `json_chars <= 22500` | `nested_keys <= 350` | `top_level_keys <= 27` |

These four budgets measure compact machine payloads. For
`heartbeat_prompt_json`, the measured payload is the actual
Expand Down Expand Up @@ -241,3 +241,40 @@ RRULE, unchanged-state clear flag, and short identity/profile signatures needed
to detect reset transitions. Full identity/profile snapshots stay off the hot
path; use status, history, active state, or a focused regression fixture when
debugging why a reset token changed.

### Status projection envelope budget decision

The unchanged dashboard fixture measured 19,455 compact JSON characters, 244
nested keys and 25 top-level keys before the projection envelope; the initial
envelope measured 21,518 / 332 / 26. The old 19,500 / 260 / 25 ceilings were
regression budgets, not transport limits. The operator needs source read times,
read failures and scope coverage to distinguish a cached or partial observation
from a current, complete view. Per-source rows support diagnosis and replay;
removing them would lose that contract. The bounded five-source envelope is
retained rather than shortening names or shrinking the fixture. Ceilings become
22,500 / 350 / 27, leaving 982 characters, 18 nested keys and one top-level key
above the measured head for variation. Other hot surfaces retain their budgets.
This adds a read contract to default status; it grants no execution authority.

同一 dashboard 负载在新增 envelope 前为 19,455 字符/244 个嵌套键/25 个顶层键,
初始 head 为 21,518/332/26。旧上限属于回归预算而非传输硬限制。操作员需要
来源读取时间、错误与范围覆盖来识别缓存和部分观察;逐来源数据还支撑诊断和重放,
不能为过线删除。保留五个有界来源,不缩小负载或改短字段名,将上限同步调整为
22,500/350/27,较实测 head 保留 982 字符、18 个嵌套键和一个顶层键的余量。
其他热表面预算保持原值。默认 status 新增读合同,不授予执行权限。

The emitted CLI matrix separately measured +2,801 pretty JSON characters,
+102 lines and +1,910 compact characters on small, crowded and multi-agent
status fixtures. Markdown added 127 characters before the explicit schema
marker. The existing schema-transition mechanism grants **only status and its
explicit task-graph variant**, and only `none -> loopx_projection_envelope_v0`,
3,000 JSON chars/bytes, 110 lines and 2,048 compact chars; Markdown receives
192 chars/224 bytes and three lines. The marker makes this transition visible
and review-required. Unknown schemas, reverse transitions, unrelated surfaces
and subsequent v0 growth retain ordinary budgets. Absolute ceilings stay intact.

CLI 同负载差分另测得 JSON 增加 2,801 字符、102 行、1,910 个紧凑字符;Markdown
在显式 schema 标识前增加 127 字符。沿用既有 schema 迁移预算机制,仅 status 及
其 task-graph 显式变体的 none → v0 获得一次 3,000 JSON 字符/字节、110 行、
2,048 紧凑字符余量;Markdown 余量为 192 字符/224 字节和三行。该迁移必须评审。
未知 schema、反向迁移、其他表面和后续 v0 增长使用普通预算,绝对上限保持不变。
Loading
Loading