From d6a0720e739c08d43bffec7d088f3cacc9d9977c Mon Sep 17 00:00:00 2001 From: wefio <48851810+wefio@users.noreply.github.com> Date: Sun, 20 Sep 2026 22:59:42 +0800 Subject: [PATCH 01/13] docs(vocabulary): the collaboration protocol and its task-unit sub-protocol get names Two levels with one meaning each. Protocol-governed collaboration is the umbrella: agents coordinate through a published protocol and the mechanics stay with the program. The Task-Unit Protocol is the sub-protocol this line builds - a task is declared as a unit and the protocol fixes how it is adopted into one run, claimed, delivered and independently judged, and which facts the runtime decides. The parts keep the names they already have: Task Board is the sub-protocol's surface, managed entry is what a governed entry is, run is the frozen object, adopt is the binding transition, and the caller that decides to adopt is called the adopter. The words that came to hand were already taken - fusion (units sharing one session), scheduler (which the task-unit semantics decision deliberately does not enable), governance (the board addressing line), ledger (three of them) and executor (the bounded context adapters) - so each is recorded as considered and rejected rather than reused. No identifier or file name changes, for the reason the check-runner renaming recorded: `ooo` and the existing paths are cited by dated measurement records and frozen run archives. The name lives in the concept map with its aliases, and docs/glossary.yaml is untouched because it owns the repository and process vocabulary, not product concepts. --- ...6-09-20-name-the-collaboration-protocol.md | 69 +++++++++++++++++++ ...0-name-the-collaboration-protocol.zh-CN.md | 55 +++++++++++++++ docs/design/task-unit-semantics.md | 2 + docs/guides/concept-map.md | 2 + docs/guides/concept-map.zh-CN.md | 2 + 5 files changed, 130 insertions(+) create mode 100644 docs/decisions/implemented/2026-09-20-name-the-collaboration-protocol.md create mode 100644 docs/decisions/implemented/2026-09-20-name-the-collaboration-protocol.zh-CN.md diff --git a/docs/decisions/implemented/2026-09-20-name-the-collaboration-protocol.md b/docs/decisions/implemented/2026-09-20-name-the-collaboration-protocol.md new file mode 100644 index 00000000..2cdc294d --- /dev/null +++ b/docs/decisions/implemented/2026-09-20-name-the-collaboration-protocol.md @@ -0,0 +1,69 @@ +# Name the collaboration protocol and its task-unit sub-protocol + +[中文](2026-09-20-name-the-collaboration-protocol.zh-CN.md) + +**Status:** implemented +**Approved:** explicit +**Relates to:** [Task unit semantics](../../design/task-unit-semantics.md), [The dispatch loop is shared](2026-09-19-dispatch-loop-is-shared.md) + +## Problem + +The arrangement this line is building had no name, so every discussion had to re-derive what it +meant: whether the subject was the board, the run object, the verbs an Agent uses, or the rules that +decide. The words that came to hand were already taken in ways that invite the wrong reading. +_Fusion_ names one mechanism (several units sharing a session: `sharedSessionLegal`, +`fusionAccounting`), and a _scheduler_ is what the task-unit semantics decision explicitly does not +enable. _Governance_ is the board addressing and readability line, _ledger_ already names three +things (assumptions, budget, disclosure), and _executor_ names the bounded context adapters and the +Pi SDK execution path. Naming the new thing after any of those would make the conversation shorter +and the understanding worse. + +## Decision + +Two levels, each with one meaning: + +- **Protocol-governed collaboration** is the umbrella: agents coordinate through a published + protocol, and the mechanics of that coordination belong to the program rather than to the model's + discussion. It is the larger idea the board's verbs are one instance of. +- **Task-Unit Protocol** is the sub-protocol this line builds: a task is declared as a unit (inputs, + dependencies, acceptance, capability, budget), and the protocol fixes how that unit is adopted into + one run, claimed, delivered and independently judged, and which facts the runtime decides and owns. + "Task unit" is the word the design already uses; the protocol half is what was missing. + +The parts keep their existing names instead of acquiring new ones: **Task Board** is the +sub-protocol's surface, **managed entry** is what an entry becomes once a run governs it, **run** is +the object whose plan, tasks and facts are frozen, and **adopt** is the transition that binds an +entry to a run. The one thing with no name is the caller that decides to adopt; this record calls it +the **adopter**. + +## Alternatives considered + +- **Keep saying OoO.** Rejected as the umbrella: `ooo` is the project's own name for the scheduling + model it implements and is load-bearing in more than a hundred documents, but it names the model, + not the two-part arrangement of who discusses and who decides, so it does not remove the ambiguity + the naming exists for. +- **Board-governed execution.** Rejected: `governance` is already the board addressing and + readability line, and the phrase reads as the board deciding, which is the opposite of the point + that the program decides. +- **Task-unit lifecycle protocol.** Rejected as too long to say; the lifecycle reading is recoverable + from the definition without being carried in the name. +- **Managed run protocol.** Rejected because it drops the semantics half, which is the larger part of + the design and the half the board cannot check by itself. +- **A name built on "fusion".** Rejected: execution fusion already names one mechanism, and adoption + and dispatch happen whether or not any session is fused. +- **Rename code or the design document to match.** Rejected for the reason the check-runner renaming + recorded: the `ooo` term and the existing paths are cited by dated measurement records and frozen + run archives, so a rename would leave evidence pointing at paths that no longer exist. + +## Consequences + +- A discussion can now name its level: protocol-governed collaboration for the whole arrangement, the + Task-Unit Protocol for the semantics-plus-runtime sub-protocol, the adopter for the missing caller, + and the existing words (board, run, adopt, managed entry) for the parts. +- No identifier changes. `run`, `adopt`, `managed`, `dispatch`, the `ooo-` file prefix and every file + name stay as they are, so no historical record needs repairing. +- The name lives in the concept map with aliases for search. `docs/glossary.yaml` is untouched: it + owns the repository and process vocabulary, and this is a product concept. +- The naming builds nothing. The sub-protocol's mechanism exists (the run surface, the managed-write + fence, the shared dispatch loop) and the product path still has no adopter, which is the next piece + of work rather than a consequence of the name. diff --git a/docs/decisions/implemented/2026-09-20-name-the-collaboration-protocol.zh-CN.md b/docs/decisions/implemented/2026-09-20-name-the-collaboration-protocol.zh-CN.md new file mode 100644 index 00000000..27cfb315 --- /dev/null +++ b/docs/decisions/implemented/2026-09-20-name-the-collaboration-protocol.zh-CN.md @@ -0,0 +1,55 @@ +# 给协作协议与它的任务单元子协议定名 + +[English](2026-09-20-name-the-collaboration-protocol.md) + +**Status:** implemented +**Approved:** explicit +**Relates to:** [任务单元语义](../../design/task-unit-semantics.md)、[派发循环是共享的](2026-09-19-dispatch-loop-is-shared.zh-CN.md) + +## 问题 + +这条线在做的东西一直没有名字,于是每次讨论都要重新推导指的是什么:是黑板、是 run 这个对象、是 +Agent 用的那几个动词,还是拍板的那些规则。顺手能用的词又都已经被占,而且占法会引导出错误理解: +**融合** 已经指一个具体机制(多单元共享一个会话:`sharedSessionLegal`、`fusionAccounting`); +**调度器** 是任务单元语义那条决策明确说不启用的东西;**治理** 已经是黑板地址与可读性那一线; +**台账** 已经指三样(假设、预算、披露);**executor** 已经指受限的 context executor 适配器和 Pi +SDK 执行路径。用其中任何一个给新东西命名,讨论是短了,理解会更差。 + +## 决策 + +分两层,每一层一个含义: + +- **协议化协作(Protocol-Governed Collaboration)** 是大类:agent 之间通过一份公开的协议协商, + 而协商的机制属于程序,不属于模型的讨论。黑板的那些动词只是它的一个实例。 +- **任务单元协议(Task-Unit Protocol)** 是这条线在做的子协议:把任务声明成单元(输入、依赖、 + 验收、能力、预算),并规定它如何被收编进一次 run、如何被认领、交付与独立裁决,以及哪些事实由 + 运行时拍板并负责。"任务单元"是设计里已有的词,缺的是"协议"的那一半。 + +各组成部分沿用已有名字,不另起:**黑板(Task Board)** 是这个子协议的面;**受管条目(managed +entry)** 是条目被某次 run 治理之后的状态;**run** 是计划、任务与事实被冻结的那个对象; +**adopt(收编)** 是把条目绑到一次 run 的那次转移。唯一没有名字的是"决定收编的调用方",本记录叫它 +**收编者(adopter)**。 + +## 考虑过的替代方案 + +- **继续叫 OoO。** 作为大类被拒:`ooo` 是项目给自己实现的调度模型起的名字、在 100 多份文档里承重, + 但它指的是模型,不是"谁讨论、谁拍板"这个二分,所以消不掉定名要消的歧义。 +- **黑板治理的执行(Board-Governed Execution)。** 被拒:`治理` 已经是黑板地址与可读性那一线,而且 + 这个说法读起来像"黑板在拍板",与"程序拍板"正好相反。 +- **任务单元生命周期协议。** 被拒:太长,不好说;生命周期那层意思从定义里读得出来,不必写进名字。 +- **受管运行协议(Managed Run Protocol)。** 被拒:丢了语义那一半,而那是设计的主体,也是黑板自己 + 查不了的那一半。 +- **名字里带"融合"。** 被拒:执行融合已经指一个具体机制(多单元共享一个会话),而收编与派发无论有 + 没有融合会话都会发生。 +- **为了让名字成立去改代码或设计文档名。** 被拒,理由与"给检查身份与检查运行器定名"那条记录一致: + `ooo` 这个词与现有路径被带日期的测量记录和冻结的运行归档引用,改名会让证据指向不存在的路径。 + +## 后果 + +- 讨论可以指名层次了:整体叫协议化协作,语义加运行时那一层叫任务单元协议,缺的调用方叫收编者, + 其余部分沿用黑板、run、adopt、受管条目这些已有词。 +- 不改任何标识符。`run`、`adopt`、`managed`、`dispatch`、`ooo-` 文件前缀以及所有文件名保持原样, + 因此没有历史记录需要修补。 +- 名字住在概念图里并带别名以便检索。`docs/glossary.yaml` 不动:它负责仓库与流程词汇,而这是产品概念。 +- 定名本身不建任何东西。子协议的机制已经在(run 面、受管写入围栏、共享派发循环),产品路径仍然没有 + 收编者——那是下一步工作,不是这个名字的后果。 diff --git a/docs/design/task-unit-semantics.md b/docs/design/task-unit-semantics.md index f9f83e48..2aa49f69 100644 --- a/docs/design/task-unit-semantics.md +++ b/docs/design/task-unit-semantics.md @@ -5,6 +5,8 @@ ## 问题与目标 +本仓库的术语里,本文描述的协议叫**任务单元协议(Task-Unit Protocol)**,是**协议化协作(Protocol-Governed Collaboration)**的子协议;定名记录见[命名决策](../decisions/implemented/2026-09-20-name-the-collaboration-protocol.md)。 + 任务单元是可以交接、验证和独立作废的工作;Agent 是执行这些工作的资源。两者不必一一对应。先表达工作成立的条件,再由运行时决定分配给谁、是否连续执行,才可能同时获得细粒度并发与上下文复用。 本文提出现有任务契约的内部编译视图(Task IR),不新增用户输入语言或独立持久化 schema。首个检验对象是冻结仓库上的提案型工作。自然语言仍描述意图;结构化字段描述输入、产物、权限和可检查义务。无法形式化的意图保留人工或独立评审,不伪装成编译器能证明的事实。 diff --git a/docs/guides/concept-map.md b/docs/guides/concept-map.md index 9541afd8..8b8ce21a 100644 --- a/docs/guides/concept-map.md +++ b/docs/guides/concept-map.md @@ -45,6 +45,8 @@ directory; `get` opens selected exact evidence. | QPP | Optional prediction of whether retrieval is broad and complete enough | Progressive recall needs a reason to stop, expand, or fold noise | [retrieval confidence controller](../design/retrieval-confidence-controller.md) | | Memory chain | A bounded ordered view over existing memory IDs | Some tasks need event order or explicit dependencies without copying evidence | [design.md §7.6](../design/design.md#76-static-temporal-and-logical-memory-chains) | | Task Board | Attributed, expiring, task-scoped Agent coordination outside semantic memory | Private AGs cannot communicate directly across Agents | [memory-graphs.md §2.1](../design/memory-graphs.md#21-task-board-outside-the-three-memory-graphs) | +| Protocol-governed collaboration | Agents coordinate through a published protocol, while the mechanics and the deciding stay with the program | Coordination needs a name that separates who discusses from who decides | [Name the collaboration protocol](../decisions/implemented/2026-09-20-name-the-collaboration-protocol.md) | +| Task-Unit Protocol | A task is declared as a unit and adopted into one run, where it is claimed, delivered and independently judged | The board alone cannot say what a unit's inputs, dependencies and acceptance are | [task-unit-semantics.md](../design/task-unit-semantics.md) | | Maintenance and consolidation | Deterministic index work plus evidence-gated semantic promotion or topology proposals | Write cost must stay bounded and repeated use must not manufacture truth | [design.md §10](../design/design.md#10-incremental-storage-and-index-maintenance) | | Learnable controller | An optional numeric policy over hard-bounded allocation, fold, and rerank decisions | Natural outcome evidence may improve control without making memory graphs differentiable | [design.md §12](../design/design.md#12-learnable-routing-and-minimal-differentiable-query-graphs) | | Lab | Explicitly leased optional capabilities such as reasoning workspace and graph reasoner | Experimental mechanisms must be usable without silently becoming defaults | [design.md §12ter](../design/design.md#12ter-session-reasoning-workspace-and-compaction-checkpoint) | diff --git a/docs/guides/concept-map.zh-CN.md b/docs/guides/concept-map.zh-CN.md index 3c8eb671..1e70c62b 100644 --- a/docs/guides/concept-map.zh-CN.md +++ b/docs/guides/concept-map.zh-CN.md @@ -41,6 +41,8 @@ flowchart LR | QPP | 可选的检索广度与充分性预测 | 渐进式回忆需要决定停止、扩展或折叠噪声 | [检索置信度控制器](../design/retrieval-confidence-controller.md) | | Memory chain | 对现有 memory ID 的有界有序视图 | 时间顺序和显式依赖不应复制证据 | [design.md §7.6](../design/design.md#76-static-temporal-and-logical-memory-chains) | | Task Board | 位于语义记忆之外、有归因、有过期时间的任务级协作区 | 私有 AG 不能直接完成跨 Agent 通讯 | [memory-graphs.md §2.1](../design/memory-graphs.md#21-task-board-outside-the-three-memory-graphs) | +| 协议化协作 | agent 通过一份公开协议协商,机制与拍板留在程序侧 | 协作需要一个把“谁讨论、谁拍板”分开的名字 | [命名决策](../decisions/implemented/2026-09-20-name-the-collaboration-protocol.zh-CN.md) | +| 任务单元协议 | 任务被声明为单元并收编进一次 run,在其中被认领、交付与独立裁决 | 黑板自己说不清一个单元的输入、依赖与验收是什么 | [task-unit-semantics.md](../design/task-unit-semantics.md) | | 维护与巩固 | 确定性索引维护,加上有证据门控的语义晋升或拓扑 proposal | 写入成本必须有界,重复检索不能制造“事实” | [design.md §10](../design/design.md#10-incremental-storage-and-index-maintenance) | | 可学习控制器 | 在硬预算内可选地学习 allocate、fold 和 rerank 的数值策略 | 可利用自然结果改进控制,但无需让记忆图本身可微 | [design.md §12](../design/design.md#12-learnable-routing-and-minimal-differentiable-query-graphs) | | Lab | 通过显式 lease 使用的 reasoning workspace、graph reasoner 等可选能力 | 实验机制可以被使用,但不能静默成为默认行为 | [design.md §12ter](../design/design.md#12ter-session-reasoning-workspace-and-compaction-checkpoint) | From ebc053d6c264cb6a1afd97e1d775cdbcc96c34cf Mon Sep 17 00:00:00 2001 From: wefio <48851810+wefio@users.noreply.github.com> Date: Sun, 20 Sep 2026 23:22:54 +0800 Subject: [PATCH 02/13] docs(vocabulary): the names get one index, with their owners The definitions were spread over the design, the obligations ledger and several decision records, so reading them meant opening four files at once. The naming record now carries an index section: one line of meaning and a pointer per name, for both levels, the board verbs and the channel rules, the run and its facts, the legal action set and the dispatch loop. It ends with the two words deliberately not used for this protocol - scheduler and OoO - each with the owner of its real meaning, because reusing either is what made the discussion ambiguous in the first place. The section states its own status: an index, not a second specification, and where a row and its owner disagree the owner wins. Same content in the zh pair. Found while checking the links: the concept map's Task Board row points at an anchor that does not exist in memory-graphs.md, and docs:check does not catch it because it validates file existence rather than headings. The index uses the heading that is really there. --- ...6-09-20-name-the-collaboration-protocol.md | 35 +++++++++++++++++++ ...0-name-the-collaboration-protocol.zh-CN.md | 34 ++++++++++++++++++ 2 files changed, 69 insertions(+) diff --git a/docs/decisions/implemented/2026-09-20-name-the-collaboration-protocol.md b/docs/decisions/implemented/2026-09-20-name-the-collaboration-protocol.md index 2cdc294d..d599a932 100644 --- a/docs/decisions/implemented/2026-09-20-name-the-collaboration-protocol.md +++ b/docs/decisions/implemented/2026-09-20-name-the-collaboration-protocol.md @@ -36,6 +36,41 @@ the object whose plan, tasks and facts are frozen, and **adopt** is the transiti entry to a run. The one thing with no name is the caller that decides to adopt; this record calls it the **adopter**. +## The names, in one place + +This is an index, not a second specification: one line of meaning and a pointer per name, and where a +row and its owner disagree, the owner wins. It exists because the definitions used to be reachable +only by opening the design, the obligations ledger and the decision records side by side. + +The two levels and what they are made of: + +| Name | One line | Contract owner | +| ------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Protocol-governed collaboration | Agents coordinate through a published protocol, while the mechanics and the deciding stay with the program | this record; [concept map](../../guides/concept-map.md) | +| Task-Unit Protocol | A task is declared as a unit and the protocol fixes its adoption, claim, delivery, judging and the facts the runtime owns | [task-unit-semantics.md](../../design/task-unit-semantics.md), [its obligations ledger](../../design/task-unit-semantics-obligations.md) | +| task unit | A unit of work that can be handed off, verified and independently voided, declared by inputs, dependencies, acceptance, capability and budget | [task-unit-semantics.md](../../design/task-unit-semantics.md) | +| Task Board | Attributed, expiring, task-scoped Agent coordination outside semantic memory | [memory-graphs.md §2](../../design/memory-graphs.md#shared-task-board-cross-agent-coordination-not-a-memory-graph) | +| entry | One board item - goal, question, handoff, blocker, result, note or decision - carrying a claim lease, deliveries and verdicts | [board-find-serial-a2a-compat](../../design/board-find-serial-a2a-compat-2026-08-13.md) | +| wake | A directed entry notifies that Agent's session; the board does not decide who works, only that someone was addressed | [board-find-serial-a2a-compat](../../design/board-find-serial-a2a-compat-2026-08-13.md) | +| serial channel | At most one un-directed actionable entry is pushed at a time; the next is promoted when the outstanding one is claimed or resolved | [board-find-serial-a2a-compat](../../design/board-find-serial-a2a-compat-2026-08-13.md) | +| claim, release, resolve | The lease verbs: one holder at a time, expiry returns the entry to the pool, a resolve closes it | [board governance and addressing](2026-09-06-board-governance-addressing.md) | +| deliver, judge | A claim ends in a digest-bound deliverable, judged by a different Agent (accepted, rejected, undecidable) | [board governance and addressing](2026-09-06-board-governance-addressing.md) | +| managed entry | An entry a run has adopted: its lifecycle verbs are refused outside that run's coordinated scope | [obligations ledger, B6](../../design/task-unit-semantics-obligations.md) | +| run | The frozen object: a registered run, one frozen plan, bound entries and the fact log, reached over `taskRun` | [obligations ledger, D11 and D13](../../design/task-unit-semantics-obligations.md) | +| adopt | The transition that binds a board entry to a run, recorded as the run fact `entry-bound` | [obligations ledger, D12](../../design/task-unit-semantics-obligations.md) | +| adopter | The caller that decides to adopt; the one name here with no product implementation yet | this record; [obligations ledger, what is left](../../design/task-unit-semantics-obligations.md) | +| run fact | One recorded transition of a run: `entry-bound`, `board-claim`, `board-deliver`, `board-judge`, `run-cancelled` | `src/integration/task-coordinator.ts` | +| legal action set | The deterministically computed set a source may rank within, cut to the declared slot budget | [declared slot budget](2026-09-18-declared-slot-budget.md), `src/integration/ooo-execution.ts` | +| dispatch loop | The shared sequential loop that drives a plan unit by unit, with the board behind a port | [the dispatch loop is shared](2026-09-19-dispatch-loop-is-shared.md) | +| session, fusion | A unit's session is keyed on the board rather than the harness; fusion is several units sharing one session, a mechanism and not this protocol's name | [session identity comes from the board](2026-09-19-session-identity-comes-from-the-board.md), [fusion legality and accounting](2026-09-18-fusion-legality-and-accounting.md) | + +Two words that are deliberately not used for this protocol: + +| Word | Why not | Owner of the real meaning | +| ----------- | ----------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------ | +| scheduler | The semantics deliberately does not enable one: ordering is deterministic rules over the legal set plus a declared budget | [task-unit semantics](2026-09-13-task-unit-semantics.md) | +| OoO, `ooo-` | The project's own name for the scheduling model it implements, and the file prefix; it names the model, not who discusses and who decides | [ooo-execution-bootstrap](../../design/ooo-execution-bootstrap.md) | + ## Alternatives considered - **Keep saying OoO.** Rejected as the umbrella: `ooo` is the project's own name for the scheduling diff --git a/docs/decisions/implemented/2026-09-20-name-the-collaboration-protocol.zh-CN.md b/docs/decisions/implemented/2026-09-20-name-the-collaboration-protocol.zh-CN.md index 27cfb315..347ab1af 100644 --- a/docs/decisions/implemented/2026-09-20-name-the-collaboration-protocol.zh-CN.md +++ b/docs/decisions/implemented/2026-09-20-name-the-collaboration-protocol.zh-CN.md @@ -30,6 +30,40 @@ entry)** 是条目被某次 run 治理之后的状态;**run** 是计划、 **adopt(收编)** 是把条目绑到一次 run 的那次转移。唯一没有名字的是"决定收编的调用方",本记录叫它 **收编者(adopter)**。 +## 命名一览 + +这是索引,不是第二份规范:每个名字一行含义加一个指针,行与 owner 冲突时 owner 胜。它存在的原因是这些定义 +原本只能把设计文档、义务台账和几条决策记录摊在一起才看得全。 + +两个层次与它们的组成: + +| 名字 | 一句话含义 | 契约 owner | +| ------------------ | ---------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------- | +| 协议化协作 | agent 通过一份公开协议协商,机制与拍板留在程序侧 | 本记录;[概念图](../../guides/concept-map.zh-CN.md) | +| 任务单元协议 | 把任务声明为单元,并规定它被收编、认领、交付、裁决的方式以及运行时拥有哪些事实 | [task-unit-semantics.md](../../design/task-unit-semantics.md)、[义务台账](../../design/task-unit-semantics-obligations.md) | +| 任务单元 | 可以交接、验证和独立作废的工作,由输入、依赖、验收、能力与预算声明 | [task-unit-semantics.md](../../design/task-unit-semantics.md) | +| 黑板(Task Board) | 位于语义记忆之外、有归因、有过期时间的任务级协作区 | [memory-graphs.md §2](../../design/memory-graphs.md#shared-task-board-cross-agent-coordination-not-a-memory-graph) | +| 条目 entry | 一条黑板项(goal / question / handoff / blocker / result / note / decision),带认领租约、交付与裁决 | [board-find-serial-a2a-compat](../../design/board-find-serial-a2a-compat-2026-08-13.md) | +| 唤醒 wake | 定向条目通知那个 agent 的会话;黑板只负责把人叫到,不决定谁干活 | [board-find-serial-a2a-compat](../../design/board-find-serial-a2a-compat-2026-08-13.md) | +| 串行通道 | 同一时刻只推送一条未定向的 actionable;它被认领或关闭时晋升下一条 | [board-find-serial-a2a-compat](../../design/board-find-serial-a2a-compat-2026-08-13.md) | +| 认领 / 释放 / 关闭 | 租约动词:同一时刻一个持有者,过期回池,关闭即结束 | [黑板治理与能力寻址](2026-09-06-board-governance-addressing.zh-CN.md) | +| 交付 / 裁决 | 认领以 digest 绑定的交付物收尾,由另一个 agent 裁决(accepted / rejected / undecidable) | [黑板治理与能力寻址](2026-09-06-board-governance-addressing.zh-CN.md) | +| 受管条目 | 已被某次 run 收编的条目:在 run 的协调范围之外,它的生命周期动词被拒 | [义务台账 B6](../../design/task-unit-semantics-obligations.md) | +| run(运行) | 冻结的对象:已注册的 run、一份冻结计划、绑定的条目与事实日志,经 `taskRun` 到达 | [义务台账 D11、D13](../../design/task-unit-semantics-obligations.md) | +| adopt(收编) | 把黑板条目绑到一次 run 的那次转移,记为运行事实 `entry-bound` | [义务台账 D12](../../design/task-unit-semantics-obligations.md) | +| 收编者 adopter | 决定收编的调用方;本表里唯一还没有产品实现的名字 | 本记录;[义务台账“还剩什么”](../../design/task-unit-semantics-obligations.md) | +| 运行事实 run fact | 一次已记录的运行转移:`entry-bound`、`board-claim`、`board-deliver`、`board-judge`、`run-cancelled` | `src/integration/task-coordinator.ts` | +| 合法动作集合 | 确定性算出的、来源只能在其中排序的集合,按声明的槽预算切分 | [声明的槽预算](2026-09-18-declared-slot-budget.zh-CN.md)、`src/integration/ooo-execution.ts` | +| 派发循环 | 共享的顺序循环,逐单元驱动一份计划,黑板在端口之后 | [派发循环是共享的](2026-09-19-dispatch-loop-is-shared.zh-CN.md) | +| 会话 / 融合 | 单元的会话按黑板取键而非按 harness;融合是多单元共享一个会话——一个机制,不是本协议的名字 | [会话身份来自黑板](2026-09-19-session-identity-comes-from-the-board.zh-CN.md)、[融合的合法性与记账](2026-09-18-fusion-legality-and-accounting.zh-CN.md) | + +刻意不用的两个词: + +| 词 | 为什么不用 | 真实含义的 owner | +| ---------------- | ---------------------------------------------------------------------------------- | ------------------------------------------------------------------ | +| 调度器 scheduler | 语义层刻意不启用:排序是合法集合上的确定性规则加声明的预算 | [任务单元语义](2026-09-13-task-unit-semantics.zh-CN.md) | +| OoO、`ooo-` | 项目给自己实现的调度模型起的名字,也是文件前缀;它指的是模型,不是“谁讨论、谁拍板” | [ooo-execution-bootstrap](../../design/ooo-execution-bootstrap.md) | + ## 考虑过的替代方案 - **继续叫 OoO。** 作为大类被拒:`ooo` 是项目给自己实现的调度模型起的名字、在 100 多份文档里承重, From 44318cb2ad322f7f3b812580c0f73f238fc3c52c Mon Sep 17 00:00:00 2001 From: wefio <48851810+wefio@users.noreply.github.com> Date: Sun, 20 Sep 2026 23:23:25 +0800 Subject: [PATCH 03/13] docs(concept-map): the Task Board row points at a heading that exists The row's anchor named "2.1 Task Board outside the three memory graphs", which is not a heading in memory-graphs.md: the section there is "Shared Task Board (cross-Agent coordination, not a memory graph)". Clicking the old link still opened the file, so it read as working. docs:check validates that a link's file exists rather than its anchor, which is why this survived; the row now names the heading that is really there, in both languages. --- docs/guides/concept-map.md | 2 +- docs/guides/concept-map.zh-CN.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/guides/concept-map.md b/docs/guides/concept-map.md index 8b8ce21a..3754ebd7 100644 --- a/docs/guides/concept-map.md +++ b/docs/guides/concept-map.md @@ -44,7 +44,7 @@ directory; `get` opens selected exact evidence. | `activeGraphId` | The stable ID of one retrieval projection, passed from `search` to `get` | Exact disclosure must be budgeted, session-owned, and attributable to its search | [design.md §2.1](../design/design.md#21-cli-and-resident-service) | | QPP | Optional prediction of whether retrieval is broad and complete enough | Progressive recall needs a reason to stop, expand, or fold noise | [retrieval confidence controller](../design/retrieval-confidence-controller.md) | | Memory chain | A bounded ordered view over existing memory IDs | Some tasks need event order or explicit dependencies without copying evidence | [design.md §7.6](../design/design.md#76-static-temporal-and-logical-memory-chains) | -| Task Board | Attributed, expiring, task-scoped Agent coordination outside semantic memory | Private AGs cannot communicate directly across Agents | [memory-graphs.md §2.1](../design/memory-graphs.md#21-task-board-outside-the-three-memory-graphs) | +| Task Board | Attributed, expiring, task-scoped Agent coordination outside semantic memory | Private AGs cannot communicate directly across Agents | [memory-graphs.md §2](../design/memory-graphs.md#shared-task-board-cross-agent-coordination-not-a-memory-graph) | | Protocol-governed collaboration | Agents coordinate through a published protocol, while the mechanics and the deciding stay with the program | Coordination needs a name that separates who discusses from who decides | [Name the collaboration protocol](../decisions/implemented/2026-09-20-name-the-collaboration-protocol.md) | | Task-Unit Protocol | A task is declared as a unit and adopted into one run, where it is claimed, delivered and independently judged | The board alone cannot say what a unit's inputs, dependencies and acceptance are | [task-unit-semantics.md](../design/task-unit-semantics.md) | | Maintenance and consolidation | Deterministic index work plus evidence-gated semantic promotion or topology proposals | Write cost must stay bounded and repeated use must not manufacture truth | [design.md §10](../design/design.md#10-incremental-storage-and-index-maintenance) | diff --git a/docs/guides/concept-map.zh-CN.md b/docs/guides/concept-map.zh-CN.md index 1e70c62b..3f9d94a3 100644 --- a/docs/guides/concept-map.zh-CN.md +++ b/docs/guides/concept-map.zh-CN.md @@ -40,7 +40,7 @@ flowchart LR | `activeGraphId` | 一次检索投影的稳定 ID,由 `search` 传给 `get` | 精确披露需要受预算、会话所有权和检索归因约束 | [design.md §2.1](../design/design.md#21-cli-and-resident-service) | | QPP | 可选的检索广度与充分性预测 | 渐进式回忆需要决定停止、扩展或折叠噪声 | [检索置信度控制器](../design/retrieval-confidence-controller.md) | | Memory chain | 对现有 memory ID 的有界有序视图 | 时间顺序和显式依赖不应复制证据 | [design.md §7.6](../design/design.md#76-static-temporal-and-logical-memory-chains) | -| Task Board | 位于语义记忆之外、有归因、有过期时间的任务级协作区 | 私有 AG 不能直接完成跨 Agent 通讯 | [memory-graphs.md §2.1](../design/memory-graphs.md#21-task-board-outside-the-three-memory-graphs) | +| Task Board | 位于语义记忆之外、有归因、有过期时间的任务级协作区 | 私有 AG 不能直接完成跨 Agent 通讯 | [memory-graphs.md §2](../design/memory-graphs.md#shared-task-board-cross-agent-coordination-not-a-memory-graph) | | 协议化协作 | agent 通过一份公开协议协商,机制与拍板留在程序侧 | 协作需要一个把“谁讨论、谁拍板”分开的名字 | [命名决策](../decisions/implemented/2026-09-20-name-the-collaboration-protocol.zh-CN.md) | | 任务单元协议 | 任务被声明为单元并收编进一次 run,在其中被认领、交付与独立裁决 | 黑板自己说不清一个单元的输入、依赖与验收是什么 | [task-unit-semantics.md](../design/task-unit-semantics.md) | | 维护与巩固 | 确定性索引维护,加上有证据门控的语义晋升或拓扑 proposal | 写入成本必须有界,重复检索不能制造“事实” | [design.md §10](../design/design.md#10-incremental-storage-and-index-maintenance) | From a16571bbd0f1e17f865e56b3b3d6d2ba8ed528ab Mon Sep 17 00:00:00 2001 From: wefio <48851810+wefio@users.noreply.github.com> Date: Sun, 20 Sep 2026 23:34:34 +0800 Subject: [PATCH 04/13] docs(vocabulary): where the words came from, and one stale sentence about speculation The naming record now carries the conceptual history the names only make sense inside: the board becoming a protocol on 2026-08-13 (identity and discovery that wake no LLM, directed delivery, serial admission decided by claim/resolve/expiry), governance and addressing on 09-06, out-of-order execution borrowed from the CPU with its limits written down on 09-09..09-11, the task unit's semantics on 09-13, the paid arms measuring both borrowed halves on 09-18..09-19, and the board absorbing the execution half through 09-20. The OoO step is stated as the CPU correspondence rather than as "parallelism": out-of-order execution with in-order commit, where the plan and its tickets are the reorder buffer, the coordinator is the only retire stage, and the acceptance rules define what counts as commit. Its two limits are recorded with it, because the second one is the point the fusion and speculation arms then measured: the CPU's premise that a wrong guess wastes resources already committed or free does not transfer to agents, where a wrong guess spends tokens and wall time (a worker call measured at 40-56 s). Hence the decision adopts only the class whose wrong guess costs no tokens and refuses the class that hides a wait by guessing it, and the arms record "cost with no gain" at the shape tried: 43k tokens over six units, one false fact wasting 20 332 tokens, 0 of 3 prepared candidates publishable, and 175 ms of verification against 6.2 s of work once the fact holds. Commit-level lineage stays where it already lives (implementation-lineage.md, from its Task Board row) rather than being copied into the record. Also corrected: the bootstrap design still said the speculation decision was proposed and that the no-speculation rule therefore stayed in force until it was implemented. The decision has been implemented since 2026-09-11, so the sentence now states what is actually in force - the host-side free class may be done, hiding a wait by guessing stays forbidden. --- ...6-09-20-name-the-collaboration-protocol.md | 21 +++++++++++++++++++ ...0-name-the-collaboration-protocol.zh-CN.md | 16 ++++++++++++++ docs/design/ooo-execution-bootstrap.md | 2 +- 3 files changed, 38 insertions(+), 1 deletion(-) diff --git a/docs/decisions/implemented/2026-09-20-name-the-collaboration-protocol.md b/docs/decisions/implemented/2026-09-20-name-the-collaboration-protocol.md index d599a932..90c63b23 100644 --- a/docs/decisions/implemented/2026-09-20-name-the-collaboration-protocol.md +++ b/docs/decisions/implemented/2026-09-20-name-the-collaboration-protocol.md @@ -71,6 +71,27 @@ Two words that are deliberately not used for this protocol: | scheduler | The semantics deliberately does not enable one: ordering is deterministic rules over the legal set plus a declared budget | [task-unit semantics](2026-09-13-task-unit-semantics.md) | | OoO, `ooo-` | The project's own name for the scheduling model it implements, and the file prefix; it names the model, not who discusses and who decides | [ooo-execution-bootstrap](../../design/ooo-execution-bootstrap.md) | +## Where the words came from + +The vocabulary is the residue of six steps, none of them planned as a naming exercise. Commit-level +lineage lives in [implementation-lineage.md](../../design/implementation-lineage.md); this is the +conceptual one, because the names only make sense in this order. + +| When | What happened | Where it is owned | +| ------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------- | +| 2026-08-13 | The board was already an explicit multi-Agent coordination surface shared by several adapters, and it became a **protocol**: identity registration and discovery that wake no LLM, directed delivery (`to=`), and serial admission decided by claim, resolve and expiry rather than by arrival or acknowledgement - taking over is the claim, so a delivery is not a handover. A2A compatibility was researched at the same time. | [board find/direct and serial](../../design/board-find-serial-a2a-compat-2026-08-13.md) | +| 2026-09-06 | Governance and addressing: capability keys, compact reads, writer attribution. Still coordination - nothing here executes anything. | [board governance and addressing](2026-09-06-board-governance-addressing.md) | +| 2026-09-09 to 09-11 | **Out-of-order execution is borrowed from the CPU, with its limits written down.** The correspondence that holds is out-of-order execution _with in-order commit_: workers run out of order, the plan and its tickets are the reorder buffer, the coordinator is the only retire stage, and the acceptance rules define what counts as commit. Two limits are recorded with it. This design is stricter than the field's default of unbounded parallelism: only a real external wait may pass the queue head, and no sleep may manufacture reordering. And speculation is gated on measured payoff, because the CPU's premise does not transfer - there a wrong guess wastes resources already committed or free, while here it spends tokens and wall time (a worker call measured at 40-56 s). The decision adopts only the class whose wrong guess costs no tokens and refuses the class that hides a wait by guessing it; where a guess would almost always hold, the recorded advice is to remove the dependency rather than speculate it. | [bootstrap design](../../design/ooo-execution-bootstrap.md), [gate speculation on measured payoff](2026-09-11-ooo-speculation.md) | +| 2026-09-13 | Borrowing OoO makes the decomposition the bottleneck, and that half gets its own semantics: a **task unit** declared by inputs, dependencies, acceptance, capability and budget, compiled into an internal view (Task IR). Deliberately not a new user-facing language and not a second schema. | [task-unit-semantics.md](../../design/task-unit-semantics.md) | +| 2026-09-18 to 09-19 | **The paid arms measure both borrowed halves.** Fusing sessions buys wall time and costs tokens. Bounded speculation measures as cost with no gain at the shape tried: 43k tokens over six units, a false fact wasting 20 332 tokens, prepared candidates publishable in 0 of 3 holds, and 175 ms of verification against 6.2 s of work once the fact holds. The half of out-of-order execution that assumes a guess is cheap is therefore not available to agents, and the records say so instead of claiming a gain. | [arms archive](../../experiments/execution/archive/ooo-arms-2026-09-19/README.md), [obligations ledger F5](../../design/task-unit-semantics-obligations.md) | +| 2026-09-12 to 09-20 | **The board absorbs the execution half.** The first shape gave OoO its own session surface and its own tool; that was abandoned in favour of changing the board protocol itself. The board gained a deliverable bound to a digest, an independent judge, attempt fencing, then the run as a frozen object, adoption as a fact, the managed-write fence and the daemon's run surface - and the round's own tool left the product tool directory, so an ordinary handoff now carries semantics, execution and acceptance. | [collaboration absorbs OoO](../../design/task-unit-semantics-obligations.md), [the dispatch loop is shared](2026-09-19-dispatch-loop-is-shared.md) | + +The sequence is what the two names describe: protocol-governed collaboration, because the board was +already a protocol before it became an execution surface, and the Task-Unit Protocol, because the fine +decomposition is the part a task's semantics had to supply. It also explains the words recorded as +rejected above: a scheduler is what this model deliberately did not become, OoO is the borrowing's own +name, and fusion is one measured mechanism inside it rather than the whole. + ## Alternatives considered - **Keep saying OoO.** Rejected as the umbrella: `ooo` is the project's own name for the scheduling diff --git a/docs/decisions/implemented/2026-09-20-name-the-collaboration-protocol.zh-CN.md b/docs/decisions/implemented/2026-09-20-name-the-collaboration-protocol.zh-CN.md index 347ab1af..c4324ab5 100644 --- a/docs/decisions/implemented/2026-09-20-name-the-collaboration-protocol.zh-CN.md +++ b/docs/decisions/implemented/2026-09-20-name-the-collaboration-protocol.zh-CN.md @@ -64,6 +64,22 @@ entry)** 是条目被某次 run 治理之后的状态;**run** 是计划、 | 调度器 scheduler | 语义层刻意不启用:排序是合法集合上的确定性规则加声明的预算 | [任务单元语义](2026-09-13-task-unit-semantics.zh-CN.md) | | OoO、`ooo-` | 项目给自己实现的调度模型起的名字,也是文件前缀;它指的是模型,不是“谁讨论、谁拍板” | [ooo-execution-bootstrap](../../design/ooo-execution-bootstrap.md) | +## 这些词是怎么长出来的 + +这套词是后面六步的沉积,没有一步是为了命名而做的。提交级谱系在 +[implementation-lineage.md](../../design/implementation-lineage.md)(它的第 44 行起就是黑板/A2A);这里写概念史,因为这些名字只有按这个顺序看才成立。 + +| 时间 | 发生了什么 | 归属 | +| ------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------- | +| 2026-08-13 | 黑板当时已经是多个适配器共享的、显式的多 Agent 协作面,这一步把它变成一份**协议**:身份注册与发现不叫醒任何 LLM、定向投递(`to=`)、串行放行由 claim / resolve / 过期决定而不是由“已投递”或“已读”决定——接手就是认领,所以投递不等于交接;同时做了 A2A 兼容研究。 | [先广播后定向 + 串行交接](../../design/board-find-serial-a2a-compat-2026-08-13.md) | +| 2026-09-06 | 治理与寻址:能力键、紧凑读取、写入者归因。仍然只是协作——这一步里没有任何东西在执行。 | [黑板治理与能力寻址](2026-09-06-board-governance-addressing.zh-CN.md) | +| 2026-09-09 至 09-11 | **从 CPU 借乱序执行,同时把它的限度写下来。**真正成立的对应是**乱序执行 + 顺序提交**:worker 乱序运行,计划与票据充当 reorder buffer,协调器是唯一的 retire 阶段,验收规则就是“什么算提交”的定义。写下时带了两条限度:本设计比领域的默认(无界并行)更严——只有真实外部等待可以越过队首,也不许用 sleep 造出乱序;以及推测必须按实测盈亏门控,因为 CPU 的前提在这里不成立:那边猜错只浪费已经提交或本来就空闲的资源,这边猜错要花 token 和墙钟时间(实测一次 worker 调用 40–56 s)。因此该决策只采纳“猜错不花 token”的那一类,拒绝“靠猜掩盖等待”的那一类;若某个依赖的假设长期几乎必中,记录下来的做法是取消这个依赖,而不是推测它。 | [自举设计](../../design/ooo-execution-bootstrap.md)、[按实测盈亏门控推测](2026-09-11-ooo-speculation.zh-CN.md) | +| 2026-09-13 | 借了乱序执行,拆分就成了瓶颈,于是这一半有了自己的语义:**任务单元**由输入、依赖、验收、能力和预算声明,编译成内部视图(Task IR)。刻意不是新的用户语言,也不是第二份 schema。 | [task-unit-semantics.md](../../design/task-unit-semantics.md) | +| 2026-09-18 至 09-19 | **付费臂量了借来的两半。** 会话融合买到墙钟时间、付出更多 token;有界推测在试过的形状上是“成本真实、收益为零”:六个单元 43k token,一次假事实浪费 20 332 token,准备好的候选在 3 次成立中 0 次可发布,而事实成立后校验只要 175 ms(对应 6.2 s 的活)。所以“猜对了不亏”那一半对 agent 不可用,记录照实写,不声称收益。 | [臂归档](../../experiments/execution/archive/ooo-arms-2026-09-19/README.md)、[义务台账 F5](../../design/task-unit-semantics-obligations.md) | +| 2026-09-12 至 09-20 | **黑板吞下执行那一半。** 最早的形状给 OoO 单独的会话面和单独的工具;这被改成“改黑板协议本身”。黑板先后得到 digest 绑定的交付物、独立裁决、attempt 围栏,然后是冻结的 run、作为事实的收编、受管写入围栏和 daemon 的 run 面——而轮次自己的工具离开了产品工具目录,于是一次普通交接就带着语义、执行与验收。 | [协作吞下 OoO](../../design/task-unit-semantics-obligations.md)、[派发循环是共享的](2026-09-19-dispatch-loop-is-shared.zh-CN.md) | + +这个顺序就是两个名字的来历:大类叫“协议化协作”,因为黑板在成为执行面之前就已经是一份协议;子协议叫“任务单元协议”,因为更细的拆分是任务语义必须供上的那一半。它也解释了上面被记为拒绝的那些词:调度器是这个模型刻意没有变成的东西,OoO 是借来的那个思想自身的名字,而融合是它内部一个已实测的机制,不是整体。 + ## 考虑过的替代方案 - **继续叫 OoO。** 作为大类被拒:`ooo` 是项目给自己实现的调度模型起的名字、在 100 多份文档里承重, diff --git a/docs/design/ooo-execution-bootstrap.md b/docs/design/ooo-execution-bootstrap.md index 0848567b..4abb4f14 100644 --- a/docs/design/ooo-execution-bootstrap.md +++ b/docs/design/ooo-execution-bootstrap.md @@ -150,7 +150,7 @@ S2 的退出条件“故障注入后无重复完成、错误解锁或无限等 ### 推测的落点(规则由决策记录拥有,本节只列可落地的位置) -是否启用、以及启用条件,由[推测盈亏平衡决策](../decisions/implemented/2026-09-11-ooo-speculation.md)拥有;本节只回答“哪些点适合”。该决策仍为 proposed,所以上面“不推测”的现行规则继续有效,直到它进入 implemented。按**猜错花掉什么**分三类,这一点决定了一切。 +是否启用、以及启用条件,由[推测盈亏平衡决策](../decisions/implemented/2026-09-11-ooo-speculation.md)拥有;本节只回答“哪些点适合”。该决策已 implemented(2026-09-11):宿主侧免费类可做,靠猜测掩盖等待仍然禁止,直到有实测盈亏支持它。按**猜错花掉什么**分三类,这一点决定了一切。 **已有前提**。安全推测需要的三件事已经就位:(a)错误的推测产物必须能丢弃且不得生效——补丁契约只允许产出提案、由宿主应用,worker 没有不可逆副作用,所以 squash 无需回滚任何状态;(b)提交点必须能廉价判定猜测对错——真实检查终态带检查身份与 digest,正好是那个判定点;(c)提交顺序不变。`inputDigest` 会把假设绑定进输入,因此**猜错的产物自动失效**(fenced)而不是被误接受:这正是 ROB 保证精确状态的那件事在我们这里的对应物。 From ef33abc3f49730ab17455331e7a499c3e7fdb9ce Mon Sep 17 00:00:00 2001 From: wefio <48851810+wefio@users.noreply.github.com> Date: Mon, 21 Sep 2026 00:28:42 +0800 Subject: [PATCH 05/13] docs(decisions): the program answers legality, and nothing else Adds a proposed record for the question the naming decision unblocked: the primitives have names and the shared layer can already compute the legal set (an ordered legal set, that set cut to the declared slot budget, and a refusal reason per unit), but no document states whose decision each step is. The proposal splits the work three ways and gives the program exactly one share. The protocol owns what the primitives are, the definition and criteria of legality, the named constraint list, and the two structural rules that cannot be negotiated because they are what "legal" means here: a claim (lease plus attempt fence) is the only arbiter of who holds a unit, and a deliverer never judges its own delivery. The accompanying program computes the ordered legal set from the declared plan and current facts, cuts it to the declared slot budget, and states for each unit why it is legal or not - it chooses nobody, adopts nothing, judges nothing, wakes nobody. Agents own the arrangement: the plan's content, the order they agree on, who takes which unit, who adopts a run, who judges - all of it as board facts, so the arrangement is readable instead of being implied by a program's choice. Constraints are named by the protocol and enabled by the plan, so a preference such as repair-first is neither program policy nor advice; and legality is asked rather than published, because a published handoff occupies a serial slot by the board's own protocol, so fusing the two would make a question cost a slot. The plan is three steps and only the first lands here: this record, then a readable answer through an existing board action, then promoting the preferences that still live as shared planning policy (repair-first first) into constraints a plan declares - which is what makes the "not enabled means absent from the answer" criterion true. The record names that third step as the one place the proposal is not yet true. Deliberately not done here: no product surface change, no driver, no wake, no new tool or channel. --- ...2026-09-20-the-program-answers-legality.md | 105 ++++++++++++++++++ ...9-20-the-program-answers-legality.zh-CN.md | 60 ++++++++++ 2 files changed, 165 insertions(+) create mode 100644 docs/decisions/proposed/2026-09-20-the-program-answers-legality.md create mode 100644 docs/decisions/proposed/2026-09-20-the-program-answers-legality.zh-CN.md diff --git a/docs/decisions/proposed/2026-09-20-the-program-answers-legality.md b/docs/decisions/proposed/2026-09-20-the-program-answers-legality.md new file mode 100644 index 00000000..bb8e95e8 --- /dev/null +++ b/docs/decisions/proposed/2026-09-20-the-program-answers-legality.md @@ -0,0 +1,105 @@ +# The program answers legality, and nothing else + +[中文](2026-09-20-the-program-answers-legality.zh-CN.md) + +**Status:** proposed +**Relates to:** [Name the collaboration protocol and its task-unit sub-protocol](../implemented/2026-09-20-name-the-collaboration-protocol.md), [Task unit semantics](../../design/task-unit-semantics.md), [The contract's obligations](../../design/task-unit-semantics-obligations.md) + +## Problem + +The primitives have names, and the shared layer can already compute the legal set: an ordered legal set, +that set cut to the declared slot budget, and a refusal reason per unit when a claim is checked. What no +document states is **whose decision each step is**. The cost of that gap is observable: every caller +that wants to drive a run has to redraw the boundary for itself, so both recorded failure modes come +back - a program that picks the person for the agents, which is the scheduler the naming decision +records as the word this model deliberately did not become, and agents that cannot tell what the program +guarantees, so they ask for what they actually need in the only terms available: more slots, stranger +ordering. + +[The obligations ledger](../../design/task-unit-semantics-obligations.md) says where the boundary must +not move - no second task body, no dedicated tool, no dedicated channel - but it does not say what the +program's answer is. + +## Proposal + +Three owners, three kinds of statement. The program's share is exactly one: **it answers legality**. + +- **The protocol owns** what the primitives are (already named; this record does not restate them), the + definition and criteria of legality, which constraints exist **by name**, and the two structural + rules that are what "legal" means here and therefore cannot be negotiated: a claim (lease plus + attempt fence) is the only arbiter of who holds a unit, and a deliverer never judges its own + delivery. +- **The accompanying program owns** computing the ordered legal set from the declared plan and current + facts, cutting it to the declared slot budget, and stating for each unit why it is legal or not - + reusing the reasons it already refuses claims with. It chooses nobody, adopts nothing, judges + nothing, and wakes nobody. +- **Agents own the arrangement**: the plan's content, the order they agree on, who takes which unit, + who adopts a run, who judges. All of it lands as board facts, so the arrangement is readable and + auditable instead of being implied by a program's choice. + +**Constraints are named by the protocol and enabled by the plan.** A preference such as repair-first is +neither a program policy nor merely advice: it is a named constraint that a plan - or the adopter +declaring it - enables, and the legality answer is computed under the enabled set. A constraint that is +not enabled must be genuinely absent from the answer, or the declaration is decoration. + +**Legality is asked, not published.** It is a function of current facts, so a query returns the ordered +legal set with a reason per unit, changing no state. A published handoff on the board is a different +thing: by the board's own protocol it occupies a serial slot. Fusing the two into one action would make +asking a question consume a slot. + +## Plan + +1. This record: write down the division of labour and the standing of constraints. No product surface. +2. Make legality readable through an existing board action (the action name lives in the shared + tool contract, its description is generated from the prompt source), keeping the read free of state. +3. Promote the preferences that currently live as shared planning policy - repair-first first - into + named constraints a plan declares, which is what makes the "disabled means absent" criterion true. + +Each step is verifiable on its own; none of them requires a driver, a wake, or a new tool. + +## Alternatives considered + +- **Let the program select the next unit** (today's `next()` used as policy). Rejected: a bound is not a + decision - it does not choose among legal successors - and a program that picks the person is a + scheduler again, which the naming decision records as the word this model deliberately did not become. +- **Let the program store state only, with agents asserting their own legality.** Rejected: "legal" + would then be whatever the loudest caller says, and the two structural rules (one holder per claim, + no self-judging) would lose their home. +- **Demote preferences to advice.** Rejected: the arms compare runs on the premise that the same plan + plus the same facts yield the same answer, and advice that may be ignored removes exactly that. +- **Publish the legal set as a board offering** (make the query a handoff). Rejected: it makes a + question consume a serial slot, and withdrawing an offer is the board protocol's business. +- **Give the arrangement its own tool or channel.** Rejected: the ledger forbids it; the primitives must + take effect through an ordinary handoff. +- **Keep repair-first as hidden program policy.** Rejected: same class as a program that picks the + person. + +## Acceptance criteria + +- The answer is per unit, ordered, and carries the reason for each legal or refused unit. +- The answer names no person, no adopter and no judge. +- Asking changes no state: no entry, no claim, no slot, no wake. Asking twice on an unchanged store + returns the same answer, and the store's version is unchanged. +- A constraint that is not enabled has no effect on the answer: enabling and disabling the same named + constraint on one plan yields different legal sets. +- The declared slot budget still cuts the answer, and declaring more than one slot still requires a + declared handoff target. +- The two structural rules hold in the answer's own terms: a second claim on a held unit is refused, + and a deliverer cannot judge. +- Who adopted, who claimed and who judged exist only as board facts; no program field asserts them. +- The ledger's prohibition still holds: no new tool, no dedicated channel, no second task body. + +## Risks + +- With no constraint declared, a wide legal set can read as permission to fan out, recreating the + "no discussion, everything at once" problem somewhere new. Mitigation: the slot budget stays a + declared cap rather than a social one. +- Repair-first currently lives as policy inside the shared planning code, so making it a declared + constraint is a change rather than a rename; until it moves, the answer is computed under an implicit + constraint, which is the one place this proposal is not yet true. +- Reasons become an interface: a caller that parses a reason string creates a second source of truth. + Reasons are for people and logs; any caller decision must go through a claim or an explicit field. +- With agents free to choose order, the arms' equal-instrument premise depends on each run recording + which constraints were enabled; an unrecorded set makes two runs incomparable. +- A query invites polling. Polling is cheap, and a caller that asks and then acts can still race - the + claim remains the arbiter, which is the point. diff --git a/docs/decisions/proposed/2026-09-20-the-program-answers-legality.zh-CN.md b/docs/decisions/proposed/2026-09-20-the-program-answers-legality.zh-CN.md new file mode 100644 index 00000000..698b6f1c --- /dev/null +++ b/docs/decisions/proposed/2026-09-20-the-program-answers-legality.zh-CN.md @@ -0,0 +1,60 @@ +# 程序只回答合法性,别的都不管 + +[English](2026-09-20-the-program-answers-legality.md) + +**Status:** proposed +**Relates to:** [给协作协议及其任务单元子协议命名](../implemented/2026-09-20-name-the-collaboration-protocol.zh-CN.md)、[任务单元语义](../../design/task-unit-semantics.md)、[契约的义务](../../design/task-unit-semantics-obligations.md) + +## 问题 + +元语有了名字,共享层也已经能算出合法集合:有序合法集合、按声明槽位预算切一刀、认领被拒时给出每个单元的理由。但**没有任何一份文档在说"每一步是谁的决定"**。这个缺口有可观察的代价:任何想驱动一次 run 的调用者都得自己重新划一遍边界,于是两种已被记录在案的失败模式都回来了——程序顺手替 agent 挑人(也就是命名决策里记为"这个模型刻意没有变成"的那个词:调度器),以及 agent 不知道程序到底给什么保证,只能用唯一能用的说法去要它真正需要的东西:更多槽位、更怪的顺序。 + +[义务台账](../../design/task-unit-semantics-obligations.md)写明了边界不能往哪边动——不得再有第二个任务体、专用工具、专用频道——但它没有回答程序给出的那个答案是什么。 + +## 提案 + +三个所有者,三类陈述。程序那一份只有一条:**它回答合法性**。 + +- **协议拥有**:元语是什么(已被命名,本文不复述)、合法性的定义与判据、**约束的具名清单**(有哪些约束可以启用),以及两条结构性规则——它们就是"合法"在这里的含义,因此不可协商:认领(租约 + attempt 围栏)是"谁持有某个单元"的唯一仲裁;交付者不裁决自己的交付。 +- **配套程序拥有**:按声明的计划与当下事实算出有序合法集合,按声明的槽位预算切一刀,并给出每个单元合法或不合法的理由——复用它现在拒绝认领时用的那些理由。它不挑人、不收编、不裁决、不唤醒。 +- **agent 拥有安排本身**:计划的内容、他们商定的顺序、谁拿哪个单元、谁收编一次 run、谁裁决。所有这些都落成黑板上的事实,所以安排是可读、可审的,而不是由程序的选择暗示出来的。 + +**约束由协议具名、由计划启用。** 修复优先这类偏好既不是程序策略,也不只是建议:它是一条具名约束,由计划(或声明它的收编者)启用,合法性答案在启用集合下计算。没被启用的约束必须在答案里真的不生效,否则这个声明就是装饰。 + +**合法性被问,不被发布。** 它是当下事实的函数,所以查询返回有序合法集合,外加每个单元的理由,且不改变任何状态。黑板上发布出去的 handoff 是另一回事:按黑板自己的协议,它占用一个串行槽位。把两个合成一个动作,就会让"问一句"花掉一个槽位。 + +## 计划 + +1. 本文:写下分工与约束的地位。不动产品面。 +2. 让合法性可读——走已有的黑板动作(动作名在共享 tool contract 里,描述由 prompt 源生成),并保持这次读取不写状态。 +3. 把目前活在共享规划策略里的偏好,首先是修复优先,提升为由计划声明的具名约束;这一步才让"未启用即不生效"这条验收标准成立。 + +每一步都能单独验证;没有一步需要驱动器、唤醒或新工具。 + +## 考虑过的替代方案 + +- **让程序挑下一个单元**(把现在的 `next()` 当策略用)。拒绝:一个上限不是决定——它不在合法后继里做选择;而挑人的程序就是调度器,命名决策已把"调度器"记为这个模型刻意没有变成的词。 +- **程序只存状态,合法性由 agent 各自声明**。拒绝:那么"合法"就是嗓门最大的调用者说的;两条结构性规则(一个认领只有一个持有者、交付者不能自裁)会失去家。 +- **把偏好降成建议**。拒绝:arm 之间的可比性建立在"同一计划加同样事实给出同一答案"上,而可被无视的建议恰好拿走这一点。 +- **把合法集合作为黑板 offer 发布**(把查询做成 handoff)。拒绝:这样"问一句"要占串行槽位,而且撤回 offer 是黑板协议的事。 +- **为这套安排新开工具或频道**。拒绝:台账已禁;元语必须经普通交接生效。 +- **保留修复优先作为隐藏的程序策略**。拒绝:与"程序挑人"同属一类。 + +## 验收标准 + +- 答案按单元给出、有序,并带每个单元合法或不合格的理由。 +- 答案里不出现人、收编者或裁决者。 +- 问不改变状态:不产生条目、认领、槽位或唤醒。在未变更的 store 上问两次得到同一答案,且版本未变。 +- 未启用的约束对答案没有影响:同一计划上启用与停用同一条具名约束,合法集合不同。 +- 声明的槽位预算仍然切这个答案;声明多于一个槽位仍然要求声明 handoff 目标。 +- 两条结构性规则在答案自身的术语里成立:对已持有的单元再次认领被拒;交付者不能裁决。 +- 谁收编、谁认领、谁裁决只作为黑板事实存在,没有任何程序字段声称它们。 +- 台账的禁令仍然成立:没有新工具、没有专用频道、没有第二个任务体。 + +## 风险 + +- 一条约束都不声明时,宽的合法集合可能被读成"可以一起上"的许可,把"没商量同时并行"的问题换个地方重演。缓解:槽位预算仍是声明的上限,而不是社会约定。 +- 修复优先现在住在共享规划代码里当策略,所以把它变成声明式约束是一次改动,不只是改名;搬完之前,答案是在一条隐含约束下算出来的——这是本提案唯一还不成立的地方。 +- 理由一旦成为接口,解析理由字符串的调用者就造出了第二个事实源。理由是给人看和进日志的;调用者的任何决定都必须走认领或一个显式字段。 +- agent 自由选序之后,arm 的 equal-instrument 前提取决于每次 run 记录了自己启用了哪些约束;没记录约束集合,两次 run 就不可比。 +- 查询会招来轮询。轮询便宜,而"先问后动"仍可能撞车——认领仍是仲裁者,这正是设计意图。 From 4db614034e449018117c05f4542d760ffc05a770 Mon Sep 17 00:00:00 2001 From: wefio <48851810+wefio@users.noreply.github.com> Date: Mon, 21 Sep 2026 00:34:23 +0800 Subject: [PATCH 06/13] docs(decisions): read the legality record together with board governance The proposed record now relates to board governance and capability addressing (2026-09-06), and its Problem says what that record does not cover: the board's correctness line - reviewable finalize, authentic content, scope isolation, capability addressing, and the deliver/judge/attempt-fencing slice - governs how an entry is treated once it exists, not who decides what happens next. The two records are two halves of one protocol, and reading them together is what makes the distributed-systems half visible: claimTaskBoardEntry is a single atomic compare-and-set ("lease-based claiming ... on a losing CAS the failure is diagnosed against a fresh read"), lease expiry is enforced lazily with no background sweeper - that is a suspecting failure detector, not a death certificate - attempt fencing exists precisely because a lapsed lease cannot be trusted ("without this, work reassigned to another agent could still be read through a stale artifact"), and the deliverable/verdict pair binds an acceptance to an artifact identity. Idempotent acknowledgements and the never-recycled monotonic identity counter are the other two primitives already in place. No prose in either record names this, so every discussion of the boundary re-derives it. --- .../proposed/2026-09-20-the-program-answers-legality.md | 7 +++++-- .../2026-09-20-the-program-answers-legality.zh-CN.md | 4 ++-- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/docs/decisions/proposed/2026-09-20-the-program-answers-legality.md b/docs/decisions/proposed/2026-09-20-the-program-answers-legality.md index bb8e95e8..88344263 100644 --- a/docs/decisions/proposed/2026-09-20-the-program-answers-legality.md +++ b/docs/decisions/proposed/2026-09-20-the-program-answers-legality.md @@ -3,7 +3,7 @@ [中文](2026-09-20-the-program-answers-legality.zh-CN.md) **Status:** proposed -**Relates to:** [Name the collaboration protocol and its task-unit sub-protocol](../implemented/2026-09-20-name-the-collaboration-protocol.md), [Task unit semantics](../../design/task-unit-semantics.md), [The contract's obligations](../../design/task-unit-semantics-obligations.md) +**Relates to:** [Board governance and capability addressing](../implemented/2026-09-06-board-governance-addressing.md), [Name the collaboration protocol and its task-unit sub-protocol](../implemented/2026-09-20-name-the-collaboration-protocol.md), [Task unit semantics](../../design/task-unit-semantics.md), [The contract's obligations](../../design/task-unit-semantics-obligations.md) ## Problem @@ -18,7 +18,10 @@ ordering. [The obligations ledger](../../design/task-unit-semantics-obligations.md) says where the boundary must not move - no second task body, no dedicated tool, no dedicated channel - but it does not say what the -program's answer is. +program's answer is. Neither does the board's correctness line - reviewable finalize, authentic +content, scope isolation, capability addressing ([board governance and capability +addressing](../implemented/2026-09-06-board-governance-addressing.md)) - which governs how an entry is +treated once it exists, not who decides what happens next. ## Proposal diff --git a/docs/decisions/proposed/2026-09-20-the-program-answers-legality.zh-CN.md b/docs/decisions/proposed/2026-09-20-the-program-answers-legality.zh-CN.md index 698b6f1c..4e378a8e 100644 --- a/docs/decisions/proposed/2026-09-20-the-program-answers-legality.zh-CN.md +++ b/docs/decisions/proposed/2026-09-20-the-program-answers-legality.zh-CN.md @@ -3,13 +3,13 @@ [English](2026-09-20-the-program-answers-legality.md) **Status:** proposed -**Relates to:** [给协作协议及其任务单元子协议命名](../implemented/2026-09-20-name-the-collaboration-protocol.zh-CN.md)、[任务单元语义](../../design/task-unit-semantics.md)、[契约的义务](../../design/task-unit-semantics-obligations.md) +**Relates to:** [黑板治理与能力寻址](../implemented/2026-09-06-board-governance-addressing.zh-CN.md)、[给协作协议及其任务单元子协议命名](../implemented/2026-09-20-name-the-collaboration-protocol.zh-CN.md)、[任务单元语义](../../design/task-unit-semantics.md)、[契约的义务](../../design/task-unit-semantics-obligations.md) ## 问题 元语有了名字,共享层也已经能算出合法集合:有序合法集合、按声明槽位预算切一刀、认领被拒时给出每个单元的理由。但**没有任何一份文档在说"每一步是谁的决定"**。这个缺口有可观察的代价:任何想驱动一次 run 的调用者都得自己重新划一遍边界,于是两种已被记录在案的失败模式都回来了——程序顺手替 agent 挑人(也就是命名决策里记为"这个模型刻意没有变成"的那个词:调度器),以及 agent 不知道程序到底给什么保证,只能用唯一能用的说法去要它真正需要的东西:更多槽位、更怪的顺序。 -[义务台账](../../design/task-unit-semantics-obligations.md)写明了边界不能往哪边动——不得再有第二个任务体、专用工具、专用频道——但它没有回答程序给出的那个答案是什么。 +[义务台账](../../design/task-unit-semantics-obligations.md)写明了边界不能往哪边动——不得再有第二个任务体、专用工具、专用频道——但它没有回答程序给出的那个答案是什么。黑板那条正确性线也没有:可复核的终结、内容真实性、作用域隔离、能力寻址([黑板治理与能力寻址](../implemented/2026-09-06-board-governance-addressing.zh-CN.md))管的是一个条目在存在之后怎么被对待,不是下一步该由谁定。 ## 提案 From 9832bf6a4c28c2cf4523afddc73932af2869877f Mon Sep 17 00:00:00 2001 From: wefio <48851810+wefio@users.noreply.github.com> Date: Mon, 21 Sep 2026 00:40:41 +0800 Subject: [PATCH 07/13] docs(design): inventory the umbrella - its parts, the gaps, and the distributed concepts behind them Your read was that part of this is a distributed-systems problem, and reading the legality record together with board governance showed the framing has no home: the governance record is an implemented decision with a fixed format, and the 2026-08-13 document is board mechanics. This adds the inventory as a design document, and the naming record now points at it from its history section. It lists fifteen parts with owner and state: entries and addressing, identity and authenticity, ownership and time, termination integrity, attention and wake, scope and visibility, truth versus coordination state, the task unit, legality and admission, ordering and constraints, budget and accounting, cancellation and fencing, recovery and replay, handoff, and evidence. The task-unit protocol is the complete one, and no single part completes a task: a unit's declaration, ownership and time, termination integrity, attention, legality, budget, truth separation and handoff have to hold at once. It then names seven gaps, each with the concept that fits it rather than a new vocabulary: the adopter has no clause (a commit boundary, and orphan reaping for the restart responsibility), legality has no readable answer (admission control with a read-only precondition check), constraints have no declaration (declarative policy, plus deterministic replay for comparability), read guarantees are unwritten (monotonic reads, read-your-writes, causal consistency across one agent's sessions), disagreement has no arbiter beyond veto and racing a claim (optimistic concurrency with conflict detection, or a plain single-writer decision since one arbiter already exists), cross-run resources have no mutual exclusion (a fenced mutex, not only the slot budget and the throwaway-worktree practice), and in-doubt work has no resolution (an in-doubt transaction: the fact is known, the outcome is not). It also records what is deliberately not needed - consensus, leader election, quorum, two-phase commit, exactly-once delivery, distributed transactions, because truth lives in one store written by one daemon - and three analogies that mislead: a lease expiry is a failure detector's suspicion and not a death certificate, which is why attempt fencing must stay; the board is not a message queue, so re-reading is normal and re-doing is the problem; and agents are not replicas, their independent judgement being exactly why a delivery is recorded with a judge instead of a self-report trusted. --- ...6-09-20-name-the-collaboration-protocol.md | 4 + ...0-name-the-collaboration-protocol.zh-CN.md | 2 + .../design/protocol-governed-collaboration.md | 91 +++++++++++++++++++ .../protocol-governed-collaboration.zh-CN.md | 55 +++++++++++ 4 files changed, 152 insertions(+) create mode 100644 docs/design/protocol-governed-collaboration.md create mode 100644 docs/design/protocol-governed-collaboration.zh-CN.md diff --git a/docs/decisions/implemented/2026-09-20-name-the-collaboration-protocol.md b/docs/decisions/implemented/2026-09-20-name-the-collaboration-protocol.md index 90c63b23..536b835e 100644 --- a/docs/decisions/implemented/2026-09-20-name-the-collaboration-protocol.md +++ b/docs/decisions/implemented/2026-09-20-name-the-collaboration-protocol.md @@ -92,6 +92,10 @@ decomposition is the part a task's semantics had to supply. It also explains the rejected above: a scheduler is what this model deliberately did not become, OoO is the borrowing's own name, and fusion is one measured mechanism inside it rather than the whole. +The umbrella's parts are inventoried separately - which sub-protocols exist, which are complete, which +are still missing, and which of them already have a name in distributed systems: +[protocol-governed collaboration](../../design/protocol-governed-collaboration.md). + ## Alternatives considered - **Keep saying OoO.** Rejected as the umbrella: `ooo` is the project's own name for the scheduling diff --git a/docs/decisions/implemented/2026-09-20-name-the-collaboration-protocol.zh-CN.md b/docs/decisions/implemented/2026-09-20-name-the-collaboration-protocol.zh-CN.md index c4324ab5..db2c2b12 100644 --- a/docs/decisions/implemented/2026-09-20-name-the-collaboration-protocol.zh-CN.md +++ b/docs/decisions/implemented/2026-09-20-name-the-collaboration-protocol.zh-CN.md @@ -80,6 +80,8 @@ entry)** 是条目被某次 run 治理之后的状态;**run** 是计划、 这个顺序就是两个名字的来历:大类叫“协议化协作”,因为黑板在成为执行面之前就已经是一份协议;子协议叫“任务单元协议”,因为更细的拆分是任务语义必须供上的那一半。它也解释了上面被记为拒绝的那些词:调度器是这个模型刻意没有变成的东西,OoO 是借来的那个思想自身的名字,而融合是它内部一个已实测的机制,不是整体。 +大类有哪些组成部分另有一份清单:哪些子协议已有、哪些是齐的、哪些还缺,以及其中哪些在分布式领域已经有名字:[协议化协作](../../design/protocol-governed-collaboration.zh-CN.md)。 + ## 考虑过的替代方案 - **继续叫 OoO。** 作为大类被拒:`ooo` 是项目给自己实现的调度模型起的名字、在 100 多份文档里承重, diff --git a/docs/design/protocol-governed-collaboration.md b/docs/design/protocol-governed-collaboration.md new file mode 100644 index 00000000..fcc43069 --- /dev/null +++ b/docs/design/protocol-governed-collaboration.md @@ -0,0 +1,91 @@ +# Protocol-governed collaboration: the parts, the gaps, and the distributed systems behind them + +[中文](protocol-governed-collaboration.zh-CN.md) + +The umbrella name is [protocol-governed collaboration](../decisions/implemented/2026-09-20-name-the-collaboration-protocol.md). +This document is an inventory of it: which sub-protocols exist, which one is complete, which are missing, +and which of those - existing or missing - already have a name in distributed systems. + +It is an index, not a second specification. Each part's owner is named in its row, and where this +document and an owner disagree, the owner wins. + +## What the umbrella is made of + +| Part | What it governs | State | Owner | +| ------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | +| Entries and addressing | Typed, attributed, expiring entries; compact reads, cursors, inbox; directed delivery (`to=`), discovery, capability addressing (`need`) | Complete | [board governance and capability addressing](../decisions/implemented/2026-09-06-board-governance-addressing.md), [board find/direct and serial](board-find-serial-a2a-compat-2026-08-13.md) | +| Identity and authenticity | Writer attribution, content hash by default, signing across a trust boundary | Complete | [board governance and capability addressing](../decisions/implemented/2026-09-06-board-governance-addressing.md) | +| Ownership and time | Atomic compare-and-set claim, lease with lazy expiry, renewal, release, serial promotion of the next pending entry | Mechanism complete; the responsibility to restart work is not assigned | `src/core/store/base.ts`, [board find/direct and serial](board-find-serial-a2a-compat-2026-08-13.md) | +| Termination integrity | Reviewable finalize and veto, `deliver` with a digest, independent `judge`, attempt fencing, `undecidable` as a first-class outcome | Complete | [board governance and capability addressing](../decisions/implemented/2026-09-06-board-governance-addressing.md) | +| Attention and wake | Directed wake of one session, membership by subscription, silent notes, acknowledgement suppressing re-notification | Complete | [board find/direct and serial](board-find-serial-a2a-compat-2026-08-13.md) | +| Scope and visibility | An entry targets an authorized agent subset, so boards are not all-to-all | Complete | [board governance and capability addressing](../decisions/implemented/2026-09-06-board-governance-addressing.md) | +| Truth versus coordination state | The board is a temporary coordination medium; durable memory is the truth store; `memory=` pointers | Complete | [board governance](../decisions/implemented/2026-09-06-board-governance-addressing.md), [memory graphs](memory-graphs.md) | +| The task unit | A unit declared by inputs, dependencies, acceptance, capability and budget; decomposition; the internal compile view; the run, its adoption, and the facts the runtime owns | Complete | [task unit semantics](task-unit-semantics.md), [the obligations ledger](task-unit-semantics-obligations.md) | +| Legality and admission | The ordered legal set, the cut to the declared slot budget, the reason each unit is or is not legal | Mechanism complete; the readable answer is missing | `src/integration/ooo-board.ts`, [the program answers legality](../decisions/proposed/2026-09-20-the-program-answers-legality.md) | +| Ordering and constraints | Determinism (one plan plus one set of facts yields one answer), repair-first, tie-breaking by plan order | Mechanism complete; the declaration is missing | [fusion planning: repair-first](../decisions/implemented/2026-09-19-fusion-planning-repair-first.md), [the program answers legality](../decisions/proposed/2026-09-20-the-program-answers-legality.md) | +| Budget and accounting | Declared slot budget, token and cache accounting, the offline cost ceiling | Complete | [declared slot budget](../decisions/implemented/2026-09-18-declared-slot-budget.md), [the cost model](../experiments/execution/ooo-cost-model-2026-09-17.md) | +| Cancellation and fencing | Explicit cancellation, no orphan worker or check after it, late artifacts fenced | Complete | [long checks run detached](../decisions/implemented/2026-09-18-detached-long-checks.md), [the bootstrap design](ooo-execution-bootstrap.md) | +| Recovery and replay | Log plus replay of a round, the transactional outbox drained after commit and on restart, no stealing another's work after a crash | Complete inside the arms; the product side is not wired | [the bootstrap design](ooo-execution-bootstrap.md) | +| Handoff | Session identity comes from the board; a continuation surface carries what the next holder needs | Complete | [session identity comes from the board](../decisions/implemented/2026-09-19-session-identity-comes-from-the-board.md), [session continuation obligations](task-unit-semantics-obligations.md) | +| Evidence and audit | Run facts, verification evidence, traceability aggregation | Complete | [rtm evidence aggregation](../decisions/implemented/2026-09-20-rtm-evidence-aggregation.md) | + +No single part completes a task. A task completes when the unit's declaration, ownership and time, +termination integrity, attention, legality, budget, truth separation and handoff all hold at once; the +rows above are the ones a given piece of work cannot do without. + +## The gaps, and the concepts available for them + +1. **The adopter has no protocol clause.** Who may adopt a discussion into a run, and who is responsible + for restarting work after a lease lapses, is not stated. The mechanism exists (`expiry`, serial + promotion, the dispatch loop), the responsibility does not. Distributed systems call the first one a + **commit boundary** - the single point that makes a discussed arrangement binding - and the second an + **orphan reaper / restart responsibility**. +2. **Legality has no readable answer.** A caller can claim and be refused, but cannot ask what is legal + and why. That is **admission control** with a **read-only precondition check**. +3. **Constraints have no declaration.** Repair-first currently lives as policy inside shared planning + code. Making preferences named constraints a plan enables is **declarative policy**, and the + requirement that one plan plus one set of facts yields one answer is **deterministic replay**. +4. **Read guarantees are unwritten.** Board reads are cursor-based and may lag; nothing states whether a + reader may assume monotonic reads or read-your-writes. The concepts are **monotonic reads**, + **read-your-writes** and, across one agent's own sequence of sessions, **causal consistency**. +5. **Disagreement has no arbiter.** Two agents that disagree about what to do next can only contest a + completion (`veto`) or race a claim. There is no clause for settling the disagreement itself. The + closest concepts are **optimistic concurrency with conflict detection** and, because one arbiter + already exists, a plain **single-writer decision**. Competitive allocation was already refused + ([board governance](../decisions/implemented/2026-09-06-board-governance-addressing.md)). +6. **Cross-run resources have no mutual exclusion.** Several runs competing for one worktree or one file + are handled by practice (a throwaway worktree per arm) and by the slot budget, not by a rule. The + concept is a **fenced mutex** - a lock whose holder cannot be believed after its lease lapses. +7. **In-doubt work has no resolution.** A delivery that nobody judged is `undecidable` when the run ends, + but an artifact left in doubt across runs has no clause. This is precisely an **in-doubt transaction**: + the fact is known, the outcome is not, and somebody has to resolve it or write down why not. + +## The concepts this system deliberately does not need + +- **Consensus, leader election, quorum, two-phase commit.** Truth lives in one store written by one + daemon, so there are no replicas to agree; the only fact needing agreement is who holds a claim, and a + single atomic compare-and-set provides it. +- **Exactly-once delivery.** What is delivered is an artifact plus a judgement, not a message. Repeating + work costs tokens and wall time, and that difference from a CPU, where replay is nearly free, is why + speculation measured as cost without gain in [the arms](../experiments/execution/archive/ooo-arms-2026-09-19/README.md). +- **Distributed transactions.** One run commits once; there is no atomic commit spanning parts. + +## Three analogies that mislead + +1. **Lease expiry is not death.** Expiry is a failure detector, and a detector reports a suspicion, never + a fact. The system already answers this correctly with attempt fencing - a new holder starts attempt + N+1, which voids the previous attempt's artifact and verdict - and that property must not be traded + away for convenience. +2. **The board is not a message queue.** It is a shared, attributed, expiring entry space: entries persist + and are re-read, so re-reading is normal and re-doing is the problem. Treating it as a queue would + make every reader a consumer and every re-read a duplicate. +3. **Agents are not replicas.** They do not agree by construction, they each judge independently, and + that independence is the point - it is why the board records what was delivered and who judged it + rather than trusting a self-report. + +## What would make this inventory complete + +Rows 9 and 10 are the interfaces the legality proposal covers: the readable answer and the declared +constraint set. Rows 1, 4, 5, 6 and 7 are gaps with no owner yet. Until each gap either gets a clause or +is written down as deliberately absent, "complete" for this umbrella means the parts that have owners, +not the parts a task needs. diff --git a/docs/design/protocol-governed-collaboration.zh-CN.md b/docs/design/protocol-governed-collaboration.zh-CN.md new file mode 100644 index 00000000..480178eb --- /dev/null +++ b/docs/design/protocol-governed-collaboration.zh-CN.md @@ -0,0 +1,55 @@ +# 协议化协作:组成部分、空缺,以及它们背后的分布式概念 + +[English](protocol-governed-collaboration.md) + +大类的名字是[协议化协作](../decisions/implemented/2026-09-20-name-the-collaboration-protocol.zh-CN.md)。本文是它的一份清单:有哪些子协议、哪一个是全的、还缺哪些,以及这些(已有的和缺的)在分布式领域里有哪些已经有了名字。 + +它是一份索引,不是第二份规格。每一部分的归属写在它那一行里;本文与归属方不一致时,以归属方为准。 + +## 大类由什么组成 + +| 部分 | 管什么 | 状态 | 归属 | +| ------------------ | -------------------------------------------------------------------------------------------------- | ------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| 条目与寻址 | 有类型、有归因、会过期的条目;紧凑读、游标、收件箱;定向投递(`to=`)、发现、能力寻址(`need`) | 完整 | [黑板治理与能力寻址](../decisions/implemented/2026-09-06-board-governance-addressing.zh-CN.md)、[黑板先广播后定向与串行](board-find-serial-a2a-compat-2026-08-13.md) | +| 身份与真实性 | 写入者归因、默认内容哈希、跨信任边界签名 | 完整 | [黑板治理与能力寻址](../decisions/implemented/2026-09-06-board-governance-addressing.zh-CN.md) | +| 拥有与时间 | 原子比较交换认领、租约与惰性过期、续租、释放、下一个待处理条目的串行晋升 | 机制完整;"谁来重活"这份责任没有归属 | `src/core/store/base.ts`、[黑板先广播后定向与串行](board-find-serial-a2a-compat-2026-08-13.md) | +| 终止完整性 | 可复核的终结与否决、带 digest 的 `deliver`、独立 `judge`、attempt 围栏、`undecidable` 作为一等结果 | 完整 | [黑板治理与能力寻址](../decisions/implemented/2026-09-06-board-governance-addressing.zh-CN.md) | +| 注意力与唤醒 | 只唤醒被点名的一个会话、按订阅确定成员、note 静默、ack 抑制重通知 | 完整 | [黑板先广播后定向与串行](board-find-serial-a2a-compat-2026-08-13.md) | +| 作用域与可见性 | 条目的可见范围是授权过的 agent 子集,黑板不是 all-to-all | 完整 | [黑板治理与能力寻址](../decisions/implemented/2026-09-06-board-governance-addressing.zh-CN.md) | +| 真值与协调状态分离 | 黑板是临时协调面,真值在 durable memory,`memory=` 做指针 | 完整 | [黑板治理](../decisions/implemented/2026-09-06-board-governance-addressing.zh-CN.md)、[记忆图](memory-graphs.md) | +| 任务单元 | 由输入、依赖、验收、能力、预算声明的单元;拆分;内部编译视图;run、它的收编、以及运行时拥有的事实 | 完整 | [任务单元语义](task-unit-semantics.md)、[义务台账](task-unit-semantics-obligations.md) | +| 合法性与准入 | 有序合法集合、按声明槽位预算切一刀、每个单元合法与否的理由 | 机制完整;可读的答案缺失 | `src/integration/ooo-board.ts`、[程序只回答合法性](../decisions/proposed/2026-09-20-the-program-answers-legality.zh-CN.md) | +| 顺序与约束 | 确定性(同一计划加同一组事实只产出一个答案)、修复优先、按计划顺序破平 | 机制完整;声明方式缺失 | [融合规划的修复优先](../decisions/implemented/2026-09-19-fusion-planning-repair-first.zh-CN.md)、[程序只回答合法性](../decisions/proposed/2026-09-20-the-program-answers-legality.zh-CN.md) | +| 预算与计量 | 声明槽位预算、token 与缓存计量、离线成本上限 | 完整 | [声明式槽位预算](../decisions/implemented/2026-09-18-declared-slot-budget.zh-CN.md)、[成本模型](../experiments/execution/ooo-cost-model-2026-09-17.md) | +| 取消与围栏 | 显式取消、取消后不留孤儿 worker 或检查、迟到产物被围栏挡住 | 完整 | [长检查分离运行](../decisions/implemented/2026-09-18-detached-long-checks.zh-CN.md)、[自举设计](ooo-execution-bootstrap.md) | +| 恢复与重放 | 轮次的日志加重放、事务性 outbox 在提交后与重启时排空、崩溃后不偷别人的活 | 臂内完整;产品面尚未接线 | [自举设计](ooo-execution-bootstrap.md) | +| 交接 | 会话身份来自黑板;延续面把下一位持有者需要的东西带过去 | 完整 | [会话身份来自黑板](../decisions/implemented/2026-09-19-session-identity-comes-from-the-board.zh-CN.md)、[会话延续义务](task-unit-semantics-obligations.md) | +| 证据与审计 | run 事实、验证证据、可追溯性汇总 | 完整 | [rtm 证据汇总](../decisions/implemented/2026-09-20-rtm-evidence-aggregation.zh-CN.md) | + +没有任何单独一部分能完成任务。一次任务完成,需要单元的声明、拥有与时间、终止完整性、注意力、合法性、预算、真值分离、交接**同时**成立;上面这些行就是一件活不能缺的那几样。 + +## 空缺,以及可以为它们借用的概念 + +1. **收编者没有协议条款。** 谁有资格把一次讨论收编成一次 run,以及租约失效后谁负责把活重新推出去,都没有写。机制是有的(`expiry`、串行晋升、派发循环),责任没有。分布式里把前者叫做**提交边界**——让一份商定的安排生效的那个唯一点;把后者叫做**孤儿回收/重启责任**。 +2. **合法性没有可读的答案。** 调用者可以去认领然后被拒,但无法问"现在什么合法、为什么"。这是**准入控制**加一次**只读的前置条件检查**。 +3. **约束没有声明方式。** 修复优先目前活在共享规划代码里当策略。把偏好变成计划可启用的具名约束,是**声明式策略**;而"同一计划加同一组事实只产出一个答案"这个要求,是**确定性重放**。 +4. **读的保证没有写下来。** 黑板读基于游标、可能落后;没有一处说明读者是否可以假设单调读或读己所写。概念是**单调读**、**读己所写**,以及跨同一个 agent 自己一串会话的**因果一致性**。 +5. **分歧没有仲裁者。** 两个 agent 对下一步做什么意见不合时,只能争夺一次完成(`veto`)或抢认领。对分歧本身没有条款。最近的概念是**乐观并发加冲突检测**,以及——因为仲裁者本来就存在——干脆的**单写者决定**。竞价式分配早就被拒绝了([黑板治理](../decisions/implemented/2026-09-06-board-governance-addressing.zh-CN.md))。 +6. **跨 run 的资源没有互斥。** 多个 run 抢同一个工作树或同一个文件,现在靠实践(每条臂一个一次性 worktree)和槽位预算应付,没有规则。概念是**带围栏的互斥锁**——租约失效之后,它的持有者就不能再被相信。 +7. **悬而未决的活没有归属。** 没人裁决的交付在 run 结束时算 `undecidable`,但跨 run 之后仍悬着的产物没有条款。这正是一个**存疑事务(in-doubt transaction)**:事实已知、结果未知,必须有人解掉它,或者写下为什么不。 + +## 这套系统刻意不需要的概念 + +- **共识、领导者选举、quorum、两阶段提交。** 真值在一个由唯一 daemon 写入的 store 里,没有副本需要达成一致;唯一需要一致的只有"谁持有认领",而一次原子比较交换就给了它。 +- **恰好一次投递。** 交付的是产物加一次判断,不是消息。重复做活的代价是 token 和墙钟时间,而这个与 CPU(重放几乎免费)的差别,正是[臂的实测](../experiments/execution/archive/ooo-arms-2026-09-19/README.md)里推测呈现为"成本真实、收益为零"的原因。 +- **分布式事务。** 一次 run 只提交一次,没有跨部分的原子提交。 + +## 三个会误导的类比 + +1. **租约过期不是死亡。** 过期是故障检测器,而检测器给出的是疑似,从不是事实。系统已经用 attempt 围栏正确回答了这一点——新的持有者从第 N+1 次尝试开始,作废上一代的产物与裁决——这条性质不能为了方便让掉。 +2. **黑板不是消息队列。** 它是一个共享的、带归因的、会过期的条目空间:条目留着、会被重读,所以重读是常态,重做才是问题。把它当队列,会让每个读者都变成消费者、每次重读都变成重复。 +3. **agent 不是副本。** 他们不靠构造达成一致,各自独立判断,而这种独立正是关键——所以黑板记录"交付了什么、谁判的",而不是相信自报。 + +## 这份清单要怎样才算完整 + +第 9、10 行就是合法性提案覆盖的两个接口:可读的答案与可声明的约束集合。第 1、4、5、6、7 行是还没有主的空缺。在每个空缺或者拿到条款、或者被明确写成"刻意没有"之前,这个大类的"完整"指的只是那些有归属的部分,而不是一件任务真正需要的那几样。 From d93ae396ac0d321435c3cb69eecece646d01dac4 Mon Sep 17 00:00:00 2001 From: wefio <48851810+wefio@users.noreply.github.com> Date: Mon, 21 Sep 2026 22:33:19 +0800 Subject: [PATCH 08/13] docs(decisions): the frame, its data format, and its storage The primitives have names and the program's job is proposed, but the frame itself was undefined: which fields exist, how values are encoded, where they live in the store, and how a peer protocol sits beside the Task-Unit Protocol. Researching the finished specifications changed the shape of the problem - every layer but one is already defined by somebody - so this record chooses and names rather than invents. What it borrows, per layer: CloudEvents 1.0 for the envelope (only id/source/specversion/type required, context attributes inspected without deserializing the event data, lowercase names of at most 20 characters, data reserved); A2A 1.0.0 for the task model (terminal states, contextId, cursor paging chosen explicitly over offsets, historyLength, includeArtifacts false by default); NLIP/ECMA-430 for the payload discriminator and the progressive shape (messagetype/format/subformat/content/submessages/label, the first submessage inlined because most messages carry none, structured with subformat uri carrying a URI instead of bytes); MCP 2025-06-18 for LLM-facing results (content blocks with annotations, structured content beside text, isError so a failure is data); RFC 8941 for a one-line text type system; FIPA-ACL for the act and conversation fields; RFC 6709 for the unknown-field choice; ECMA-434 for the security layer. The one layer nobody defines is ownership, lease, fencing and legality, because every agent protocol assumes a task belongs to one agent. Decisions it records: the frame is a state record and not a message, so FIPA's performative is a rendering of existing columns rather than a column; the header stays in columns because the claim is one atomic compare-and-set UPDATE and a header inside JSON would turn that into read-modify-write; the payload is one opaque document, with the invariant that every payload can be NULL and T0/T1 still render while claim, wake, expiry and compact read still work (verified on the store's SQLite 3.53.3); a payload field a protocol wants to query is indexed by a generated column plus an expression index rather than a new board column, which is what keeps the schema from growing with the number of protocols; the protocol is a value with a version, a peer protocol supplies declaration validation, legality, projection and acceptance, and it plugs into the existing DispatchBoard and RoundQueryPort rather than into the board; unknown protocol means readable and not actionable, and unknown fields are decided by the protocol's own version rule, stated rather than assumed. Data format, in three separate questions: JSON on the existing RPC wire (protobuf-as-normative-source is not worth it with one binding); flat key/value for T0/T1 and natural language for negotiation, because format restrictions measurably degrade reasoning and escaping is a failure source, with tool schemas shaped for strict mode (additionalProperties false, every property required, absence as nullable); typed columns plus one JSON payload document in the store. TOON and CBOR are deliberately not adopted: TOON's saving applies to flat uniform data pushed into context, which is what pointer-first avoids, and CBOR waits for a transport with a bandwidth constraint. Security is the layer that is empty. ECMA-434 is mandatory for NLIP conformance and its companion guidelines score fifteen threats with prompt injection highest; two rules are adopted as text now (an in-band session token is a session credential and must not reach the model, and a caller's token is never forwarded), and a threat list of our own is owed. The scope ceiling is stated so the change cannot grow: one group of nullable columns, one boundary adapter, one rendering rule, one test. Anything needing a new table, a new tool or a new channel is outside this decision. --- .../2026-09-21-the-frame-and-its-storage.md | 182 ++++++++++++++++++ ...6-09-21-the-frame-and-its-storage.zh-CN.md | 120 ++++++++++++ 2 files changed, 302 insertions(+) create mode 100644 docs/decisions/proposed/2026-09-21-the-frame-and-its-storage.md create mode 100644 docs/decisions/proposed/2026-09-21-the-frame-and-its-storage.zh-CN.md diff --git a/docs/decisions/proposed/2026-09-21-the-frame-and-its-storage.md b/docs/decisions/proposed/2026-09-21-the-frame-and-its-storage.md new file mode 100644 index 00000000..49a50676 --- /dev/null +++ b/docs/decisions/proposed/2026-09-21-the-frame-and-its-storage.md @@ -0,0 +1,182 @@ +# The frame, its data format, and its storage + +[中文](2026-09-21-the-frame-and-its-storage.zh-CN.md) + +**Status:** proposed +**Relates to:** [The program answers legality](2026-09-20-the-program-answers-legality.md), [Name the collaboration protocol and its task-unit sub-protocol](../implemented/2026-09-20-name-the-collaboration-protocol.md), [Protocol-governed collaboration: the parts, the gaps](../../design/protocol-governed-collaboration.md), [Board governance and capability addressing](../implemented/2026-09-06-board-governance-addressing.md), [Task unit semantics](../../design/task-unit-semantics.md) + +## Problem + +The primitives are named, the umbrella is inventoried, and the program's job - answer legality - is +proposed. What is still undefined is the frame itself: which fields exist, how their values are encoded, +where they live in the store, and how a **peer** protocol sits beside the Task-Unit Protocol. Left open, +every addition re-opens the same questions, and the natural answers are the expensive ones: parse the +payload in order to route, add a column per protocol, invent a fresh vocabulary per protocol. + +A survey of finished specifications changes the shape of the problem. Every layer but one is already +defined by somebody, so the work is choosing and naming, not inventing: + +| Layer | Already defined by | +| ----------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Event envelope: identity, time, version, content type, mode | CloudEvents 1.0 - required are only `id`/`source`/`specversion`/`type`; context attributes are "inspected at the destination without having to deserialize the event data"; attribute names are lowercase ASCII, at most 20 characters, never `data`; the type set is closed | +| Task, parts, states, paging | A2A 1.0.0 - terminal-state semantics, `contextId`, cursor paging (`pageToken`/`nextPageToken`, chosen explicitly over offsets), `historyLength`, and `includeArtifacts` defaulting false "to reduce payload size" | +| Payload discriminator and progressive shape | NLIP / ECMA-430 - `messagetype`, `format`, `subformat`, `content`, `submessages[]`, `label`; the first submessage is inlined at the top because most messages carry none; `format: structured` with `subformat: uri` carries a URI instead of bytes | +| LLM-facing results and errors | MCP 2025-06-18 - content blocks with `annotations.audience`, `structuredContent` beside text, and `isError` in the result, so a failure is data | +| One-line text encoding | RFC 8941 Structured Fields - List/Dictionary/Item with Parameters, intentionally strict | +| Acts and conversation threading | FIPA-ACL - `performative` is the only required field, beside `protocol`, `conversation-id`, `in-reply-to`, `reply-by` | +| Unknown-field policy | RFC 6709 - ignoring and refusing by name are both legitimate; the choice has to be stated | +| Security | ECMA-434 and the NLIP security guidelines - mandatory for NLIP conformance, with a scored threat list | +| **Ownership, lease, fencing, legality** | **Nobody.** Every agent protocol assumes a task belongs to one agent, so "who may hold it" does not arise | + +## Proposal + +### 1. The frame + +The frame is a board entry, and it is a **state record, not a message**. That distinction decides the +field set: the acts are already state columns (a claim, a delivery, a verdict, a resolution), so FIPA's +`performative` is a rendering of those columns rather than a column of its own. + +- **Header columns** (the board acts on these alone): `id`, `channel`, `kind` (the seven collaboration + kinds, unchanged), `state`, `author`, `source_session`, `to`, `need`, `scope`, `created_at`, + `expires_at`, `lease{holder, until}`, `attempt`, `digest`, `verdict`, `acked`, `resolution`, plus the + protocol selectors `protocol` and `protocol_version`. +- **One payload document**: whatever the named protocol needs, opaque to the board. +- **Rendering, not storage**: the act an entry shows (FIPA's `performative`), and the progressive tiers + T0 header, T1 summary, T2 structure, T3 truth. + +### 2. The data format, three separate questions + +- **Wire bytes.** JSON over the daemon's existing RPC. Unchanged; no second binding exists to justify + code generation, so A2A's protobuf-as-normative-source answer is not adopted yet. +- **LLM tokens.** T0 and T1 render as flat key/value lines, not as JSON with braces and quotes. The + negotiation text stays natural language: forcing a model to emit JSON for prose costs reasoning + (format constraints measurably degrade it, and stricter constraints degrade it more), and escaping is + a failure source. Structured content is never inlined in the body - it is reached through the + discriminator and a pointer. An agent's own writes are tool arguments validated by the harness, and + those schemas follow strict mode: `additionalProperties: false` on every object, every property listed + in `required`, absence expressed as nullable rather than by omission. +- **Storage.** Typed columns for the header, one JSON document for the payload (section 3). + +Deliberately not adopted at this layer: TOON (its 30-60% saving applies to flat uniform data that has to +be pushed into context, which is exactly what pointer-first avoids), CBOR with a JSON fallback (ECMA-432 +deferred until a transport with a bandwidth constraint exists), and protobuf as a normative source. + +### 3. Storage layout and its invariant + +The header stays in columns because the claim is one atomic compare-and-set `UPDATE`. A header inside a +JSON document would turn atomic claiming into read-modify-write. That is a mechanism constraint, not a +preference. + +The payload is one document beside those columns, and the board never parses it. The store already keeps +this shape: `claims_json`, `markers_json`, `scope_json`, `evidence_ids_json` and others sit beside typed +columns in the same tables. + +**Invariant (testable):** with `payload` set to NULL for every row, T0 and T1 still render and claim, +wake, expiry and compact read still work. Verified against the store's own SQLite (3.53.3). + +When a protocol needs to query a field inside its own payload, it does not ask the board for a column: a +generated column plus an expression index answers it (also verified on the same SQLite). This is what +keeps the schema from growing with the number of protocols. + +Cost, stated plainly: JSON columns give up database-level typing, a trade the store already makes, and +cross-protocol queries depend on generated columns or the protocol's own tables. + +### 4. Peer protocols and replacement + +The protocol is a **value**, not a schema. The Task-Unit Protocol is one value; a peer protocol is +another, and the board's coordination layer stays protocol-agnostic (envelope, addressing, time and +lease, claim and fencing, delivery digest and verdict, wake, scope, retention). + +A peer protocol supplies four narrow things: + +1. **Declaration validation** - refuse by name when a field it cannot map is missing, never downgrade it + to free text (the semantics design already states this rule). +2. **Legality** - the definition of which units are legal under the current facts. This is the party that + defines the answer "the program answers legality" is about. +3. **Projection** - the T1 summary line and the T2 structure. Required because the payload is opaque to + the board, and the shape MCP and A2A already use for the same reason. +4. **Acceptance** - the criteria for accepting a delivery. The form (digest plus independent verdict) + does not change. + +The insertion points already exist: `DispatchBoard` and `RoundQueryPort`. A protocol plugs in on the +semantics side, and replacing one means another implementation plus another `protocol` value; the payloads +of entries written under the old value stay exactly as they are - readable, not actionable, and no +migration. + +Unknown handling is stated rather than assumed: an unknown `protocol` makes an entry readable and not +actionable, and an unknown field inside a known protocol is decided by that protocol's version rule, +which must say whether it is ignored or refused by name. + +### 5. Security, the layer that is empty + +ECMA-434 is a conformance requirement for NLIP, and its companion security guidelines score fifteen +threats - prompt injection highest, then confused-deputy and token passthrough, session hijack, memory +injection. Two rules are adoptable now as text: + +- An in-band session token is a session credential: it must not be handed to the agent's model or exposed + in application-level payloads. The board's session identity is in-band and the board renders into model + context, so this rule applies to us directly. +- A caller's token is never forwarded; a scoped token is minted instead. This becomes load-bearing only + when entries cross a trust boundary, which today they do not. + +What is owed is a threat list of our own, with the ones we accept and the ones we do not defend. + +### 6. Scope ceiling + +The change surface is one group of nullable columns, one boundary adapter, one rendering rule and one +test. Anything that needs a new table, a new tool or a new channel is outside this decision. + +## Plan + +1. This record. No code. +2. One migration: `protocol`, `protocol_version`, `payload_format`, `payload`, plus the invariant test. +3. Refusals become data at the RPC and tool boundary; the store keeps throwing. +4. Tool schemas shaped for strict mode; flat T0/T1 rendering. +5. A threat list, with the two rules carried as text until a trust boundary exists. + +## Alternatives considered + +- **One JSON document for the whole entry, header included.** Rejected: atomic claiming becomes + read-modify-write, and every reader would have to parse to find out whether an entry is claimable. +- **A column group per protocol.** Rejected: the schema grows with the number of protocols and every new + protocol has to touch the board, which is the rework this record exists to avoid. +- **protobuf as the normative source with generated bindings** (A2A's answer). Rejected for now: there is + one wire and no second binding, so code generation buys nothing yet. +- **A single-line RFC 8941 encoding for the header.** Rejected for storage - our header lives in columns + - but kept as an option if a compact textual read is ever needed. +- **TOON.** Rejected: the saving appears on flat uniform data pushed into context, and the frame is flat + with pointers instead. +- **CBOR with a JSON text fallback.** Deferred until a transport with a bandwidth constraint exists. +- **Storing the structured payload as JSON inside the entry body.** Rejected: escaping failures, the + reasoning cost of format constraints, and MCP's own note that structured content is a different thing + from schema-constrained model generation. + +## Acceptance criteria + +- With every `payload` NULL, the board renders T0/T1 and completes claim, wake, expiry and compact read. +- Adding a peer protocol changes nothing in the board's claim, wake or expiry paths, demonstrated by a + second protocol value with its own payload shape and its own projection. +- An entry whose protocol the store does not know is readable, is not actionable, and its payload is never + parsed by the board. +- A payload field a protocol wants to query is indexed by a generated column and an expression index, + with no new board column. +- Header field names follow the borrowed naming constraints: lowercase ASCII, at most 20 characters, and + `data` is not used as a name. +- A refusal reaches the caller as data with a reason, not as an exception, at the RPC and tool boundary. +- LLM-facing schemas are strict-mode shaped, and no negotiation text is required to be a JSON string. +- Every new field names the specification it came from, or records why none fitted. + +## Risks + +- One document per entry invites over-stuffing: the header can drift into the payload until the board has + to parse it again. The invariant test and the never-parse rule are the mitigation. +- A protocol that projects its own summary can summarize dishonestly. Mitigation: projection is separate + from the verdict, and only `accepted` may be depended on. +- Generated columns and expression indexes are SQLite-specific; a portable store would have to re-examine + them. +- Refusals-as-data changes the shape existing callers see, so the arms' drivers have to move in the same + slice. +- Strict-mode schemas make absence explicit, which costs tokens per call and can make a nullable field + read as mandatory. +- A borrowed vocabulary can ossify. If a borrowed name does not fit, deviating is allowed - and the + deviation is recorded rather than renamed silently. diff --git a/docs/decisions/proposed/2026-09-21-the-frame-and-its-storage.zh-CN.md b/docs/decisions/proposed/2026-09-21-the-frame-and-its-storage.zh-CN.md new file mode 100644 index 00000000..168a2320 --- /dev/null +++ b/docs/decisions/proposed/2026-09-21-the-frame-and-its-storage.zh-CN.md @@ -0,0 +1,120 @@ +# 帧、它的数据格式与它的存储 + +[English](2026-09-21-the-frame-and-its-storage.md) + +**Status:** proposed +**Relates to:** [程序只回答合法性](2026-09-20-the-program-answers-legality.zh-CN.md)、[给协作协议及其任务单元子协议命名](../implemented/2026-09-20-name-the-collaboration-protocol.zh-CN.md)、[协议化协作:组成部分与空缺](../../design/protocol-governed-collaboration.zh-CN.md)、[黑板治理与能力寻址](../implemented/2026-09-06-board-governance-addressing.zh-CN.md)、[任务单元语义](../../design/task-unit-semantics.md) + +## 问题 + +元语有了名字,大类有了清单,程序的职责(只回答合法性)也有了提案。还没定的是**帧本身**:有哪些字段、取值怎么编码、存在库里的哪里、以及一个**平级**协议怎样与任务单元协议并排。放着不定,每次新增都会重开同一批问题,而最自然的答案恰好都是贵的:为了让路由能动就得解析载荷、每加一个协议就加一列、每个协议各造一套词。 + +对已定规范的调研改变了问题的形状:除了一层之外,每一层都已经有人定过,所以这件事是**选择与命名**,不是发明: + +| 层 | 已由谁定 | +| ------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | +| 事件信封:身份、时间、版本、内容类型、模式 | CloudEvents 1.0——必填只有 `id`/`source`/`specversion`/`type`;属性原话是"在目的地被检查而无需反序列化事件数据";属性名小写 ASCII、最多 20 字符、不得叫 `data`;类型集合封闭 | +| 任务、部件、状态、分页 | A2A 1.0.0——终态语义、`contextId`、游标分页(`pageToken`/`nextPageToken`,并明确不用 offset)、`historyLength`、`includeArtifacts` 默认 false("缩减载荷大小") | +| 载荷判别与渐进形状 | NLIP / ECMA-430——`messagetype`、`format`、`subformat`、`content`、`submessages[]`、`label`;首子消息平铺在顶层,因为多数消息不带子消息;`format: structured` 配 `subformat: uri` 时内容是一个 URI 而不是字节 | +| 面向 LLM 的结果与错误 | MCP 2025-06-18——内容块带 `annotations.audience`、`structuredContent` 与文本并存、`isError` 放在结果里,于是失败也是数据 | +| 单行文本编码 | RFC 8941 Structured Fields——List/Dictionary/Item 加 Parameters,故意严格 | +| 行为与对话线程 | FIPA-ACL——`performative` 是唯一必填,旁边是 `protocol`、`conversation-id`、`in-reply-to`、`reply-by` | +| 未知字段策略 | RFC 6709——忽略与按名字拒绝都合法,但**必须写明选了哪个** | +| 安全 | ECMA-434 与 NLIP 安全指南——对 NLIP 是符合性要求,并给出打分的威胁清单 | +| **拥有权、租约、围栏、合法性** | **没有人。** 所有 agent 协议都假设任务属于某一个 agent,"谁能持有"不成立 | + +## 提案 + +### 1. 帧 + +帧是一个黑板条目,而且它是**状态记录,不是消息**。这个区别决定了字段集:行为本身已经是状态列(一次认领、一次交付、一个裁决、一次了结),所以 FIPA 的 `performative` 是这些列的**渲染**,不是它自己的列。 + +- **头列**(板子只靠这些行动):`id`、`channel`、`kind`(七个协作用途,不变)、`state`、`author`、`source_session`、`to`、`need`、`scope`、`created_at`、`expires_at`、`lease{holder, until}`、`attempt`、`digest`、`verdict`、`acked`、`resolution`,加上协议选择子 `protocol` 与 `protocol_version`。 +- **一条载荷文档**:具名协议自己需要的东西,对板子不透明。 +- **渲染,不是存储**:条目呈现的行为(FIPA 的 `performative`),以及分层的 T0 头、T1 摘要、T2 结构、T3 真值。 + +### 2. 数据格式是三个分开的问题 + +- **线上字节**:JSON,走 daemon 现有的 RPC。不变;既然只有一条线而没有第二个绑定,就不值得为代码生成付费,所以 A2A 的"proto 作规范源"暂不采用。 +- **给 LLM 的 token**:T0 与 T1 渲染成扁平 key/value 行,不是带花括号和引号的 JSON。协商内容保持自然语言:逼模型为散文产出 JSON 会付出推理代价(格式约束会可测地降低推理,约束越严越低),而转义本身就是失败源。结构化内容永远不内联进正文——它经判别位加指针抵达。agent 自己写的东西是工具参数,由 harness 校验,而那些 schema 按 strict 模式写:每个 object 都 `additionalProperties: false`、每个属性都列进 `required`、缺省用 nullable 表达而不是省略。 +- **存储**:头进列,载荷进一条 JSON 文档(第 3 节)。 + +这一层刻意不采用:TOON(它 30–60% 的节省出现在"必须把扁平均匀数据推进上下文"的场景,而这正是指针优先要避开的)、CBOR 加 JSON 回退(ECMA-432 留到真出现有带宽约束的传输时再谈)、proto 作规范源。 + +### 3. 存储布局与它的不变量 + +头留在列里,因为认领是**一条原子比较交换的 `UPDATE`**。把头放进 JSON 文档会把原子认领变成读-改-写。这是机制约束,不是偏好。 + +载荷是与这些列并排的一条文档,板子从不解析它。这个形状库里已有:`claims_json`、`markers_json`、`scope_json`、`evidence_ids_json` 等,就是与 typed 列同表并存的。 + +**不变量(可测)**:把所有行的 `payload` 置为 NULL,T0 与 T1 仍渲染,认领、唤醒、过期、紧凑读仍工作。已在 store 自己的 SQLite(3.53.3)上验证。 + +当某个协议要查自己载荷里的字段时,它**不要**向板子要一列:生成列加表达式索引就够了(同样在该 SQLite 上验证过)。这一点正是让 schema 不随协议数量增长的机制。 + +代价说明白:JSON 列放弃数据库级类型检查(这是仓库早已做过的交换),跨协议查询依赖生成列或协议自己的表。 + +### 4. 平级协议与替换 + +协议是**值**,不是模式。任务单元协议是一个取值;平级协议是另一个取值,而板子的协调层保持协议无关(信封、寻址、时间与租约、认领与围栏、交付 digest 与裁决、唤醒、作用域、保留)。 + +一个平级协议要提供四样窄的东西: + +1. **声明校验**——它无法映射的字段缺失时按名字拒绝,绝不降级为自由文本(语义设计已经立了这条)。 +2. **合法性**——在当下事实上哪些单元合法。这就是"程序只回答合法性"里那个合法性的**定义方**。 +3. **投影**——T1 的一行摘要与 T2 的结构。必须有,因为载荷对板子不透明;MCP 与 A2A 出于同样理由也是这个形状。 +4. **验收**——接受一次交付的判据。形式(digest 加独立裁决)不变。 + +插入点已经存在:`DispatchBoard` 与 `RoundQueryPort`。协议接在语义侧,替换一个协议就是换一个实现加换一个 `protocol` 取值;旧取值下写入的载荷原样留着——可读、不可行动,且不需要迁移。 + +未知的处理要**写明**而不是默认:未知 `protocol` 使条目可读但不可行动;已知协议内的未知字段由该协议的版本规则决定,而那条规则必须说明是忽略还是按名字拒绝。 + +### 5. 安全,那个还空着的层 + +ECMA-434 对 NLIP 是符合性要求,配套安全指南给十五类威胁打分:prompt injection 最高,其次是 confused-deputy 与令牌透传、会话劫持、记忆投毒。两条现在就能以文字形式采纳: + +- 带内会话令牌就是会话凭据:不得交给 agent 的模型,也不得暴露在应用层载荷里。黑板的会话身份是带内的,而黑板会渲染进模型上下文,所以这条直接适用于我们。 +- 绝不转发调用方的令牌,改为铸一个有范围的令牌。这条只有在条目跨信任边界时才承重,而今天还没有那样的边界。 + +欠的是一份我们自己的威胁清单,写明接受哪些、不防御哪些。 + +### 6. 范围上限 + +改动面是一组可空列、一处边界适配、一条渲染规则、一条测试。**任何需要新表、新工具、新频道的东西都在本决策之外。** + +## 计划 + +1. 本文。不动代码。 +2. 一次迁移:`protocol`、`protocol_version`、`payload_format`、`payload`,加那条不变量测试。 +3. 拒绝在 RPC 与工具边界变成数据;store 继续抛异常。 +4. 工具 schema 按 strict 模式写;T0/T1 扁平渲染。 +5. 一份威胁清单,两条规则先以文字承载,等出现信任边界再谈强制。 + +## 考虑过的替代方案 + +- **整个条目一条 JSON 文档(含头)**。拒绝:原子认领会退化成读-改-写,而且每个读者都得先解析才知道条目能不能认领。 +- **每个协议一组列**。拒绝:schema 随协议数量增长,每个新协议都要动板子——那正是本文要避免的返工。 +- **proto 作规范源并生成绑定**(A2A 的答案)。暂时拒绝:只有一条线、没有第二个绑定,代码生成现在买不到东西。 +- **头用 RFC 8941 的单行编码**。存储上拒绝——我们的头活在列里——但若将来需要紧凑文本读,保留为选项。 +- **TOON**。拒绝:节省只出现在被推进上下文的扁平均匀数据上,而帧是扁平的且有指针。 +- **CBOR 加 JSON 文本回退**。留到真出现有带宽约束的传输。 +- **把结构化载荷以 JSON 塞进条目正文**。拒绝:转义失败、格式约束的推理代价,以及 MCP 自己写明的一点——结构化内容与"schema 约束的模型生成"是两回事。 + +## 验收标准 + +- 所有 `payload` 为 NULL 时,板子仍渲染 T0/T1,并仍能完成认领、唤醒、过期与紧凑读。 +- 增加一个平级协议不改变板子的认领、唤醒、过期路径;用一个自带载荷形状与投影的第二协议取值演示。 +- store 不认识的协议:条目可读、不可行动,且板子从不解析它的载荷。 +- 协议想查的载荷字段由生成列加表达式索引支撑,板子不加新列。 +- 头字段名遵守借来的命名约束:小写 ASCII、最多 20 字符、不使用 `data` 作为名字。 +- 拒绝在 RPC 与工具边界以带理由的数据抵达调用者,而不是异常。 +- 面向 LLM 的 schema 按 strict 模式成型,且没有任何协商文本被要求写成 JSON 字符串。 +- 每个新字段都指名它来自哪份规范,或者记录下为什么没有一份合适。 + +## 风险 + +- 每条一条文档会诱发"什么都往里塞":头可能慢慢漂进载荷,直到板子又得解析它。缓解不变量测试与"板子从不解析"这条规则。 +- 自己给自己做投影的协议可以说得不诚实。缓解:投影与裁决分离,且只有 `accepted` 可被依赖。 +- 生成列与表达式索引是 SQLite 特有的;换可移植存储时要重新审视。 +- "拒绝即数据"改变现有调用者看到的形状,所以臂的 driver 必须在同一个切片里一起动。 +- strict 模式的 schema 让缺省显式化,每次调用多花 token,而且可能让读者把可空字段读成必填。 +- 借来的词汇会僵化。若某个借来的名字不合身,允许偏离——但要**记录**偏离,而不是悄悄改名。 From 9e1041d7ff1d28d3c342d1b31efe0a3b1b38c737 Mon Sep 17 00:00:00 2001 From: wefio <48851810+wefio@users.noreply.github.com> Date: Mon, 21 Sep 2026 22:46:19 +0800 Subject: [PATCH 09/13] docs(decisions): answer the frame record's review - mapping, seam, two retractions Four review points, all accepted, plus the code checks behind them. 1. The field mapping was missing, and it changes what the invariant means. The record now carries an existing-to-proposed table: content stays the single source of the entry body (readTaskBoardPreviews projects taskBoardPreview(entry.content) - whitespace collapsed, 200 characters, a memory= body returned verbatim), so "payload NULL still renders T1" is a property of the board's read path and not something the storage format produces by itself; the frozen declaration was never a body, it lives in task_run_manifest and task_run_tasks reached by run_id and task_id, with task_run_facts' own payload column as the precedent; and the proposed payload carries nothing the Task-Unit Protocol needs, so it stays NULL for Task-Unit entries. A rule follows from it: a payload may not restate a field that already has a column or a table, which is how the second task body would appear. 2. "The insertion points already exist" was not true. RoundQueryPort carries two typed reads, cancelled() and accepted(); the dispatch loop fails a claim whose ticket has no patch, so patch work is the only work shape today and a peer protocol must bring its own. The record now says what is reusable material and what is still a plan step, and adds that step: widen the query port or add one beside DispatchBoard. 3. Two storage arguments are withdrawn. Atomicity does not require columns: measured on the store's SQLite 3.53.3, a conditional UPDATE over a JSON payload (WHERE json_extract(payload,'$.state')='open') succeeded once and changed zero rows on the second claim, identical to the column form - so the reason is clarity, direct indexing, CHECK constraints and PRAGMA table_info migration guards. And a generated column is a column, an index is a schema object: peer protocols are not schema-free. What is bought is narrower and still worth it - the added schema is purely additive, belongs to the protocol rather than the board, and no field is stored twice. 4. Three borrowings were applied too widely. source_session_id is provenance, not a credential, so the security rule is now stated as a constraint on what may be added rather than a defence of what exists. The refusal rule now keeps MCP's split: a domain refusal becomes data, a request-level error stays an error - which the dispatch loop already does by separating refused from failure. And the CloudEvents naming constraint is stated as borrowing (lowercase letters and digits; twenty characters recommended), with protocol_version's underscore recorded as a deviation rather than as compliance. --- .../2026-09-21-the-frame-and-its-storage.md | 119 ++++++++++++------ ...6-09-21-the-frame-and-its-storage.zh-CN.md | 71 +++++++---- 2 files changed, 129 insertions(+), 61 deletions(-) diff --git a/docs/decisions/proposed/2026-09-21-the-frame-and-its-storage.md b/docs/decisions/proposed/2026-09-21-the-frame-and-its-storage.md index 49a50676..06ecb919 100644 --- a/docs/decisions/proposed/2026-09-21-the-frame-and-its-storage.md +++ b/docs/decisions/proposed/2026-09-21-the-frame-and-its-storage.md @@ -16,17 +16,17 @@ payload in order to route, add a column per protocol, invent a fresh vocabulary A survey of finished specifications changes the shape of the problem. Every layer but one is already defined by somebody, so the work is choosing and naming, not inventing: -| Layer | Already defined by | -| ----------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| Event envelope: identity, time, version, content type, mode | CloudEvents 1.0 - required are only `id`/`source`/`specversion`/`type`; context attributes are "inspected at the destination without having to deserialize the event data"; attribute names are lowercase ASCII, at most 20 characters, never `data`; the type set is closed | -| Task, parts, states, paging | A2A 1.0.0 - terminal-state semantics, `contextId`, cursor paging (`pageToken`/`nextPageToken`, chosen explicitly over offsets), `historyLength`, and `includeArtifacts` defaulting false "to reduce payload size" | -| Payload discriminator and progressive shape | NLIP / ECMA-430 - `messagetype`, `format`, `subformat`, `content`, `submessages[]`, `label`; the first submessage is inlined at the top because most messages carry none; `format: structured` with `subformat: uri` carries a URI instead of bytes | -| LLM-facing results and errors | MCP 2025-06-18 - content blocks with `annotations.audience`, `structuredContent` beside text, and `isError` in the result, so a failure is data | -| One-line text encoding | RFC 8941 Structured Fields - List/Dictionary/Item with Parameters, intentionally strict | -| Acts and conversation threading | FIPA-ACL - `performative` is the only required field, beside `protocol`, `conversation-id`, `in-reply-to`, `reply-by` | -| Unknown-field policy | RFC 6709 - ignoring and refusing by name are both legitimate; the choice has to be stated | -| Security | ECMA-434 and the NLIP security guidelines - mandatory for NLIP conformance, with a scored threat list | -| **Ownership, lease, fencing, legality** | **Nobody.** Every agent protocol assumes a task belongs to one agent, so "who may hold it" does not arise | +| Layer | Already defined by | +| ----------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Event envelope: identity, time, version, content type, mode | CloudEvents 1.0 - required are only `id`/`source`/`specversion`/`type`; context attributes are "inspected at the destination without having to deserialize the event data"; attribute names are lowercase letters and digits, where the twenty-character limit is a recommendation rather than a rule, and `data` is not used as a name; the type set is closed | +| Task, parts, states, paging | A2A 1.0.0 - terminal-state semantics, `contextId`, cursor paging (`pageToken`/`nextPageToken`, chosen explicitly over offsets), `historyLength`, and `includeArtifacts` defaulting false "to reduce payload size" | +| Payload discriminator and progressive shape | NLIP / ECMA-430 - `messagetype`, `format`, `subformat`, `content`, `submessages[]`, `label`; the first submessage is inlined at the top because most messages carry none; `format: structured` with `subformat: uri` carries a URI instead of bytes | +| LLM-facing results and errors | MCP 2025-06-18 - content blocks with `annotations.audience`, `structuredContent` beside text, and `isError` for a tool execution error, which is a different thing from a protocol error and stays an error | +| One-line text encoding | RFC 8941 Structured Fields - List/Dictionary/Item with Parameters, intentionally strict | +| Acts and conversation threading | FIPA-ACL - `performative` is the only required field, beside `protocol`, `conversation-id`, `in-reply-to`, `reply-by` | +| Unknown-field policy | RFC 6709 - ignoring and refusing by name are both legitimate; the choice has to be stated | +| Security | ECMA-434 and the NLIP security guidelines - mandatory for NLIP conformance, with a scored threat list | +| **Ownership, lease, fencing, legality** | **Nobody.** Every agent protocol assumes a task belongs to one agent, so "who may hold it" does not arise | ## Proposal @@ -44,6 +44,18 @@ field set: the acts are already state columns (a claim, a delivery, a verdict, a - **Rendering, not storage**: the act an entry shows (FIPA's `performative`), and the progressive tiers T0 header, T1 summary, T2 structure, T3 truth. +What already holds the data, existing field to proposed field - nothing below moves: + +| Existing | In this frame | +| --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `content` (NOT NULL) | The single source of the entry body. `readTaskBoardPreviews` projects `taskBoardPreview(entry.content)`: whitespace collapsed, cut to 200 characters, and a body that is only a `memory=` pointer returned verbatim | +| `kind`, `status`, `serial_state`, `created_at`, `expires_at`, `resolved_at`, `resolved_by`, `resolution` | Header columns, unchanged | +| `agent_id`, `to`, `source_session_id` | Header columns, unchanged. `source_session_id` is provenance - which session wrote the entry - not a credential | +| `claimed_by`, `claimed_at`, `claim_expires_at`, `attempt` | The lease and the attempt fence, unchanged | +| `delivered_by`, `delivered_at`, `deliverable_digest`, `deliverable_ref`, `deliverable_summary`, `judged_by`, `judged_at`, `judged_digest`, `verdict`, `verdict_reason`, `vetoed_by`, `vetoed_at`, `veto_reason` | Delivery reference, verdict and veto, unchanged | +| `task_run_manifest`, `task_run_tasks` (`input`, `dependencies`, `effect`, `wait_event`, `operation`, `kind`, `patch_files`, `patch_editable`) | The frozen task declaration, already typed, reached by `run_id` and `task_id`; per-attempt facts append to `task_run_facts`, whose own `payload` column is this record's precedent | +| - | The proposed `payload` carries nothing the Task-Unit Protocol needs. It is the slot a peer protocol's declaration uses, and for Task-Unit entries it stays NULL | + ### 2. The data format, three separate questions - **Wire bytes.** JSON over the daemon's existing RPC. Unchanged; no second binding exists to justify @@ -63,20 +75,32 @@ deferred until a transport with a bandwidth constraint exists), and protobuf as ### 3. Storage layout and its invariant -The header stays in columns because the claim is one atomic compare-and-set `UPDATE`. A header inside a -JSON document would turn atomic claiming into read-modify-write. That is a mechanism constraint, not a -preference. +The header stays in columns for clarity, direct indexing, `CHECK` constraints on enums and migration +guards that can read `PRAGMA table_info` - not because atomicity demands it. A conditional `UPDATE` +claims over a JSON payload just as well: measured on the store's SQLite 3.53.3, `UPDATE ... WHERE +json_extract(payload,'$.state')='open'` succeeded once and changed zero rows on the second claim, +identical to the column form. An earlier draft of this record argued from atomicity; that argument is +withdrawn. The payload is one document beside those columns, and the board never parses it. The store already keeps this shape: `claims_json`, `markers_json`, `scope_json`, `evidence_ids_json` and others sit beside typed columns in the same tables. -**Invariant (testable):** with `payload` set to NULL for every row, T0 and T1 still render and claim, -wake, expiry and compact read still work. Verified against the store's own SQLite (3.53.3). +**Invariant (testable):** the board's own reads and actions never consult `payload` - rendering T0/T1, +claiming, waking, expiry and compact read all work with it absent, and with it present they are +unaffected. This is a property of the board's read path, not something the storage format produces by +itself: T1 comes from `content`, as the mapping above shows. Under the Task-Unit Protocol `payload` is +NULL by construction, because the frozen declaration already has typed tables of its own. + +A payload may not restate a field that already has a column or a table - no digest, no verdict, no +holder, no instruction. Restating one is how the second task body this record exists to prevent would +come into being. -When a protocol needs to query a field inside its own payload, it does not ask the board for a column: a -generated column plus an expression index answers it (also verified on the same SQLite). This is what -keeps the schema from growing with the number of protocols. +When a protocol needs to query a field inside its own payload, it adds a generated column and an +expression index (both verified on the same SQLite). A generated column is a column and an index is a +schema object, so this does not make peer protocols schema-free; an earlier draft claimed it did, and +that claim is withdrawn. What it buys is narrower and still worth it: the added schema is purely +additive, it belongs to the protocol rather than to the board, and no field is stored twice. Cost, stated plainly: JSON columns give up database-level typing, a trade the store already makes, and cross-protocol queries depend on generated columns or the protocol's own tables. @@ -98,10 +122,16 @@ A peer protocol supplies four narrow things: 4. **Acceptance** - the criteria for accepting a delivery. The form (digest plus independent verdict) does not change. -The insertion points already exist: `DispatchBoard` and `RoundQueryPort`. A protocol plugs in on the -semantics side, and replacing one means another implementation plus another `protocol` value; the payloads -of entries written under the old value stay exactly as they are - readable, not actionable, and no -migration. +Reusable material exists; an interface with all four roles does not. `RoundQueryPort` carries two typed +reads - `cancelled()` and `accepted()` - and the dispatch loop fails a claim whose ticket has no `patch`, +so patch work is the only work shape today and a peer protocol has to bring its own. Widening that seam +(or adding a port beside `DispatchBoard`) is a step of this record, not a fact that already holds. What is +already in place is the legality computation the board calls, the read-only query port pattern, and the +dispatch loop's own separation of a refusal to use a slot from a failed unit. + +Once that seam exists, replacing a protocol means another implementation plus another `protocol` value; +the payloads of entries written under the old value stay exactly as they are - readable, not actionable, +and no migration. Unknown handling is stated rather than assumed: an unknown `protocol` makes an entry readable and not actionable, and an unknown field inside a known protocol is decided by that protocol's version rule, @@ -113,12 +143,16 @@ ECMA-434 is a conformance requirement for NLIP, and its companion security guide threats - prompt injection highest, then confused-deputy and token passthrough, session hijack, memory injection. Two rules are adoptable now as text: -- An in-band session token is a session credential: it must not be handed to the agent's model or exposed - in application-level payloads. The board's session identity is in-band and the board renders into model - context, so this rule applies to us directly. +- If an in-band session token is ever introduced, it is a session credential: it must not be handed to an + agent's model or exposed in application-level payloads. No such token exists in the frame today, so this + rule is a constraint on what may be added, not a defence of what is there. - A caller's token is never forwarded; a scoped token is minted instead. This becomes load-bearing only when entries cross a trust boundary, which today they do not. +The frame's `source_session_id` is provenance - which session wrote the entry - and not a credential. +Treating it as one because of its name would be exactly the over-extended borrowing this record has to +avoid. + What is owed is a threat list of our own, with the ones we accept and the ones we do not defend. ### 6. Scope ceiling @@ -130,14 +164,19 @@ test. Anything that needs a new table, a new tool or a new channel is outside th 1. This record. No code. 2. One migration: `protocol`, `protocol_version`, `payload_format`, `payload`, plus the invariant test. -3. Refusals become data at the RPC and tool boundary; the store keeps throwing. +3. Domain refusals become data at the RPC and tool boundary while request-level errors stay errors; the + store keeps throwing. 4. Tool schemas shaped for strict mode; flat T0/T1 rendering. -5. A threat list, with the two rules carried as text until a trust boundary exists. +5. The four-role protocol seam: widen the query port or add one beside `DispatchBoard`, and let a peer + protocol bring its own work shape instead of requiring `patch`. +6. A threat list, with the two rules carried as text until a trust boundary exists. ## Alternatives considered -- **One JSON document for the whole entry, header included.** Rejected: atomic claiming becomes - read-modify-write, and every reader would have to parse to find out whether an entry is claimable. +- **One JSON document for the whole entry, header included.** Rejected: every reader would have to parse + the header to learn whether an entry is claimable, `CHECK` constraints and the `PRAGMA table_info` + migration guards would no longer see the fields, and every read path would depend on JSON extraction. + Atomicity is not the reason - a conditional `UPDATE` over a JSON field claims exactly once. - **A column group per protocol.** Rejected: the schema grows with the number of protocols and every new protocol has to touch the board, which is the rework this record exists to avoid. - **protobuf as the normative source with generated bindings** (A2A's answer). Rejected for now: there is @@ -153,23 +192,31 @@ test. Anything that needs a new table, a new tool or a new channel is outside th ## Acceptance criteria -- With every `payload` NULL, the board renders T0/T1 and completes claim, wake, expiry and compact read. +- With every `payload` NULL, and again with payloads present, the board renders T0/T1 identically and + completes claim, wake, expiry and compact read. - Adding a peer protocol changes nothing in the board's claim, wake or expiry paths, demonstrated by a second protocol value with its own payload shape and its own projection. - An entry whose protocol the store does not know is readable, is not actionable, and its payload is never parsed by the board. - A payload field a protocol wants to query is indexed by a generated column and an expression index, - with no new board column. -- Header field names follow the borrowed naming constraints: lowercase ASCII, at most 20 characters, and - `data` is not used as a name. -- A refusal reaches the caller as data with a reason, not as an exception, at the RPC and tool boundary. + with nothing added to the board's own read and write paths. +- No field is stored twice: no header column is also carried inside a payload. +- Header field names borrow CloudEvents' attribute convention (lowercase letters and digits, twenty + characters recommended) or record the deviation. `protocol_version` has an underscore and so does not + follow it; that is recorded as a deviation rather than presented as compliance. +- A domain refusal - a unit outside the legal set, no published handoff - reaches the caller as data with a + reason, while a request-level error - a malformed call, an unknown protocol, an unknown tool - stays an + error. The dispatch loop already separates `refused` from `failure` in this spirit. - LLM-facing schemas are strict-mode shaped, and no negotiation text is required to be a JSON string. - Every new field names the specification it came from, or records why none fitted. ## Risks - One document per entry invites over-stuffing: the header can drift into the payload until the board has - to parse it again. The invariant test and the never-parse rule are the mitigation. + to parse it again. The invariant test and the never-parse rule are the mitigation, and the mapping table + is where duplication would start - a field carried in both a column and a payload is the failure mode. +- Withdrawn arguments invite re-use: atomicity does not require columns, and peer protocols do not avoid + schema changes. Both retractions are in section 3 so a later reader does not restate them. - A protocol that projects its own summary can summarize dishonestly. Mitigation: projection is separate from the verdict, and only `accepted` may be depended on. - Generated columns and expression indexes are SQLite-specific; a portable store would have to re-examine diff --git a/docs/decisions/proposed/2026-09-21-the-frame-and-its-storage.zh-CN.md b/docs/decisions/proposed/2026-09-21-the-frame-and-its-storage.zh-CN.md index 168a2320..67128a6b 100644 --- a/docs/decisions/proposed/2026-09-21-the-frame-and-its-storage.zh-CN.md +++ b/docs/decisions/proposed/2026-09-21-the-frame-and-its-storage.zh-CN.md @@ -11,17 +11,17 @@ 对已定规范的调研改变了问题的形状:除了一层之外,每一层都已经有人定过,所以这件事是**选择与命名**,不是发明: -| 层 | 已由谁定 | -| ------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | -| 事件信封:身份、时间、版本、内容类型、模式 | CloudEvents 1.0——必填只有 `id`/`source`/`specversion`/`type`;属性原话是"在目的地被检查而无需反序列化事件数据";属性名小写 ASCII、最多 20 字符、不得叫 `data`;类型集合封闭 | -| 任务、部件、状态、分页 | A2A 1.0.0——终态语义、`contextId`、游标分页(`pageToken`/`nextPageToken`,并明确不用 offset)、`historyLength`、`includeArtifacts` 默认 false("缩减载荷大小") | -| 载荷判别与渐进形状 | NLIP / ECMA-430——`messagetype`、`format`、`subformat`、`content`、`submessages[]`、`label`;首子消息平铺在顶层,因为多数消息不带子消息;`format: structured` 配 `subformat: uri` 时内容是一个 URI 而不是字节 | -| 面向 LLM 的结果与错误 | MCP 2025-06-18——内容块带 `annotations.audience`、`structuredContent` 与文本并存、`isError` 放在结果里,于是失败也是数据 | -| 单行文本编码 | RFC 8941 Structured Fields——List/Dictionary/Item 加 Parameters,故意严格 | -| 行为与对话线程 | FIPA-ACL——`performative` 是唯一必填,旁边是 `protocol`、`conversation-id`、`in-reply-to`、`reply-by` | -| 未知字段策略 | RFC 6709——忽略与按名字拒绝都合法,但**必须写明选了哪个** | -| 安全 | ECMA-434 与 NLIP 安全指南——对 NLIP 是符合性要求,并给出打分的威胁清单 | -| **拥有权、租约、围栏、合法性** | **没有人。** 所有 agent 协议都假设任务属于某一个 agent,"谁能持有"不成立 | +| 层 | 已由谁定 | +| ----------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| 事件信封:身份、时间、版本、内容类型、模式 | CloudEvents 1.0--必填只有 `id`/`source`/`specversion`/`type`;属性原话是"在目的地被检查而无需反序列化事件数据";属性名只允许小写字母与数字(20 字符是建议上限而非规则),且不使用 `data` 作为名字;类型集合封闭 | +| 任务、部件、状态、分页 | A2A 1.0.0——终态语义、`contextId`、游标分页(`pageToken`/`nextPageToken`,并明确不用 offset)、`historyLength`、`includeArtifacts` 默认 false("缩减载荷大小") | +| 载荷判别与渐进形状 | NLIP / ECMA-430——`messagetype`、`format`、`subformat`、`content`、`submessages[]`、`label`;首子消息平铺在顶层,因为多数消息不带子消息;`format: structured` 配 `subformat: uri` 时内容是一个 URI 而不是字节 | +| 面向 LLM 的结果与错误 | MCP 2025-06-18--内容块带 `annotations.audience`、`structuredContent` 与文本并存、`isError` 用于工具执行错误,它与协议错误是两回事,后者仍是错误 | +| 单行文本编码 | RFC 8941 Structured Fields——List/Dictionary/Item 加 Parameters,故意严格 | +| 行为与对话线程 | FIPA-ACL——`performative` 是唯一必填,旁边是 `protocol`、`conversation-id`、`in-reply-to`、`reply-by` | +| 未知字段策略 | RFC 6709——忽略与按名字拒绝都合法,但**必须写明选了哪个** | +| 安全 | ECMA-434 与 NLIP 安全指南——对 NLIP 是符合性要求,并给出打分的威胁清单 | +| **拥有权、租约、围栏、合法性** | **没有人。** 所有 agent 协议都假设任务属于某一个 agent,"谁能持有"不成立 | ## 提案 @@ -33,6 +33,18 @@ - **一条载荷文档**:具名协议自己需要的东西,对板子不透明。 - **渲染,不是存储**:条目呈现的行为(FIPA 的 `performative`),以及分层的 T0 头、T1 摘要、T2 结构、T3 真值。 +现有字段到拟议字段的映射——下面没有一项要搬: + +| 现有 | 在本帧里的位置 | +| --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `content`(NOT NULL) | 条目正文的唯一来源。`readTaskBoardPreviews` 投影的是 `taskBoardPreview(entry.content)`:空白折叠、截到 200 字符,而整条正文只是一个 `memory=` 指针时原样返回 | +| `kind`、`status`、`serial_state`、`created_at`、`expires_at`、`resolved_at`、`resolved_by`、`resolution` | 头列,不变 | +| `agent_id`、`to`、`source_session_id` | 头列,不变。`source_session_id` 是**来源归因**(哪个会话写的),不是凭据 | +| `claimed_by`、`claimed_at`、`claim_expires_at`、`attempt` | 租约与 attempt 围栏,不变 | +| `delivered_by`、`delivered_at`、`deliverable_digest`、`deliverable_ref`、`deliverable_summary`、`judged_by`、`judged_at`、`judged_digest`、`verdict`、`verdict_reason`、`vetoed_by`、`vetoed_at`、`veto_reason` | 交付物引用、裁决与否决,不变 | +| `task_run_manifest`、`task_run_tasks`(`input`、`dependencies`、`effect`、`wait_event`、`operation`、`kind`、`patch_files`、`patch_editable`) | 冻结任务声明,已经是 typed 表,由 `run_id` 与 `task_id` 抵达;每次尝试的事实追加进 `task_run_facts`,而后者自己的 `payload` 列正是本文形状的先例 | +| —— | 拟议的 `payload` **不承载任务单元协议需要的任何东西**。它是平级协议声明用的槽位,对任务单元条目保持 NULL | + ### 2. 数据格式是三个分开的问题 - **线上字节**:JSON,走 daemon 现有的 RPC。不变;既然只有一条线而没有第二个绑定,就不值得为代码生成付费,所以 A2A 的"proto 作规范源"暂不采用。 @@ -43,13 +55,15 @@ ### 3. 存储布局与它的不变量 -头留在列里,因为认领是**一条原子比较交换的 `UPDATE`**。把头放进 JSON 文档会把原子认领变成读-改-写。这是机制约束,不是偏好。 +头留在列里是为了清晰、可直接索引、对枚举加 `CHECK` 约束,以及能用 `PRAGMA table_info` 读的迁移守卫——**不是**因为原子性要求如此。条件 `UPDATE` 对 JSON 载荷一样能认领:在 store 的 SQLite 3.53.3 上实测,`UPDATE ... WHERE json_extract(payload,'$.state')='open'` 第一次成功、第二次更新零行,与列表单形式一致。本文早先的草稿从原子性论证,那个论证**撤回**。 载荷是与这些列并排的一条文档,板子从不解析它。这个形状库里已有:`claims_json`、`markers_json`、`scope_json`、`evidence_ids_json` 等,就是与 typed 列同表并存的。 -**不变量(可测)**:把所有行的 `payload` 置为 NULL,T0 与 T1 仍渲染,认领、唤醒、过期、紧凑读仍工作。已在 store 自己的 SQLite(3.53.3)上验证。 +**不变量(可测)**:板子自己的读与行动从不查 `payload`——`payload` 缺席时 T0/T1 渲染、认领、唤醒、过期、紧凑读照旧;`payload` 在场时不受影响。这是**读路径的性质,不是存储格式自然产生的性质**:如上面的映射所示,T1 来自 `content`。在任务单元协议下 `payload` 按构造就是 NULL,因为冻结声明已经有自己的 typed 表。 + +载荷**不得重述**已经有列或表承载的字段——不含 digest、不含裁决、不含持有人、不含指令。重述一个,就是让本文要防的那种“第二份任务体”出现的方式。 -当某个协议要查自己载荷里的字段时,它**不要**向板子要一列:生成列加表达式索引就够了(同样在该 SQLite 上验证过)。这一点正是让 schema 不随协议数量增长的机制。 +当某个协议要查自己载荷里的字段时,它加生成列与表达式索引(同样在该 SQLite 上验证过)。生成列**是列**,索引**是 schema 对象**,所以这并不能让平级协议“无 schema 变更”;本文早先的草稿这么声称过,那个说法**撤回**。它买到的是更窄但仍然值的东西:新增的 schema 是纯追加的、属于该协议而不属于板子,且没有字段被存两遍。 代价说明白:JSON 列放弃数据库级类型检查(这是仓库早已做过的交换),跨协议查询依赖生成列或协议自己的表。 @@ -64,7 +78,9 @@ 3. **投影**——T1 的一行摘要与 T2 的结构。必须有,因为载荷对板子不透明;MCP 与 A2A 出于同样理由也是这个形状。 4. **验收**——接受一次交付的判据。形式(digest 加独立裁决)不变。 -插入点已经存在:`DispatchBoard` 与 `RoundQueryPort`。协议接在语义侧,替换一个协议就是换一个实现加换一个 `protocol` 取值;旧取值下写入的载荷原样留着——可读、不可行动,且不需要迁移。 +可复用的材料存在,但四项职责齐全的接口不存在。`RoundQueryPort` 只有两个 typed 读——`cancelled()` 与 `accepted()`;派发循环对没有 `patch` 的票直接判失败,所以今天唯一的“工作形态”就是 patch,平级协议必须自带它自己的。把这条缝拓宽(或在 `DispatchBoard` 旁边加一个 port)是本文的**计划步骤**,不是既成事实。已经到位的是板子调用的合法性计算、只读查询端口的模式,以及派发循环里已有的“拒绝使用槽位”与“单元失败”之分。 + +那条缝一旦存在,替换一个协议就是换一个实现加换一个 `protocol` 取值;旧取值下写入的载荷原样留着——可读、不可行动,且不需要迁移。 未知的处理要**写明**而不是默认:未知 `protocol` 使条目可读但不可行动;已知协议内的未知字段由该协议的版本规则决定,而那条规则必须说明是忽略还是按名字拒绝。 @@ -72,9 +88,11 @@ ECMA-434 对 NLIP 是符合性要求,配套安全指南给十五类威胁打分:prompt injection 最高,其次是 confused-deputy 与令牌透传、会话劫持、记忆投毒。两条现在就能以文字形式采纳: -- 带内会话令牌就是会话凭据:不得交给 agent 的模型,也不得暴露在应用层载荷里。黑板的会话身份是带内的,而黑板会渲染进模型上下文,所以这条直接适用于我们。 +- 若将来引入带内会话令牌,它就是会话凭据:不得交给 agent 的模型,也不得暴露在应用层载荷里。今天帧里没有这样的令牌,所以这条是对“可新增什么”的约束,而不是对已有东西的防御。 - 绝不转发调用方的令牌,改为铸一个有范围的令牌。这条只有在条目跨信任边界时才承重,而今天还没有那样的边界。 +帧里的 `source_session_id` 是**来源归因**(哪个会话写的),不是凭据。仅因为名字里有 session 就把它当凭据,正是本文必须避免的那类过度套用。 + 欠的是一份我们自己的威胁清单,写明接受哪些、不防御哪些。 ### 6. 范围上限 @@ -85,13 +103,14 @@ ECMA-434 对 NLIP 是符合性要求,配套安全指南给十五类威胁打 1. 本文。不动代码。 2. 一次迁移:`protocol`、`protocol_version`、`payload_format`、`payload`,加那条不变量测试。 -3. 拒绝在 RPC 与工具边界变成数据;store 继续抛异常。 +3. 域内拒绝在 RPC 与工具边界变成数据,而请求级错误仍是错误;store 继续抛异常。 4. 工具 schema 按 strict 模式写;T0/T1 扁平渲染。 -5. 一份威胁清单,两条规则先以文字承载,等出现信任边界再谈强制。 +5. 四项职责的协议缝:拓宽查询端口,或在 `DispatchBoard` 旁边加一个;让平级协议自带工作形态,而不是要求 `patch`。 +6. 一份威胁清单,两条规则先以文字承载,等出现信任边界再谈强制。 ## 考虑过的替代方案 -- **整个条目一条 JSON 文档(含头)**。拒绝:原子认领会退化成读-改-写,而且每个读者都得先解析才知道条目能不能认领。 +- **整个条目一条 JSON 文档(含头)**。拒绝:每个读者都得先解析头才知道条目能不能认领;`CHECK` 约束与读 `PRAGMA table_info` 的迁移守卫都看不到字段;每条读路径都要依赖 JSON 抽取。**理由不是原子性**——对 JSON 字段的条件 `UPDATE` 恰好只认领一次。 - **每个协议一组列**。拒绝:schema 随协议数量增长,每个新协议都要动板子——那正是本文要避免的返工。 - **proto 作规范源并生成绑定**(A2A 的答案)。暂时拒绝:只有一条线、没有第二个绑定,代码生成现在买不到东西。 - **头用 RFC 8941 的单行编码**。存储上拒绝——我们的头活在列里——但若将来需要紧凑文本读,保留为选项。 @@ -101,20 +120,22 @@ ECMA-434 对 NLIP 是符合性要求,配套安全指南给十五类威胁打 ## 验收标准 -- 所有 `payload` 为 NULL 时,板子仍渲染 T0/T1,并仍能完成认领、唤醒、过期与紧凑读。 +- 所有 `payload` 为 NULL 时、以及载荷在场时,板子渲染的 T0/T1 一致,并都能完成认领、唤醒、过期与紧凑读。 - 增加一个平级协议不改变板子的认领、唤醒、过期路径;用一个自带载荷形状与投影的第二协议取值演示。 - store 不认识的协议:条目可读、不可行动,且板子从不解析它的载荷。 -- 协议想查的载荷字段由生成列加表达式索引支撑,板子不加新列。 -- 头字段名遵守借来的命名约束:小写 ASCII、最多 20 字符、不使用 `data` 作为名字。 -- 拒绝在 RPC 与工具边界以带理由的数据抵达调用者,而不是异常。 +- 协议想查的载荷字段由生成列加表达式索引支撑,板子自己的读写路径不加东西。 +- 没有字段被存两遍:没有哪个头列同时也在载荷里。 +- 头字段名**借鉴** CloudEvents 的属性约定(只小写字母与数字,20 字符是建议),或者记录偏离。`protocol_version` 带下划线、并不遵守它;这记作偏离,而不是声称遵守。 +- 域内拒绝(单元在合法集之外、没有公布交接)以带理由的数据抵达调用者;请求级错误(畸形的调用、未知协议、未知工具)仍是错误。派发循环已经在这个意义上区分 `refused` 与 `failure`。 - 面向 LLM 的 schema 按 strict 模式成型,且没有任何协商文本被要求写成 JSON 字符串。 - 每个新字段都指名它来自哪份规范,或者记录下为什么没有一份合适。 ## 风险 -- 每条一条文档会诱发"什么都往里塞":头可能慢慢漂进载荷,直到板子又得解析它。缓解不变量测试与"板子从不解析"这条规则。 +- 每条一条文档会诱发“什么都往里塞”:头可能慢慢漂进载荷,直到板子又得解析它。缓解是不变量测试与“板子从不解析”这条规则;而**映射表就是重复开始的地方**——一个字段同时躺在列里和载荷里,就是那个失败模式。 +- 撤回的论证会被重新用起来:原子性不要求列,平级协议也躲不开 schema 变更。两条撤回都写在第三节,免得后来的读者再把它们复述一遍。 - 自己给自己做投影的协议可以说得不诚实。缓解:投影与裁决分离,且只有 `accepted` 可被依赖。 - 生成列与表达式索引是 SQLite 特有的;换可移植存储时要重新审视。 -- "拒绝即数据"改变现有调用者看到的形状,所以臂的 driver 必须在同一个切片里一起动。 +- “域内拒绝即数据”改变现有调用者看到的形状,所以臂的 driver 必须在同一个切片里一起动。 - strict 模式的 schema 让缺省显式化,每次调用多花 token,而且可能让读者把可空字段读成必填。 - 借来的词汇会僵化。若某个借来的名字不合身,允许偏离——但要**记录**偏离,而不是悄悄改名。 From 41867e5c83adb316489f802f89b5da3b5b079456 Mon Sep 17 00:00:00 2001 From: wefio <48851810+wefio@users.noreply.github.com> Date: Mon, 21 Sep 2026 23:33:07 +0800 Subject: [PATCH 10/13] docs(decisions): mechanism, not policy - the division both records needed The legality record and the frame record each needed the same sentence and neither stated it: the legality record says what the program does and what it refuses to decide, the frame record says what the core knows and what it never parses. Those are one division seen from the side of the decision and from the side of the data. Because nothing named it, it had to be re-argued every time a field or a rule appeared, and the cost was already visible: "work means a patch" had reached six places, one of them a legality rule written in the kernel's own vocabulary (refuseWidening decides permission closure by reading parent.patch.editable). This record takes Hydra's principle - a kernel provides mechanisms and refuses policy - and states it as two contracts. Mechanism: one claim (compare-and-set, lease, attempt fence), an opaque declaration with a digest, an opaque artifact with a digest, a verdict from someone else, the lifecycle, and the legal set (which units are legal now, in order, cut to the declared slot budget, with a reason per unit). Policy: what a unit's inputs are, the artifact class, how the artifact is judged, how the work runs, its concurrency preconditions, the wording and enablement of legality rules, and who is chosen, who adopts, who judges. It also supplies the two things the slogan lacked. A decision procedure - for each field, type and code path, ask mechanism or policy, with a rule's check counting as mechanism while its name, wording and enablement count as policy. And a mechanically checkable rule: no policy word may appear in the mechanism layer's code, type declarations or schema paths, from a maintained list (patch, editable, files, instruction, checks, repair-first), with the path carried because a word can be policy in one place and mechanism in another. The consequence for work shapes is stated: a work shape is not a core concept, it is the policy layer's name for the declaration-and-artifact pair the core carries opaquely. The six leak sites are classified in a table, and the legality rule found today is the refinement the record adds - its check belongs to the program while permission closure's name and enablement belong to a declaration, so the fix is not to move the check out of the program but to stop hard-wiring the rule in the kernel's vocabulary. That is the same shape as plan step 3 of the legality record, where repair-first becomes a declared constraint instead of shared planning policy; two independent fixes taking the same shape is the evidence that the classification is right. Three rows are deliberately left undecided so that they are not settled by preference: task_run_tasks.effect is mechanism only if the mechanism must enforce write-set disjointness, the granularity of input and dependencies is a separate question from whether dependencies are mechanism, and operation may be policy. Alternatives rejected: keeping the slogan and deciding case by case, putting the principle in either record (it is wider than both), a plugin registry above the core with no second policy to justify it, and deciding the undecided rows now. Risks named: gutting the core (so the practical form is one default policy the core may carry but must not require), an ossifying word list, an undecided row becoming permanent, a policy word legitimately remaining for a release, and grep being gameable by synonyms. The legality record and the frame record now point here, so the division has one home. --- ...2026-09-20-the-program-answers-legality.md | 2 +- ...9-20-the-program-answers-legality.zh-CN.md | 2 +- .../2026-09-21-mechanism-not-policy.md | 129 ++++++++++++++++++ .../2026-09-21-mechanism-not-policy.zh-CN.md | 91 ++++++++++++ .../2026-09-21-the-frame-and-its-storage.md | 2 +- ...6-09-21-the-frame-and-its-storage.zh-CN.md | 2 +- 6 files changed, 224 insertions(+), 4 deletions(-) create mode 100644 docs/decisions/proposed/2026-09-21-mechanism-not-policy.md create mode 100644 docs/decisions/proposed/2026-09-21-mechanism-not-policy.zh-CN.md diff --git a/docs/decisions/proposed/2026-09-20-the-program-answers-legality.md b/docs/decisions/proposed/2026-09-20-the-program-answers-legality.md index 88344263..bf8fb31f 100644 --- a/docs/decisions/proposed/2026-09-20-the-program-answers-legality.md +++ b/docs/decisions/proposed/2026-09-20-the-program-answers-legality.md @@ -3,7 +3,7 @@ [中文](2026-09-20-the-program-answers-legality.zh-CN.md) **Status:** proposed -**Relates to:** [Board governance and capability addressing](../implemented/2026-09-06-board-governance-addressing.md), [Name the collaboration protocol and its task-unit sub-protocol](../implemented/2026-09-20-name-the-collaboration-protocol.md), [Task unit semantics](../../design/task-unit-semantics.md), [The contract's obligations](../../design/task-unit-semantics-obligations.md) +**Relates to:** [Mechanism, not policy](2026-09-21-mechanism-not-policy.md), [Board governance and capability addressing](../implemented/2026-09-06-board-governance-addressing.md), [Name the collaboration protocol and its task-unit sub-protocol](../implemented/2026-09-20-name-the-collaboration-protocol.md), [Task unit semantics](../../design/task-unit-semantics.md), [The contract's obligations](../../design/task-unit-semantics-obligations.md) ## Problem diff --git a/docs/decisions/proposed/2026-09-20-the-program-answers-legality.zh-CN.md b/docs/decisions/proposed/2026-09-20-the-program-answers-legality.zh-CN.md index 4e378a8e..b0366c34 100644 --- a/docs/decisions/proposed/2026-09-20-the-program-answers-legality.zh-CN.md +++ b/docs/decisions/proposed/2026-09-20-the-program-answers-legality.zh-CN.md @@ -3,7 +3,7 @@ [English](2026-09-20-the-program-answers-legality.md) **Status:** proposed -**Relates to:** [黑板治理与能力寻址](../implemented/2026-09-06-board-governance-addressing.zh-CN.md)、[给协作协议及其任务单元子协议命名](../implemented/2026-09-20-name-the-collaboration-protocol.zh-CN.md)、[任务单元语义](../../design/task-unit-semantics.md)、[契约的义务](../../design/task-unit-semantics-obligations.md) +**Relates to:** [机制,不是策略](2026-09-21-mechanism-not-policy.zh-CN.md)、[黑板治理与能力寻址](../implemented/2026-09-06-board-governance-addressing.zh-CN.md)、[给协作协议及其任务单元子协议命名](../implemented/2026-09-20-name-the-collaboration-protocol.zh-CN.md)、[任务单元语义](../../design/task-unit-semantics.md)、[契约的义务](../../design/task-unit-semantics-obligations.md) ## 问题 diff --git a/docs/decisions/proposed/2026-09-21-mechanism-not-policy.md b/docs/decisions/proposed/2026-09-21-mechanism-not-policy.md new file mode 100644 index 00000000..85734743 --- /dev/null +++ b/docs/decisions/proposed/2026-09-21-mechanism-not-policy.md @@ -0,0 +1,129 @@ +# Mechanism, not policy + +[中文](2026-09-21-mechanism-not-policy.zh-CN.md) + +**Status:** proposed +**Relates to:** [The program answers legality](2026-09-20-the-program-answers-legality.md), [The frame, its data format, and its storage](2026-09-21-the-frame-and-its-storage.md), [Board governance and capability addressing](../implemented/2026-09-06-board-governance-addressing.md), [Protocol-governed collaboration: the parts, the gaps](../../design/protocol-governed-collaboration.md), [Task unit semantics](../../design/task-unit-semantics.md) + +## Problem + +Two proposed records each need the same sentence, and neither states it. The legality record says what the +program does and what it refuses to decide. The frame and storage record says what the core knows and what +it never parses. Those are the same division, seen from the side of the decision and from the side of the +data, and because nothing names it, the division has to be re-argued every time a field or a rule appears. + +The cost is already visible. The assumption that work means a patch reached six places, and one of them is a +legality rule written in the kernel's own vocabulary: `refuseWidening` decides permission closure by reading +`parent.patch.editable`, so a rule about write sets is expressed in terms of a work shape the kernel is not +supposed to know. + +The split also needs to be sharp enough to settle an argument rather than to serve as a slogan. "Keep policy +out of the core" does not decide whether `effect` or `operation` may be a column, or whether a legality rule +may live in the program at all. + +## Proposal + +Take Hydra's principle - a kernel provides mechanisms and refuses policy - and state it as two contracts. + +**Mechanism contract** (the core: the board, the store, the program): + +- one claim: compare-and-set, lease, attempt fence +- an opaque declaration with a digest +- an opaque artifact with a digest +- a verdict given by someone else +- the lifecycle: expiry, reaping, wake delivery, compact read +- the legal set: which units are legal now, in order, cut to the declared slot budget, with a reason per unit + +**Policy contract** (above the core: the protocol, the plan, the agents): + +- what a unit's inputs are, the artifact class, how the artifact is judged, how the work runs (synchronous + or detached, interruptibility, whether abandoning it midway is safe), and its concurrency preconditions +- the wording and the enablement of legality rules: named by the protocol, enabled by the plan +- who is chosen, in what order, who adopts, who judges + +**The decision procedure.** For each field, type and code path, ask whether it is mechanism or policy. A +rule's _check_ is mechanism; the rule's _name, wording and enablement_ are policy. + +**The mechanically checkable rule.** No policy word may appear in the mechanism layer's code, type +declarations or schema paths. The list is maintained - `patch`, `editable`, `files`, `instruction`, +`checks`, `repair-first` - and a hit is a leak rather than a style question. The list may grow, and adding +a word is recorded. A word alone is not enough to judge: `files` is a policy word in a ticket type and a +mechanism word at a filesystem boundary, so the check carries the path. + +**Rows deliberately left undecided**, so that they are not settled by accident: + +- `task_run_tasks.effect`, the declared write set, is mechanism only if the mechanism must enforce that + write sets do not overlap. If enforcement is a protocol obligation, it is policy data. +- the granularity of `input` and `dependencies`: dependencies affect legality and order, which is mechanism, + but whether an input carries content or only a digest is a separate question. +- `operation`, which may be policy. + +## What this makes of the work shape + +A work shape is not a core concept. It is the policy layer's name for the declaration-and-artifact pair +that the core carries without understanding it. The six sites where the patch assumption lives classify as +follows. + +| Site | Mechanism or policy | Where it belongs | +| ---------------------------------------------------------------------------- | ----------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------- | +| `BoardTicket.patch?: FrozenPatchTask & { digest }` | policy | the ticket carries an opaque `declaration` with a digest | +| `patchFrozen()` calling `preparePatchWork`, and the `not a patch task` throw | policy inside the core | freezing and field validation move into the shape adapter; the core only hands back the opaque declaration | +| `refuseWidening` reading `parent.patch.editable` | check is mechanism, wording and enablement are policy | permission closure becomes a declared constraint the program enforces and reports reasons for | +| the ten `FrozenPatchWork` signatures in the session mechanism | mixed | rendering a declaration into a prompt and submitting an artifact are policy; session lifecycle, metrics and cancellation are mechanism | +| `task_run_tasks.patch_files` / `patch_editable`, parsed in the store | policy in the schema | belongs in the payload document; the mechanism part is the run, task, revision, input, dependency and operation columns | +| drivers asserting `ticket.patch!.digest` | policy assertion | the mechanism assertion is that a declaration carries a digest, that a claim binds the attempt, and that a delivery binds the digest | + +The legality rule found today is the clearest case of the refinement this record adds: its check belongs to +the program, while permission closure's name and enablement belong to a declaration. The fix is therefore not +to move the check out of the program but to stop hard-wiring the rule in the kernel's vocabulary - the same +shape of fix as plan step 3 of the legality record, where repair-first becomes a declared constraint rather +than shared planning policy. Two independent fixes taking the same shape is evidence that the classification +is the right one. + +## Alternatives considered + +- **Keep the slogan and decide case by case.** Rejected: that is what produced six sites, and it offers no + test to apply. +- **Put the principle in the frame record.** Rejected: the principle is wider than the frame - it also + governs the program's share of decisions - so the frame record would become the owner of a rule about the + program. +- **Put it in the legality record.** Rejected for the same reason in reverse: that record is narrower, being + about the program's decisions, and what the core may know is not a legality question. +- **A plugin framework with a registry above the core.** Rejected: no second policy exists to justify the + machinery; one default adapter plus one other shape tests the seam. +- **Decide the undecided rows now.** Rejected: choosing them by preference is precisely what this record + replaces with evidence. + +## Acceptance criteria + +- The classification covers every site that mentions a policy word, each row marked mechanism, policy, or + undecided. +- No policy word appears in the mechanism layer's read and write paths, type declarations, or schema paths, + from a maintained list and checked mechanically. +- A legality rule's name and enablement come from a declaration, while its check runs in the program and + returns a reason per unit. +- Adding a second work shape changes no mechanism code, demonstrated by the value work the data-check + runner already performs. +- Every undecided row is marked as undecided and is resolved by an experiment rather than by preference. +- The legality record and the frame record both point here, so the division has one home. + +## Risks + +- **Gutting the core.** Hydra's lesson cuts both ways: a kernel with no default policy is unusable. The + practical form is that the core may carry one default policy, patch work, and must not require it. +- **The policy-word list can ossify.** A word may be mechanism in one place and policy in another, so the + check needs the path rather than the word alone, and the list has to accept additions. +- **An undecided row can become permanent.** A row marked undecided without an experiment behind it is a + policy nobody declared. +- **A policy word may legitimately remain for a release.** This record does not require a big-bang rename; it + requires that each remaining occurrence is listed. +- **A grep check can be gamed by synonyms.** The check is a floor, not a proof of separation. + +## Plan + +1. This record. No code. +2. The classification table, with the undecided rows marked. +3. The check: a maintained policy-word list plus a script, wired into the static set. +4. Both records point here. +5. Then the work-shape slice: the mechanism side carries an opaque declaration, with patch as the default + adapter rather than a type. diff --git a/docs/decisions/proposed/2026-09-21-mechanism-not-policy.zh-CN.md b/docs/decisions/proposed/2026-09-21-mechanism-not-policy.zh-CN.md new file mode 100644 index 00000000..b1bdb670 --- /dev/null +++ b/docs/decisions/proposed/2026-09-21-mechanism-not-policy.zh-CN.md @@ -0,0 +1,91 @@ +# 机制,不是策略 + +[English](2026-09-21-mechanism-not-policy.md) + +**Status:** proposed +**Relates to:** [程序只回答合法性](2026-09-20-the-program-answers-legality.zh-CN.md)、[帧、它的数据格式与它的存储](2026-09-21-the-frame-and-its-storage.zh-CN.md)、[黑板治理与能力寻址](../implemented/2026-09-06-board-governance-addressing.zh-CN.md)、[协议化协作:组成部分与空缺](../../design/protocol-governed-collaboration.zh-CN.md)、[任务单元语义](../../design/task-unit-semantics.md) + +## 问题 + +两份提案各需要同一句话,而谁都没把它说出来。合法性记录讲的是**程序做什么**、以及它拒绝决定什么;帧与存储记录讲的是**核心知道什么**、以及它从不解析什么。这是同一个划分,一面从决定看、一面从数据看;因为没有名字,每次出现一个新字段或一条新规则,这个划分都得重新论证一遍。 + +代价已经看得见:**"工作就是补丁"这个假设走到了六处**,其中一处是用核心自己的词汇写出来的合法性规则——`refuseWidening` 靠读 `parent.patch.editable` 来判定 permission closure,于是"关于写集"的规则被写成了"关于某个核心不该知道的工作形态"的规则。 + +而且这个划分必须锐到能**终结争论**,而不是当口号。"别把策略放进核心"这句话,并不能决定 `effect` 或 `operation` 能不能是一列,也不能决定一条合法性规则到底能不能住在程序里。 + +## 提案 + +取 Hydra 的原则——**内核提供机制、拒绝策略**——把它写成两条契约。 + +**机制契约**(核心:黑板、存储、程序): + +- 一次认领:比较交换、租约、attempt 围栏 +- 一份**不透明**声明 + 摘要 +- 一份**不透明**产物 + 摘要 +- 一个**由别人给出**的裁决 +- 生命周期:过期、回收、唤醒投递、紧凑读 +- 合法集:此刻哪些单元合法、有序、按声明的槽位预算裁剪、每个单元给理由 + +**策略契约**(核心之上:协议、计划、各 agent): + +- 单元的输入是什么、产物属于哪一类、产物怎么被判、工作怎么跑(同步还是分离式检查、可否打断、中途放弃是否安全)、以及它的并发前提 +- 合法性规则的**措辞与启用**:由协议具名、由计划声明启用 +- 选谁、按什么顺序、谁采纳、谁裁决 + +**判定程序**:对每个字段、每个类型、每条代码路径问一次——**这是机制还是策略?** 一条规则的**检查**是机制,规则的**名字、措辞与启用**是策略。 + +**可机械检查的规则**:**机制层(核心与程序)的代码、类型声明与 schema 路径里不得出现策略词汇。** 清单是维护的——`patch`、`editable`、`files`、`instruction`、`checks`、`repair-first`——命中即泄漏,不是风格问题。清单可以加词,加词要记录。光看词不足以判定:`files` 在票的类型里是策略词,在文件系统边界上是机制词,所以检查要带上**路径**。 + +**刻意留作待判的行**(免得被顺手定掉): + +- `task_run_tasks.effect`(声明的写集):只有当**机制必须强制写集不相交**时它才是机制;若强制是协议的义务,它就是策略数据。 +- `input` 与 `dependencies` 的粒度:依赖影响合法性与顺序,这是机制;但"输入"是携带内容还是只携带摘要,是另一个问题。 +- `operation`:可能是策略。 + +## 这给"工作形态"定了位 + +**工作形态不是核心概念**,它是策略层对"核心不透明携带的那对声明与产物"的叫法。六处站点的分类如下。 + +| 站点 | 机制还是策略 | 归属 | +| ---------------------------------------------------------------------- | ---------------------------- | ----------------------------------------------------------------------------------- | +| `BoardTicket.patch?: FrozenPatchTask & { digest }` | 策略 | 票上带一份不透明的 `declaration` 加摘要 | +| `patchFrozen()` 调 `preparePatchWork`、以及 `throw "not a patch task"` | 策略(在核心里) | 冻结与字段校验移进形态适配器;核心只交出不透明声明 | +| `refuseWidening` 读 `parent.patch.editable` | 检查是机制;措辞与启用是策略 | permission closure 变成程序**执行并给理由**的一条声明约束 | +| 会话机制里十处 `FrozenPatchWork` | 混合 | 把声明渲染成提示、提交产物是策略;会话生命周期、指标、取消是机制 | +| `task_run_tasks.patch_files` / `patch_editable`(store 里解析) | 策略进了 schema | 该进载荷文档;机制部分是 run、task、revision、input、dependencies、operation 那几列 | +| 驱动断言 `ticket.patch!.digest` | 策略断言 | 机制断言是:声明带摘要、认领绑 attempt、交付绑摘要 | + +今天那条合法性规则正是本文新增的那点修正的最清楚的例子:**它的检查属于程序,而 permission closure 的名字与启用属于声明。** 所以修法不是把检查搬出程序,而是**别再用核心的词汇硬编码这条规则**——这与合法性记录计划第 3 步(把 repair-first 从共享规划策略变成声明的约束)是**同一形状的修法**。两处各自独立的修法落在同一形状上,是"这个分类是对的"的证据。 + +## 考虑过的替代方案 + +- **留着口号,逐案判断。** 拒绝:那就正是产出六处站点的方式,而且它不提供可以套用的检验。 +- **把这条原则放进帧记录。** 拒绝:原则比帧更宽——它也管程序那半决定——那样帧记录会变成"关于程序的规则"的拥有者。 +- **放进合法性记录。** 反向的同样理由拒绝:那份记录更窄(讲程序的决定),而"核心可以知道什么"不是合法性问题的。 +- **核心之上搞一个带注册表的插件框架。** 拒绝:还没有第二个策略来支撑这套机械;一个默认适配器加另一个形态就足以检验这条缝。 +- **现在就定掉待判的行。** 拒绝:凭偏好挑一边,正是本文要用证据替换掉的做法。 + +## 验收标准 + +- 分类覆盖所有出现策略词汇的站点,每行标为机制、策略或待判。 +- 机制层的读写路径、类型声明与 schema 路径里不出现策略词汇(清单维护,机械检查)。 +- 合法性规则的名字与启用来自声明,检查在程序里跑,并按单元返回理由。 +- 增加第二个工作形态不改变任何机制代码;用数据式检查运行器已经在做的"值工作"演示。 +- 每一行待判都标为待判,并且由**实验**而不是偏好来定。 +- 合法性记录与帧记录都指到这里,让这个划分只有一个出处。 + +## 风险 + +- **把核心做空。** Hydra 的教训是双向的:一个不带默认策略的内核不可用。务实形式是核心**可以带一个默认策略**(补丁工作),但**不得要求**它。 +- **策略词清单会僵化。** 同一个词在一处是机制、在另一处是策略,所以检查要带路径而不只是词,而清单必须接受加词。 +- **待判的行可能永久待判。** 一行标着待判却没有实验在背后,就是一个没人声明的策略。 +- **某个策略词可能合理地再留一个版本。** 本文不要求一次性重命名;它要求每一处残留都被**列出来**。 +- **grep 检查可以被同义词绕过。** 检查是地板,不是"已经分离"的证明。 + +## 计划 + +1. 本文。不动代码。 +2. 分类表,标出待判的行。 +3. 那条检查:维护的策略词清单加一个脚本,接进静态集合。 +4. 两份记录都指过来。 +5. 然后才是工作形态切片:机制侧携带不透明声明,`patch` 作为默认适配器而不再是类型。 diff --git a/docs/decisions/proposed/2026-09-21-the-frame-and-its-storage.md b/docs/decisions/proposed/2026-09-21-the-frame-and-its-storage.md index 06ecb919..9111fca4 100644 --- a/docs/decisions/proposed/2026-09-21-the-frame-and-its-storage.md +++ b/docs/decisions/proposed/2026-09-21-the-frame-and-its-storage.md @@ -3,7 +3,7 @@ [中文](2026-09-21-the-frame-and-its-storage.zh-CN.md) **Status:** proposed -**Relates to:** [The program answers legality](2026-09-20-the-program-answers-legality.md), [Name the collaboration protocol and its task-unit sub-protocol](../implemented/2026-09-20-name-the-collaboration-protocol.md), [Protocol-governed collaboration: the parts, the gaps](../../design/protocol-governed-collaboration.md), [Board governance and capability addressing](../implemented/2026-09-06-board-governance-addressing.md), [Task unit semantics](../../design/task-unit-semantics.md) +**Relates to:** [Mechanism, not policy](2026-09-21-mechanism-not-policy.md), [The program answers legality](2026-09-20-the-program-answers-legality.md), [Name the collaboration protocol and its task-unit sub-protocol](../implemented/2026-09-20-name-the-collaboration-protocol.md), [Protocol-governed collaboration: the parts, the gaps](../../design/protocol-governed-collaboration.md), [Board governance and capability addressing](../implemented/2026-09-06-board-governance-addressing.md), [Task unit semantics](../../design/task-unit-semantics.md) ## Problem diff --git a/docs/decisions/proposed/2026-09-21-the-frame-and-its-storage.zh-CN.md b/docs/decisions/proposed/2026-09-21-the-frame-and-its-storage.zh-CN.md index 67128a6b..71c966ba 100644 --- a/docs/decisions/proposed/2026-09-21-the-frame-and-its-storage.zh-CN.md +++ b/docs/decisions/proposed/2026-09-21-the-frame-and-its-storage.zh-CN.md @@ -3,7 +3,7 @@ [English](2026-09-21-the-frame-and-its-storage.md) **Status:** proposed -**Relates to:** [程序只回答合法性](2026-09-20-the-program-answers-legality.zh-CN.md)、[给协作协议及其任务单元子协议命名](../implemented/2026-09-20-name-the-collaboration-protocol.zh-CN.md)、[协议化协作:组成部分与空缺](../../design/protocol-governed-collaboration.zh-CN.md)、[黑板治理与能力寻址](../implemented/2026-09-06-board-governance-addressing.zh-CN.md)、[任务单元语义](../../design/task-unit-semantics.md) +**Relates to:** [机制,不是策略](2026-09-21-mechanism-not-policy.zh-CN.md)、[程序只回答合法性](2026-09-20-the-program-answers-legality.zh-CN.md)、[给协作协议及其任务单元子协议命名](../implemented/2026-09-20-name-the-collaboration-protocol.zh-CN.md)、[协议化协作:组成部分与空缺](../../design/protocol-governed-collaboration.zh-CN.md)、[黑板治理与能力寻址](../implemented/2026-09-06-board-governance-addressing.zh-CN.md)、[任务单元语义](../../design/task-unit-semantics.md) ## 问题 From 4cfbe115c0bd4d07724bb0322ae27cb943387812 Mon Sep 17 00:00:00 2001 From: wefio <48851810+wefio@users.noreply.github.com> Date: Tue, 22 Sep 2026 21:25:29 +0800 Subject: [PATCH 11/13] docs(design): a layered model, with the checks that decide whether it earns a place Records the model that came out of the last exchange as a draft rather than as a record, because it was reached by stacking analogies and nothing about it has been measured. The model: a drawing pipeline has four stages and so does ours - producer (a work-shape adapter), surface (the board entry: payload plus a discriminator and a digest, delivery as commit, resolve and expiry as release, wake as the frame callback), compositor (the board and the program: claim, lease, fence, serial and wake, expiry and reaping, the legal set), and display (the presentation end: tiers, flat lines, compact read, the tool descriptions an agent reads). From it come three ownership classes: content and semantics belong to the owner and are semantically opaque to the middle (a compositor reads pixels but cannot read what a button means, so it asks the owner; our board reads declared structure and never payload semantics); description is the owner's obligation (geometry, level, shape, dirty regions, terminal state, identity, a snapshot able to stand in for the owner - ours is the discriminator, digest, dependencies, scope, and the protocol's projection); and arbitration is only the middle's, because stacking, occlusion, visibility, focus, capture, timing and reaping are global properties. The boundary test that replaces a slogan: no reasoning needed means the object stays opaque; structure needed means the owner supplies a description and the middle reads the description but not the semantics; a global judgement means only the middle can make it, which is why the description must be cheap and fresh and why staleness needs a meaning. The document also carries its own case against itself: the analogies did the reasoning and were each partly overruled by a fuller view, one of the four stages is a name with no seam or owner or test, the only measurable claim in this arc (that the seam was narrow) was falsified by a grep within a minute, and nothing got smaller - no code changed, no concept removed, three prose records added. So it states an eligibility rule - an analogy earns a place in a record only when a check can falsify it - and records its predictions before measuring: (a) policy words in the middle, predicted at fifteen or more hits concentrated in the board and the legality module; (b) the two ends, predicted to be concentrated and separable at the producer end and scattered with no seam at the presentation end; (c) a second shape through the dispatch loop, predicted to change five or more files. The reduction test is named in advance too: less code (the middle must lose a type and a freeze method, or the model failed), fewer concepts (the three classes must absorb the two lists they replace rather than sit beside them), fewer change points (measured by how many places a new shape touches), and better maintainability (a new shape adds files under one owner and changes no test in the middle). --- docs/design/mechanism-in-the-middle.md | 106 +++++++++++++++++++ docs/design/mechanism-in-the-middle.zh-CN.md | 65 ++++++++++++ 2 files changed, 171 insertions(+) create mode 100644 docs/design/mechanism-in-the-middle.md create mode 100644 docs/design/mechanism-in-the-middle.zh-CN.md diff --git a/docs/design/mechanism-in-the-middle.md b/docs/design/mechanism-in-the-middle.md new file mode 100644 index 00000000..d692dac1 --- /dev/null +++ b/docs/design/mechanism-in-the-middle.md @@ -0,0 +1,106 @@ +# Mechanism in the middle, adapters at both ends + +**Status:** draft +**Created:** 2026-09-21 + +A layered model of the collaboration protocol, and the checks that decide whether the model earns a place in +the records. This document owns the model, the boundary test, and the checks. It does not restate the parts +inventory, which lives in [protocol-governed-collaboration.md](protocol-governed-collaboration.md), nor the +decisions, which live in three proposed records: [mechanism, not policy](../decisions/proposed/2026-09-21-mechanism-not-policy.md), +[the frame and its storage](../decisions/proposed/2026-09-21-the-frame-and-its-storage.md), and +[the program answers legality](../decisions/proposed/2026-09-20-the-program-answers-legality.md). + +## The model + +A drawing pipeline has four stages, and so does ours. + +| Stage | What it does | What it is here | Where it lives today | +| ---------- | --------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------- | +| Producer | turns something into a buffer | a work-shape adapter: validate the declaration, prepare the inputs, run, submit | `ooo-patch.ts`, `ooo-session-mechanism.ts`, `evals/ooo-execution/data-check-runner.ts` | +| Surface | the shared, opaque exchange object with a role, commit, release and frame callbacks | the board entry: `payload` plus a discriminator and a digest, delivery as commit, resolve and expiry as release, wake as the frame callback | `task_board_entries` with its added columns | +| Compositor | stacking, occlusion, blending, timing, culling, and playing a snapshot when the owner is away | the board and the program: claim, lease, fence, serial and wake, expiry and reaping, the legal set | `src/integration/ooo-board.ts`, `src/integration/ooo-dispatch.ts`, `task-semantics.ts` | +| Display | mode, refresh rate, colour space, scaling - the same frame presented differently per device | the presentation end: T0-T3 tiers, flat lines, compact read, the tool descriptions an agent reads | `taskBoardPreview`, the tier rendering, `src/prompts/nmg-prompts.yaml` | + +Three ownership classes follow, and they are the model's real content: + +1. **Content and semantics** belong to the owner and are semantically opaque to the middle. The compositor + reads pixels to blur or to know an occlusion shape, but it cannot read what a button means; for meaning it + asks the owner. Our board may read declared structure and never reads payload semantics. +2. **Description** is the owner's obligation: geometry, level and parent relationships, shape and opaque and + dirty regions, the terminal state of an animation, an identity for restoration, and a snapshot able to + stand in for the owner. Ours is the discriminator, the digest, dependencies, scope and need, and the + projection a protocol supplies. +3. **Arbitration** is only the middle's, because only the middle has the global view: stacking and occlusion + are global properties, and so are visibility, focus, capture permission, timing and reaping. Ours is the + legal set, claim and lease, wake, expiry. + +The boundary test that replaces a slogan: + +- the middle does not need to reason about the object, so the object stays opaque and belongs to its owner; +- the middle needs structure, so the owner must supply a description and the middle reads the description + but not the semantics; +- the judgement is global, so only the middle can make it - which requires the description to be cheap and + fresh, and requires staleness to have a meaning (an expired lease is a suspicion, not a death certificate). + +Two consequences are already load-bearing elsewhere. The middle must compose in a form that is independent +of how it will be shown, so it never learns a reader's format. And presentation is not verifiable - a display +applies its own colour, crop and scaling - so an acceptance can only ever rest on the artifact and its digest, +never on how it was shown. + +## Why this is a draft and not a record + +- It was reached by stacking analogies, and each analogy was already partly overruled by a fuller view: first + a kernel split, then a window split into frame and content, now four stages. The analogies are doing the + reasoning, and that is the smell this section exists to record. +- One of the four stages is a name and nothing else. The presentation end has no seam, no owner and no tests; + it is a description of scattered code. +- The only measurable claim made in this arc - that the mechanism/policy seam was narrow - was falsified by a + grep within a minute: the patch assumption sits in six places, one of them a legality rule. +- Nothing has got smaller. No code changed, no concept was removed, and three prose records were added. On + today's evidence the model is strictly more concepts than the thing it describes. + +## The eligibility rule + +**An analogy earns a place in a record only when a check can falsify it.** The model above is allowed to stay +in this document while its checks are pending, and it may be promoted into a record only if the checks come +out in its favour. If they do not, this document is archived rather than promoted. + +## The checks + +Predictions are recorded before the measurement, so a surprise is visible rather than rationalised. + +**(a) Policy words in the middle.** Grep the middle layer - `src/core/store/`, `src/integration/ooo-board.ts`, +`src/integration/ooo-dispatch.ts`, `src/integration/task-semantics.ts`, `src/integration/ooo-execution.ts`, +`src/integration/ooo-candidate.ts` - for the policy word list: `patch`, `editable`, `instruction`, `checks`, +`files`, `repair-first`. Report every hit with its path, not just a count, because a word can be mechanism at +one path and policy at another. Prediction: fifteen or more hits, concentrated in the board and the legality +module. Expected finding: the board's own ticket type and the freeze method are the centre of it. + +**(b) The two ends.** Enumerate every site that translates between the board's canonical form and a producer +or reader format. Prediction: the producer end is concentrated and separable (about four sites), while the +presentation end is scattered across the store's preview function, the tier rendering, the prompt file, the +CLI and the extension, with no seam at all. If the presentation end cannot be separated, the fourth stage is a +name for a fact rather than a design, and it should be dropped rather than built. + +**(c) A second shape.** Run one unit through the dispatch loop whose work is not a patch, with a minimal +resolver (patch stays the default; an unknown shape is refused by name rather than failed). Count the files +that have to change. Prediction: five or more. + +## The reduction test + +The model earns its place only by making something smaller, and the four things it must make smaller are +named in advance: + +- **Less code:** taking the seam means the middle loses a type and a freeze method, and the shape's field + validation moves to the adapter that owns the shape. If the net is an increase, the model failed. +- **Fewer concepts:** the three ownership classes must _absorb_ the two lists they replace - the mechanism + list and the policy list - rather than being added beside them. If a reader has to hold both, the model is + a restatement, not an abstraction. +- **Fewer change points:** the six sites must become fewer, or the same six in one place instead of six. The + number that matters is how many places a _new_ shape must touch. +- **Better maintainability:** a new shape adds files under one owner and changes no test in the middle; the + middle's suite passes untouched with a shape it has never seen. + +## Results + +Pending. diff --git a/docs/design/mechanism-in-the-middle.zh-CN.md b/docs/design/mechanism-in-the-middle.zh-CN.md new file mode 100644 index 00000000..f0fcdd7e --- /dev/null +++ b/docs/design/mechanism-in-the-middle.zh-CN.md @@ -0,0 +1,65 @@ +# 机制在中间,适配器在两端 + +**Status:** draft +**Created:** 2026-09-21 + +协作协议的分层模型,以及决定这个模型有没有资格进记录的那几项检查。本文拥有模型、那条界线测试、以及这些检查。它不复述大类清单(那在 [protocol-governed-collaboration.md](protocol-governed-collaboration.zh-CN.md)),也不复述决策(那在三份提案里:[机制,不是策略](../decisions/proposed/2026-09-21-mechanism-not-policy.zh-CN.md)、[帧、它的数据格式与它的存储](../decisions/proposed/2026-09-21-the-frame-and-its-storage.zh-CN.md)、[程序只回答合法性](../decisions/proposed/2026-09-20-the-program-answers-legality.zh-CN.md))。 + +## 模型 + +一条绘图流水线有四段,我们也是。 + +| 段 | 它做什么 | 我们这边的对应 | 今天在哪 | +| ------- | ---------------------------------------------------------- | ----------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------- | +| 生产者 | 把某样东西变成缓冲 | 工作形态适配器:校验声明、准备输入、跑、提交 | `ooo-patch.ts`、`ooo-session-mechanism.ts`、`evals/ooo-execution/data-check-runner.ts` | +| surface | 共享的、不透明的交换物:role、commit、release、帧回调 | 黑板条目:`payload` 加判别位与摘要;交付是 commit,resolve 与过期是 release,唤醒是帧回调 | `task_board_entries` 及其追加列 | +| 合成器 | 堆叠、遮挡、混合、时序、剔除、以及拥有者不在场时替它演快照 | 板子与程序:认领、租约、围栏、串行与唤醒、过期与回收、合法集 | `src/integration/ooo-board.ts`、`src/integration/ooo-dispatch.ts`、`task-semantics.ts` | +| 显示器 | 模式、刷新率、色彩空间、缩放——同一帧在不同设备上呈现不同 | 呈现端:T0–T3 分层、扁平行、紧凑读、agent 读到的工具描述 | `taskBoardPreview`、分层渲染、`src/prompts/nmg-prompts.yaml` | + +由这条流水线出来三类归属,而它们才是模型真正的内容: + +1. **内容与语义**归拥有者,对中间层**语义不透明**。合成器为了模糊或遮挡形状会读像素,但读不出"这颗按钮是什么意思";要含义它得问拥有者。我们的板子可以读声明的结构,从不读载荷的语义。 +2. **描述**是拥有者的义务:几何、层级与父子关系、形状与不透明区与脏区、动画的**终态**、用于恢复的身份、以及能替拥有者顶一阵的快照。我们这边是判别位、摘要、依赖、scope 与 need、以及协议提供的投影。 +3. **裁定**只归中间层,因为只有它有全局视野:堆叠与遮挡是全局性质,可见性、焦点、捕获权限、时序与回收同理。我们这边是合法集、认领与租约、唤醒、过期。 + +取代口号的那条界线测试: + +- 中间层**不需要**就这个对象推理 → 对象保持不透明,归拥有者; +- 中间层**需要结构** → 拥有者必须交描述,中间层只读描述、不读语义; +- 这个判断**是全局的** → 只能中间层做——因此描述必须廉价且够新,并且"陈旧"要有明确语义(租约过期是怀疑,不是死亡证明)。 + +两条推论在别处已经承重:中间层必须在**与将来怎么呈现无关的形式**里做合成,所以它永远不该学会某个读者的格式;而**呈现不可验证**(显示器会自己调色、裁切、缩放),所以验收只能依赖产物与摘要,绝不能依赖呈现方式。 + +## 为什么它现在还只是草稿 + +- 它是靠**叠比喻**得到的,而每个比喻都已经被更完整的视角部分否掉过:先是内核那套,再是窗口的框与内容,现在是四段。**是比喻在替设计推理**——这一节存在的意义就是记下这个味道。 +- 四段里有一段**只是个名字**。呈现端没有缝、没有归属、没有测试;它是对一堆散落代码的描述。 +- 这一段弧里唯一可测的判断——机制/策略那条缝很窄——**在一分钟内被 grep 否证**:补丁假设坐在六处,其中一处是合法性规则。 +- **没有任何东西变小。** 没改代码、没消除概念,反而加了三份散文记录。按今天的证据,这个模型比它描述的对象**多了**概念。 + +## 资格规则 + +**一个比喻只有在能被一次检查否证时,才有资格进记录。** 在上面的检查完成之前,这个模型只能留在本文;只有当检查结果对它有利,它才可能被提升进记录。若结果不利,本文进归档,而不是被提升。 + +## 检查 + +预测先写下来再测量,这样"意外"是可见的,而不是事后被圆过去的。 + +**(a)中间层里的策略词。** 对中间层——`src/core/store/`、`src/integration/ooo-board.ts`、`src/integration/ooo-dispatch.ts`、`src/integration/task-semantics.ts`、`src/integration/ooo-execution.ts`、`src/integration/ooo-candidate.ts`——grep 这份策略词清单:`patch`、`editable`、`instruction`、`checks`、`files`、`repair-first`。**每一处命中都要带路径列出,不能只给计数**,因为同一个词在一条路径上是机制、在另一条路径上是策略。预测:十五处以上,集中在板子与合法性模块。预期的发现:板子自己的票类型与那个冻结方法就是中心。 + +**(b)两端。** 清点所有"在板子的规范形式与某个生产者或读者格式之间翻译"的站点。预测:生产端**集中且可分**(约四处),呈现端**散落**在 store 的预览函数、分层渲染、提示词文件、CLI 与扩展里,**完全没有缝**。如果呈现端分离不出来,那第四段就只是一个事实的名字,而不是一个设计——那就该丢掉,而不是去建它。 + +**(c)第二个形态。** 让一个不是补丁的单元跑同一条派发循环,用一个最小 resolver(补丁仍是默认;未知形态按名字拒绝,而不是判失败)。数要改几个文件。预测:五个以上。 + +## 变小测试 + +这个模型只能靠"让某样东西变小"来赢得位置,而它必须变小的四样,事先写清: + +- **更少的代码**:接上这条缝意味着中间层**丢掉**一个类型和一个冻结方法,而那套字段校验搬去拥有该形态的适配器。如果净增,模型没通过。 +- **更少的概念**:三类归属必须**吸收**它们所替代的两张清单(机制清单与策略清单),而不是被并排加上去。如果读者还得同时握两套,那这个模型是复述,不是抽象。 +- **更少的改动点**:六处要么变少,要么从六处收成一处。真正重要的数字是:**加一个新形态要碰几处**。 +- **更好的可维护性**:加一个形态只在**一个归属者**名下加文件,中间层的测试一个都不改;中间层那套测试在一个它从没见过的形态下原样通过。 + +## 结果 + +待填。 From 9ea3296ab33070ba6a3830b692e6057061d033aa Mon Sep 17 00:00:00 2001 From: wefio <48851810+wefio@users.noreply.github.com> Date: Tue, 22 Sep 2026 21:26:45 +0800 Subject: [PATCH 12/13] docs(design): fill in checks a and b, and correct the display stage the counting exposed Both checks were run against the prediction recorded before them. (a) Policy words in the middle: predicted fifteen or more hits, measured 195 raw - patch 106, files 39, checks 23, editable 14, instruction 12, repair-first 1 - concentrated exactly where predicted, in src/integration/ooo-board.ts (53 patch hits), src/integration/task-semantics.ts (27 patch, 9 editable) and src/core/store/base.ts (13 patch). Two corrections forced by the measurement. The raw count overstates the case, which is the caveat the check was written with: files in src/core/store/writes.ts and retrieval.ts is a mechanism word (a path inside a store) and checks in src/integration/ooo-candidate.ts names the check runner, also mechanism, so the word list drops files and checks and keeps patch, editable and instruction. What is left is still about 120 hits, so "the middle is policy-free" is false as a description of today, an order of magnitude past the prediction, and the model's support is directional: the leak is real, large and concentrated in three files. (b) The two ends: the producer end was predicted concentrated and separable and measured four to six functions over three files (preparePatchWork, patchPrompt, patchCandidate, patchSubmission, snapshotText, runTestFile); the presentation end was predicted scattered with no seam and measured six sites (the preview text, the entry and preview types, the wire shape, the service, the agent-facing renderer, and the generated tool descriptions). Both held. The measurement also corrected this document: the display row claimed T0-T3 tiers, and no such thing exists for a board entry - tieredDisclosure and tier belong to memory retrieval, and a board entry has exactly one presentation, the 200-character preview plus raw fields. That row now says what exists, so the second borrowed vocabulary is recorded rather than left in the table. (c) A second shape is not run yet, and the reduction test is unchanged: nothing has got smaller, the middle's three files hold the available reduction, and whether it pays is what (c) measures. The counting earned its keep twice - it produced the word-and-path rule the earlier prose could not state, and it caught this document borrowing the memory side's vocabulary as if it were the board's. --- docs/design/mechanism-in-the-middle.md | 48 +++++++++++++++++--- docs/design/mechanism-in-the-middle.zh-CN.md | 24 +++++++--- 2 files changed, 58 insertions(+), 14 deletions(-) diff --git a/docs/design/mechanism-in-the-middle.md b/docs/design/mechanism-in-the-middle.md index d692dac1..7a0fc009 100644 --- a/docs/design/mechanism-in-the-middle.md +++ b/docs/design/mechanism-in-the-middle.md @@ -14,12 +14,12 @@ decisions, which live in three proposed records: [mechanism, not policy](../deci A drawing pipeline has four stages, and so does ours. -| Stage | What it does | What it is here | Where it lives today | -| ---------- | --------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------- | -| Producer | turns something into a buffer | a work-shape adapter: validate the declaration, prepare the inputs, run, submit | `ooo-patch.ts`, `ooo-session-mechanism.ts`, `evals/ooo-execution/data-check-runner.ts` | -| Surface | the shared, opaque exchange object with a role, commit, release and frame callbacks | the board entry: `payload` plus a discriminator and a digest, delivery as commit, resolve and expiry as release, wake as the frame callback | `task_board_entries` with its added columns | -| Compositor | stacking, occlusion, blending, timing, culling, and playing a snapshot when the owner is away | the board and the program: claim, lease, fence, serial and wake, expiry and reaping, the legal set | `src/integration/ooo-board.ts`, `src/integration/ooo-dispatch.ts`, `task-semantics.ts` | -| Display | mode, refresh rate, colour space, scaling - the same frame presented differently per device | the presentation end: T0-T3 tiers, flat lines, compact read, the tool descriptions an agent reads | `taskBoardPreview`, the tier rendering, `src/prompts/nmg-prompts.yaml` | +| Stage | What it does | What it is here | Where it lives today | +| ---------- | --------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Producer | turns something into a buffer | a work-shape adapter: validate the declaration, prepare the inputs, run, submit | `ooo-patch.ts`, `ooo-session-mechanism.ts`, `evals/ooo-execution/data-check-runner.ts` | +| Surface | the shared, opaque exchange object with a role, commit, release and frame callbacks | the board entry: `payload` plus a discriminator and a digest, delivery as commit, resolve and expiry as release, wake as the frame callback | `task_board_entries` with its added columns | +| Compositor | stacking, occlusion, blending, timing, culling, and playing a snapshot when the owner is away | the board and the program: claim, lease, fence, serial and wake, expiry and reaping, the legal set | `src/integration/ooo-board.ts`, `src/integration/ooo-dispatch.ts`, `task-semantics.ts` | +| Display | mode, refresh rate, colour space, scaling - the same frame presented differently per device | the presentation end: what a reader is shown | one presentation of a board entry exists - `taskBoardPreview`, 200 characters, plus raw fields - at six sites; the tier vocabulary lives only on the memory side (`tieredDisclosure`), never for board entries | Three ownership classes follow, and they are the model's real content: @@ -103,4 +103,38 @@ named in advance: ## Results -Pending. +**(a) Policy words in the middle.** The prediction was fifteen or more hits. Measured 195 raw across the +listed files: `patch` 106, `files` 39, `checks` 23, `editable` 14, `instruction` 12, `repair-first` 1. The +concentration is where it was predicted: `src/integration/ooo-board.ts` carries 53 `patch` hits, +`src/integration/task-semantics.ts` 27 `patch` and 9 `editable`, `src/core/store/base.ts` 13 `patch`. + +The measurement forced two corrections. First, the raw count overstates the case, which is the caveat this +check was written with: `files` in `src/core/store/writes.ts` and `src/core/store/retrieval.ts` is a mechanism +word - a path inside a store - and `checks` in `src/integration/ooo-candidate.ts` names the check _runner_, +which is mechanism too. The word list should therefore drop `files` and `checks` and keep `patch`, `editable` +and `instruction`. Second, what remains is still about 120 hits, so "the middle is policy-free" is false as a +description of today, at a scale an order of magnitude past the prediction. What the model has is directional +support: the leak is real, large, and concentrated in three files, which is exactly what a seam would have to +remove. + +**(b) The two ends.** The producer end was predicted concentrated and separable, and measured four to six +functions over three files: `preparePatchWork`, `patchPrompt`, `patchCandidate` and `patchSubmission` in +`src/integration/ooo-patch.ts`, `snapshotText` in `src/integration/ooo-session-mechanism.ts`, and `runTestFile` +in `evals/ooo-execution/data-check-runner.ts`. The prediction held. The presentation end was predicted +scattered with no seam, and measured six sites: the preview text in `src/core/store/base.ts`, the entry and +preview types in `src/core/types.ts`, the wire shape in `src/cli/protocol.ts`, the service in +`src/cli/service.ts`, the agent-facing renderer in `.pi/extensions/nmg/index.ts`, and the generated tool +descriptions from `src/prompts/nmg-prompts.yaml`. That prediction held too, with one correction against this +document - see the display row above. A board entry has exactly one presentation, so the fourth stage is +thinner than the table first claimed, and the tier vocabulary in it had been borrowed from the memory side +rather than found on the board side. + +**(c) A second shape.** Not run yet. + +## What the two checks say about the reduction test + +Nothing has got smaller yet, and the checks say where a reduction would have to come from: three files in the +middle hold about a hundred and twenty policy-sense hits, and six sites present a board entry with no owner +between them. The reduction available today is in the middle's three files; whether it pays is what check (c) +measures. The counting earned its keep twice: it produced the word-and-path rule that the earlier prose could +not state, and it caught this document borrowing another subsystem's vocabulary as if it were the board's. diff --git a/docs/design/mechanism-in-the-middle.zh-CN.md b/docs/design/mechanism-in-the-middle.zh-CN.md index f0fcdd7e..9034c7da 100644 --- a/docs/design/mechanism-in-the-middle.zh-CN.md +++ b/docs/design/mechanism-in-the-middle.zh-CN.md @@ -9,12 +9,12 @@ 一条绘图流水线有四段,我们也是。 -| 段 | 它做什么 | 我们这边的对应 | 今天在哪 | -| ------- | ---------------------------------------------------------- | ----------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------- | -| 生产者 | 把某样东西变成缓冲 | 工作形态适配器:校验声明、准备输入、跑、提交 | `ooo-patch.ts`、`ooo-session-mechanism.ts`、`evals/ooo-execution/data-check-runner.ts` | -| surface | 共享的、不透明的交换物:role、commit、release、帧回调 | 黑板条目:`payload` 加判别位与摘要;交付是 commit,resolve 与过期是 release,唤醒是帧回调 | `task_board_entries` 及其追加列 | -| 合成器 | 堆叠、遮挡、混合、时序、剔除、以及拥有者不在场时替它演快照 | 板子与程序:认领、租约、围栏、串行与唤醒、过期与回收、合法集 | `src/integration/ooo-board.ts`、`src/integration/ooo-dispatch.ts`、`task-semantics.ts` | -| 显示器 | 模式、刷新率、色彩空间、缩放——同一帧在不同设备上呈现不同 | 呈现端:T0–T3 分层、扁平行、紧凑读、agent 读到的工具描述 | `taskBoardPreview`、分层渲染、`src/prompts/nmg-prompts.yaml` | +| 段 | 它做什么 | 我们这边的对应 | 今天在哪 | +| ------- | ---------------------------------------------------------- | ----------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------- | +| 生产者 | 把某样东西变成缓冲 | 工作形态适配器:校验声明、准备输入、跑、提交 | `ooo-patch.ts`、`ooo-session-mechanism.ts`、`evals/ooo-execution/data-check-runner.ts` | +| surface | 共享的、不透明的交换物:role、commit、release、帧回调 | 黑板条目:`payload` 加判别位与摘要;交付是 commit,resolve 与过期是 release,唤醒是帧回调 | `task_board_entries` 及其追加列 | +| 合成器 | 堆叠、遮挡、混合、时序、剔除、以及拥有者不在场时替它演快照 | 板子与程序:认领、租约、围栏、串行与唤醒、过期与回收、合法集 | `src/integration/ooo-board.ts`、`src/integration/ooo-dispatch.ts`、`task-semantics.ts` | +| 显示器 | 模式、刷新率、色彩空间、缩放——同一帧在不同设备上呈现不同 | 呈现端:读者被展示到什么 | 黑板条目的呈现只有一种——`taskBoardPreview`,200 字符,加原始字段——被六个站点消费;分层的词汇只活在记忆那一侧(`tieredDisclosure`),从不用于黑板条目 | 由这条流水线出来三类归属,而它们才是模型真正的内容: @@ -62,4 +62,14 @@ ## 结果 -待填。 +**(a)中间层里的策略词。** 预测十五处以上。实测六个文件里共 195 处:`patch` 106、`files` 39、`checks` 23、`editable` 14、`instruction` 12、`repair-first` 1。集中处与预测一致:`src/integration/ooo-board.ts` 带 53 处 `patch`,`src/integration/task-semantics.ts` 27 处 `patch` 与 9 处 `editable`,`src/core/store/base.ts` 13 处 `patch`。 + +测量迫使两条修正。**第一,原始计数高估了情况**——而这正是写这条检查时要带上的那个前提:`src/core/store/writes.ts` 与 `src/core/store/retrieval.ts` 里的 `files` 是机制词(存储里的路径),`src/integration/ooo-candidate.ts` 里的 `checks` 指的是检查的**运行器**,也是机制。所以词表应**去掉 `files` 与 `checks`**,只留 `patch`、`editable`、`instruction`。**第二,剩下的仍有约 120 处**,所以「中间层是无策略的」作为**今天的描述**是假的,而且超出一个数量级。模型拿到的是**方向性支持**:泄漏真实、量大、集中在三个文件里——那正是接上一条缝要拿掉的东西。 + +**(b)两端。** 生产端预测「集中且可分」,实测三到六个函数散在三个文件:`src/integration/ooo-patch.ts` 里的 `preparePatchWork`、`patchPrompt`、`patchCandidate`、`patchSubmission`,`src/integration/ooo-session-mechanism.ts` 里的 `snapshotText`,`evals/ooo-execution/data-check-runner.ts` 里的 `runTestFile`。预测成立。呈现端预测「散落、没有缝」,实测六个站点:`src/core/store/base.ts` 的预览文本、`src/core/types.ts` 的条目与预览类型、`src/cli/protocol.ts` 的线上形状、`src/cli/service.ts` 的服务、`.pi/extensions/nmg/index.ts` 里面向 agent 的渲染、以及由 `src/prompts/nmg-prompts.yaml` 生成的工具描述。这条预测也成立,但有**一处对本文的修正**——见上表「显示器」一行:黑板条目只有一种呈现,所以第四段比表里最初写的更薄,而它里面的分层词汇是从记忆那一侧**借**来的,不是黑板这一侧**找到**的。 + +**(c)第二个形态。** 还没跑。 + +## 这两条检查对变小测试说了什么 + +现在还什么都没变小,而检查指出了变小只能从哪里来:中间层三个文件里约 120 处策略语义命中,以及六个站点共同呈现一个黑板条目、彼此之间没有归属者。今天唯一可取的变小是中间层那三个文件;它是否值得,由检查(c)测。这次清点两次证明了它的价值:它产出了散文说不出的「词 + 路径」规则,并且抓出本文把另一个子系统的词汇当黑板自己的用。 From d56270d0710448fdcf896fd4ceab8a893f4d8786 Mon Sep 17 00:00:00 2001 From: wefio <48851810+wefio@users.noreply.github.com> Date: Tue, 22 Sep 2026 22:49:19 +0800 Subject: [PATCH 13/13] Give the board entry two faces, and the read face one home The model in mechanism-in-the-middle.md is retired and replaced: a board entry is a medium with a write face and a read face, the compositor is brought by a protocol rather than by the board, and the roles are roles rather than files - src/core/store/base.ts is the medium, the compositor and the reader's rule at once, and src/cli/service.ts is both the wire face and a formatter. The rule that decides where a seam goes needs no taste, only a count: a boundary is built when it has a second implementation, and only declared with one. The read face has three, so it lands here. src/core/board-entry-view.ts owns the rule, the store's compact read and the host adapter's broadcast both call it, and the store's private copy and the adapter's bare slice are gone. One of the three was not merely duplicated but wrong - the adapter's 140-character slice did not collapse whitespace, so a multi-line body reached a broadcast as a multi-line broadcast. The write face's type and the middle's seam are declared and not built, each with a named trigger. Check (c) measured why: making the loop shape-agnostic is one file with fifteen insertions and eighteen deletions, and tsc then reports zero errors while twenty-five tests fail, every one of them because the claim admits no work. The coupling is invisible to the compiler and fatal at runtime, so the seam is prepaid cost until an adopter declares a non-patch unit. Docs and code land together, because the read face's home is what the two-face model implements. --- .pi/extensions/nmg/index.ts | 3 +- docs/design/mechanism-in-the-middle.md | 167 ++++++++++++------- docs/design/mechanism-in-the-middle.zh-CN.md | 74 +++++--- src/core/board-entry-view.ts | 18 ++ src/core/store/base.ts | 13 +- tests/core/task-board.test.ts | 12 ++ 6 files changed, 193 insertions(+), 94 deletions(-) create mode 100644 src/core/board-entry-view.ts diff --git a/.pi/extensions/nmg/index.ts b/.pi/extensions/nmg/index.ts index cfc9e275..239dd179 100644 --- a/.pi/extensions/nmg/index.ts +++ b/.pi/extensions/nmg/index.ts @@ -38,6 +38,7 @@ import { loadPrompts, renderDisclosure } from "../../../src/prompts/load.ts"; import { memoryDisclosureEntries } from "../../../src/integration/search-projection.ts"; import { PI_BOARD_ACTIONS, PI_REMEMBER_ACTIONS } from "../../../src/integration/tool-contract.ts"; import { TASK_BOARD_VERDICTS } from "../../../src/core/types.ts"; +import { boardEntryView } from "../../../src/core/board-entry-view.ts"; import { resolveSkillOptPolicyChannels } from "../../../src/lab/skillopt-policy.ts"; import type { ActiveGraphBudget, @@ -2998,7 +2999,7 @@ export async function maybeBroadcastToWorld(input: { entryIds: [entry.id], })) as { delivered: string[]; suppressed: boolean }; if (worldCheck.delivered.includes(entry.id)) return false; - const excerpt = entry.content.length > 140 ? `${entry.content.slice(0, 140)}…` : entry.content; + const excerpt = boardEntryView(entry.content, 140); const label = kindLabel(entry.kind); const broadcast = `[NMG board 协作广播] 频道 ${entry.taskId} 有 #${entry.id} 未认领的${label}(open):${excerpt}。有空的 agent 可用 nmg_board read taskId=${entry.taskId} 查看详情、claim 认领处理。`; await invoke("taskBoard", { diff --git a/docs/design/mechanism-in-the-middle.md b/docs/design/mechanism-in-the-middle.md index 7a0fc009..d87af946 100644 --- a/docs/design/mechanism-in-the-middle.md +++ b/docs/design/mechanism-in-the-middle.md @@ -2,62 +2,93 @@ **Status:** draft **Created:** 2026-09-21 +**Updated:** 2026-09-21 -A layered model of the collaboration protocol, and the checks that decide whether the model earns a place in -the records. This document owns the model, the boundary test, and the checks. It does not restate the parts -inventory, which lives in [protocol-governed-collaboration.md](protocol-governed-collaboration.md), nor the -decisions, which live in three proposed records: [mechanism, not policy](../decisions/proposed/2026-09-21-mechanism-not-policy.md), +A model of how a board entry travels, and the checks that decide whether the model earns a place in the +records. This document owns the model, the rule that says when a boundary earns a seam, and the checks. It +does not restate the parts inventory, which lives in [protocol-governed-collaboration.md](protocol-governed-collaboration.md), +nor the decisions, which live in three proposed records: [mechanism, not policy](../decisions/proposed/2026-09-21-mechanism-not-policy.md), [the frame and its storage](../decisions/proposed/2026-09-21-the-frame-and-its-storage.md), and [the program answers legality](../decisions/proposed/2026-09-20-the-program-answers-legality.md). ## The model -A drawing pipeline has four stages, and so does ours. - -| Stage | What it does | What it is here | Where it lives today | -| ---------- | --------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| Producer | turns something into a buffer | a work-shape adapter: validate the declaration, prepare the inputs, run, submit | `ooo-patch.ts`, `ooo-session-mechanism.ts`, `evals/ooo-execution/data-check-runner.ts` | -| Surface | the shared, opaque exchange object with a role, commit, release and frame callbacks | the board entry: `payload` plus a discriminator and a digest, delivery as commit, resolve and expiry as release, wake as the frame callback | `task_board_entries` with its added columns | -| Compositor | stacking, occlusion, blending, timing, culling, and playing a snapshot when the owner is away | the board and the program: claim, lease, fence, serial and wake, expiry and reaping, the legal set | `src/integration/ooo-board.ts`, `src/integration/ooo-dispatch.ts`, `task-semantics.ts` | -| Display | mode, refresh rate, colour space, scaling - the same frame presented differently per device | the presentation end: what a reader is shown | one presentation of a board entry exists - `taskBoardPreview`, 200 characters, plus raw fields - at six sites; the tier vocabulary lives only on the memory side (`tieredDisclosure`), never for board entries | - -Three ownership classes follow, and they are the model's real content: - -1. **Content and semantics** belong to the owner and are semantically opaque to the middle. The compositor - reads pixels to blur or to know an occlusion shape, but it cannot read what a button means; for meaning it - asks the owner. Our board may read declared structure and never reads payload semantics. -2. **Description** is the owner's obligation: geometry, level and parent relationships, shape and opaque and - dirty regions, the terminal state of an animation, an identity for restoration, and a snapshot able to - stand in for the owner. Ours is the discriminator, the digest, dependencies, scope and need, and the - projection a protocol supplies. -3. **Arbitration** is only the middle's, because only the middle has the global view: stacking and occlusion - are global properties, and so are visibility, focus, capture permission, timing and reaping. Ours is the - legal set, claim and lease, wake, expiry. - -The boundary test that replaces a slogan: - -- the middle does not need to reason about the object, so the object stays opaque and belongs to its owner; -- the middle needs structure, so the owner must supply a description and the middle reads the description - but not the semantics; -- the judgement is global, so only the middle can make it - which requires the description to be cheap and - fresh, and requires staleness to have a meaning (an expired lease is a suspicion, not a death certificate). - -Two consequences are already load-bearing elsewhere. The middle must compose in a form that is independent -of how it will be shown, so it never learns a reader's format. And presentation is not verifiable - a display -applies its own colour, crop and scaling - so an acceptance can only ever rest on the artifact and its digest, -never on how it was shown. +A board entry is a medium with two faces, and the roles around it are three. + +``` + producer ──[ producer-side interface · write ]──┐ ┌──[ presentation interface · read ]──▶ reader + ▼ ▲ + ┌────────────────────────────────────┐ + │ board entry (the medium) │ + │ write face read face │ + └────────────────────────────────────┘ + │ ▲ + Task-Unit Protocol ──▶ compositor +``` + +| Edge | What it is | Where it lives today | State | +| -------------- | ------------------------------------- | ------------------------------------------------------ | ------------------------------------------------------------------------------------------- | +| the write face | what a producer puts on the medium | `put` and `deliver`; a body arrives as `content` | three fillers - a patch artifact, a human broadcast, a `memory=` pointer - and no shape | +| the read face | what a reader is shown | `boardEntryView` in `src/core/board-entry-view.ts` | one rule, one home, one test | +| the middle | the compositor, brought by a protocol | `ooo-board.ts`, `ooo-dispatch.ts`, `task-semantics.ts` | one protocol brings it: the Task-Unit Protocol | + +Three facts about the shape, each measured rather than assumed. + +1. **The middle comes from a protocol, not from the board.** The board offers two faces and nothing else; a + protocol names the constraints that put a compositor between them, and the Task-Unit Protocol is the one + that brings ours. That is why "the program only answers legality" says the same thing: no declared + legality, no judgement. Of the seven entry kinds, only `handoff` and `result` are ever claimed, delivered + and judged - a note, a goal and a question travel from the write face straight to the read face. +2. **The roles are not files.** `src/core/store/base.ts` is the medium (columns and transactions), the + compositor (the lease-based claim compare-and-set and the clock) and the reader's rule at once, and + `src/cli/service.ts` is both the wire face and a formatter. So this is a map of roles, and a check can sit + only on the artifacts that cross an edge, never on module imports. +3. **Feedback is a replay, not a reversal.** A reader becomes the next writer and enters through the write + face again; the middle is never traversed backwards. The broadcast path in `.pi/extensions/nmg/index.ts` + (it reads an entry, then writes a new one) is a place where two roles live side by side, and rejection + followed by re-issue moves forward as well - the middle writes a new declaration onto the medium and the + producer reads it from the write face. + +## Which boundary earns a seam + +**A boundary is built when it has a second implementation; with one, it is only declared.** The rule needs no +taste, only a count. + +| Boundary | Implementations today | Consequence | +| -------------- | ----------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------- | +| the write face | three fillers for one body | declared: a type is worth writing when a fourth filler appears | +| the read face | three truncation rules - 200 characters collapsing, a generic helper, and a bare 140 slice that collapsed nothing | **built**: one rule, one home, one test | +| the middle | one protocol | declared only: a seam is built when a second protocol brings a second middle | + +The alternative - building every seam now, each against a single implementation - is the mistake this +document already made once by stacking analogies. A seam designed against one shape is a seam designed +against an imagined second shape, and the imagined one is the one that gets built wrong. + +## What landed with this document + +- **The read face has one home.** `src/core/board-entry-view.ts` owns the rule: a lone `memory=` pointer + is returned whole, anything longer is collapsed to one bounded line, and the bound belongs to the caller, + because how much of a body fits is the reader's decision. The store's compact read and the host adapter's + broadcast both call it; the store's private copy and the adapter's bare slice are gone. One of the three + rules was also wrong rather than merely duplicated: the 140-character slice in `.pi/extensions/nmg/index.ts` + did not collapse whitespace, so a multi-line body reached a broadcast as a multi-line broadcast. + `tests/core/task-board.test.ts` pins the rule directly. The adapter's `excerpt` helper stays for memory + results, which are not board entries and keep their own rule. +- **The write face's type did not land.** The three fillers are three kinds of entry rather than one shape + drawn three ways, so the trigger for a type is a fourth filler. +- **The middle's seam did not land.** Check (c) says why. ## Why this is a draft and not a record -- It was reached by stacking analogies, and each analogy was already partly overruled by a fuller view: first - a kernel split, then a window split into frame and content, now four stages. The analogies are doing the - reasoning, and that is the smell this section exists to record. -- One of the four stages is a name and nothing else. The presentation end has no seam, no owner and no tests; - it is a description of scattered code. +- The pipeline form of this document - four stages borrowed from a drawing pipeline - was reached by stacking + analogies and was **retired on 2026-09-21** in favour of the two-faces model above, which came from + measurement instead: roles cohabit files, feedback replays rather than reverses, and a protocol is what + brings the middle. - The only measurable claim made in this arc - that the mechanism/policy seam was narrow - was falsified by a grep within a minute: the patch assumption sits in six places, one of them a legality rule. -- Nothing has got smaller. No code changed, no concept was removed, and three prose records were added. On - today's evidence the model is strictly more concepts than the thing it describes. +- One thing has got smaller: the read face's three rules are now one, in one home, with one test. The middle's + hundred-and-twenty policy-sense hits and the write face's unshaped body have not. +- The model is still more concepts than the code it describes, so it stays a draft. ## The eligibility rule @@ -125,16 +156,36 @@ scattered with no seam, and measured six sites: the preview text in `src/core/st preview types in `src/core/types.ts`, the wire shape in `src/cli/protocol.ts`, the service in `src/cli/service.ts`, the agent-facing renderer in `.pi/extensions/nmg/index.ts`, and the generated tool descriptions from `src/prompts/nmg-prompts.yaml`. That prediction held too, with one correction against this -document - see the display row above. A board entry has exactly one presentation, so the fourth stage is -thinner than the table first claimed, and the tier vocabulary in it had been borrowed from the memory side -rather than found on the board side. - -**(c) A second shape.** Not run yet. - -## What the two checks say about the reduction test - -Nothing has got smaller yet, and the checks say where a reduction would have to come from: three files in the -middle hold about a hundred and twenty policy-sense hits, and six sites present a board entry with no owner -between them. The reduction available today is in the middle's three files; whether it pays is what check (c) -measures. The counting earned its keep twice: it produced the word-and-path rule that the earlier prose could -not state, and it caught this document borrowing another subsystem's vocabulary as if it were the board's. +document: a board entry has exactly one presentation, and the tier vocabulary in the old table had been +borrowed from the memory side rather than found on the board side. A later pass over the same sites found +what the count alone could not: the six sites are a chain, not six formatters, and the duplication sits in +three of them - the store's preview rule, the adapter's generic helper, and a bare slice in the adapter's +broadcast path. + +**(c) A second shape.** The prediction was five or more files. The change was made on a throwaway branch and +then reverted: the loop was made shape-agnostic by replacing `DispatchTicket.patch?: PatchWork` with an opaque +`declaration?: {shape, digest, frozen}`, pointing `PlanWorker` at that declaration, and deleting the +`preparePatchWork` call from the unit dispatcher - one file, fifteen insertions and eighteen deletions. Two +results followed, and neither was the result the prediction was aiming at. + +- `tsc --noEmit` reported **zero errors**. The coupling is invisible to the compiler: the field is optional, + and structural typing still lets the board's ticket, which carries `patch`, satisfy the port, so the change + produced no compile-time signal at all. +- The suite reported **25 failing tests**, every one with the same cause - "the claim admits no work". A second + shape today would not fail as a new shape; it would refuse every unit. + +The coupling's surface, counted by grep: three source files name the frozen patch work (`ooo-dispatch.ts`, +`ooo-patch.ts`, `ooo-session-mechanism.ts`, the last with about ten uses), the board must change although it +never reads the field, seven test files read `ticket.patch` directly, and the worker side couples through the +`PlanWorker` type, which breaks the driver's three worker kinds. The prediction held and then some, and the +decision it forced is the opposite of building: the seam is prepaid cost, deferred with a named trigger - the +first non-patch unit an adopter declares - rather than built now against a single shape. + +## What the checks say about the reduction test + +One thing is smaller: three rules for showing a board entry became one rule in one home with one test, and the +rule that disappeared was the wrong one. Everything else stands. The middle's three files still hold about a +hundred and twenty policy-sense hits, and the write face's body still has no shape. The counting earned its +keep three times: it produced the word-and-path rule that the earlier prose could not state, it caught this +document borrowing another subsystem's vocabulary as if it were the board's, and it turned "should the seam +exist" from a matter of taste into a measured file count. diff --git a/docs/design/mechanism-in-the-middle.zh-CN.md b/docs/design/mechanism-in-the-middle.zh-CN.md index 9034c7da..a5ce8203 100644 --- a/docs/design/mechanism-in-the-middle.zh-CN.md +++ b/docs/design/mechanism-in-the-middle.zh-CN.md @@ -2,40 +2,61 @@ **Status:** draft **Created:** 2026-09-21 +**Updated:** 2026-09-21 -协作协议的分层模型,以及决定这个模型有没有资格进记录的那几项检查。本文拥有模型、那条界线测试、以及这些检查。它不复述大类清单(那在 [protocol-governed-collaboration.md](protocol-governed-collaboration.zh-CN.md)),也不复述决策(那在三份提案里:[机制,不是策略](../decisions/proposed/2026-09-21-mechanism-not-policy.zh-CN.md)、[帧、它的数据格式与它的存储](../decisions/proposed/2026-09-21-the-frame-and-its-storage.zh-CN.md)、[程序只回答合法性](../decisions/proposed/2026-09-20-the-program-answers-legality.zh-CN.md))。 +一个黑板条目怎么走,以及决定这个模型有没有资格进记录的那几项检查。本文拥有模型、"一条边界什么时候够资格建缝"的那条规则、以及这些检查。它不复述大类清单(那在 [protocol-governed-collaboration.md](protocol-governed-collaboration.zh-CN.md)),也不复述决策(那在三份提案里:[机制,不是策略](../decisions/proposed/2026-09-21-mechanism-not-policy.zh-CN.md)、[帧、它的数据格式与它的存储](../decisions/proposed/2026-09-21-the-frame-and-its-storage.zh-CN.md)、[程序只回答合法性](../decisions/proposed/2026-09-20-the-program-answers-legality.zh-CN.md))。 ## 模型 -一条绘图流水线有四段,我们也是。 +一个黑板条目是一个**有正反两面的介质**,围着它的角色有三个。 -| 段 | 它做什么 | 我们这边的对应 | 今天在哪 | -| ------- | ---------------------------------------------------------- | ----------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------- | -| 生产者 | 把某样东西变成缓冲 | 工作形态适配器:校验声明、准备输入、跑、提交 | `ooo-patch.ts`、`ooo-session-mechanism.ts`、`evals/ooo-execution/data-check-runner.ts` | -| surface | 共享的、不透明的交换物:role、commit、release、帧回调 | 黑板条目:`payload` 加判别位与摘要;交付是 commit,resolve 与过期是 release,唤醒是帧回调 | `task_board_entries` 及其追加列 | -| 合成器 | 堆叠、遮挡、混合、时序、剔除、以及拥有者不在场时替它演快照 | 板子与程序:认领、租约、围栏、串行与唤醒、过期与回收、合法集 | `src/integration/ooo-board.ts`、`src/integration/ooo-dispatch.ts`、`task-semantics.ts` | -| 显示器 | 模式、刷新率、色彩空间、缩放——同一帧在不同设备上呈现不同 | 呈现端:读者被展示到什么 | 黑板条目的呈现只有一种——`taskBoardPreview`,200 字符,加原始字段——被六个站点消费;分层的词汇只活在记忆那一侧(`tieredDisclosure`),从不用于黑板条目 | +``` + 生产者 ──[ 生产者端 · 写 ]──┐ ┌──[ 呈现端 · 读 ]──▶ 读者 + ▼ ▲ + ┌──────────────────────────────────┐ + │ 黑板条目(介质) │ + │ 写面 读面 │ + └──────────────────────────────────┘ + │ ▲ + 任务单元协议 ──▶ 裁定者(合成) +``` -由这条流水线出来三类归属,而它们才是模型真正的内容: +| 边 | 是什么 | 今天在哪 | 状态 | +| ---- | ------------------------ | ------------------------------------------------------ | -------------------------------------------------------------- | +| 写面 | 生产者放到介质上的东西 | `put` 与 `deliver`;内容以 `content` 到达 | 三种填法——补丁产物、人类广播、`memory=` 指针——**没有形状** | +| 读面 | 读者被展示到什么 | `src/core/board-entry-view.ts` 的 `boardEntryView` | **一条规则、一个家、一个测试** | +| 中间 | 裁定者,由**协议**带进来 | `ooo-board.ts`、`ooo-dispatch.ts`、`task-semantics.ts` | 只有一份协议带它:任务单元协议 | -1. **内容与语义**归拥有者,对中间层**语义不透明**。合成器为了模糊或遮挡形状会读像素,但读不出"这颗按钮是什么意思";要含义它得问拥有者。我们的板子可以读声明的结构,从不读载荷的语义。 -2. **描述**是拥有者的义务:几何、层级与父子关系、形状与不透明区与脏区、动画的**终态**、用于恢复的身份、以及能替拥有者顶一阵的快照。我们这边是判别位、摘要、依赖、scope 与 need、以及协议提供的投影。 -3. **裁定**只归中间层,因为只有它有全局视野:堆叠与遮挡是全局性质,可见性、焦点、捕获权限、时序与回收同理。我们这边是合法集、认领与租约、唤醒、过期。 +关于这个形状的三条事实,每一条都是**量出来的**,不是假定的。 -取代口号的那条界线测试: +1. **中间来自协议,不来自黑板。** 黑板只提供两个面;是**协议**点名了那些约束,才把裁定者接在两面之间,而带进我们这一段的是**任务单元协议**。所以"程序只回答合法性"说的是同一件事:没有声明的合法性,就没有裁定。七种条目类型里只有 `handoff` 与 `result` 会被认领、交付与裁决——`note`、`goal`、`question` 从写面直接到读面。 +2. **角色不是文件。** `src/core/store/base.ts` 同时是介质(列与事务)、裁定者(基于租约的认领 compare-and-set 与时钟)和读者的那条规则;`src/cli/service.ts` 同时是线上面与格式化器。所以这是一张**角色图**,检查只能落在**跨边的产物**上,绝不能落在 import 邻接上。 +3. **反馈是重放,不是倒放。** 读者成为下一个写者,再次从**写面**进来;中间从不被反向穿过。`.pi/extensions/nmg/index.ts` 的广播那条路(先读一个条目、再写一个新条目)就是两个角色并排住在一处的地方;而拒绝之后的重发同样是向前走——中间把新声明写到介质上,生产者从写面把它读走。 -- 中间层**不需要**就这个对象推理 → 对象保持不透明,归拥有者; -- 中间层**需要结构** → 拥有者必须交描述,中间层只读描述、不读语义; -- 这个判断**是全局的** → 只能中间层做——因此描述必须廉价且够新,并且"陈旧"要有明确语义(租约过期是怀疑,不是死亡证明)。 +## 哪条边界够资格建缝 -两条推论在别处已经承重:中间层必须在**与将来怎么呈现无关的形式**里做合成,所以它永远不该学会某个读者的格式;而**呈现不可验证**(显示器会自己调色、裁切、缩放),所以验收只能依赖产物与摘要,绝不能依赖呈现方式。 +**一条边界只有在出现第二个实现时才建;只有一个实现时,只声明。** 这条规则不需要品味,只需要一个计数。 + +| 边界 | 今天的实现数 | 后果 | +| ---- | ------------------------------------------------------------------------------- | ------------------------------------------ | +| 写面 | 一个条目内容有三种填法 | 只声明:等到出现**第四种**填法再写类型 | +| 读面 | 三条截断规则——200 字符折叠、一个通用助手、以及一条**连空白都不折叠**的 140 裸切 | **建**:一条规则、一个家、一个测试 | +| 中间 | 一份协议 | 只声明:等第二份协议带来第二段中间,再建缝 | + +另一种做法——现在就把每条缝都建起来、每条缝对着一个实现——正是本文已经犯过一次的错误:**叠比喻**。对着一个实现设计出来的缝,是**对着想象出来的第二个实现**设计的,而被想错的那个,恰恰就是真会被建错的那个。 + +## 随本文一起落地的东西 + +- **读面有了一个家。** `src/core/board-entry-view.ts` 拥有这条规则:孤立的 `memory=` 指针原样返回;更长的内容折叠成**一行有界文本**;界**归调用方**,因为"塞得下多少"是读者的决定。store 的紧凑读与宿主适配器的广播都调它;store 里那份私有副本和适配器里那条裸切都删掉了。三条规则里有一条不只是重复,而是**错的**:`.pi/extensions/nmg/index.ts` 那条 140 字符裸切不折叠空白,于是一个多行内容会以多行广播的形态发出去。`tests/core/task-board.test.ts` 直接钉住这条规则。适配器里的 `excerpt` 助手留给记忆结果——那不是黑板条目,保留它自己的规则。 +- **写面的类型没有落地。** 三种填法是三种**条目**,不是同一个形状画了三遍;所以写类型的触发条件是出现第四种填法。 +- **中间的缝没有落地。** 检查(c)说明了为什么。 ## 为什么它现在还只是草稿 -- 它是靠**叠比喻**得到的,而每个比喻都已经被更完整的视角部分否掉过:先是内核那套,再是窗口的框与内容,现在是四段。**是比喻在替设计推理**——这一节存在的意义就是记下这个味道。 -- 四段里有一段**只是个名字**。呈现端没有缝、没有归属、没有测试;它是对一堆散落代码的描述。 +- 本文的**流水线形态**(从绘图流水线借来的四段)是靠**叠比喻**得到的,并于 2026-09-21 被上面那个**两个面**的模型**退役**——后者来自测量而不是比喻:角色会同住文件、反馈是重放不是倒放、中间是协议带进来的。 - 这一段弧里唯一可测的判断——机制/策略那条缝很窄——**在一分钟内被 grep 否证**:补丁假设坐在六处,其中一处是合法性规则。 -- **没有任何东西变小。** 没改代码、没消除概念,反而加了三份散文记录。按今天的证据,这个模型比它描述的对象**多了**概念。 +- **有一件事变小了**:读面的三条规则变成了一条,一个家,一个测试。中间层约 120 处策略语义命中、以及写面没有形状的条目内容,都还没变。 +- 这个模型仍然比它描述的代码**多了**概念,所以它还是草稿。 ## 资格规则 @@ -66,10 +87,15 @@ 测量迫使两条修正。**第一,原始计数高估了情况**——而这正是写这条检查时要带上的那个前提:`src/core/store/writes.ts` 与 `src/core/store/retrieval.ts` 里的 `files` 是机制词(存储里的路径),`src/integration/ooo-candidate.ts` 里的 `checks` 指的是检查的**运行器**,也是机制。所以词表应**去掉 `files` 与 `checks`**,只留 `patch`、`editable`、`instruction`。**第二,剩下的仍有约 120 处**,所以「中间层是无策略的」作为**今天的描述**是假的,而且超出一个数量级。模型拿到的是**方向性支持**:泄漏真实、量大、集中在三个文件里——那正是接上一条缝要拿掉的东西。 -**(b)两端。** 生产端预测「集中且可分」,实测三到六个函数散在三个文件:`src/integration/ooo-patch.ts` 里的 `preparePatchWork`、`patchPrompt`、`patchCandidate`、`patchSubmission`,`src/integration/ooo-session-mechanism.ts` 里的 `snapshotText`,`evals/ooo-execution/data-check-runner.ts` 里的 `runTestFile`。预测成立。呈现端预测「散落、没有缝」,实测六个站点:`src/core/store/base.ts` 的预览文本、`src/core/types.ts` 的条目与预览类型、`src/cli/protocol.ts` 的线上形状、`src/cli/service.ts` 的服务、`.pi/extensions/nmg/index.ts` 里面向 agent 的渲染、以及由 `src/prompts/nmg-prompts.yaml` 生成的工具描述。这条预测也成立,但有**一处对本文的修正**——见上表「显示器」一行:黑板条目只有一种呈现,所以第四段比表里最初写的更薄,而它里面的分层词汇是从记忆那一侧**借**来的,不是黑板这一侧**找到**的。 +**(b)两端。** 生产端预测「集中且可分」,实测三到六个函数散在三个文件:`src/integration/ooo-patch.ts` 里的 `preparePatchWork`、`patchPrompt`、`patchCandidate`、`patchSubmission`,`src/integration/ooo-session-mechanism.ts` 里的 `snapshotText`,`evals/ooo-execution/data-check-runner.ts` 里的 `runTestFile`。预测成立。呈现端预测「散落、没有缝」,实测六个站点:`src/core/store/base.ts` 的预览文本、`src/core/types.ts` 的条目与预览类型、`src/cli/protocol.ts` 的线上形状、`src/cli/service.ts` 的服务、`.pi/extensions/nmg/index.ts` 里面向 agent 的渲染、以及由 `src/prompts/nmg-prompts.yaml` 生成的工具描述。这条预测也成立,但有**一处对本文的修正**:黑板条目只有一种呈现,而旧表里的分层词汇是从记忆那一侧**借**来的,不是黑板这一侧**找到**的。对同一批站点后来的一遍检查看到计数看不到的东西:那六个站点是一条**链**,不是六个格式化器,而重复就坐在其中三处——store 的预览规则、适配器的通用助手、以及适配器广播路径里的那条裸切。 + +**(c)第二个形态。** 预测五个以上文件。改动在一个**用完即弃的分支**上做完,然后退回:把循环改成形态无关——`DispatchTicket.patch?: PatchWork` 换成不透明的 `declaration?: {shape, digest, frozen}`,`PlanWorker` 收这个声明,并从单元派发里删掉 `preparePatchWork` 那次调用——一个文件,15 增 18 删。得到两个结果,而两个都不是预测瞄着的那个。 + +- `tsc --noEmit` 报 **0 个错误**。这个耦合**对编译器隐形**:字段是**可选**的,而结构类型让带着 `patch` 的产品板 ticket 依然满足这个端口,所以这次改动**没有任何编译期信号**。 +- 测试报 **25 个用例失败**,每一个的原因都相同——"the claim admits no work"。今天加第二个形态,不会是"新形态失败",而是**每一个单元一起拒绝工作**。 -**(c)第二个形态。** 还没跑。 +耦合的面,按 grep 数:三个源文件点名那份冻结的补丁工作(`ooo-dispatch.ts`、`ooo-patch.ts`、`ooo-session-mechanism.ts`,最后一个约十处),板子**虽然从不读这个字段也必须改**,七个测试文件直接读 `ticket.patch`,而 worker 一侧通过 `PlanWorker` 类型耦合——它打断驱动里的三种 worker。预测成立,而且不止于此;它逼出来的决定与"建"相反:这条缝是**预付成本**,被**带着一个点名的触发条件**推迟——adopter 声明的第一个非补丁单元——而不是现在对着一个实现把它建起来。 -## 这两条检查对变小测试说了什么 +## 这些检查对变小测试说了什么 -现在还什么都没变小,而检查指出了变小只能从哪里来:中间层三个文件里约 120 处策略语义命中,以及六个站点共同呈现一个黑板条目、彼此之间没有归属者。今天唯一可取的变小是中间层那三个文件;它是否值得,由检查(c)测。这次清点两次证明了它的价值:它产出了散文说不出的「词 + 路径」规则,并且抓出本文把另一个子系统的词汇当黑板自己的用。 +**有一件事变小了**:展示一个黑板条目的三条规则,变成一条规则、一个家、一个测试,而且消失的那条正是错的那条。其余照旧。中间层那三个文件里仍有约 120 处策略语义命中,写面的条目内容仍然没有形状。这次清点三度证明了它的价值:它产出了散文说不出的「词 + 路径」规则;它抓出本文把另一个子系统的词汇当黑板自己的用;它把"这条缝该不该存在"从品味之争变成了一个量出来的文件数。 diff --git a/src/core/board-entry-view.ts b/src/core/board-entry-view.ts new file mode 100644 index 00000000..46889b4b --- /dev/null +++ b/src/core/board-entry-view.ts @@ -0,0 +1,18 @@ +/** + * The read face's one rule for a board entry's body. + * + * An entry's body is opaque to the middle and may be long: a memory pointer, a note, a delivered + * artifact, a broadcast that quotes another entry. Whoever shows an entry to a reader shapes that body + * the same way, so the rule lives here once, and both the store's compact read and the host adapters use + * it instead of each writing its own truncation. A lone `memory=` pointer is already the intended + * low-context form and is returned whole; anything longer is collapsed to a single bounded line, so a + * read never carries a body its reader did not ask for. + * + * The bound is the caller's, because how much of a body fits is the reader's decision. What a body looks + * like when it is shown is this rule's. + */ +export function boardEntryView(content: string, max = 200): string { + if (content.trim().startsWith("memory=")) return content.trim(); + const single = content.replace(/\s+/g, " ").trim(); + return single.length <= max ? single : `${single.slice(0, max - 1)}…`; +} diff --git a/src/core/store/base.ts b/src/core/store/base.ts index cb3560ed..2dd4f016 100644 --- a/src/core/store/base.ts +++ b/src/core/store/base.ts @@ -37,6 +37,7 @@ import type { VectorEmbedder, } from "../types.ts"; import { TASK_BOARD_VERDICTS, WORLD_BOARD_ID } from "../types.ts"; +import { boardEntryView } from "../board-entry-view.ts"; import { currentlyValid, notExpired } from "./clock.ts"; import { histogramAdd } from "../perf.ts"; import { Router } from "../router.ts"; @@ -591,7 +592,7 @@ export class NmgStoreBase { ackCount: entry.ackedBy.length, createdAt: entry.createdAt, resolvedAt: entry.resolvedAt, - preview: taskBoardPreview(entry.content), + preview: boardEntryView(entry.content), })); return { previews, nextCursor }; } @@ -3034,16 +3035,6 @@ export class NmgStoreBase { } } -/** Bounded preview used by the compact read (readTaskBoardPreviews): a lone - * memory pointer is already the intended low-context form and is returned whole; - * anything longer is collapsed to a single bounded line so a sync never carries - * a full body it did not ask for. */ -function taskBoardPreview(content: string, max = 200): string { - if (content.trim().startsWith("memory=")) return content.trim(); - const single = content.replace(/\s+/g, " ").trim(); - return single.length <= max ? single : `${single.slice(0, max - 1)}…`; -} - function mapTaskBoardEntry(row: Row): TaskBoardEntry { return { id: String(row.id), diff --git a/tests/core/task-board.test.ts b/tests/core/task-board.test.ts index 0dc6860a..c4b39b2e 100644 --- a/tests/core/task-board.test.ts +++ b/tests/core/task-board.test.ts @@ -5,6 +5,7 @@ import { join } from "node:path"; import test from "node:test"; import { NmgStore } from "../../src/core/store.ts"; +import { boardEntryView } from "../../src/core/board-entry-view.ts"; function withStore(run: (store: NmgStore) => void): void { const directory = mkdtempSync(join(tmpdir(), "nmg-task-board-")); @@ -103,6 +104,17 @@ test("task board supports cursor reads, cross-agent resolution, and expiry", () }); }); +test("the read face has one rule for a board entry's body", () => { + // A pointer is already the low-context form the reader wants, so it is returned whole and never cut. + assert.equal(boardEntryView("memory=abc123"), "memory=abc123"); + // Anything else becomes one bounded line, so a read never carries a body it did not ask for. + assert.equal(boardEntryView("first line\n\nsecond line"), "first line second line"); + assert.equal(boardEntryView("x".repeat(10), 10), "x".repeat(10)); + const bounded = boardEntryView("y".repeat(11), 10); + assert.equal(bounded.length, 10); + assert.equal(bounded, `${"y".repeat(9)}…`); +}); + test("compact preview read omits long bodies but keeps ordering and cursor", () => { withStore((store) => { const first = store.putTaskBoardEntry({