Skip to content

Commit 96892b7

Browse files
feat(projection): kernel-owned projection envelope for status, global-summary and global-gates (#5085)
* feat(projection): kernel-owned projection envelope for status and global views Add loopx_projection_envelope_v0, sealed by the TypeScript kernel through projection.envelope.seal. Python adapters pass compact read facts only; TS decides freshness, alerts, and completeness. - status / --goal-id: per-source last_read_at and coverage relative to the requested scope; cached copies keep observed_at and restamp served_at - global-summary / global-gates: goal_quota source, upstream status envelope, and outside_current_registry omissions - Markdown renders a red projection line when stale, unreadable, missing, or incomplete - RFC: TypeScript control-plane migration section 2.6, baseline row, and correctness rule; reference contract and status data contract Signed-off-by: huangruiteng <14976749+huangruiteng@users.noreply.github.com> Co-authored-by: Cursor <cursoragent@cursor.com> Signed-off-by: huangruiteng <14976749+huangruiteng@users.noreply.github.com> Co-authored-by: Cursor <cursoragent@cursor.com> * fix(projection): preserve unknown membership and qualify status output budget Signed-off-by: huangruiteng <14976749+huangruiteng@users.noreply.github.com> * test(projection): qualify one-time emitted envelope growth at status boundary Signed-off-by: huangruiteng <14976749+huangruiteng@users.noreply.github.com> * refactor(projection): isolate output migration qualification rule Signed-off-by: huangruiteng <14976749+huangruiteng@users.noreply.github.com> --------- Signed-off-by: huangruiteng <14976749+huangruiteng@users.noreply.github.com> Co-authored-by: huangruiteng <14976749+huangruiteng@users.noreply.github.com> Co-authored-by: Cursor <cursoragent@cursor.com>
1 parent a5546af commit 96892b7

22 files changed

Lines changed: 1616 additions & 40 deletions

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

Lines changed: 51 additions & 1 deletion
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-09-13
6+
- Last revised: 2026-09-26
77
- Scope: an incremental, replacement-first migration of the LoopX control-plane
88
core from Python to TypeScript without maintaining two semantic
99
implementations
@@ -1407,6 +1407,52 @@ while public, persisted, RPC, or extension input still reaches its semantic
14071407
core through an unvalidated assertion. TypeScript complements runtime
14081408
validation; it does not replace it.
14091409

1410+
### 2.6 Projection envelope is a kernel read contract
1411+
1412+
The kernel already binds writes to receipts, fences and CAS. Reads need a
1413+
matching contract. A read model such as status, a global summary or a context
1414+
packet combines several sources read at different times, and it is often
1415+
consumed later from a cache, a saved file or a pasted packet. Without a
1416+
machine-checkable statement of what it observed, consumers, agents above all,
1417+
treat an old or partial projection as the current whole state. `ok: true`, a
1418+
passing check or a healthy host says nothing about that.
1419+
1420+
Every operator- or agent-facing projection therefore carries one
1421+
`projection_envelope` (`loopx_projection_envelope_v0`):
1422+
1423+
- `observed_at` and `served_at`: when the sources were read and when this copy
1424+
was emitted. A cache hit or replay keeps the first and restamps the second.
1425+
- One row per source with `last_read_at`, `read_status`, window, staleness and
1426+
alert reasons. A derived projection inherits its upstream rows, so it cannot
1427+
look fresher than the oldest read it depends on.
1428+
- `coverage` of the requested scope: expected and included counts, and named
1429+
omissions. Display truncation is disclosed separately and is not an
1430+
incompleteness alert.
1431+
1432+
Ownership follows this RFC instead of creating new migration debt.
1433+
`projection_envelope.ts` alone decodes the facts and decides staleness, alerts
1434+
and completeness, through runtime method `projection.envelope.seal`.
1435+
Python-owned projections only pass compact read facts. That is one request per
1436+
projection, on paths that already pin a runtime revision and make dozens of TS
1437+
calls. It is not a leaf migration: it keeps a new cross-cutting rule from
1438+
being born in Python and migrated later. The Python facts adapter exits with
1439+
its projection: when status projection moves into the kernel (already a
1440+
facade-exit condition in §4), TS gathers the facts directly and the adapter is
1441+
deleted.
1442+
1443+
Rollout. `status` (including `--goal-id` and projection-cache hits),
1444+
`global-summary` and `global-gates` now carry the envelope. Every other
1445+
`collect_status` caller receives the status envelope in its payload but does
1446+
not yet emit its own. Next, in order: `global-todos` and `global-risks` (the
1447+
same composition, one call each), `quota should-run`, `review-packet`, and
1448+
Decision Context packets. A read model added to or migrated into TypeScript
1449+
emits the envelope in the same PR; §6 makes this a promotion gate.
1450+
1451+
Consumers treat a missing envelope as unknown freshness, and disclose an
1452+
alerting one before stating any conclusion that depends on it. Field
1453+
semantics and the consumer rule are in the
1454+
[projection envelope contract](../../reference/contracts/projection-envelope-contract.md).
1455+
14101456
## 3. Current baseline and phase transition
14111457

14121458
Effect Program moved first because it joins ordered steps, identity,
@@ -1426,6 +1472,7 @@ choice is now implemented rather than hypothetical.
14261472
| 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 |
14271473
| 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 |
14281474
| 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 |
1475+
| 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 |
14291476
| 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 |
14301477

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

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

Lines changed: 42 additions & 1 deletion
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-09-13
6+
- Last revised:2026-09-26
77
- Scope:LoopX 控制面核心从 Python 到 TypeScript 的增量、replacement-first
88
迁移;不长期维护两份语义实现
99
- Tracking issue:[#3225](https://github.com/huangruiteng/loopx/issues/3225)
@@ -1049,6 +1049,43 @@ validator、负向边界覆盖和移除 owner。只要 public、持久化、RPC
10491049
输入仍通过未经验证的断言进入已迁 domain 的 semantic core,该 domain 就不能
10501050
通过 promotion gate。TypeScript 补充运行时验证,而不是替代它。
10511051

1052+
### 2.6 Projection envelope 是 kernel 级读合同
1053+
1054+
kernel 已经用 receipt、fence 与 CAS 约束写入,读取也需要对应的合同。status、
1055+
全局摘要、context packet 这类读模型由多个在不同时间读取的来源组合而成,而且常常
1056+
在事后通过 cache、保存的文件或粘贴的 packet 被消费。如果没有机器可检查的"它看到
1057+
了什么",消费者(尤其是 agent)会把旧的或不完整的投影当作当前的全貌。`ok: true`、
1058+
检查通过或 host 健康都不说明这一点。
1059+
1060+
因此每个面向 operator 或 agent 的投影都携带一个 `projection_envelope`
1061+
(`loopx_projection_envelope_v0`):
1062+
1063+
- `observed_at` 与 `served_at`:来源何时被读取、这份副本何时被输出。cache 命中或
1064+
重放保留前者、重盖后者。
1065+
- 每个来源一行,含 `last_read_at`、`read_status`、窗口、staleness 与告警原因。
1066+
派生投影继承上游的来源行,因此不可能显得比它依赖的最旧读取更新。
1067+
- 对所请求范围的 `coverage`:期望数与已包含数,以及具名的缺漏。显示截断单独披露,
1068+
不算不完整告警。
1069+
1070+
归属遵循本 RFC,而不是制造新的迁移债务。只有 `projection_envelope.ts` 解码这些
1071+
facts,并决定 staleness、告警与完整性,runtime 方法为 `projection.envelope.seal`。
1072+
Python 拥有的投影只传入紧凑的读取 facts。每个投影一次请求,而这些路径本来就固定
1073+
了 runtime revision、每次要发起几十次 TS 调用。这不是 leaf 迁移:它避免一条新的
1074+
横切规则先在 Python 里诞生、之后再迁移。Python facts adapter 随其投影退出:当
1075+
status projection 迁入 kernel(§4 已将其列为 facade 退出条件),由 TS 直接收集
1076+
facts,adapter 随之删除。
1077+
1078+
推广顺序。`status`(含 `--goal-id` 与 projection cache 命中)、`global-summary`
1079+
与 `global-gates` 现已携带 envelope。其他 `collect_status` 调用方会在 payload 里
1080+
收到 status envelope,但尚未输出自己的 envelope。下一步依次为:`global-todos` 与
1081+
`global-risks`(同样的组合,各一次调用)、`quota should-run`、`review-packet`,
1082+
以及 Decision Context packet。新加入或迁入 TypeScript 的读模型在同一个 PR 里输出
1083+
envelope;§6 把这一条定为 promotion 门禁。
1084+
1085+
消费者把缺失 envelope 视为新鲜度未知;envelope 告警时,必须先披露,再陈述依赖它的
1086+
结论。字段语义与消费者规则见
1087+
[projection envelope 合同](../../reference/contracts/projection-envelope-contract.md)(仅英文)。
1088+
10521089
## 3. 当前基线与阶段转换
10531090

10541091
Effect Program 先迁,是因为它连接 ordered step、identity、short-circuit failure、
@@ -1067,6 +1104,7 @@ replay、receipt 与 settlement。这个架构选择已经落地,不再是假
10671104
| 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 |
10681105
| Runtime decoder([#3443](https://github.com/huangruiteng/loopx/pull/3443)) | 稳定 primitive decoding 进入一个很小的共享模块;domain decoder 仍留在本地 | 没有理由建设更大的 schema framework |
10691106
| 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 |
1107+
| Projection envelope | TypeScript 拥有 `loopx_projection_envelope_v0` 的解码,以及全部 freshness、告警、完整性与重放判定 | 在 `status`、`global-summary`、`global-gates` 迁移前,Python 仍为它们收集读取 facts |
10701108

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

13871428
#### Caller 可观测语义是 promotion 门禁

‎docs/reference/contracts/README.md‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ implementation modules.
77
- [Dashboard budget governance](dashboard-budget-governance-contract.md)
88
- [Dashboard reward write boundary](dashboard-reward-write-boundary.md)
99
- [Reward gate direct-write contract](reward-gate-direct-write-contract.md)
10+
- [Projection envelope contract](projection-envelope-contract.md)
1011
- [Status data contract](../../status-data-contract.md)
1112
- [Quota allocation](../../quota-allocation.md)
1213
- [Project agent todo contract](../../project-agent-todo-contract.md)

‎docs/reference/contracts/interface-budget-contract.md‎

Lines changed: 38 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ and size/count budgets.
1111
| `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` |
1212
| `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` |
1313
| `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` |
14-
| `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` |
14+
| `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` |
1515

1616
These four budgets measure compact machine payloads. For
1717
`heartbeat_prompt_json`, the measured payload is the actual
@@ -241,3 +241,40 @@ RRULE, unchanged-state clear flag, and short identity/profile signatures needed
241241
to detect reset transitions. Full identity/profile snapshots stay off the hot
242242
path; use status, history, active state, or a focused regression fixture when
243243
debugging why a reset token changed.
244+
245+
### Status projection envelope budget decision
246+
247+
The unchanged dashboard fixture measured 19,455 compact JSON characters, 244
248+
nested keys and 25 top-level keys before the projection envelope; the initial
249+
envelope measured 21,518 / 332 / 26. The old 19,500 / 260 / 25 ceilings were
250+
regression budgets, not transport limits. The operator needs source read times,
251+
read failures and scope coverage to distinguish a cached or partial observation
252+
from a current, complete view. Per-source rows support diagnosis and replay;
253+
removing them would lose that contract. The bounded five-source envelope is
254+
retained rather than shortening names or shrinking the fixture. Ceilings become
255+
22,500 / 350 / 27, leaving 982 characters, 18 nested keys and one top-level key
256+
above the measured head for variation. Other hot surfaces retain their budgets.
257+
This adds a read contract to default status; it grants no execution authority.
258+
259+
同一 dashboard 负载在新增 envelope 前为 19,455 字符/244 个嵌套键/25 个顶层键,
260+
初始 head 为 21,518/332/26。旧上限属于回归预算而非传输硬限制。操作员需要
261+
来源读取时间、错误与范围覆盖来识别缓存和部分观察;逐来源数据还支撑诊断和重放,
262+
不能为过线删除。保留五个有界来源,不缩小负载或改短字段名,将上限同步调整为
263+
22,500/350/27,较实测 head 保留 982 字符、18 个嵌套键和一个顶层键的余量。
264+
其他热表面预算保持原值。默认 status 新增读合同,不授予执行权限。
265+
266+
The emitted CLI matrix separately measured +2,801 pretty JSON characters,
267+
+102 lines and +1,910 compact characters on small, crowded and multi-agent
268+
status fixtures. Markdown added 127 characters before the explicit schema
269+
marker. The existing schema-transition mechanism grants **only status and its
270+
explicit task-graph variant**, and only `none -> loopx_projection_envelope_v0`,
271+
3,000 JSON chars/bytes, 110 lines and 2,048 compact chars; Markdown receives
272+
192 chars/224 bytes and three lines. The marker makes this transition visible
273+
and review-required. Unknown schemas, reverse transitions, unrelated surfaces
274+
and subsequent v0 growth retain ordinary budgets. Absolute ceilings stay intact.
275+
276+
CLI 同负载差分另测得 JSON 增加 2,801 字符、102 行、1,910 个紧凑字符;Markdown
277+
在显式 schema 标识前增加 127 字符。沿用既有 schema 迁移预算机制,仅 status 及
278+
其 task-graph 显式变体的 none → v0 获得一次 3,000 JSON 字符/字节、110 行、
279+
2,048 紧凑字符余量;Markdown 余量为 192 字符/224 字节和三行。该迁移必须评审。
280+
未知 schema、反向迁移、其他表面和后续 v0 增长使用普通预算,绝对上限保持不变。

0 commit comments

Comments
 (0)