From 39f98176c057f68a3c0273909220804b88327dcb Mon Sep 17 00:00:00 2001 From: huashen <2494946808@qq.com> Date: Thu, 3 Sep 2026 11:44:55 +0800 Subject: [PATCH 1/7] =?UTF-8?q?docs:=20=E6=B7=BB=E5=8A=A0=20Akashic=20v4?= =?UTF-8?q?=20=E8=AE=BE=E8=AE=A1=E6=8F=90=E6=A1=88?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/INDEX.md | 1 + docs/design/0902-reviewed-v4.md | 1472 +++++++++++++++++++++++++++++++ 2 files changed, 1473 insertions(+) create mode 100644 docs/design/0902-reviewed-v4.md diff --git a/docs/INDEX.md b/docs/INDEX.md index 51accaf4..137a3157 100644 --- a/docs/INDEX.md +++ b/docs/INDEX.md @@ -108,6 +108,7 @@ | Compaction 与 Markdown 记忆普通插件化 | `projectneed` CTX-007、MEM-001~MEM-011 → [0052](decisions/0052-compaction-and-markdown-memory-are-ordinary-plugins.md) → [插件化任务合同](design/compaction-markdown-memory-plugin-task-contract.md) → [0030](decisions/0030-session-context-compaction-ledger.md) → [持久化状态地图](design/persistence-state-map.md) | `agent/core/passive_turn.py`、`agent/plugin_composition/`、`session/`、`plugins/compaction/`、`plugins/markdown_memory/` 与任务合同 P01~P09 | | 主动流程、Wake、Drift、调度 | `projectneed` 第 6、9、12~13 节 → [持久化状态地图](design/persistence-state-map.md) → [Wake 最近主动消息上下文](design/wake-recent-delivery-context.md) → [Content / Wake 现有原子能力与第一阶段](design/content-wake-existing-atoms-first-stage.md) → [Content / Wake / Proactive 分层任务合同](design/content-wake-proactive-migration-task-contract.md) → [0040](decisions/0040-wake-duty-gate-lives-in-scoped-react.md) → [0048](decisions/0048-eventmail-keeps-three-mail-lifecycles.md) | `plugins/eventmail/`、`plugins/wake/`、`plugins/drift/`、`plugins/scheduler/`、`plugins/subagent/`、`agent/plugin_composition/timers.py`、`agent/plugin_composition/background_jobs.py` | | React Core 原子能力、Scheduler/Subagent 非特权插件 | `projectneed` 第 6、9~13 节 → [0034](decisions/0034-turn-is-the-logical-work-unit.md) → [0039](decisions/0039-react-core-atoms-keep-sources-unprivileged.md) → [React Core 与 Scheduler/Subagent 设计](design/react-core-scheduler-subagent.md) → [分阶段任务合同](design/react-core-scheduler-subagent-task-contract.md) → [持久化状态地图](design/persistence-state-map.md) | `agent/looping/core.py`、`agent/core/passive_turn.py`、`agent/lifecycle/`、`agent/scheduler.py`、`agent/background/subagent_manager.py`、`agent/plugin_composition/`、`agent/plugins/snapshot.py` | +| AgentLoop、被动回复与普通插件化 v4 提案 | `projectneed` 第 6~10、12~13 节 → [0034](decisions/0034-turn-is-the-logical-work-unit.md) → [0039](decisions/0039-react-core-atoms-keep-sources-unprivileged.md) → [Akashic v4 设计提案(待批准)](design/0902-reviewed-v4.md) → [持久化状态地图](design/persistence-state-map.md) | `agent/looping/core.py`、`agent/core/passive_turn.py`、`agent/lifecycle/`、`agent/control/runtime.py`、`agent/plugin_composition/`、`agent/plugins/snapshot.py`、`session/`;提案未批准前不得实现或迁移 | | 正式启动、Supervisor、自重启、停止信号 | `projectneed` RUN-001~RUN-004 → [Linux Supervisor 安全自重启提议](design/linux-supervisor-safe-self-restart.md) → [`docker/debug/README.md`](../docker/debug/README.md) | `main.py`、`agent/supervisor.py`、`agent/restart.py`、`agent/tools/agent_restart.py`、`scripts/stop-runtime.sh`、restart Gate 报告 | | 容器、云主机运行适配、Host Bridge、插件 Workload、hua-home迁移 | `projectneed` RUN-013~RUN-016、PLG-017、WSP-005~WSP-006 → [0032](decisions/0032-host-bridge-preserves-host-equivalent-execution.md) → [0053](decisions/0053-plugins-declare-managed-workloads.md) → [Computer 插件与 Workload 合同](design/computer-plugin-workload-task-contract.md) → [容器与 Linux 主机运行适配设计](design/akashic-container-cloud-runtime-adaptation.md) → [Core 与 Host Bridge 安装设计](design/akashic-core-bridge-installer.md) → [非迁移实验合同](design/akashic-container-host-bridge-experiment-contract.md) → [Unified Shell Execution 设计](design/unified-shell-execution.md) → [持久化状态地图](design/persistence-state-map.md) | `agent/plugin_composition/`、`agent/plugins/`、Workload Controller、exact-commit 安装、runtime identity、Supervisor 与隔离实验;正式 profile 迁移前先运行 plan-only 清单并取得独立批准 | | Provider、模型角色、运行时切换、usage、首次配置、模型普通插件化 | `projectneed` RUN-005~RUN-012、ONB-001、CTX-001 → [0050](decisions/0050-model-revision-lives-in-ordinary-plugin.md) → [0054](decisions/0054-model-sync-refreshes-public-capabilities.md) → [模型普通插件与 Provider 组合规格](design/model-plugin-ordinary-capability-spec.md) → [0027](decisions/0027-runtime-models-use-generation-leases.md) → [0028](decisions/0028-model-credentials-live-with-workspace-connections.md) → [现行实现与历史验收基线](design/runtime-model-registry-and-onboarding.md) → [持久化状态地图](design/persistence-state-map.md) | `plugins/models/`、`plugins/opencode_go/`、`agent/plugin_composition/`、`agent/model_runtime/`、`agent/provider.py`、`bootstrap/settings_api.py`、`frontend/chat/src` | diff --git a/docs/design/0902-reviewed-v4.md b/docs/design/0902-reviewed-v4.md new file mode 100644 index 00000000..b08825bf --- /dev/null +++ b/docs/design/0902-reviewed-v4.md @@ -0,0 +1,1472 @@ +# Akashic v4:从 Message 到 `react` 的插件架构 + +- 文档版本:`0902-reviewed-v4` +- 日期:2026-09-03 +- 状态:设计提案,等待批准 +- 当前代码基线:`47896b4200731183a54081e2eca77602a0881a0a` +- 需求来源:2026-09-02 Codex 设计会话(私有原始记录未提交) +- 输入:`0902-reviewed-v3.md`、`0902-02.md`、当前项目合同与真实代码 +- 本设计不授权:实现、数据库迁移、正式 workspace 写入、删除或部署 + +## 结论 + +v4 只保留一句话: + +> Core 不决定 Agent 怎样思考;Core 只保证一次被接受的交流有一个 Turn,每次实际运行只看一个 Root 和一份权限,完成时把该进入 Session 的 Message 整批原子写入。 + +Agent 怎样思考,由普通 `REACT` 插件决定。被动消息怎样进入系统,由普通 `MESSAGE_HANDLER` 插件决定。默认的 Prompt、上下文裁切、模型调用、Tool 选择和 `while` 循环全部属于 `default-react` 插件,不再属于 Core。 + +v4 的目标结构是: + +```text +原始渠道输入 + │ + ▼ +Channel Adapter ── 只管协议、去重、ACK + │ + ▼ +Incoming ── exact Root ── MESSAGE_HANDLER + │ accept() + ▼ +Turn ── Run 1 ── interrupted + │ Run 2 ── failed + │ Run 3 ── REACT ── Draft Messages + │ + ▼ complete(),一个 SessionDB 事务 +Messages + seq + Turn outcome + SessionReceipt + │ │ + │ └── 普通插件各自追 cursor + ▼ +Delivery Owner ── Channel Adapter ── 外部平台 +``` + +最终只新增两个可替换的行为 Service: + +1. `MESSAGE_HANDLER`:处理一次原始入站消息。 +2. `REACT`:读取一个 Run,产生一组待提交 Message。 + +其余名字都必须拥有独立事实,不能只是换名包装: + +- `Message`:一条有稳定身份的内容。 +- `Turn`:用户能理解的一次完整交流。 +- `Run`:Turn 内部一次实际执行,只是内部运行坐标。 +- `Session`:已经提交的 Message 顺序。 +- `Root`:一次运行看到的插件世界。 +- `TurnGrant`:这个 Turn 允许触及的能力上界。 + +不再保留公共 `Attempt`、`AgentProgram`、`RunLock`、通用 `CommitPlan`、通用 `CommitIntent`、通用 `DerivedStore` 或“Session 本身就是另一条变更日志”这些概念。 + +--- + +## 一、这份 v4 要满足的真实要求 + +这些要求来自原始 Codex session,而不是从 v3 文案反推: + +1. 先承认已经实现的 v3 插件底座,不把已有能力重新设计一遍。 +2. 当前被动回复仍是硬编码特权链;目标是让它由普通插件组合出来。 +3. AgentLoop 的本质要比 DeepSeek Harness 更简单,而不是再做一个更大的框架。 +4. Tool、LLM、Prompt、Memory、Scheduler、Proactive 和来源插件都不能靠 Core 产品分支获得特权。 +5. ToolSearch 自己必须是普通插件;“展示给模型”不能伪装成“授予权限”。 +6. Akasha 已经是普通 Memory 插件,不得重新塞回 Core。 +7. `U1 → interrupt → U2 → interrupt → U3 → A` 是一个 Turn,而不是三个用户交流。 +8. Proactive、schedule fire、wake 和 spawn completion 在真正产生交流时,也能用同一个 Turn 模型。 +9. 概念尽可能少,词尽可能普通;每个概念只能拥有一个变化轴。 +10. 先做减法。旧设计没有证明价值的层、DTO、事件和兼容桥必须退出目标态。 + +### 判定标签 + +本文固定区分三类内容: + +- **F(事实)**:当前代码、schema 或 accepted 文档已经证明。 +- **T(目标)**:v4 推荐的最终设计,尚未实现。 +- **U(未知)**:必须由维护者批准或在迁移前补证据。 + +T 不能写成当前事实,U 不能被实现者自行猜成需求。 + +--- + +## 二、当前真实起点 + +v4 不是从空白开始。 + +| 结论 | 状态 | 证据 | +|---|---|---| +| Root、Fiber、Effect、generation、stable/latest 和 snapshot lease 已存在 | F | `agent/plugins/snapshot.py:876`、`agent/plugins/snapshot.py:1567`、`agent/plugin_composition/effect.py:16` | +| 当前 Core 仍直接创建 `DefaultReasoner` 和 `PassiveTurnPipeline` | F | `agent/looping/core.py:335` | +| 当前被动链固定为 BeforeTurn → BeforeReasoning → Reasoner → AfterReasoning → AfterTurn | F | `agent/core/passive_turn.py:355` | +| ToolSearch 开关、Tool 展示顺序、Prompt phase 和 step phase 仍在 `DefaultReasoner` 内 | F | `agent/core/passive_turn.py:958` | +| completed 被动 Turn 已能把多条 user input 与最终 assistant 作为一批准备 | F | `agent/lifecycle/phases/after_reasoning.py:257` | +| `TurnCommitted` 目前仍同步 fanout;插件失败可以卡住后续 dispatch | F | `agent/lifecycle/phases/after_turn.py:243` | +| Session message 与 metadata 已在一个 SQLite 事务中追加 | F | `session/manager.py:642` | +| 当前 `message_id` 仍由 `session_key:seq` 生成 | F | `session/store.py:4744` | +| 当前 `turns` 表保存的是 execution attempt 进度,不是逻辑 Turn | F | `session/store.py:2392`、决策 0034 | +| completed interaction 已有整组删除、备份、embedding 删除和 compaction 失效流程 | F | `session/store.py:5385` | +| Akasha 已按普通插件边界拥有 Prompt、Tool 与自己的投影 | F | 决策 0041 | +| Core 尚无 `MESSAGE_HANDLER`、`REACT`、`TOOL_SELECTOR` 或 `SessionReceipt` | F | 当前代码符号盘点 | + +### DeepSeek Harness 值得借,但不能照抄 + +DeepSeek Harness 做对了三件事: + +1. 模型 adapter、Tool registry、Session log 和 Agent loop 都是插件。 +2. 插件注册是可逆 effect,卸载会撤销自己拥有的注册。 +3. profile 是插件树,默认 loop 可以被替换。 + +但它当前的具体 `agent-loop` 仍直接: + +- 开关 Turn 和 Step; +- 组装 system prompt; +- 派生模型消息; +- 调 LLM; +- 执行 Tool; +- 追加 Session event。 + +这适合作为“默认 loop 也是插件”的证据,不适合作为 Akashic 的最终拆分。Akashic v4 再向前一步:Core 只给运行和提交端口,默认 `while` 循环整体搬进 `default-react`。 + +--- + +## 三、最小领域模型 + +### 3.1 Message:身份与顺序分开 + +```text +Message = (MessageId, role, content, source, placement) +placement = session(SessionId) | run-only +``` + +规则: + +1. `MessageId` 是不透明、稳定、全局唯一的字符串。 +2. `MessageId` 不从 `seq`、`TurnId`、ordinal、时间或正文计算。 +3. 旧 `session_key:seq` 值继续作为合法的不透明旧 ID;迁移不改写它。 +4. 新 Message 推荐使用 UUIDv7,但 UUIDv7 的时间位没有业务语义。 +5. ordinal 只表示 Message 在 Turn 内的位置。 +6. `seq` 只表示 Message 在 Session 内的位置。 +7. 客户端同步继续只需要 `message_id + seq`;不得按正文或时间猜对应关系。 +8. `run-only` Message 是一份真实、稳定的 Turn 输入,但不进入正式 Session,因此永远没有 seq。 +9. 外部输入的 `source` 带经过 Channel 校验的稳定 source_ref;同一 source_ref 最多接纳成一个 MessageId。 + +`placement` 不是让插件临时挑选的提交计划。来源在 Turn 第一次接纳时就固定它: + +- 普通 user/assistant Message 放进目标 Session; +- proactive 或 schedule 的内部 Task Message 可以是 `run-only`; +- proactive 最终 Assistant Message 放进目标 Session; +- PromptPart、stream delta、thinking、interrupt 和未封口 Tool call 不是 Message。 + +不可信 envelope、模型输出和 Handler 都不能自报 placement。Channel/Source adapter 只能在自己的受信任 admission API 中提出来源类型,ConversationStore 按该入口的固定规则写入 placement;REACT 返回的 DraftMessage 不带 SessionId,RunHost 只允许它进入 Turn 已固定的目标 Session。 + +因此下面三件事互不替代: + +```text +MessageId 谁 +position(turn, message) 在这个 Turn 的第几个 +seq(session, message) 在这个 Session 的第几个 +``` + +`(TurnId, ordinal)` 可以定位关系,但不能取代 Message 身份。附件绑定、引用、重试、跨客户端同步和显式删除都需要稳定 MessageId。 + +### 3.2 Turn:一次完整交流 + +目标定义: + +```text +Turn T = ( + turn_id, + session_id, + cause, + grant, + messages = , + outcome +) + +n >= 1 +``` + +字段只各管一件事: + +- `turn_id`:这次交流的稳定身份。 +- `session_id`:它最终归入哪个 Session。 +- `cause`:谁发起,如 user、schedule、wake、spawn 或 outbound call。 +- `grant`:整次交流的能力上界。 +- `messages`:属于这次交流的有序 MessageId。 +- `outcome`:这次交流是否已经结束,以及怎样结束。 + +更精确地写,Turn 不是一份复制正文的新大对象: + +```text +H_t = (turn_id, session_id, cause, grant) # 一次写下的 header +M_t = # 有序 Message 引用 +O_t = open | one terminal outcome # 唯一结果 + +T_t = project(H_t, M_t, O_t) +``` + +存储只需要拥有 header、Message membership 和 outcome;`TurnView` 从它们投影,不复制 Message body、Session rows、Run facts 或 Delivery facts。这样 Turn 可以被查询和恢复,却不会成为第二份对话真相。 + +对已经进入 Session 的 Message,再定义: + +```text +m_i ~ m_j 当且仅当 turn(m_i) = turn(m_j) +``` + +同一个等价类就是 Session 中可见的一个 Turn。组内按 seq 排,Turn 之间按每组最小 seq 排。由于一个 completed `session_batch(T)` 原子追加,同一 Turn 的可见 Message 必须形成连续块,中间不能夹进另一 Turn。 + +Turn 不拥有 Root,不拥有模型,不拥有当前运行 task,也不拥有 Delivery 状态。这些事实都可以在 Turn 不变时独立变化。 + +`TurnGrant` 保存的是稳定 capability key、scope 和限制,不保存 secret、连接对象或具体插件实例。真正的 credential handle、模型 adapter 和 Tool 实现由每个 Run 的 exact Root 冻结。授权流程可以在 grant 已允许的上界内批准一次具体调用;它不能在同一 Turn 中引入 grant 之外的新能力。 + +目标 outcome 只保留用户能理解的终态: + +```text +open +completed(reply_message_id | no_reply) +superseded(by_turn_id) +abandoned(reason) +``` + +`interrupted` 和一次 provider error 不直接成为 Turn outcome,因为它们只结束一次 Run。显式重试仍可继续同一个 Turn。用户明确放弃,或失败后普通新输入选择 fresh,才关闭旧 Turn。 + +### 3.3 Run:内部运行坐标,不是第二个领域对象 + +```text +RunRef = (TurnId, run_seq) +run_seq = 1, 2, 3, ... +``` + +Run 只回答:“这个 Turn 第几次真的开始执行?” + +它需要持久化,是因为中断、崩溃恢复、Tool 幂等和精确诊断都需要区分多次运行;但它不进入用户公共领域词,也不叫公共 `AttemptId`。 + +运行时可以给客户端一个短期 `interrupt_token`。它只授权中断当前 Run: + +- Run 终结后 token 立即失效; +- token 不能作为 Turn、Message 或重试身份; +- UI 显示 TurnId,不显示内部 `run_seq`; +- Core 用 token 精确找到当前 Run,不需要创造另一个公共对象。 + +每个 Run 有自己的终态: + +```text +succeeded(draft_ids) +interrupted(cause) +failed(error_code) +``` + +`succeeded` 也不等于 Turn 已完成。只有 ConversationStore 的原子提交成功,Turn 才能变成 `completed`。 + +一个 `open` Turn 不一定正在运行。它可以在某个 Run interrupted/failed 后安静等待下一次继续。UI 的 `running / interrupted / failed` 来自 `latest_run_state` 投影,不是假装成 Turn outcome;只有显式继续、替代或放弃才改变 Turn 本身。 + +### 3.4 Session:已提交 Message 的唯一顺序 + +```text +Session S = <(seq_1, message_id_1), ..., (seq_n, message_id_n)> + +seq_1 < ... < seq_n +``` + +定义 Turn 对某个 Session 的投影: + +```text +session_batch(T) = + +``` + +Session 不是 `Commit/Delete` event 的 fold,也不是旁边一条日志的投影。权威内容仍是 `sessions.db/messages`。`session_batch(T)` 只是从 Turn 选择应进入该 Session 的 Message,不产生第二份正文。 + +规则: + +1. 正常完成只 INSERT Message。 +2. 同一 Session 的 `seq` 在提交事务内单调增加且不复用。 +3. completed Turn 的整个 `session_batch(T)` 一次提交;不能出现半个可见批次。 +4. Prompt 裁切、compaction、索引或插件重载无权改写正文。 +5. 只有用户显式撤销或删除可以减少正文,并必须走单独的数据管理协议。 + +普通被动 Turn 的 batch 是 `U1...Un+A`;proactive/schedule 可以是 `A`,内部 Task Message 留在 Run/Turn 事实中。这样既保留“Message 组成 Turn”,也不把机器任务伪装成用户对话。 + +### 3.5 SessionReceipt:提交后的引用通知 + +普通插件需要知道 Session 新增或移除了什么,但不能因此重定义 Session。v4 增加一个很窄的引用 feed: + +```text +SessionReceipt = ( + feed_seq, + receipt_id, + kind, # messages_appended | interaction_removed + session_id, + session_version, + turn_id, + message_ids, + final_run_ref?, + audit_ref? +) +``` + +它有五条硬规则: + +1. 与非空 `session_batch(T)` 的提交或显式删除在同一个 SessionDB 事务中写入。 +2. 只保存引用和版本,不复制正文、Prompt、Tool trace 或插件 payload。 +3. 插件按 MessageId 从窄只读接口取自己需要的事实。 +4. 每个插件在自己的 plugin-data 中拥有 cursor 和幂等 receipt。 +5. Session 读取永远不通过 Receipt 反推正文;Receipt 丢失是损坏,不是“Session 为空”。 +6. v4 首版不自动裁掉 Receipt;未来 retention 必须先有可证明的 snapshot/watermark 协议,不能猜所有插件都追上了。 + +它不是通用 EventBus,不负责插件执行,也不保证“所有插件都已经处理”。它只消除“Session 已提交但进程在通知插件前崩溃”的丢通知窗口。 + +### 3.6 六条代数不变量 + +```text +I1 MessageId 稳定;seq 与 ordinal 都不承担身份。 + +I2 一个 Message 最多属于一个 Turn;一个 completed Turn 的 + session_batch 在 Session 中保持同一顺序。 + +I3 同一 Turn 同时最多一个 active Run;同一 Session 同时最多一个 + active conversation Turn。 + +I4 一个 Run 只看一个 exact Root;同一 Run 的模型、Prompt contributor、 + Tool catalog 和执行端口不跨 generation 漂移。 + +I5 effective authority = TurnGrant ∩ Root capabilities ∩ call permit; + 每一层只能缩小,不能放大。 + +I6 Turn 只完成一次;非空 Session 批次、Turn outcome 和 SessionReceipt + 要么一起提交,要么都不提交。session_batch 为空时只提交 Run/Turn 终态, + 不伪造 SessionReceipt。 +``` + +### 3.7 不存在 ε Turn + +没有 Message,就没有 Turn。 + +- 一个被过滤的垃圾输入在 `Incoming.accept()` 前被忽略:没有 Turn。 +- `/stop` 只是精确中断当前 Run:没有新 Message,也没有新 Turn。 +- wake tick 只检查状态后决定不交流:它是 wake 插件自己的 tick,不是 Turn。 +- wake 或 schedule 真正调用 `react` 时,先创建一条明确的 `run-only` Task Message,再创建 Turn。 +- 一次维护 job 没有交流内容时,就是 job,不借 Turn 记账。 + +这让 Turn 永远保持“交流的最小单元”,不再兼任所有后台工作的统一盒子。 + +### 3.8 典型场景 + +#### 被动中断续接 + +```text +Turn T +├── Message U1 +├── Run (T,1) ── interrupted +├── Message U2 +├── Run (T,2) ── interrupted +├── Message U3 +├── Run (T,3) ── succeeded +└── Message A ── complete ── Session batch [U1,U2,U3,A] +``` + +#### 显式 retry 与普通 fresh + +```text +Run failed +├── retry(original MessageId) ── same Turn, new Run +└── ordinary new input ── old Turn superseded, new Turn +``` + +#### Schedule + +```text +Schedule tick ── private due/misfire rules + ├── skip:只写 schedule 自己的状态 + └── run:run-only Task Message → Turn → REACT + └── Assistant Message → target Session +``` + +#### Proactive + +```text +observation → private gate + ├── skip:不是 Turn + └── speak:run-only Task Message → Turn → REACT + └── Assistant Message → Delivery +``` + +来源只负责“何时创建 Message、把结果送到哪里”。一旦进入 `react`,不再有 passive、schedule 或 proactive 专用 Loop。 + +--- + +## 四、每个事实只有一个 owner + +| Owner | 唯一拥有 | 明确不拥有 | +|---|---|---| +| Channel Adapter | 外部协议解析、envelope custody/重投/ACK、一次真实 provider 调用的协议映射 | Turn、Session、durable delivery 状态、Prompt、Tool 权限 | +| ConversationStore | 已接纳 source_ref 的唯一性、Message、Turn、内部 Run 记录、Session 顺序、SessionReceipt 的原子事务 | LLM、Tool 执行、渠道调用、插件 cursor | +| RunHost | session lane、active task、interrupt token、exact Root lease、短期资源清理 | Session 正文、长期插件状态、外部效果最终状态 | +| Plugin Runtime | artifact、Root、Fiber、Effect、generation、stable/latest、发布与 drain | Turn outcome、Delivery outcome、插件业务数据 | +| `default-react` | 默认 Prompt/Context/Tool/LLM `while` 算法和自己的 step 状态 | Session commit、Delivery、权限授予、Root 发布 | +| Tool plugin | Tool schema、参数边界、执行、自己的外部效果 ledger 和幂等 | Turn 总控制、其他 Tool、Session 任意写入 | +| Delivery Owner | durable envelope、dedupe、provider receipt、uncertain/rejected/delivered/settled 状态 | 渠道协议实现、Session 正文所有权 | +| Memory/Projection plugin | 自己的索引、cursor、receipt 和 rebuild | 原始 Message 保留、Turn commit、其他插件 readiness | + +### Channel 与 Delivery 的精确分工 + +二者不能都声称“拥有投递”。 + +```text +Delivery Owner Channel Adapter +────────────────────────────── ───────────────────────── +创建 durable envelope 把 envelope 映射成平台 API +决定 dedupe/idempotency 执行一次真实调用 +记录 provider_started 返回 provider 原始 receipt +记录 uncertain/rejected/success 不保存长期状态机 +恢复未完成操作 不自行猜是否该重试 +``` + +外部调用的 durable 真相只在 Delivery Owner。Channel Adapter 是协议 driver。 + +### live 资源与 durable 外部效果不能共用一个领域词 + +Root/Fiber/Effect 只管理本进程短期资源:listener、timer、连接、临时 task、注册项。它们随 Root 卸载逆序清理。 + +已经可能发生的外部操作由真实领域 owner 管理: + +- 消息发送属于 Delivery Owner; +- Tool 写操作属于该 Tool 插件; +- schedule 状态属于 Scheduler 插件; +- Memory 投影属于 Memory 插件。 + +旧 Root 不需要为了一个长期 pending delivery 永远活着。插件必须先把 durable operation 交给稳定 owner,才能释放 Run lease。 + +--- + +## 五、最小公共 API + +以下代码只说明可观察合同,不是最终 Python 语法。 + +### 5.1 一个来源无关的 Turn 入口 + +Channel、Scheduler、Proactive 和 Spawn 都需要同一扇窄门,但它不是另一套 Loop: + +```python +class TurnPort(Protocol): + async def start(self, request: TurnStart) -> Run: ... + +class TurnStart: + session_id: SessionId + message: NewMessage + placement: "session" | "run-only" + cause_ref: CauseRef +``` + +调用者不提供 TurnId、RunRef、TurnGrant、Root、模型或 Tool。TurnPort 根据已认证来源、调用插件 scope、目标 Session 和部署 policy 生成这些事实,并在 durable admission 成功后才返回 Run。 + +它是 Core 的原子能力,不是行为策略: + +- `Incoming.accept()` 是一个已经填好并限制为最多调用一次的 TurnPort 请求; +- Scheduler/Wake 用 scoped TurnPort 接纳 `run-only` Task Message; +- Subagent 用自己的 Session 和父子 cause_ref; +- 来源不能借它取得任意 Session repository 或扩大 grant。 + +### 5.2 两个行为 Service + +```python +MESSAGE_HANDLER: ServiceKey[MessageHandler] +REACT: ServiceKey[React] + +class MessageHandler(Protocol): + async def handle(self, incoming: Incoming) -> None: ... + +class React(Protocol): + async def react(self, run: ReactRun) -> DraftBatch: ... +``` + +没有 `AGENT_PROGRAM`。`react` 已经是项目接受的普通动词:输入 Message,产生输出 Message。再增加一个 Program 只会建立第二套执行模型。 + +### 5.3 `Incoming` 是一次性 capability + +```python +class Incoming: + view: IncomingView + + async def accept(self) -> Run: ... +``` + +规则: + +1. `view` 是经过 Channel 边界校验的只读数据。 +2. `accept()` 最多成功一次。 +3. session、source、cause、lane key 和最大 TurnGrant 在创建 Incoming 时已经由 host 固定;Handler 不能扩权或换 Session。 +4. Handler 返回时从未调用 `accept()`,表示忽略。 +5. Handler 调用 `accept()` 后,必须让 Run 到达内部 terminal;否则 Host 记录合同违反并让 Run failed,不静默完成。 +6. 不存在 `decide() → plan payload → handle(plan)` 两段协议。 + +`accept()` 在同一个 admission 事务里固定 source_ref → MessageId → TurnId → RunRef。Channel 因 ACK 丢失重投同一 envelope 时,ConversationStore 返回同一接纳结果,不增加第二个 Message;Channel Adapter 仍独自拥有外部 custody 和何时 ACK 的协议状态。 + +Dispatcher 在调用 Handler 前先租用 exact Root。`accept()` 把同一份 lease 转成 Run lease,因此 Handler 与后续 `REACT` 不会跨 generation。 + +`/stop` 不进入 Handler。Channel Adapter 在协议边界识别它,使用当前 `interrupt_token` 调 RunHost;它不创建 Message,也不触发 Prompt 或 Memory hook。 + +默认 passive handler 本身也是普通插件,它可以直接写成: + +```python +async def handle(incoming: Incoming) -> None: + if await inbound_filters.ignore(incoming.view): + return + + run = await incoming.accept() + command_reply = await commands.try_handle(run, run.current_input) + if command_reply.handled: + await run.complete(command_reply.drafts) + return + + drafts = await run.react() + await run.complete(drafts) +``` + +`inbound_filters` 和 `commands` 可以是这个插件依赖的普通 registry;Core 不知道有哪些 filter 或 command。顺序由默认 Handler 明确拥有,不能再散成无 owner 的 waterfall。替换整个 Handler 就能替换这套入口策略。 + +### 5.4 `Run` 只暴露必要端口 + +```python +class Run: + ref: RunRef + turn: TurnView + current_input: MessageView + runtime_history_view: RuntimeHistoryView + signal: CancelSignal + stream: StreamPort + resources: RunResources + + def bind_model(self) -> ModelExecution: ... + def tool_port(self) -> ToolPort: ... + async def react(self) -> DraftBatch: ... + async def complete(self, drafts: DraftBatch) -> CompletedTurn: ... +``` + +这些字段都有独立 owner: + +- `turn` 和 `runtime_history_view` 来自 ConversationStore 的冻结只读视图; +- `bind_model()` 第一次调用时从 exact Root 内普通 models 插件冻结一份 `ModelExecution`,重复调用返回同一份; +- `tool_port()` 只暴露 `TurnGrant ∩ exact Root`,不返回全局 registry; +- `stream` 只发 live delta,不建立持久真相; +- `resources` 只登记必须在本 Run 结束前关闭的短期资源; +- `signal` 只表示当前 Run 是否还活着。 + +Run 明确没有: + +- `history` 这种无修饰名字; +- 任意 `metadata` 或 `extra` 袋子; +- 任意 SQL 或全功能 Session repository; +- `loop_input`; +- 通用 `commit_intents`; +- 任意 Root lookup; +- 改 TurnGrant 的方法。 + +`run.react()` 只调用本 Run exact Root 中的 `REACT`。`run.complete()` 只把经过结构校验的 DraftBatch 交给 ConversationStore;REACT 插件不能自己写 Session。 + +模型和 Tool 端口按使用绑定,缺失时在第一次调用处 fail-loud,而不是在 Run admission 时伪造空实现。于是 echo Handler 可以完全不安装 LLM/Tool;一个要用模型的 REACT 则必须声明并验证自己的 models 依赖。 + +### 5.5 `DraftBatch` + +```python +DraftBatch = tuple[DraftMessage, ...] +``` + +对 `REACT` 的返回: + +1. 至少有一条输出 Message。 +2. 恰好一条 terminal assistant。 +3. 每个 DraftMessage 在交给外部 effect 前由 RunHost 获得稳定 MessageId。 +4. Draft 不是 Session 事实;只有 `complete()` 事务成功后才进入 Session。 +5. stream delta、thinking 和未封口 Tool 调用都不是 DraftMessage。 +6. DraftMessage 不能指定 SessionId、placement、TurnId 或 TurnGrant。 + +命令可以由 Handler 直接构造 DraftBatch 后 `complete()`,不必调用 `react`。如果一个已经有输入 Message 的命令明确选择不回复,Handler 可以调用 `complete(())`,Turn 结束为 `completed(no_reply)`;空输入加空输出仍被拒绝。这不需要 Core 的 `Skip`、`Enter` 或 `Return` 控制对象。 + +### 5.6 Run 内部状态机 + +```text +created + │ + ├── start ── active ── react succeeded ── completing ── succeeded + │ ├── interrupt ───────────────────── interrupted + │ └── error ───────────────────────── failed + └── admission error ──────────────────────────────────── failed +``` + +ConversationStore 只有在 `completing` 时接受一次 complete。重复 complete 必须按同一 draft identity 幂等;内容漂移 fail-loud。 + +### 5.7 Session lane 是 Host 细节 + +同一 Session 的 conversation Turn 串行,不同 Session 可并发。用于排队的 key 是稳定服务 namespace 加 SessionId,例如: + +```text +("conversation", SessionId) +``` + +它不能包含 generation、provider、模型或 Handler 实例身份。否则热换后旧、新 Root 会各拿一把锁,导致同一 Session 意外并发。 + +`RunLock` 不进入公共 API。插件只看见 `accept()` 成功、busy 或取消,不持有锁对象。 + +--- + +## 六、`default-react` 普通插件 + +### 6.1 它完整拥有默认算法 + +```text +runtime history view + │ + ▼ +CONTEXT_VIEW ── prompt history + │ +PROMPT_PARTS ── immutable prompt parts + │ +TOOL_SELECTOR ─ visible tool names + │ + ▼ +build one frozen model request + │ + ▼ +Model stream + │ + ├── no tool call ── terminal DraftBatch + │ + └── tool calls ── ToolPort ── append run-local closed facts + │ + └── next model call +``` + +伪代码: + +```python +async def react(run: ReactRun) -> DraftBatch: + local = DefaultReactState() + model = run.bind_model() + tools = run.tool_port() + + while True: + prompt_history = context_view.project( + runtime_history_view=run.runtime_history_view, + closed_run_facts=local.closed_facts, + ) + prompt_parts = await prompt_parts_registry.collect(run, local) + tool_view = await tool_selector.select(run, local) + request = freeze_request( + model=model, + prompt_parts=prompt_parts, + prompt_history=prompt_history, + tools=tools.schemas(tool_view.names), + ) + reply = await model.stream(request, run.stream, run.signal) + + if not reply.tool_calls: + return seal_terminal_reply(reply) + + results = await tools.execute_visible( + calls=reply.tool_calls, + visible=tool_view, + signal=run.signal, + ) + local.append_closed(reply, results) +``` + +Step 是这个算法内部的循环计数,不进入 Core 领域模型。另一个 REACT 插件可以一次模型调用就结束、完全不用 Tool,或使用 plan/execute;Core 都不需要新增分支。 + +### 6.2 Prompt 是不可变贡献 + +`PROMPT_PARTS` 是 `default-react` 依赖的普通 registry,不是 Core 的第三条控制轴。 + +每个贡献是: + +```text +PromptPart = (key, kind, order, content, source_ref, trust) +kind = instruction | context +``` + +规则: + +1. 插件只能返回自己的新 PromptPart,不能取得共享可变 Prompt。 +2. 一次模型调用收集完后整体冻结。 +3. 排序键只保证重放稳定,不证明两个 instruction 在语义上可交换。 +4. 重复 key、互斥 slot、越过字节预算和非法 trust 提升在 candidate Gate fail-loud。 +5. 当前 user Message 永远独立,不被 context 插件改写。 +6. Memory、skill 和检索内容必须带来源与 trust,不能伪装成用户原话。 + +稳定排序可以使用: + +```text +(kind_order, order, plugin_id, key) +``` + +但 candidate profile 必须额外证明语义兼容。不能用“排序是确定的”替代“组合是合理的”。 + +### 6.3 `CONTEXT_VIEW` 只产生 prompt history + +输入与输出必须写全名: + +```text +persistent history Session 中完整已提交正文 +runtime history view 当前 Run 可读取的冻结工作视图 +prompt history 本次模型调用真正使用的投影 +``` + +`CONTEXT_VIEW` 只能: + +- 从 runtime history view 选择完整逻辑单元; +- 做通用 token 预算与 compaction; +- 保留当前 user anchor、成对 Tool 事实与外部效果证据; +- 返回带来源的 prompt history。 + +它不能: + +- UPDATE/DELETE Session Message; +- 改当前输入; +- 改 Prompt instruction; +- 改 ToolGrant 或 ToolView; +- 改模型绑定; +- 用空列表掩盖损坏数据。 + +Akasha 继续是普通 PromptPart/Tool/SessionReceipt consumer,不成为 CONTEXT_VIEW 内部分支。 + +### 6.4 ToolGrant 与 ToolView 完全分开 + +```text +TurnGrant ToolView +───────────────────────── ───────────────────────── +安全能力上界 本次模型调用展示哪些 schema +Turn 建立时冻结 每次模型调用可变化 +只能被交集缩小 可以搜索、排序、分页 +由 admission/policy 拥有 由 TOOL_SELECTOR 拥有 +执行时必须检查 只影响模型可发现性 +``` + +真正执行一项 Tool 必须同时满足: + +```text +tool ∈ TurnGrant +tool ∈ exact Root catalog +call permit accepts arguments +``` + +Tool 是否显示不能授予权限。Tool 没显示但模型凭名字猜到时,`default-react` 返回明确 `tool_not_visible`;这只是模型协议错误,不是安全拒绝。Tool 不在 grant 时返回 `tool_not_allowed`,两者不能混成一句字符串。 + +ToolSelector 收到的候选集已经是 `TurnGrant ∩ exact Root catalog`。它不能枚举、搜索或泄露 grant 之外的 Tool metadata;`tool_search` 本身也必须同时在 grant 和初始 ToolView 中,profile 缺少这条闭环时在加载期失败。 + +### 6.5 ToolSearch 是完整普通插件 + +`tool-search` 插件拥有: + +1. `TOOL_SELECTOR` provider; +2. `tool_search` 这个普通 Tool; +3. 搜索索引与排序; +4. 当前 Run 的 selection state; +5. provider schema 上限下的展示策略。 + +它不拥有: + +- ToolGrant; +- Tool registry; +- Tool 执行; +- Agent 重启授权; +- Core metadata flag; +- 跨 Run 的隐式已解锁集合。 + +`tool_search` 返回选择结果并改变该插件自己的 Run-local selection state。新 Tool 只在下一次模型调用进入 ToolView。卸载它后,可以换成一个 `show-all-selector` 或 `fixed-selector`,不改 Core 和 REACT 接口。 + +目标态删除当前这些耦合: + +- `tool_search_enabled`; +- `always_on/preloadable/requires_turn_search` 驱动的 Core 分支; +- Turn ContextVar 中的 search grant; +- ToolSearch 与 `agent_restart` 授权绑定; +- Reasoner 内部 provider schema 挤压特判。 + +必要的“默认总要展示哪些 Tool”由 selector 配置表达,不再成为 Tool 的安全属性。 + +### 6.6 三种 retry 不能混在一起 + +| retry | 身份 | owner | +|---|---|---| +| 同一 HTTP/model call 的网络重试 | 同一 Run、同一模型调用 | Provider plugin | +| context overflow 后重新投影并再次请求 | 同一 Run、新 model call | `default-react` + `CONTEXT_VIEW` | +| failed Run 后用户显式 retry | 同一 Turn、新 Run | ConversationStore/入口协议 | + +任何 retry 都不能恢复隐藏思维,也不能重放结果不明的外部 Tool effect。 + +### 6.7 不强制永久保存完整模型请求 + +每次模型调用必须留下足以问责的 binding receipt: + +```text +RunRef + call_seq +Root logical digest + root instance token +ModelExecution identity +PromptPart refs/digests +prompt history source refs +ToolView names/schema digests +compaction generation +provider outcome/usage/request id +``` + +完整 Prompt 和完整 provider payload 只在显式诊断开关下保存,并且必须有容量、保留期、权限和脱敏规则。它不是 v4 的永久事实要求。 + +Root 使用两个身份: + +- logical digest:证明插件拓扑和 catalog 内容相同; +- instance token:区分两个物理 Root 实例,避免把重建后的对象当成原对象。 + +--- + +## 七、提交、恢复与外部效果 + +### 7.1 被动 Turn 的正常完成 + +```text +1. Incoming.accept + └── durable Turn/input facts + RunRef;尚未进入 Session + +2. REACT + └── run-local model/tool facts;Session 正文不变 + +3. Delivery Owner 可先 prepare 一个不执行外部调用的 envelope + +4. Run.complete,单个 SessionDB 事务 + ├── 验证 Turn open 且 Run 是当前运行 + ├── 给 session_batch(T) 分配连续 seq + ├── INSERT session_batch(T) messages + ├── 写 Turn completed(reply_message_id | no_reply) + ├── 写 Run succeeded + ├── 更新 session metadata/head + └── session_batch 非空时 INSERT SessionReceipt + +5. 事务提交后发布 Receipt wakeup + +6. Delivery Owner 根据 prepared envelope / Receipt 发送 +``` + +第 5 步只是唤醒优化。进程在第 4、5 步之间崩溃,插件和 Delivery Owner 仍能按 `feed_seq` 追上。 + +如果第 4 步失败: + +- Session 不出现任何本 Turn Message; +- Turn 不变成 completed; +- Draft MessageId 仍只属于 pending/run 事实; +- 外部发送不能开始。 + +pending 正文与 Session 正文不能长期成为两个 owner。目标存储在同一事务中先校验并 INSERT `session_batch(T)`,再把对应 pending slot 收敛为 `MessageId + digest` 引用;事务失败时 pending 原文保持不变。`run-only` Task Message 不做这次 handoff,它的正文与 retention 继续由创建它的来源/Turn 事实 owner 管理。具体表结构与旧数据处理必须在持久化 ADR 中批准。 + +### 7.2 中断或失败 + +中断只原子结束当前 Run: + +```text +active Run → interrupted +Turn → open +Session → unchanged +``` + +失败同理。下一条输入如何处理由已接受规则决定: + +- interrupted 后普通输入:同 Turn、新 Message、新 Run; +- retryable failed 后显式 retry:同 Turn、复用原 Message、新 Run; +- failed 后普通 fresh:旧 Turn `superseded`,新 Turn。 + +Run terminal 必须先落 durable control fact,再释放 Root lease。不能先把内存指针清空后假装已经终结。 + +每条 active Run 还记录创建它的 runtime boot identity。进程重启时,ConversationStore 在取得唯一恢复 owner 后,把不存在 live owner 的 active Run 原子终结为 `interrupted(process_lost)`,保持 Turn open,再释放 session lane。它绝不自动重跑模型、Tool 或 Delivery;这些领域分别按自己的 receipt 对账。 + +### 7.3 Tool 外部效果 + +不建立通用 `CommitIntent`。 + +一个可能写外部世界的 Tool 自己保存: + +```text +(turn_id, run_seq, call_id, operation_key, state, domain_receipt) +``` + +规则: + +1. Tool owner 在调用前固定 operation key。 +2. started/succeeded/rejected/uncertain 由 Tool 自己的 durable store 表达。 +3. Run 重试先查这个 owner 的 receipt,不盲目重放。 +4. Turn 完成后,该 Tool 如需消费 SessionReceipt,使用自己的 cursor。 +5. Core 不理解支付、发信、文件写入、重启或日历事件 payload。 + +这既避免外部效果丢失,也避免 Core 变成第二个工作流引擎。 + +### 7.4 SessionReceipt consumer + +```text +SessionReceipt feed + ├── Akasha cursor/data + ├── Markdown memory cursor/data + ├── FTS/embedding cursor/data + └── UI/notification cursor/data +``` + +每个 consumer 独立: + +- 自己决定是否适用; +- 自己保存 cursor; +- 自己保证同 receipt 幂等; +- 自己报告 lag/failure; +- 一个 consumer 失败不回滚 Session,也不阻止其他 consumer。 + +新装或升级后的 consumer 不从“第一条 Receipt”盲扫全历史。它使用 ConversationStore 的窄只读操作在一个一致性读点取得: + +```text +canonical Message snapshot + current feed_seq watermark +``` + +插件先从 snapshot 建自己的投影并原子保存 cursor=watermark,再消费更大的 feed_seq。candidate 只能在隔离数据上演练这套过程;正式 promotion 后的新 Fiber 才继续 production cursor。这样插件是否安装、何时更新,与 Session 写入保持正交。 + +因此删除同步 `TurnCommitted` 大 fanout 后,Memory 故障不会卡住用户回复的外部 dispatch。 + +### 7.5 两种 Delivery 顺序 + +#### 被动回复与 Akashic/Web/Mobile 主动消息:Session first + +```text +prepare envelope → complete Session → send/notify → settle delivery +``` + +发送失败不回滚 Session。Web/Mobile 以 Session head 和连续最大 seq 恢复,通知只负责更快看到更新。 + +#### 外部渠道 proactive:provider first + +```text +prepare envelope → provider call → durable provider receipt + → idempotent Session append → settle delivery +``` + +只有完整 provider success 才追加 Session。部分成功或结果不明停在 Delivery Owner 的结构化状态,不能靠返回文案猜成功,也不能自动重复发送。 + +这两种顺序属于 Delivery 领域的明确分支,不升级成通用 `CommitPlan` 平台。 + +### 7.6 显式删除 + +删除不是普通 Session 生命周期事件。它必须继续满足: + +1. 用户主动发起名称明确的撤销/删除操作; +2. 精确到 completed Turn/interaction; +3. 执行前 SQLite online backup 与 `integrity_check`; +4. 在 session lane 和 compaction fence 下串行; +5. messages、embedding、compaction invalidation、audit 与 `interaction_removed` Receipt 原子提交; +6. Message seq 不复用; +7. 派生插件失败时不能继续提供撤销前的陈旧结果。 + +目标态 completed Turn/Run 只保留被删 Message 的 identity、digest 和非正文终态,不保留第二份正文。迁移前必须额外盘点当前 `turns.items/input`、tool trace、delivery envelope 和诊断文件中是否复制了内容;是否随“撤销对话”一起减少要逐类写进数据管理合同,不能由 v4 文档猜测。 + +`SessionReceipt` 只能通知启用中的 consumer。永久删除某插件的 opaque plugin-data 需要单独的 Data Governance inventory:列出 owner、数据位置、备份、清除动作与证明。v4 不声称一个通用 receipt 能自动发现和清掉所有未来插件数据。 + +--- + +## 八、Root、热更新与自我更新 + +### 8.1 一个 Run 一个 exact Root + +Root 在 Handler 开始前选择,并保持到 Run terminal 与短期资源 cleanup 完成。 + +同一个 Turn 的下一 Run 可以使用更新后的 committed Root: + +```text +Turn T +├── Run 1 → Root G10 → interrupted +└── Run 2 → Root G11 → completed +``` + +TurnGrant 不随 Root 更新而扩大。Run 2 的有效能力仍是: + +```text +TurnGrant(T) ∩ capabilities(Root G11) +``` + +如果新 Root 缺少 Turn 继续所需的 Service,admission fail-loud;不能偷偷回退到全局单例或旧插件。 + +### 8.2 candidate 只验证变化面 + +沿用当前增量 candidate 原则: + +1. 以 stable snapshot 为基线。 +2. 从变化插件和它真实依赖的 provider closure 建 candidate Root。 +3. 未变化插件不重启,不复制正式数据库,不运行正式 listener。 +4. candidate 使用只读或隔离的 plugin-data、临时端口和 staged event bus。 +5. 重复 Service、缺依赖、catalog 冲突、manifest 不一致在 latest-ready 前失败。 + +不同变化面跑不同 probe: + +| 变化面 | 最小真实 probe | +|---|---| +| `MESSAGE_HANDLER` | 原始 envelope → accept/ignore/command;无模型假成功 | +| `REACT` | 固定 Message → 真实或可重放 model/tool trace → DraftBatch | +| PromptPart | 最终 part refs、顺序、digest、预算和冲突 | +| Tool/Selector | catalog、grant/view 分离、真实调用、参数拒绝 | +| Delivery | prepared/provider_started/receipt/uncertain 恢复 | +| Memory consumer | SessionReceipt replay、cursor、幂等、删除屏障 | + +“能 import”“manifest healthy”或“plugin-doctor 通过”都不能替代行为 probe。 + +### 8.3 自我安装与 promotion + +推荐流程: + +```text +owner Turn + │ + ├── staged install → immutable artifact → latest-ready + ├── programmatic child 用 exact latest 做真实 probe + ├── owner Turn 回复并完成 Session/Delivery + └── 释放自己的 stable Root lease + │ + ▼ +runtime-owned promotion operation + ├── fence 新 admission + ├── drain 旧 Run leases + ├── 停旧 formal Root + ├── 启新 formal Root + ├── commit stable pointer/snapshot + └── 失败则按 journal 重建旧 stable Root +``` + +不能在安装自己的 Turn 内同步等待旧 Root drain;该 Turn 自己正持有旧 lease,会形成环形等待。 + +### 8.4 drain 与 cleanup + +1. 指针切换后旧 Root 不接新 Run。 +2. 旧 Run 继续使用旧 Root,直到内部 terminal。 +3. Run 内短期资源全部逆序 cleanup 后才释放 lease。 +4. cleanup 失败时 Root 保持 `cleanup_failed/degraded` drain blocker,并保留全部错误。 +5. 不能像当前 `Effect._close()` 一样在 cleanup 报错后仍把 owner 移除并标为 closed。 +6. durable external operation 已交给领域 owner 后,不再阻塞旧 Root;没有 handoff 的 operation 必须阻塞。 + +### 8.5 Python 插件仍是可信代码 + +Root 隔离解决生命周期、状态污染和发布一致性,不是恶意代码沙箱。被安装的 Python 插件仍可执行宿主权限下的代码。 + +因此自我安装还必须保持: + +- 明确来源与 source revision/tree digest; +- immutable artifact; +- 用户或既有授权链; +- candidate 不接正式 secret/write port; +- 安装失败可恢复; +- 正式 promotion 有 journal 和旧代重建证据。 + +--- + +## 九、从 v3 和 `0902-02` 再做一次减法 + +| 旧概念或提议 | v4 处理 | +|---|---| +| 公共 `Attempt` / `AttemptId` | 删除;保留内部 `RunRef=(TurnId, run_seq)` 和短期 interrupt token | +| `AGENT_LOOP` | 改为普通 `REACT`;Loop 只是默认插件的一种实现 | +| `AgentProgram` / `ProgramOutput` | 删除;与 `react` 重复 | +| `MESSAGE_HANDLER.decide()+handle()` | 压成一次 `handle(Incoming)`;`accept()` 是一次性 capability | +| public `RunLock` | 删除;session lane 由 RunHost 内部拥有 | +| `TurnRules` | 收敛为 immutable `TurnGrant`;不放 model、delivery、memory switch | +| `AgentRun` 大袋子 | 改成固定窄端口;删除 extra、metadata、loop_input、任意 repository | +| Core `TOOL_VIEW` 第三选择轴 | 降为 `default-react` 依赖的普通 `TOOL_SELECTOR` | +| `(TurnId, ordinal)` 取代 MessageId | 拒绝;它只表示成员位置 | +| Session = `Commit/Delete(turn)` fold | 拒绝;Session 仍是 canonical Message rows + seq | +| 通用 `SessionChange` | 收窄为同事务 `SessionReceipt` 引用 feed | +| `DERIVED_STORES` registry | 删除;每个插件拥有自己的 cursor/data/rebuild | +| 通用 `CommitIntent` / evidence bag | 删除;Tool/Delivery/Memory 各自保存领域 pending record | +| 强制永久 `FinalModelRequest` | 删除;只强制 binding receipt,完整 payload 是有界诊断 | +| Fiber Effect 与外部效果同名 | 分开;Effect 只管 Root 资源,外部效果归领域 owner | +| ChannelHost 与 DeliveryHost 都拥有 delivery | 修正;Delivery Owner 持久化,Channel Adapter 执行协议调用 | +| 通用 phase/EventBus 主链 | 迁到具体 Service/registry/Receipt;只保留有独立领域意义的事件 | +| `post_commit` 业务开关袋 | 用来源明确的 Session/Delivery 流程替代;迁移期兼容,目标态删除 | +| passive/schedule/proactive 各有 loop | 来源只创建 Message/Turn;共用 `REACT` | + +### 现有生命周期接线的归宿 + +旧 phase 不能整包换名后继续存在。逐项归位: + +| 当前环节 | v4 owner | +|---|---| +| BeforeTurn 的 Session/load/lane | TurnPort + ConversationStore + RunHost | +| 入站过滤、命令与是否进入回复 | 普通 `MESSAGE_HANDLER` 插件 | +| BeforeReasoning 的模型输入准备 | `default-react` 的 PromptParts、ContextView、ToolSelector | +| PromptRender mutable frame | 删除;改成 immutable PromptPart 收集与冻结 | +| BeforeStep/AfterStep | `default-react` 私有循环;有独立领域意义的 Tool/Model 事件留在各自 owner | +| 模型回复 parse/seal | Provider adapter + `default-react` 输出校验 | +| assistant attachment import | Attachment owner 在 complete 前返回 immutable refs;ConversationStore 同事务绑定 | +| AfterReasoning 的 Session 写入 | ConversationStore 的唯一 `complete()` | +| TurnCommitted 同步 fanout | 删除;换成 durable SessionReceipt + wakeup | +| Memory、embedding、presence | 各自的 SessionReceipt consumer | +| AfterTurn dispatch | Delivery Owner;Channel Adapter 只执行协议调用 | +| budget/log/trace | 诊断 owner,不能阻塞 Session 与 Delivery | +| success/failure extras | 回到产生该事实的领域插件;无独立 owner 的删除 | + +迁移原则不是“每个 phase 变成一个新 Service”。只有跨插件需要替换、并且拥有独立规则的能力才成为 Service;其余逻辑内联到真实 owner。 + +### 被动链特殊规则的最终 owner + +| 当前特殊规则 | v4 归宿 | +|---|---| +| command catalog 短路 | default Handler 依赖的 ordinary command registry | +| compaction 水位、完整 Turn 切点、overflow 重试 | `CONTEXT_VIEW`/compaction 插件;ModelPort 只执行已冻结请求 | +| Tool schema 预载、数量上限、`tool_search` 解锁 | `TOOL_SELECTOR` 与 tool-search 插件 | +| 空回复 retry | `default-react` 的模型输出规则;不是 Core retry | +| terminal Tool deadline | Tool 声明终结语义,`default-react` 拥有循环 deadline | +| provider continuation | Provider adapter 给出 typed finish,`default-react` 决定是否再调一次 | +| attachment/media import | Attachment owner;Message 只绑定 immutable ref | +| error response 文案 | default Handler 的 source-facing error mapper;RunHost 只返回 typed error | +| Citation | Citation 插件的 PromptPart/Tool/Receipt consumer,不进 REACT 分支 | +| Meme | Meme 插件的 Tool/PromptPart/独立数据 owner,不进 REACT 分支 | +| Akasha/Markdown post-memory | 各自的 SessionReceipt consumer | +| `agent_restart` | runtime-operation Tool + TurnGrant;与 ToolSearch 展示完全分开 | +| delivery、partial、uncertain、ACK | Delivery Owner 与 Channel Adapter 的明确分工 | +| stream/thinking/status | live StreamPort/UI projection;不能反写 Session 正文 | + +这张表必须在迁移中逐行对账。某项旧逻辑没有新 owner 时不能删;某项已经有 owner 时不能再保留旧 phase 的第二个 writer。 + +### 目标态 Core 中不应出现的产品名字 + +```text +Akasha +ToolSearch +Scheduler +Proactive +Wake +Spawn profile +Telegram / QQ / Mobile +OpenAI / Anthropic / DeepSeek +Markdown memory +Meme / Citation +``` + +它们可以作为普通插件、Provider 或 Channel Adapter 存在,但 Core 不按这些名字分支。 + +--- + +## 十、目标架构 + +```text +┌─────────────────────────────────────────────────────────────────────┐ +│ Akashic Core │ +│ │ +│ ┌────────────────┐ ┌────────────────┐ ┌─────────────────────┐ │ +│ │ Plugin Runtime │ │ConversationStore│ │ RunHost │ │ +│ │ Root/Fiber │ │Message/Turn │ │lane/cancel/lease │ │ +│ │ Effect/gens │ │Session/Receipt │ │narrow live ports │ │ +│ └────────────────┘ └────────────────┘ └─────────────────────┘ │ +│ │ +│ Core 只查两个行为 key:MESSAGE_HANDLER、REACT │ +└─────────────────────────────────────────────────────────────────────┘ + │ exact Root │ receipts + ▼ ▼ +┌────────────────────────────────┐ ┌─────────────────────────────┐ +│ ordinary behavior plugins │ │ ordinary state plugins │ +│ │ │ │ +│ passive-message-handler │ │ Akasha │ +│ default-react │ │ Markdown memory │ +│ simple-react / no-tool-react │ │ Embedding / FTS │ +│ prompt-parts │ │ UI projection │ +│ context-view │ │ │ +│ tool-search-selector │ │ each owns cursor + data │ +└────────────────────────────────┘ └─────────────────────────────┘ + │ + ▼ +┌─────────────────────────────────────────────────────────────────────┐ +│ ordinary source/domain plugins │ +│ Channel · Scheduler · Proactive · Spawn · Delivery · Tools · Models│ +└─────────────────────────────────────────────────────────────────────┘ +``` + +### 最小启动证明 + +Core 在没有默认插件时也应能启动: + +```text +Plugin Runtime + ConversationStore + RunHost +``` + +此时收到普通消息应明确返回 `MESSAGE_HANDLER unavailable`,而不是偷偷走旧被动链。 + +装一个十几行的 `echo-message-handler` 后,可以完成: + +```text +Incoming → Turn → "echo" DraftMessage → Session +``` + +它不需要 LLM、Tool、Prompt、Memory 或 `default-react`。这个 Gate 是“被动能力真的不再特权”的最强证明。 + +--- + +## 十一、迁移路线 + +这不是一次大改。每阶段都必须可回滚,并先保留当前行为 oracle。 + +### Phase 0:批准语义变化,冻结证据 + +先做: + +1. 把本文与 accepted 决策的差异列成正式 ADR:公共 Attempt 降为内部 Run、MessageId 与 seq 解耦、一个 Run 一个 Root。 +2. 录制当前被动、interrupt/retry、ToolSearch、Memory、Delivery、插件自更新 fixtures。 +3. 保存真实 SessionDB、tool trace、delivery ledger、snapshot identity 和 client payload 作为 oracle。 +4. 不改正式 workspace。 + +回滚点:无代码和数据变化。 + +### Phase 1:先扶正身份与原子提交 + +目标:只改 ConversationStore,不改现有 Reasoner 行为。 + +1. 增加逻辑 Turn 与内部 Run 的明确存储表示。 +2. 新 MessageId 与 seq 解耦;旧 ID 原样保留。 +3. 增加 `SessionReceipt` 表和 global `feed_seq`。 +4. 把 completed batch、Turn outcome、Run terminal、session head 和 Receipt 收进同一事务。 +5. 现有 API 用 adapter 继续返回旧字段,禁止双写漂移。 +6. Realtime/Mobile 同时引入稳定 TurnId 与短期 interrupt token;旧“`turn_id` 实际指一次 attempt”的字段只在客户端迁移期保留,不能继续污染新存储。 + +旧 Message 只有在已有 `control_turn_id` 等明确证据时才能建立新 Turn membership。缺少身份的历史行继续由版本化 legacy reader 读取,不能按角色邻接、正文、时间或 seq 距离猜 Turn。历史 backfill 算法要作为单独的数据迁移合同批准。 + +持久化迁移前必须:完整 SQLite backup、`integrity_check`、row count/digest、隔离恢复 smoke。不得直接复用当前 `turns` 表名改变旧行语义;推荐新表承接 v4,旧表只读保留到 parity Gate 通过。 + +回滚点:旧读路径仍是 authority;新表/Receipt 只是 shadow,不能驱动外部效果。 + +### Phase 2:把当前算法整体包进 `REACT` + +1. 新建普通 `default-react` 插件。 +2. 第一版内部仍调用现有 `DefaultReasoner`,只建立接口边界。 +3. `PassiveTurnPipeline` 通过 exact Root 的 `REACT` 调它。 +4. 比较回复、tool trace、Prompt digest、usage、Session batch 和错误终态。 + +回滚点:切回旧 Reasoner adapter,不迁数据。 + +### Phase 3:把默认算法内部拆成普通依赖 + +按这个顺序: + +1. immutable `PROMPT_PARTS`; +2. `CONTEXT_VIEW`; +3. `TOOL_SELECTOR`; +4. ToolSearch 完整插件; +5. model binding 与 model-call receipt; +6. 删除 Reasoner 内对应开关、ContextVar 和 phase。 + +每删一条旧路径,先证明新插件能独立卸载、替换、热换并恢复。 + +回滚点:每个 capability 保留一个短期 adapter,但同一时刻只能有一个 writer。 + +### Phase 4:建立 `MESSAGE_HANDLER`,切被动入口 + +1. 原始渠道 envelope 在边界校验后进入 exact Root Handler。 +2. `/stop` 旁路 Handler,只调 interrupt。 +3. 默认 passive handler 调 `accept → react → complete`。 +4. 命令、过滤和直接回复迁到 Handler 或各自普通 Service。 +5. 删除 Core 对 `PassiveTurnPipeline` 和 `DefaultReasoner` 的直接构造。 + +回滚点:按 generation 原子切回旧 handler,不能按单请求随机双跑。 + +### Phase 5:迁移提交后 consumer 与 Delivery + +1. Akasha、Markdown memory、embedding、UI 改为各自消费 SessionReceipt。 +2. Delivery Owner 用 prepared envelope 消除 commit 后 crash gap。 +3. 外部渠道 proactive 验证 provider-first 恢复。 +4. 同步 `TurnCommitted` fanout 只剩诊断后删除。 +5. `post_commit`、通用 after-turn extra 和旧 response patch 退出。 + +回滚点:consumer 可从自己的 cursor 重放;不得回滚 Session 消息。 + +### Phase 6:删桥并收窄 Core + +只有全部 Gate 通过后才删除: + +- 旧 passive phase bundle; +- `DefaultReasoner` 的 Core 接线; +- public Attempt/RunLock/AgentProgram 草案类型; +- ToolSearch Core 开关与授权 ContextVar; +- 通用 CommitIntent/DerivedStore/SessionChange 试验代码; +- 无 consumer 的 lifecycle event 和 DTO。 + +删除前再次核对外部插件源码、安装 cache、动态 consumer、测试和运行日志。cache 不是 canonical source,不能因为静态 `rg` 无命中就直接删接入点。 + +--- + +## 十二、验收 Gate + +### 12.1 Turn 与身份 + +- [ ] `U1 → interrupt → U2 → interrupt → U3 → A` 只产生一个 Turn、三个内部 Run。 +- [ ] completed Session batch 恰好为 `[U1,U2,U3,A]`,顺序与 Turn ordinal 一致。 +- [ ] schedule/proactive 的 `run-only` Task Message 有稳定身份但没有 seq;目标 Session 只出现最终 A。 +- [ ] failed 后显式 retry 复用原 MessageId;普通 fresh 产生新 MessageId 和新 Turn。 +- [ ] MessageId 不依赖 seq;旧 ID 不被改写。 +- [ ] 同 Session seq 单调且不复用。 +- [ ] `/stop` 和 ignored inbound 不创建 ε Turn。 +- [ ] 同一外部 source_ref 在 accept 前后任意 crash/重投都只产生一个 MessageId、TurnId 和 RunRef。 +- [ ] 同一 Turn 只能完成一次;重复相同 complete 幂等,内容漂移失败。 + +### 12.2 并发与中断 + +- [ ] 同一 Session 同时最多一个 active conversation Turn。 +- [ ] 不同 Session 可以并发。 +- [ ] 热换 generation 不改变 lane key。 +- [ ] interrupt token 只能结束它绑定的当前 Run;旧 token 不能杀新 Run。 +- [ ] active Run 只接受 interrupt,普通输入明确 busy。 +- [ ] 重启把失去 live owner 的 active Run 收束为 `interrupted(process_lost)`,不自动重跑外部效果。 +- [ ] UI 用 TurnId 分组,用 latest_run_state 显示 running/interrupted/failed,不把 RunRef 当 TurnId。 + +### 12.3 Root 与权限 + +- [ ] Handler、REACT、Prompt、Tool schema、Tool execute 和 model binding 来自同一个 exact Root。 +- [ ] Run 1 在 G10 中断、Run 2 在 G11 恢复时,TurnGrant 不扩大。 +- [ ] hidden Tool、not-allowed Tool 和 invalid arguments 有不同结构化错误。 +- [ ] ToolSearch 改 ToolView,不改 TurnGrant。 +- [ ] ToolSelector 无法看到或泄露 grant 之外的 Tool metadata。 +- [ ] candidate Root 不能写正式 Session、Memory、plugin-data 或外部服务。 +- [ ] cleanup 失败保留 drain blocker 和全部错误。 + +### 12.4 Prompt、Context 与 Tool + +- [ ] PromptPart 输入相同则顺序与 digest 相同。 +- [ ] 两个插件不能改写彼此的 PromptPart。 +- [ ] duplicate/exclusive Prompt key 在 candidate Gate 失败。 +- [ ] `persistent history`、`runtime history view`、`prompt history` 在 API 和日志中不混名。 +- [ ] ContextView 不写 Session,也不裁开完整 logical Turn 或 Tool pair。 +- [ ] 卸载 ToolSearch 后换 selector 不改 Core。 +- [ ] 无 Tool 的 REACT 能正常工作。 + +### 12.5 提交与恢复 + +- [ ] 在 complete 事务每个语句前后注入 crash,都只得到“全有”或“全无”。 +- [ ] 非空 batch 的 Message、seq、Turn outcome、Run terminal、session head、SessionReceipt 同事务一致。 +- [ ] run-only Turn 无 Session 变化时不生成假的 SessionReceipt。 +- [ ] commit 后、wakeup 前 crash,consumer 能从 feed_seq 追上。 +- [ ] consumer 失败不回滚 Session、不阻止其他 consumer。 +- [ ] consumer 重放相同 Receipt 幂等,内容漂移失败。 +- [ ] 存储损坏 fail-loud,不变成空 Session 或 cache miss。 + +### 12.6 Delivery 与外部效果 + +- [ ] 被动回复 Session commit 后发送失败,Session 内容仍存在且客户端可补尾。 +- [ ] Akashic/Web/Mobile 通知不产生第二份 durable 正文。 +- [ ] 外部 proactive 只有 provider full success 后才追加 Session。 +- [ ] partial/uncertain 不自动重发,不伪装成功。 +- [ ] Tool effect 在 Run retry 时按 owner receipt 对账,不重复执行。 +- [ ] Run/Root cleanup 不声称回滚已经发生的外部效果。 + +### 12.7 热更新与自我更新 + +- [ ] ordinary run 只租 stable;validation child 可显式租 latest。 +- [ ] candidate 行为 probe 证明真实调用,不只证明 import/manifest。 +- [ ] owner Turn 完成并释放旧 lease 后才开始 promotion drain。 +- [ ] promotion 失败保持 latest candidate 事实并恢复旧 stable formal Root。 +- [ ] 旧 Run 不跨 Root;新 Run 不再进入退休 Root。 +- [ ] durable operation 完成 handoff 后才允许旧 Root drain。 + +### 12.8 复杂度与非特权证明 + +- [ ] Core 不 import `default-react`、ToolSearch、Akasha、Scheduler、Proactive 或具体 Provider。 +- [ ] Core 不按来源名、插件 ID、模型名或渠道名分支。 +- [ ] 无 `MESSAGE_HANDLER` 时明确 unavailable,不回退旧被动链。 +- [ ] echo handler 可在没有 LLM/Tool/Prompt/Memory 时完成 Turn。 +- [ ] `simple-react` 可替换 `default-react`,不修改 Core。 +- [ ] 目标公共词中没有 Attempt、AgentProgram、RunLock、CommitPlan、CommitIntent、DerivedStore。 +- [ ] 新 helper、DTO、event 若没有独立 owner 或第二 consumer,删除或内联。 + +--- + +## 十三、需要批准的决定 + +### 推荐直接批准 + +1. **公共 Attempt 改为内部 Run。** 保留内部持久坐标,不保留第二套用户领域身份。 +2. **保留 opaque MessageId,并与 seq 解耦。** 不采用 `(TurnId, ordinal)` 作为身份。 +3. **使用 `REACT`,不引入 `AgentProgram`。** 默认 while loop 整体是普通插件。 +4. **Session 保持 canonical Message rows。** 只增加同事务、无正文的 SessionReceipt feed。 +5. **ToolSearch 只拥有 ToolView。** 权限永远由 TurnGrant 和执行边界拥有。 +6. **一个 Run 一个 exact Root。** 同 Turn 的后续 Run 可以使用新 committed Root。 +7. **外部效果归领域 owner。** 不建立通用 CommitIntent/EffectAttempt 平台。 + +### 实现前仍需单独确认 + +1. v4 物理表名与旧 `turns` 表怎样长期归档;不能原地解释旧行。 +2. 新 MessageId 的具体编码;本文推荐 UUIDv7,但只要求 opaque 和稳定。 +3. model-call binding receipt 的保留期,以及完整 payload 诊断默认关闭多久。 +4. plugin-data 的全局数据删除 inventory 与阻断语义;它不是本轮 SessionReceipt 的职责。 +5. 外部 proactive、`message_push` 和 BackgroundJobs 各自迁移批次;不能借被动链改造顺手改产品语义。 +6. accepted 决策和 `projectneed` 中 `Attempt`、snapshot execution unit 的措辞如何升级;批准前仍以当前合同为准。 + +--- + +## 十四、证据索引 + +### 输入与恢复点 + +| 文件 | SHA-256 | +|---|---| +| `0902-reviewed-v3.md` | `d3e5e9e1ecc09cdea60a532357da31ec87207ffd68f5f6d26f6e913021bd54a0` | +| `0902-02.md` | `7c1dee104a8706f8a2030de1b22b4a00407a8ab5c74e2ef06e5485a7dd0207af` | +| `.0902-reviewed-v3.pre-v4-20260903-111415.bak` | 与 v3 相同 | +| `.0902-02.pre-v4-20260903-111415.bak` | 与 `0902-02.md` 相同 | + +### 用户要求 + +- 2026-09-02 Codex 设计会话;原始记录保留在本地,不随公开 PR 提交。 + +### 当前项目合同 + +- `docs/projectneed.md:371`:不得使用无修饰的 history。 +- `docs/projectneed.md:426`:completed Turn 持久化全有或全无。 +- `docs/projectneed.md:430`:seq 单调且不复用。 +- `docs/projectneed.md:434`:破坏性删除只接受用户显式意图。 +- `docs/projectneed.md:444`:Session 正文正常只追加。 +- `docs/projectneed.md:452`:未完成 logical interaction 的续接规则。 +- `docs/projectneed.md:458`:一个 completed interaction 拥有全部输入和唯一最终回复。 +- `docs/projectneed.md:560`:同 Session 串行,不同 Session 并发。 +- `docs/projectneed.md:564`:active execution 只接受精确中断。 +- `docs/projectneed.md:568`:每个执行单元冻结模型执行绑定。 +- `docs/projectneed.md:638`:被动 Session-first 与 Akashic 主动消息语义。 +- `docs/projectneed.md:648`:外部渠道按完整逻辑消息提交。 +- `docs/projectneed.md:658`:硬终止只关闭 execution attempt。 +- `docs/projectneed.md:664` 起:candidate、snapshot、cleanup 与插件发布不变量。 +- `docs/design/persistence-state-map.md`:持久对象 owner、增改减、备份与恢复边界。 +- 决策 0034:Turn 是逻辑工作单元,Attempt 是当前内部执行概念。 +- 决策 0039:Core 原子能力来源无关,`react` 是唯一控制流。 +- 决策 0041:Turn effect 与 Memory 插件正交,Akasha 是普通插件。 +- 决策 0045:Akashic 主动消息先提交 Session,客户端使用 `message_id + seq`。 +- 决策 0008、0036、0046:Root/generation/snapshot lease、增量 candidate 与 drain。 + +### 当前代码 + +- `agent/looping/core.py:335-369`:Core 直接组 `DefaultReasoner` 与被动 pipeline。 +- `agent/core/passive_turn.py:355-435`:固定 phase 主链。 +- `agent/core/passive_turn.py:958-1075`:Reasoner 内 ToolSearch、Prompt 和 step phase。 +- `agent/lifecycle/phases/after_reasoning.py:257-333`:多 user input 的提交准备。 +- `agent/lifecycle/phases/after_turn.py:243-281`:同步 committed fanout。 +- `session/manager.py:642-738`:Session message 批次事务。 +- `session/store.py:2392` 起:当前 `turns` execution record。 +- `session/store.py:4744`:当前 MessageId 从 session/seq 生成。 +- `session/store.py:5385-5539`:completed interaction 显式删除与恢复证据。 +- `agent/plugins/snapshot.py:876-988`、`1567-1665`:exact snapshot lease 与 drain。 +- `agent/plugin_composition/effect.py:16-148`:Effect setup/rollback/LIFO cleanup。 + +### DeepSeek Harness 参考 + +- `deepseek-harness/docs/architecture.md:11-29`:所有组成件都是插件、profile 与 live patch reload。 +- `deepseek-harness/docs/architecture.md:51-95`:默认 core package、Turn flow 与 durable Session event。 +- `deepseek-harness/packages/core/agent-loop/src/agent.ts:234-359`:默认 loop 直接拥有 pre-step、Turn、Prompt、LLM 与 Tool 驱动。 +- `deepseek-harness/packages/core/system-prompt/src/index.ts:424-536`:Prompt contribution 通过 effect 注册并确定性组装。 +- `deepseek-harness/vendor/cordis/src/fiber.ts:405-560`:effect 注册、逆序 disposer 与卸载行为。 + +--- + +## 最终判断 + +v3 的方向是对的:让 AgentLoop、ToolSearch、Memory 和来源能力回到插件。但它仍把“为了迁移而看得见的运行细节”抬成了长期领域对象,又用通用 Commit、Change、DerivedStore 和 Effect 平台去包住所有失败。 + +`0902-02.md` 做对了主要减法,却又删掉了不能删的稳定 MessageId,并把 Session 误写成变更日志,还让 Channel 与 Delivery 重复拥有同一个事实。 + +v4 的改进不是增加一个更精巧的总框架,而是把边界缩到刚好够用: + +```text +Message 组成 Turn +Turn 中该进入 Session 的 Message 整批原子提交 +Run 只是 Turn 的内部执行 +每个 Run 只看一个 Root +REACT 只是普通插件 +外部效果回到自己的 owner +``` + +如果一个新能力不能用这些积木直接表达,它必须先证明自己拥有新的权威状态、不变量、控制流或生命周期;否则不进入 Core。 From bbea1aeb6312d1535a21b3dac733532dd60fb0a6 Mon Sep 17 00:00:00 2001 From: huashen <2494946808@qq.com> Date: Thu, 3 Sep 2026 14:06:30 +0800 Subject: [PATCH 2/7] =?UTF-8?q?docs:=20=E9=87=8D=E5=86=99=20Akashic=20v4?= =?UTF-8?q?=20Session=20=E8=AE=BE=E8=AE=A1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/design/0902-reviewed-v4.md | 2204 ++++++++++++++----------------- 1 file changed, 1003 insertions(+), 1201 deletions(-) diff --git a/docs/design/0902-reviewed-v4.md b/docs/design/0902-reviewed-v4.md index b08825bf..a4ae03d8 100644 --- a/docs/design/0902-reviewed-v4.md +++ b/docs/design/0902-reviewed-v4.md @@ -1,1472 +1,1274 @@ -# Akashic v4:从 Message 到 `react` 的插件架构 +# Akashic v4:Session 是账本,其他都是投影 -- 文档版本:`0902-reviewed-v4` +- 文档版本:0902-reviewed-v4 - 日期:2026-09-03 -- 状态:设计提案,等待批准 -- 当前代码基线:`47896b4200731183a54081e2eca77602a0881a0a` -- 需求来源:2026-09-02 Codex 设计会话(私有原始记录未提交) -- 输入:`0902-reviewed-v3.md`、`0902-02.md`、当前项目合同与真实代码 -- 本设计不授权:实现、数据库迁移、正式 workspace 写入、删除或部署 +- 状态:设计提案,等待维护者批准 +- 当前代码基线:47896b4200731183a54081e2eca77602a0881a0a +- 输入:0902-reviewed-v3.md、0902-02.md、Codex 历史会话、当前项目合同、真实运行案例、DeepSeek Harness +- 本文不授权:实现、数据库迁移、正式 workspace 写入、删除、部署或合并 ## 结论 -v4 只保留一句话: +v4 只设一个消息真源: -> Core 不决定 Agent 怎样思考;Core 只保证一次被接受的交流有一个 Turn,每次实际运行只看一个 Root 和一份权限,完成时把该进入 Session 的 Message 整批原子写入。 +~~~text +Session = 一份只追加的事实账本 -Agent 怎样思考,由普通 `REACT` 插件决定。被动消息怎样进入系统,由普通 `MESSAGE_HANDLER` 插件决定。默认的 Prompt、上下文裁切、模型调用、Tool 选择和 `while` 循环全部属于 `default-react` 插件,不再属于 Core。 +一条完整 Message 的正文只在 Session 中记一次 +Turn / Run / Transcript / Model History / Mobile / Memory 都从 Session 折出来 +Delivery 只引用 Message,不复制 Message +~~~ -v4 的目标结构是: +前一版 v4 的 `MessageBody + SessionEntry + SessionReceipt + ProjectionClaim` 仍然造了多条路,应该删除。 -```text -原始渠道输入 - │ - ▼ -Channel Adapter ── 只管协议、去重、ACK - │ - ▼ -Incoming ── exact Root ── MESSAGE_HANDLER - │ accept() - ▼ -Turn ── Run 1 ── interrupted - │ Run 2 ── failed - │ Run 3 ── REACT ── Draft Messages - │ - ▼ complete(),一个 SessionDB 事务 -Messages + seq + Turn outcome + SessionReceipt - │ │ - │ └── 普通插件各自追 cursor - ▼ -Delivery Owner ── Channel Adapter ── 外部平台 -``` - -最终只新增两个可替换的行为 Service: - -1. `MESSAGE_HANDLER`:处理一次原始入站消息。 -2. `REACT`:读取一个 Run,产生一组待提交 Message。 - -其余名字都必须拥有独立事实,不能只是换名包装: - -- `Message`:一条有稳定身份的内容。 -- `Turn`:用户能理解的一次完整交流。 -- `Run`:Turn 内部一次实际执行,只是内部运行坐标。 -- `Session`:已经提交的 Message 顺序。 -- `Root`:一次运行看到的插件世界。 -- `TurnGrant`:这个 Turn 允许触及的能力上界。 - -不再保留公共 `Attempt`、`AgentProgram`、`RunLock`、通用 `CommitPlan`、通用 `CommitIntent`、通用 `DerivedStore` 或“Session 本身就是另一条变更日志”这些概念。 - ---- - -## 一、这份 v4 要满足的真实要求 - -这些要求来自原始 Codex session,而不是从 v3 文案反推: - -1. 先承认已经实现的 v3 插件底座,不把已有能力重新设计一遍。 -2. 当前被动回复仍是硬编码特权链;目标是让它由普通插件组合出来。 -3. AgentLoop 的本质要比 DeepSeek Harness 更简单,而不是再做一个更大的框架。 -4. Tool、LLM、Prompt、Memory、Scheduler、Proactive 和来源插件都不能靠 Core 产品分支获得特权。 -5. ToolSearch 自己必须是普通插件;“展示给模型”不能伪装成“授予权限”。 -6. Akasha 已经是普通 Memory 插件,不得重新塞回 Core。 -7. `U1 → interrupt → U2 → interrupt → U3 → A` 是一个 Turn,而不是三个用户交流。 -8. Proactive、schedule fire、wake 和 spawn completion 在真正产生交流时,也能用同一个 Turn 模型。 -9. 概念尽可能少,词尽可能普通;每个概念只能拥有一个变化轴。 -10. 先做减法。旧设计没有证明价值的层、DTO、事件和兼容桥必须退出目标态。 - -### 判定标签 +最小主线变成: -本文固定区分三类内容: - -- **F(事实)**:当前代码、schema 或 accepted 文档已经证明。 -- **T(目标)**:v4 推荐的最终设计,尚未实现。 -- **U(未知)**:必须由维护者批准或在迁移前补证据。 - -T 不能写成当前事实,U 不能被实现者自行猜成需求。 - ---- - -## 二、当前真实起点 - -v4 不是从空白开始。 - -| 结论 | 状态 | 证据 | -|---|---|---| -| Root、Fiber、Effect、generation、stable/latest 和 snapshot lease 已存在 | F | `agent/plugins/snapshot.py:876`、`agent/plugins/snapshot.py:1567`、`agent/plugin_composition/effect.py:16` | -| 当前 Core 仍直接创建 `DefaultReasoner` 和 `PassiveTurnPipeline` | F | `agent/looping/core.py:335` | -| 当前被动链固定为 BeforeTurn → BeforeReasoning → Reasoner → AfterReasoning → AfterTurn | F | `agent/core/passive_turn.py:355` | -| ToolSearch 开关、Tool 展示顺序、Prompt phase 和 step phase 仍在 `DefaultReasoner` 内 | F | `agent/core/passive_turn.py:958` | -| completed 被动 Turn 已能把多条 user input 与最终 assistant 作为一批准备 | F | `agent/lifecycle/phases/after_reasoning.py:257` | -| `TurnCommitted` 目前仍同步 fanout;插件失败可以卡住后续 dispatch | F | `agent/lifecycle/phases/after_turn.py:243` | -| Session message 与 metadata 已在一个 SQLite 事务中追加 | F | `session/manager.py:642` | -| 当前 `message_id` 仍由 `session_key:seq` 生成 | F | `session/store.py:4744` | -| 当前 `turns` 表保存的是 execution attempt 进度,不是逻辑 Turn | F | `session/store.py:2392`、决策 0034 | -| completed interaction 已有整组删除、备份、embedding 删除和 compaction 失效流程 | F | `session/store.py:5385` | -| Akasha 已按普通插件边界拥有 Prompt、Tool 与自己的投影 | F | 决策 0041 | -| Core 尚无 `MESSAGE_HANDLER`、`REACT`、`TOOL_SELECTOR` 或 `SessionReceipt` | F | 当前代码符号盘点 | - -### DeepSeek Harness 值得借,但不能照抄 - -DeepSeek Harness 做对了三件事: - -1. 模型 adapter、Tool registry、Session log 和 Agent loop 都是插件。 -2. 插件注册是可逆 effect,卸载会撤销自己拥有的注册。 -3. profile 是插件树,默认 loop 可以被替换。 - -但它当前的具体 `agent-loop` 仍直接: - -- 开关 Turn 和 Step; -- 组装 system prompt; -- 派生模型消息; -- 调 LLM; -- 执行 Tool; -- 追加 Session event。 - -这适合作为“默认 loop 也是插件”的证据,不适合作为 Akashic 的最终拆分。Akashic v4 再向前一步:Core 只给运行和提交端口,默认 `while` 循环整体搬进 `default-react`。 +~~~text +Envelope / Draft + │ 只有接纳或封口时才成为 Message + ▼ +Session.append(events) + │ + ├── Turn projection + ├── Run projection + ├── Transcript projection ──▶ Web / Mobile + ├── ModelHistory projection ──▶ REACT + ├── Memory projection + └── Delivery projection ──▶ provider +~~~ + +Core 不认识 passive、proactive、Wake、Scheduler、Spawn、Content、Drift 或插件名。它只保证 Session 事件合法、有序、可恢复,以及当前 Run 才能提交结果。 + +### 把我当六岁 + +把 Session 想成唯一一本不能偷偷改页码的作业本: + +~~~text +作业本 Session +┌──────────────────────────────────────────┐ +│ 10 这件事开始了 │ +│ 11 花月哥哥说:U1 │ +│ 12 第一次尝试开始 │ +│ 13 第一次尝试被打断 │ +│ 14 花月哥哥补充:U2 │ +│ 15 第二次尝试开始 │ +│ 16 Akashic 回答:A │ +│ 17 第二次尝试完成 │ +│ 18 这件事完成 │ +│ 19 把 U1 放进聊天页 │ +│ 20 把 U2 放进聊天页 │ +│ 21 把 A 放进聊天页 │ +└──────────────────────────────────────────┘ + │ │ │ + ▼ ▼ ▼ + Turn 卡片 手机聊天页 模型上下文 +~~~ + +作业本里的事实只有一份。Turn 卡片、手机聊天页和模型上下文只是用不同彩笔画出的视图,丢了可以再从作业本画出来。 + +- Message 是作业本里真正写下的一句话。 +- Turn 是把“同一件事”的页码圈在一起,不是另一本本子。 +- Run 是这件事实际试了第几次,也不是另一本本子。 +- Transcript 是用户看到的聊天页。 +- Delivery 是邮差的工作状态,只记“送哪一句、送到哪、送成没有”,不再抄一遍信。 + +U1 后被打断,再收到 U2,仍是同一个 Turn,只多一个 Run。Wake 检查后没事,连作业本都不用动;Wake 已经开始工作后决定不说话,则账本里有一个完成但没有可见回复的 Turn。 + +### 本版五个决定 + +| 问题 | v4 决定 | +|---|---| +| Message 在哪里 | 只在 Session 的 `message/append` 事件里一次 | +| Turn 和 Run 是什么 | Session 事件的稳定分组投影,不是第二套正文存储 | +| 是否新增随机 MessageId | 不新增;新 MessageRef 由 SessionId 与 message event seq 派生 | +| 手机如何同步 | 仍只认不透明 `message_id`、单调 `seq` 和已有 cursor | +| proactive 是什么 | 来源插件的一种触发故事,不是 Core 类型、字段或状态机 | --- -## 三、最小领域模型 - -### 3.1 Message:身份与顺序分开 - -```text -Message = (MessageId, role, content, source, placement) -placement = session(SessionId) | run-only -``` - -规则: - -1. `MessageId` 是不透明、稳定、全局唯一的字符串。 -2. `MessageId` 不从 `seq`、`TurnId`、ordinal、时间或正文计算。 -3. 旧 `session_key:seq` 值继续作为合法的不透明旧 ID;迁移不改写它。 -4. 新 Message 推荐使用 UUIDv7,但 UUIDv7 的时间位没有业务语义。 -5. ordinal 只表示 Message 在 Turn 内的位置。 -6. `seq` 只表示 Message 在 Session 内的位置。 -7. 客户端同步继续只需要 `message_id + seq`;不得按正文或时间猜对应关系。 -8. `run-only` Message 是一份真实、稳定的 Turn 输入,但不进入正式 Session,因此永远没有 seq。 -9. 外部输入的 `source` 带经过 Channel 校验的稳定 source_ref;同一 source_ref 最多接纳成一个 MessageId。 - -`placement` 不是让插件临时挑选的提交计划。来源在 Turn 第一次接纳时就固定它: - -- 普通 user/assistant Message 放进目标 Session; -- proactive 或 schedule 的内部 Task Message 可以是 `run-only`; -- proactive 最终 Assistant Message 放进目标 Session; -- PromptPart、stream delta、thinking、interrupt 和未封口 Tool call 不是 Message。 - -不可信 envelope、模型输出和 Handler 都不能自报 placement。Channel/Source adapter 只能在自己的受信任 admission API 中提出来源类型,ConversationStore 按该入口的固定规则写入 placement;REACT 返回的 DraftMessage 不带 SessionId,RunHost 只允许它进入 Turn 已固定的目标 Session。 - -因此下面三件事互不替代: - -```text -MessageId 谁 -position(turn, message) 在这个 Turn 的第几个 -seq(session, message) 在这个 Session 的第几个 -``` - -`(TurnId, ordinal)` 可以定位关系,但不能取代 Message 身份。附件绑定、引用、重试、跨客户端同步和显式删除都需要稳定 MessageId。 - -### 3.2 Turn:一次完整交流 +## 一、先确定什么是真源 -目标定义: +### 1.1 唯一消息载体 -```text -Turn T = ( - turn_id, - session_id, - cause, - grant, - messages = , - outcome -) +下列规则是本设计的起点: -n >= 1 -``` +1. 还没进入 Session 的输入叫 `Envelope`,不是 Message。 +2. 还没封口的模型输出叫 `Draft`,不是 Message。 +3. Message 一旦创建,身份与元数据不可变;正文只存在于该 Session event 的可擦除 body slot。 +4. 任何其他模块只能保存 `MessageRef` 或可重建 projection,不能保存第二份正文。 +5. 模型看见过的 Message、Tool 结果和请求绑定都必须能从 Session 重建。 +6. 正常运行只追加;只有用户明确删除时,Data Management 才能按受控协议擦除正文。 -字段只各管一件事: +这里的“一次”指一条已经组装完成的 canonical Message。网络分片和模型 token delta 只能短暂直播,不能写入 Session、trace、request 记录或第二张流式表;crash 后丢掉未封口 Draft,不能拿半截 token 冒充 Message。Tool call 参数是另一类事实,只在 `tool/call` 写一次;Tool result 正文只在它对应的 `message/append` 写一次。 -- `turn_id`:这次交流的稳定身份。 -- `session_id`:它最终归入哪个 Session。 -- `cause`:谁发起,如 user、schedule、wake、spawn 或 outbound call。 -- `grant`:整次交流的能力上界。 -- `messages`:属于这次交流的有序 MessageId。 -- `outcome`:这次交流是否已经结束,以及怎样结束。 +这与“Session 是用户当前看见的聊天列表”不同。Session 是完整账本;聊天列表只是其中一个 projection。未完成输入、内部 task、Tool 结果和投递状态可以在账本里,但不必出现在聊天页。 -更精确地写,Turn 不是一份复制正文的新大对象: +### 1.2 不是所有事实都是 projection -```text -H_t = (turn_id, session_id, cause, grant) # 一次写下的 header -M_t = # 有序 Message 引用 -O_t = open | one terminal outcome # 唯一结果 +“其他都是 projection”只适用于 Message 的各种读法,不能抹掉两个真实外部边界: -T_t = project(H_t, M_t, O_t) -``` +- 来源插件在 admission 前拥有自己的 trigger attempt、due、cursor 和业务去重。`no_due` 时根本没有 Session Message。 +- Tool 或 provider 已经在外部世界产生的效果由对应 owner 的 effect ledger 最终确认。Session 只记录请求与观察到的结果,不能把 Git 回滚或内存回滚说成外部效果没发生。 -存储只需要拥有 header、Message membership 和 outcome;`TurnView` 从它们投影,不复制 Message body、Session rows、Run facts 或 Delivery facts。这样 Turn 可以被查询和恢复,却不会成为第二份对话真相。 +这两类事实都不能复制 Message 正文。它们只在成功进入 Session 后保存稳定 EventRef。 -对已经进入 Session 的 Message,再定义: +### 1.3 当前行为只当反例库 -```text -m_i ~ m_j 当且仅当 turn(m_i) = turn(m_j) -``` +hua-home 私有历史里的多 Run、interrupt、retry、Wake `no_due`、model skip 和 shared 只用于构造场景。当前表名、字段和分支不自动升级成正确设计。 -同一个等价类就是 Session 中可见的一个 Turn。组内按 seq 排,Turn 之间按每组最小 seq 排。由于一个 completed `session_batch(T)` 原子追加,同一 Turn 的可见 Message 必须形成连续块,中间不能夹进另一 Turn。 +若当前行为与以下模型冲突,应登记为 migration delta,再由维护者决定修改 accepted 合同或修改本提案。 -Turn 不拥有 Root,不拥有模型,不拥有当前运行 task,也不拥有 Delivery 状态。这些事实都可以在 Turn 不变时独立变化。 - -`TurnGrant` 保存的是稳定 capability key、scope 和限制,不保存 secret、连接对象或具体插件实例。真正的 credential handle、模型 adapter 和 Tool 实现由每个 Run 的 exact Root 冻结。授权流程可以在 grant 已允许的上界内批准一次具体调用;它不能在同一 Turn 中引入 grant 之外的新能力。 +--- -目标 outcome 只保留用户能理解的终态: +## 二、从 DSH 借什么,不借什么 -```text -open -completed(reply_message_id | no_reply) -superseded(by_turn_id) -abandoned(reason) -``` +DeepSeek Harness 当前源码提供了四个重要证据: -`interrupted` 和一次 provider error 不直接成为 Turn outcome,因为它们只结束一次 Run。显式重试仍可继续同一个 Turn。用户明确放弃,或失败后普通新输入选择 fresh,才关闭旧 Turn。 +1. `Session` 是 typed `SessionEvent` 的 append-only log,是完整交互历史唯一真源。 +2. 模型历史由 `deriveMessages()` 从 log 的 surface 投影,不单独保存。 +3. `turnBoundary` 和 `turnOutline` 都是对 `turn/start`、`turn/end` 与消息事件的纯 fold。 +4. 纯 projection checkpoint 只保存 `(session, key, version, source seq, value)`;版本不符或越过 log 末尾就丢弃重建。 -### 3.3 Run:内部运行坐标,不是第二个领域对象 +v4 借这条骨架: -```text -RunRef = (TurnId, run_seq) -run_seq = 1, 2, 3, ... -``` +~~~text +durable Session events + └── pure / ordered folds + ├── runtime state + ├── model surface + └── client views +~~~ -Run 只回答:“这个 Turn 第几次真的开始执行?” +但不照抄三个 DSH 选择: -它需要持久化,是因为中断、崩溃恢复、Tool 幂等和精确诊断都需要区分多次运行;但它不进入用户公共领域词,也不叫公共 `AttemptId`。 +| DSH 当前选择 | Akashic v4 选择 | 原因 | +|---|---|---| +| `createMessage()` 先生成随机 UUID | Message 进入 Session 时由 event coordinate 得到身份 | Akashic 不需要让未接纳 Envelope 冒充 Message | +| durable inbox splice 可再次携带整条 Message | Message 正文只写一次,后续事件只引用 MessageRef | 避免同一 log 内也复制正文 | +| 单一 turn 顺序足够 | 多个 Turn 可以交错,但每个事件显式引用 TurnRef | message_push/task 不应占住会话的 history-reading lane | +| Session fork 是现成能力 | v4 不引入 durable fork | 避免在未定义 lineage、删除和 retention 前制造跨 Session Message graph | -运行时可以给客户端一个短期 `interrupt_token`。它只授权中断当前 Run: +如果未来确实需要 durable inbox,入队动作本身就追加 `message/append`;后续 `turn/input` 只引用它,不再携带正文。先证明这个消费者存在,再增加该事件。 -- Run 终结后 token 立即失效; -- token 不能作为 Turn、Message 或重试身份; -- UI 显示 TurnId,不显示内部 `run_seq`; -- Core 用 token 精确找到当前 Run,不需要创造另一个公共对象。 +DSH 在这里是“单日志 + pure fold”的参考,不是 Akashic 多 Turn 并发、异步外部 projection、删除或外部 effect 正确性的证明。尤其 DSH 的 projection contract 是同步纯函数;有网络 I/O 的 materializer 必须另外解决 started-but-unknown,而不能套一个 checkpoint 就宣称安全。 -每个 Run 有自己的终态: +--- -```text -succeeded(draft_ids) -interrupted(cause) -failed(error_code) -``` +## 三、最小权威模型 -`succeeded` 也不等于 Turn 已完成。只有 ConversationStore 的原子提交成功,Turn 才能变成 `completed`。 +### 3.1 SessionEvent 是唯一坐标系 -一个 `open` Turn 不一定正在运行。它可以在某个 Run interrupted/failed 后安静等待下一次继续。UI 的 `running / interrupted / failed` 来自 `latest_run_state` 投影,不是假装成 Turn outcome;只有显式继续、替代或放弃才改变 Turn 本身。 +~~~text +SessionEvent = ( + session_id, + event_seq, + type, + data +) -### 3.4 Session:已提交 Message 的唯一顺序 +EventRef = (SessionId, event_seq) +~~~ -```text -Session S = <(seq_1, message_id_1), ..., (seq_n, message_id_n)> +规则: -seq_1 < ... < seq_n -``` +- `event_seq` 在一个 Session 内从 0 连续增加,永不复用。 +- Session Store 只接受原子 `append_batch`;同一批事件先整体校验,再一起提交。 +- 插件可以扩展事件词汇,但必须声明 producer、consumer、重放规则和未知 reader 是否可忽略。 +- 会改变重建语义的未知事件默认 fail-loud,不能静默跳过。 +- EventRef 是位置,不是 capability;读、写、删除仍检查 Session 与调用者权限。 +- 写入某个 Session event 的所有 `TurnRef`、`MessageRef`、`RunRef`、`StepRef`、`ToolCallRef` 和 `DeliveryRef` 必须指向同一个 Session。Store 集中拒绝跨 Session ref。 +- 只有 source/effect ledger 可以从外部保存一个 Session EventRef;Session 内部不会反向引用另一个 Session 的事件。 + +所有新领域引用都只是 EventRef 的类型化名字: + +~~~text +TurnRef = ref of turn/open event +MessageRef = ref of message/append event +RunRef = ref of run/open event +StepRef = ref of step/open event +ToolCallRef = ref of tool/call event +DeliveryRef = ref of delivery/prepared event +~~~ + +因此 v4 不新增随机 TurnId、MessageId、RunId、AttemptId、StepId、ReceiptId 或 DeliveryId。SessionId 已经提供全局作用域,event_seq 提供 Session 内唯一位置。 + +### 3.2 Message 只出现一次 + +~~~text +message/append = { + turn: TurnRef, + role: user | assistant, + body: Live { + content: ContentBlock[], + attachments: AttachmentRef[] + } | Gone { redaction: EventRef }, + producer: + Admission { admission_token: AdmissionToken } + | Run { run: RunRef, step: StepRef, + output: model_output | tool_result { call: ToolCallRef } }, + private_slot: + AdmissionOnly( + Live { source: SourceRef, reply_target?: ReplyTarget } + | Gone { redaction: EventRef } + ) | NoneForRun +} +~~~ + +MessageRef 就是这条 `message/append` 事件的 EventRef,不在 payload 里再写一次 id。初次 append 只允许 `Live`;`Gone` 只可能由第八章的显式 redaction 原子转换得到。 -定义 Turn 对某个 Session 的投影: +规则: -```text -session_batch(T) = - -``` +- human 输入、插件 task 输入、模型 assistant、Tool result 都用同一个不可变 Message 结构。 +- Admission 只能由 admission capability 写,不能携带 RunRef;它把 source-scoped admission 与可选回复目标固定下来。原始 source/target 是可擦 privacy slot,immutable admission token 只是 keyed digest,不能反解地址。 +- Admission 不是绕过 Run fence 的旁门:初始 Turn 遵守 3.4 的同批规则;已有 open Turn 的新 Admission 只允许作为 5.2 continuation batch 的一部分,不能裸 append。 +- 模型输出与 Tool result 只能由 current Run capability 写,必须携带同 Session、同 Turn 的 RunRef 和 StepRef。Store 在同一写锁内验证 Run 仍 active;旧 Run 的迟到结果没有可绕过的字段。 +- Tool result 对模型仍是 user-role Message,但 `producer.kind=tool_result`,对应 `tool/call` 只保存参数和这条 MessageRef,不再保存 result body。 +- stream chunk、thinking、ACK、Prompt section、turn/run 边界和普通 debug span 不是 Message,也不得持久化 Message 正文。直播 chunk 只存在于当前进程和连接;封口后客户端改读 canonical Message。 +- 输入附件在同一个 append batch 里绑定;输出附件在 Message 提交前只是 staging object。 +- Message 只能属于一个 Session;向该 Session 之外发送由 Delivery 引用它,不复制它。 -Session 不是 `Commit/Delete` event 的 fold,也不是旁边一条日志的投影。权威内容仍是 `sessions.db/messages`。`session_batch(T)` 只是从 Turn 选择应进入该 Session 的 Message,不产生第二份正文。 +如果相同正文出现两次,就是两条 Message。content hash 不是身份。 -规则: +### 3.3 wire 上仍叫 message_id -1. 正常完成只 INSERT Message。 -2. 同一 Session 的 `seq` 在提交事务内单调增加且不复用。 -3. completed Turn 的整个 `session_batch(T)` 一次提交;不能出现半个可见批次。 -4. Prompt 裁切、compaction、索引或插件重载无权改写正文。 -5. 只有用户显式撤销或删除可以减少正文,并必须走单独的数据管理协议。 +内部 MessageRef 永远只有一种形状: -普通被动 Turn 的 batch 是 `U1...Un+A`;proactive/schedule 可以是 `A`,内部 Task Message 留在 Run/Turn 事实中。这样既保留“Message 组成 Turn”,也不把机器任务伪装成用户对话。 +~~~text +MessageRef = ref of message/append EventRef +~~~ -### 3.5 SessionReceipt:提交后的引用通知 +wire `message_id` 是边界表示,不是第二种内部身份: -普通插件需要知道 Session 新增或移除了什么,但不能因此重定义 Session。v4 增加一个很窄的引用 feed: +~~~text +WireMessageIdOut = encode_v4(MessageRef) +WireMessageIdIn = encode_v4(MessageRef) | LegacyWireMessageId -```text -SessionReceipt = ( - feed_seq, - receipt_id, - kind, # messages_appended | interaction_removed - session_id, - session_version, - turn_id, - message_ids, - final_run_ref?, - audit_ref? -) -``` +identity/legacy-token = { + message: MessageRef, + token: (key_version, HMAC(key_version, + "legacy" + session_scope + + canonical LegacyWireMessageId)) +} +~~~ -它有五条硬规则: +- 新消息只输出版本化、不透明的 `encode_v4(MessageRef)`,不再有独立随机 UUID。 +- 迁移旧 Message 时,在同一 append batch 追加一条一对一 token mapping event;Session 不保存 raw old id。LegacyIdResolver projection 可由 token events 重建。 +- v4 full snapshot 对旧消息也输出 `encode_v4(MessageRef)`。旧 id 只在 API 入站兼容期被接受:边界用 active/retained identity keys 计算候选 token,解析成 MessageRef;Core 之后看不到 Legacy 类型。 +- resolver 必须先从已经认证的 route/session context 取得 `session_scope`,再计算 token;禁止拿裸 legacy id 做跨 Session 全库查找。 +- 同一 legacy token 映到两个 MessageRef、两个 token 映到同一 MessageRef或跨 Session 使用都 fail-loud;不得按角色、时间、正文或相邻 seq 猜。 +- 映射所用 identity key version 与 AdmissionToken 遵守相同 keyring/退休规则;SessionGone 前仍能把旧 id 解析为原 MessageRef 或 Gone,却无法从 token 反推出 chat identity。 +- 客户端不能拆 message_id 获得权限或业务含义。 +- v4 feedback、附件、删除和引用都用输出的同一 message_id,不再增加 alias;旧链接只走入口 resolver。 +- 来源 transport 的 `client_message_id` 只用于 admission/retry,不是第二个领域 MessageId。 -1. 与非空 `session_batch(T)` 的提交或显式删除在同一个 SessionDB 事务中写入。 -2. 只保存引用和版本,不复制正文、Prompt、Tool trace 或插件 payload。 -3. 插件按 MessageId 从窄只读接口取自己需要的事实。 -4. 每个插件在自己的 plugin-data 中拥有 cursor 和幂等 receipt。 -5. Session 读取永远不通过 Receipt 反推正文;Receipt 丢失是损坏,不是“Session 为空”。 -6. v4 首版不自动裁掉 Receipt;未来 retention 必须先有可证明的 snapshot/watermark 协议,不能猜所有插件都追上了。 +为什么不能只用 wire `seq` 代替 message_id?因为两者回答不同问题: -它不是通用 EventBus,不负责插件执行,也不保证“所有插件都已经处理”。它只消除“Session 已提交但进程在通知插件前崩溃”的丢通知窗口。 +~~~text +message_id 指向 Session 中那一条不可变 Message +seq 指向 Transcript 最近一次 add/remove 变化的顺序 +~~~ -### 3.6 六条代数不变量 +删除或重新投影后,MessageRef 不变,但客户端看到变化的 seq 会前进。因此保留两个字段是正交,不是重复。 -```text -I1 MessageId 稳定;seq 与 ordinal 都不承担身份。 +v4 不定义 durable Session fork。复制到新 Session 会创建新的 Message event 和新的 MessageRef;可记录非权威 provenance,但不得跨 Session 引用正文。若以后要保留 fork identity,必须先用单独 ADR 定义 lineage、父 Session 删除、retention、权限和 wire 语义,不能暗中塞进 EventRef。 -I2 一个 Message 最多属于一个 Turn;一个 completed Turn 的 - session_batch 在 Session 中保持同一顺序。 +### 3.4 Turn 是事件分组,不是 row -I3 同一 Turn 同时最多一个 active Run;同一 Session 同时最多一个 - active conversation Turn。 +~~~text +turn/open ────────────────────────────── turn/close + │ │ │ │ + message run/open message outcome + U1 Run 1 U2/A +~~~ -I4 一个 Run 只看一个 exact Root;同一 Run 的模型、Prompt contributor、 - Tool catalog 和执行端口不跨 generation 漂移。 +`turn/open` 的 EventRef 就是 TurnRef。属于该 Turn 的 Message、Run、Delivery 和 policy event 显式引用 TurnRef,因此不同 Turn 可以安全交错。 -I5 effective authority = TurnGrant ∩ Root capabilities ∩ call permit; - 每一层只能缩小,不能放大。 +`turn/open` 不是一个可以空放的壳。Store 强制同一个初始 append batch 按 `turn/open → 至少一条 Admission Message → 一个 run/open` 排序;Message 用 batch local Turn handle,Run 只能引用本批已经验证的 Message。没有 Admission Message/Run 的 Turn、先开 Run 后补输入、以及纯内部空 Turn 都被拒绝。`no_due` 留在 source ledger,不能用空 Turn 代替。 -I6 Turn 只完成一次;非空 Session 批次、Turn outcome 和 SessionReceipt - 要么一起提交,要么都不提交。session_batch 为空时只提交 Run/Turn 终态, - 不伪造 SessionReceipt。 -``` +Turn projection 只回答: -### 3.7 不存在 ε Turn +- 这次逻辑工作有哪些输入和最终输出; +- 它是否仍 open; +- 它最后 completed、superseded、abandoned 或 failed; +- 它包含哪些 Run。 -没有 Message,就没有 Turn。 +Turn 不拥有 Message body、Root、Delivery、Session 可见性或 projection cache。 -- 一个被过滤的垃圾输入在 `Incoming.accept()` 前被忽略:没有 Turn。 -- `/stop` 只是精确中断当前 Run:没有新 Message,也没有新 Turn。 -- wake tick 只检查状态后决定不交流:它是 wake 插件自己的 tick,不是 Turn。 -- wake 或 schedule 真正调用 `react` 时,先创建一条明确的 `run-only` Task Message,再创建 Turn。 -- 一次维护 job 没有交流内容时,就是 job,不借 Turn 记账。 +`turn/open` 的 TurnGrant 是否包含 `finish_without_output` 也是可重放事实。`turn/close(completed)` 是否需要 final assistant Message 由这个 grant 和事件结构共同决定: -这让 Turn 永远保持“交流的最小单元”,不再兼任所有后台工作的统一盒子。 +- grant 不含 `finish_without_output` 时必须提供唯一 terminal assistant; +- grant 包含它时,只有先追加一个由 DomainDecisionPermit 授权的 `turn/decision`,才可零 assistant 完成; +- 不保存 `turn_kind` 或 `no_reply` boolean。 -### 3.8 典型场景 +### 3.5 Run 是一次执行占用 -#### 被动中断续接 +`run/open` 的 EventRef 就是 RunRef。一个 Turn 可以有多个 Run,一个 Run 可以有多个 Step;一个 Step 是一次模型请求及其 Tool 调用。 -```text +~~~text Turn T ├── Message U1 -├── Run (T,1) ── interrupted +├── Run R1 ── interrupted ├── Message U2 -├── Run (T,2) ── interrupted -├── Message U3 -├── Run (T,3) ── succeeded -└── Message A ── complete ── Session batch [U1,U2,U3,A] -``` - -#### 显式 retry 与普通 fresh +├── Run R2 +│ ├── Step S1 ── tool calls +│ └── Step S2 ── final answer +└── completed +~~~ -```text -Run failed -├── retry(original MessageId) ── same Turn, new Run -└── ordinary new input ── old Turn superseded, new Turn -``` +Turn 打开时一次性固定最大 `TurnGrant`;后续 Run 只能缩权,不能重写这个上限。Run 事件只记录不可推导的执行事实: -#### Schedule +- exact plugin Root/generation binding; +- 若读取 Session history,则记录 exact history cut; +- 本 Run 固定的 `reply_to: MessageRef`(没有发送能力时为空); +- close outcome:completed、interrupted 或 failed。 -```text -Schedule tick ── private due/misfire rules - ├── skip:只写 schedule 自己的状态 - └── run:run-only Task Message → Turn → REACT - └── Assistant Message → target Session -``` +同一 Turn 的第一次 history-reading Run 固定 base `TranscriptThrough(cut)`;continuation 和 retry 必须复用这个 cut,只增加本 Turn 的新输入与新 Run facts。这样 background transcript change 不会在 U1 与 U2 之间悄悄改变同一次工作的旧历史。fresh Turn 才取得新的 cut。 -#### Proactive +第一次真实模型请求再追加 exact `request/bound`,并强制引用 current RunRef 与 StepRef。它固定 provider artifact、model、connection、CredentialHandle、roles、CallPermit、prompt artifact refs 与 Tool schema refs,但不复制 history body、rendered Message 或 Tool result。Root 加这些引用必须足以确定地重新构造请求;无法重构的动态 context 必须先作为自己唯一的 Session fact 写一次,再由 request 引用。同一 Run 不漂移;下一 Run 可以使用新的已提交 Root 和 binding。 -```text -observation → private gate - ├── skip:不是 Turn - └── speak:run-only Task Message → Turn → REACT - └── Assistant Message → Delivery -``` +RunRef 本身就是 fence: -来源只负责“何时创建 Message、把结果送到哪里”。一旦进入 `react`,不再有 passive、schedule 或 proactive 专用 Loop。 +- 每个 Turn 同时最多一个 active Run; +- 同一 Session 同时最多一个带 `history_cut` 的 active Run; +- interrupt、append model-visible facts 和 seal 都必须证明自己仍是当前 RunRef; +- 新 RunRef 永不复用,所以旧 Run 的迟到 seal 必然失败,没有 ABA; +- 不再新增一个内容相同的 fence UUID。 ---- +StepRef 用同一条规则 fence Run 内的迭代: -## 四、每个事实只有一个 owner +- 每个 active Run 同时最多一个 current StepRef;`step/open` 只在没有 current Step 时成功。 +- `request/bound`、`tool/call`、Tool result Message 和 final assistant Message 必须同时匹配 current RunRef 与 current StepRef。 +- 一个 Step 可以并行发出多个 ToolCallRef,但每个 call 只有一个 terminal Tool-result Message;有 pending/started call 时,必须先得到真实 result,或提交 structured `outcome_unknown` result,才能 `step/close`。外部 effect ledger 的 uncertain 可继续存在。 +- final assistant Message 与 `step/close` 在同一 batch 提交;普通 Tool Step 必须等全部 call terminal 后才能 close。 +- close 后 current Step 清空;S2 打开后,S1 的迟到 callback 即使 Run 仍 active也只能命中已有幂等结果或 fail-loud,不能追加新 Message。 +- `step/open` 的 EventRef 已经不可复用,不增加 Step fence UUID。 -| Owner | 唯一拥有 | 明确不拥有 | -|---|---|---| -| Channel Adapter | 外部协议解析、envelope custody/重投/ACK、一次真实 provider 调用的协议映射 | Turn、Session、durable delivery 状态、Prompt、Tool 权限 | -| ConversationStore | 已接纳 source_ref 的唯一性、Message、Turn、内部 Run 记录、Session 顺序、SessionReceipt 的原子事务 | LLM、Tool 执行、渠道调用、插件 cursor | -| RunHost | session lane、active task、interrupt token、exact Root lease、短期资源清理 | Session 正文、长期插件状态、外部效果最终状态 | -| Plugin Runtime | artifact、Root、Fiber、Effect、generation、stable/latest、发布与 drain | Turn outcome、Delivery outcome、插件业务数据 | -| `default-react` | 默认 Prompt/Context/Tool/LLM `while` 算法和自己的 step 状态 | Session commit、Delivery、权限授予、Root 发布 | -| Tool plugin | Tool schema、参数边界、执行、自己的外部效果 ledger 和幂等 | Turn 总控制、其他 Tool、Session 任意写入 | -| Delivery Owner | durable envelope、dedupe、provider receipt、uncertain/rejected/delivered/settled 状态 | 渠道协议实现、Session 正文所有权 | -| Memory/Projection plugin | 自己的索引、cursor、receipt 和 rebuild | 原始 Message 保留、Turn commit、其他插件 readiness | +不读取 history 的 task/message_push Run 可以与 conversation Turn 共存,但所有 Session append 仍由 single-writer 事务串行。 -### Channel 与 Delivery 的精确分工 +### 3.6 Tool 与模型事实也进同一本账 -二者不能都声称“拥有投递”。 +下一 Run 恢复所需的内容直接是 Session events: -```text -Delivery Owner Channel Adapter -────────────────────────────── ───────────────────────── -创建 durable envelope 把 envelope 映射成平台 API -决定 dedupe/idempotency 执行一次真实调用 -记录 provider_started 返回 provider 原始 receipt -记录 uncertain/rejected/success 不保存长期状态机 -恢复未完成操作 不自行猜是否该重试 -``` +~~~text +step/open(S1) +├── request/bound(run, S1, refs only) +├── tool/call(run, S1, arguments once) +├── message/append(tool result, run, S1) +└── step/close(run, S1) +step/open(S2) +├── request/bound(run, S2, refs only) +└── one batch: message/append(final assistant, run, S2) + + step/close(run, S2) +~~~ -外部调用的 durable 真相只在 Delivery Owner。Channel Adapter 是协议 driver。 +`assistant/chunk` 不在 durable vocabulary 中。Tool call 参数只在 `tool/call`;Tool result 正文只在 Message,其 producer 直接引用 ToolCallRef,不再需要 result-link 或 `ToolFact` 第二张表。ModelHistory projection 只使用完整、已经闭合的 call/result 对;未闭合调用在 crash repair 中变成明确 `outcome_unknown`,不能静默重放。 -### live 资源与 durable 外部效果不能共用一个领域词 +unknown 之后的外部查询只追加无正文状态事实:`tool/effect-resolved { call, happened | not_happened, receipt_ref? }`。显式覆盖则追加 `tool/uncertain-override { call, UserPermitRef }`。二者都不改写原 unknown Message;TurnState fold 用它们清除或受审计地越过 unresolved-effect fence。 -Root/Fiber/Effect 只管理本进程短期资源:listener、timer、连接、临时 task、注册项。它们随 Root 卸载逆序清理。 - -已经可能发生的外部操作由真实领域 owner 管理: - -- 消息发送属于 Delivery Owner; -- Tool 写操作属于该 Tool 插件; -- schedule 状态属于 Scheduler 插件; -- Memory 投影属于 Memory 插件。 - -旧 Root 不需要为了一个长期 pending delivery 永远活着。插件必须先把 durable operation 交给稳定 owner,才能释放 Run lease。 +Tool 插件自己的 effect ledger 仍独占外部副作用 finality。Session 记录模型见到什么,effect ledger 记录外部世界实际发生什么,两者不能互相冒充。 --- -## 五、最小公共 API - -以下代码只说明可观察合同,不是最终 Python 语法。 +## 四、所有读模型都是 projection -### 5.1 一个来源无关的 Turn 入口 +### 4.1 一个统一 projection 规则 -Channel、Scheduler、Proactive 和 Spawn 都需要同一扇窄门,但它不是另一套 Loop: +每个 projection 都是: -```python -class TurnPort(Protocol): - async def start(self, request: TurnStart) -> Run: ... +~~~text +State(n + 1) = apply(State(n), SessionEvent[n + 1]) +~~~ -class TurnStart: - session_id: SessionId - message: NewMessage - placement: "session" | "run-only" - cause_ref: CauseRef -``` +每个 projection owner 注册: -调用者不提供 TurnId、RunRef、TurnGrant、Root、模型或 Tool。TurnPort 根据已认证来源、调用插件 scope、目标 Session 和部署 policy 生成这些事实,并在 durable admission 成功后才返回 Run。 +- 稳定 `projection_key`; +- 初始状态; +- 纯、确定性的 `apply`; +- state schema version; +- 可选 wire view; +- 若持久化,最后成功应用的 `source_event_seq`。 -它是 Core 的原子能力,不是行为策略: +cache/checkpoint 只是一条捷径: -- `Incoming.accept()` 是一个已经填好并限制为最多调用一次的 TurnPort 请求; -- Scheduler/Wake 用 scoped TurnPort 接纳 `run-only` Task Message; -- Subagent 用自己的 Session 和父子 cause_ref; -- 来源不能借它取得任意 Session repository 或扩大 grant。 +~~~text +(session_id, projection_key, state_version, source_event_seq, value) +~~~ -### 5.2 两个行为 Service +server-side checkpoint 的 `value` 只保存 refs、状态和必要的 derived value,不保存原始 MessageBody;需要展示正文时按 MessageRef 从 Session 读取。schema 版本不符、cursor 越过 Session 末尾或校验失败时,直接丢弃并从 Session 重建。禁止用 cache 反写 Session。 -```python -MESSAGE_HANDLER: ServiceKey[MessageHandler] -REACT: ServiceKey[React] +客户端 Room/浏览器状态可以持有用于离线阅读的 materialized body,但它明确是可替换 projection:不能回传覆盖 Session,必须应用 remove/SessionGone。外部 provider 在发送过程中收到正文也是外部效果边界,不因此取得 Akashic Message authority。 -class MessageHandler(Protocol): - async def handle(self, incoming: Incoming) -> None: ... +### 4.2 必需的 projection -class React(Protocol): - async def react(self, run: ReactRun) -> DraftBatch: ... -``` +| Projection | 输入 | 输出 | 可否删除重建 | +|---|---|---|---| +| TurnState | turn/run/message events | open Turn、Run、outcome | 是 | +| ModelHistory | transcript cut、当前 Turn message、tool/request events | 下一次模型输入 | 是 | +| Transcript | transcript add/remove + MessageRef | 用户聊天历史 | 是 | +| Mobile/Web | Transcript + TurnState + domain projections | 已提交 wire snapshot/delta | 是 | +| Memory/Embedding | eligible transcript + policy events | 长期检索结构 | 必须可对账、可清理、可重建 | +| DeliveryState | delivery events | pending/uncertain/settled | 是;外部效果仍以 provider receipt 为证 | -没有 `AGENT_PROGRAM`。`react` 已经是项目接受的普通动词:输入 Message,产生输出 Message。再增加一个 Program 只会建立第二套执行模型。 +Turn 和 Run 因此仍是重要领域词,但不是独立真源。删掉 projection cache 不会删掉 Turn;重新 fold 同一 Session 会得到同一 Turn。 -### 5.3 `Incoming` 是一次性 capability +partial Draft 的 token stream 不是 Message projection,也不是 durable history。它是 Run Host 到当前连接的短暂 signal,必须带 RunRef,旧 Run signal 在客户端和服务端都被丢弃。crash 后不能恢复半截 Draft;一旦 seal,所有 UI 都切到 Session 中唯一的 final Message。 -```python -class Incoming: - view: IncomingView +#### ModelHistory 的选择规则 - async def accept(self) -> Run: ... -``` +`run/open` 固定一种 `HistorySpec`: -规则: - -1. `view` 是经过 Channel 边界校验的只读数据。 -2. `accept()` 最多成功一次。 -3. session、source、cause、lane key 和最大 TurnGrant 在创建 Incoming 时已经由 host 固定;Handler 不能扩权或换 Session。 -4. Handler 返回时从未调用 `accept()`,表示忽略。 -5. Handler 调用 `accept()` 后,必须让 Run 到达内部 terminal;否则 Host 记录合同违反并让 Run failed,不静默完成。 -6. 不存在 `decide() → plan payload → handle(plan)` 两段协议。 +~~~text +HistorySpec = TranscriptThrough(session_event_seq) | NoPriorTranscript +~~~ -`accept()` 在同一个 admission 事务里固定 source_ref → MessageId → TurnId → RunRef。Channel 因 ACK 丢失重投同一 envelope 时,ConversationStore 返回同一接纳结果,不增加第二个 Message;Channel Adapter 仍独自拥有外部 custody 和何时 ACK 的协议状态。 +每个 Step 的模型输入只解析 refs,不复制 body: -Dispatcher 在调用 Handler 前先租用 exact Root。`accept()` 把同一份 lease 转成 Run lease,因此 Handler 与后续 `REACT` 不会跨 generation。 +1. `TranscriptThrough(cut)` 取 cut 时已经 visible 且未 remove 的 Message;`NoPriorTranscript` 取空。 +2. 再按 Session event order 重放当前 Turn、当前 Step 之前的 model-visible surface:所有 Admission Message,以及所有更早 Run/Step 已闭合的 `tool/call → tool result Message` 对。 +3. replay 跨 Run 保留原因果顺序。例如 R1 tool result、U2、R2 tool result 必须仍按这个顺序出现,不能把全部输入和全部 Tool 结果分成两堆。 +4. 未闭合 Tool call 在 interrupt/crash repair 时,先由 current Run capability append 一个 canonical structured `outcome_unknown` Tool-result Message,再关闭原 Step/Run;于是 pair 可按原序编入 ModelHistory,模型明确看见“外部可能已发生”。没有 terminal Message 的 pair 不能 close、不能编入历史。 +5. 忽略其他 open Turn 的内部 Message;它们只有先通过 `transcript/add`,并且 event seq 不大于 cut,才会进入后续 history。 -`/stop` 不进入 Handler。Channel Adapter 在协议边界识别它,使用当前 `interrupt_token` 调 RunHost;它不创建 Message,也不触发 Prompt 或 Memory hook。 +因此同 Session 的 background task 可以和 conversation 交错,却不会因为“恰好写在前面”污染聊天模型。Task output 是否成为未来 history 只由 `transcript/add` 决定,不由 source 名、role 或 policy 反推。带 `TranscriptThrough` 的 Run 仍独占 Session history-reading lane;`NoPriorTranscript` 的 Run 只读自己 Turn,可以并发。 -默认 passive handler 本身也是普通插件,它可以直接写成: +TurnState 还从 `outcome_unknown` 派生 unresolved-effect fence。fence 存在时,CallPermit 默认拒绝新的 effectful Tool;无法可靠区分 read-only/effectful 时就拒绝全部 Tool。只有 owner query 后追加明确 resolution,或用户用专门 permit 追加 `tool/uncertain-override`,才可再次产生外部效果。模型自己说“重试”不构成授权。 -```python -async def handle(incoming: Incoming) -> None: - if await inbound_filters.ignore(incoming.view): - return +### 4.3 Transcript 才是“聊天页” - run = await incoming.accept() - command_reply = await commands.try_handle(run, run.current_input) - if command_reply.handled: - await run.complete(command_reply.drafts) - return +Message 被写入 Session,不等于用户已经看见。可见性由引用型事件表达: - drafts = await run.react() - await run.complete(drafts) -``` +~~~text +transcript/add { message: MessageRef } +transcript/remove { message: MessageRef, reason } +~~~ -`inbound_filters` 和 `commands` 可以是这个插件依赖的普通 registry;Core 不知道有哪些 filter 或 command。顺序由默认 Handler 明确拥有,不能再散成无 owner 的 waterfall。替换整个 Handler 就能替换这套入口策略。 +这些事件不复制正文。每条 transcript 变化各占一个 Session event seq;conversation seal 可以在一个 append batch 中连续写 U1、U2、A 的三个 add。 -### 5.4 `Run` 只暴露必要端口 +Transcript fold 强制每个 Message 只能 `unseen → visible → removed`:不能重复 add、不能 remove unseen、不能在 remove 或 Message Gone 后重新 add。add/remove 与被引用 Message 必须同 Session。 -```python -class Run: - ref: RunRef - turn: TurnView - current_input: MessageView - runtime_history_view: RuntimeHistoryView - signal: CancelSignal - stream: StreamPort - resources: RunResources +手机和 Web 使用: - def bind_model(self) -> ModelExecution: ... - def tool_port(self) -> ToolPort: ... - async def react(self) -> DraftBatch: ... - async def complete(self, drafts: DraftBatch) -> CompletedTurn: ... -``` +~~~text +message_id = referenced MessageRef 的不透明编码 +seq = transcript/add 或 transcript/remove 的 event_seq +cursor = 客户端已经扫描完成的 Session event high-water +~~~ -这些字段都有独立 owner: +于是: -- `turn` 和 `runtime_history_view` 来自 ConversationStore 的冻结只读视图; -- `bind_model()` 第一次调用时从 exact Root 内普通 models 插件冻结一份 `ModelExecution`,重复调用返回同一份; -- `tool_port()` 只暴露 `TurnGrant ∩ exact Root`,不返回全局 registry; -- `stream` 只发 live delta,不建立持久真相; -- `resources` 只登记必须在本 Run 结束前关闭的短期资源; -- `signal` 只表示当前 Run 是否还活着。 +- `changes(after=cursor)` 扫描更高 Session events,只返回其中的 transcript changes,并返回本次扫描到的 `next_cursor`;即使没有 delta,cursor 也能前进; +- transcript `seq` 是稀疏但严格递增的 change watermark,客户端不得要求 `seq + 1` 连续; +- upsert/remove 仍按 message_id; +- U1…Un+A 在同一事务一起发布; +- 删除用更大的 seq 到达,不会被旧 cursor 漏掉; +- 不需要 projection_id、row alias 或第三个持久同步身份。 -Run 明确没有: +### 4.4 Projection policy 留在 Session,不另建 Grant 系统 -- `history` 这种无修饰名字; -- 任意 `metadata` 或 `extra` 袋子; -- 任意 SQL 或全功能 Session repository; -- `loop_input`; -- 通用 `commit_intents`; -- 任意 Root lookup; -- 改 TurnGrant 的方法。 +“同样的 user + assistant 形状,是否允许写长期记忆”是一个真实独立轴,不能由来源名猜,也不能继续塞进全局 `post_commit` boolean。 -`run.react()` 只调用本 Run exact Root 中的 `REACT`。`run.complete()` 只把经过结构校验的 DraftBatch 交给 ConversationStore;REACT 插件不能自己写 Session。 +最小表达是一条 Session event: -模型和 Tool 端口按使用绑定,缺失时在第一次调用处 fail-loud,而不是在 Run admission 时伪造空实现。于是 echo Handler 可以完全不安装 LLM/Tool;一个要用模型的 REACT 则必须声明并验证自己的 models 依赖。 +~~~text +projection/policy = { + turn: TurnRef, + allow: ProjectionKey[] +} +~~~ -### 5.5 `DraftBatch` +- event_seq 本身就是 policy revision,不再生成 grant id。 +- policy owner 在 `turn/open` 同一 batch 写入;来源和模型不能扩权。 +- 未知 key 默认拒绝。 +- 普通 human conversation、内部 validation、user-visible task 可以拥有不同 key 集合,但按“允许什么”表达,不按 Wake/Scheduler 名字表达。 +- 撤销时追加更高 seq 的 policy event;对应 projector 顺序应用并清理。 -```python -DraftBatch = tuple[DraftMessage, ...] -``` +v1 建议只定义当前有真实 consumer 的 key: -对 `REACT` 的返回: +| key | 允许的输出 | +|---|---| +| `message_embedding` | 为已发布 Message 写检索 embedding | +| `long_term_memory` | 把 conversation 纳入长期事实图 | +| `profile_memory` | 更新 SELF/MEMORY 类用户画像 | -1. 至少有一条输出 Message。 -2. 恰好一条 terminal assistant。 -3. 每个 DraftMessage 在交给外部 effect 前由 RunHost 获得稳定 MessageId。 -4. Draft 不是 Session 事实;只有 `complete()` 事务成功后才进入 Session。 -5. stream delta、thinking 和未封口 Tool 调用都不是 DraftMessage。 -6. DraftMessage 不能指定 SessionId、placement、TurnId 或 TurnGrant。 +Transcript 本身不受这张表控制;`transcript/add/remove` 已经是明确可见性事实。Compaction 也不受它控制;compaction 只是 ModelHistory 的无损替换 projection,不能删原文。 -命令可以由 Handler 直接构造 DraftBatch 后 `complete()`,不必调用 `react`。如果一个已经有输入 Message 的命令明确选择不回复,Handler 可以调用 `complete(())`,Turn 结束为 `completed(no_reply)`;空输入加空输出仍被拒绝。这不需要 Core 的 `Skip`、`Enter` 或 `Return` 控制对象。 +不再需要独立 `ProjectionGrant` relation,也不需要 Core 全局 `ProjectionClaim`。纯 fold 只需 source cursor;真正会向 Session 外写入的 materializer 必须在自己的 effect 边界解决不确定性。 -### 5.6 Run 内部状态机 +### 4.5 异步 projection 如何不迟到写回 -```text -created - │ - ├── start ── active ── react succeeded ── completing ── succeeded - │ ├── interrupt ───────────────────── interrupted - │ └── error ───────────────────────── failed - └── admission error ──────────────────────────────────── failed -``` +先分两类,不能拿一种协议冒充另一种: -ConversationStore 只有在 `completing` 时接受一次 complete。重复 complete 必须按同一 draft identity 幂等;内容漂移 fail-loud。 +1. **纯 fold / 同库 materialization**:apply 无 I/O;若结果与 cursor 能在同一个 SQLite transaction 提交,`last_applied_seq` CAS 足够。 +2. **外部 materialization**:embedding、图数据库或远程索引已经越过本地事务边界;owner 必须有窄的 durable effect journal。 -### 5.7 Session lane 是 Host 细节 +每个 effectful projector 自己保存: -同一 Session 的 conversation Turn 串行,不同 Session 可并发。用于排队的 key 是稳定服务 namespace 加 SessionId,例如: +~~~text +ProjectionEffect = ( + projection_key, + subject_ref, # same-Session MessageRef / TurnRef, or the SessionId itself + source_event_seq, + desired_version, + effect_key, + state = prepared | started | applied | cleanup_started | cleaned | uncertain +) +~~~ -```text -("conversation", SessionId) -``` +这不是 Core 的通用 Claim,也不保存正文。它只回答一个无法从 Session 推导的事实:“远端这次写到底发生了没有”。协议是: -它不能包含 generation、provider、模型或 Handler 实例身份。否则热换后旧、新 Root 会各拿一把锁,导致同一 Session 意外并发。 +1. 同一 subject_ref 串行推进 `desired_version`;effect journal 的 `prepared` 必须在网络 I/O 前持久化。 +2. 外部 object key 由 `(projection_key, subject_ref)` 派生;operation key 再加 `desired_version`。provider 必须支持幂等写、按 key 查询或条件版本中的至少一种。 +3. I/O 前写 `started`;crash 后先 query/retry same key。无法判断时进入 `uncertain`,不能假装没写。 +4. 更高 seq 的 remove/revoke/redaction 先提高 desired version,阻止旧 `prepared` 开始;已经 `started` 的旧写必须先被查询或收口,再执行 cleanup。 +5. cleanup 使用相同 object key 和更高 desired version。只有远端确认 cleaned、或查询证明对象不存在,owner 才推进 cleanup cursor。 +6. provider 不提供幂等、查询、条件版本或可靠删除时,不得承载需要可证明删除的 projection;既有 uncertain 会阻塞删除完成并交给人工处置。 +7. Data Management 只有在所有相关 owner 越过 deletion seq,且没有 `started/uncertain` 后才报告完成。 -`RunLock` 不进入公共 API。插件只看见 `accept()` 成功、busy 或取消,不持有锁对象。 +所以迟到写只有三种可诚实处理的结局:先完成再被 cleanup、被高版本 fence 拒绝,或进入明确 uncertain 并阻塞完成。一个本地 CAS 不能证明远端没有发生;v4 不再作这个错误承诺。 --- -## 六、`default-react` 普通插件 +## 五、Turn 怎样工作 -### 6.1 它完整拥有默认算法 +### 5.1 第一次普通输入 -```text -runtime history view - │ - ▼ -CONTEXT_VIEW ── prompt history - │ -PROMPT_PARTS ── immutable prompt parts - │ -TOOL_SELECTOR ─ visible tool names - │ - ▼ -build one frozen model request - │ - ▼ -Model stream - │ - ├── no tool call ── terminal DraftBatch - │ - └── tool calls ── ToolPort ── append run-local closed facts - │ - └── next model call -``` - -伪代码: - -```python -async def react(run: ReactRun) -> DraftBatch: - local = DefaultReactState() - model = run.bind_model() - tools = run.tool_port() - - while True: - prompt_history = context_view.project( - runtime_history_view=run.runtime_history_view, - closed_run_facts=local.closed_facts, - ) - prompt_parts = await prompt_parts_registry.collect(run, local) - tool_view = await tool_selector.select(run, local) - request = freeze_request( - model=model, - prompt_parts=prompt_parts, - prompt_history=prompt_history, - tools=tools.schemas(tool_view.names), - ) - reply = await model.stream(request, run.stream, run.signal) - - if not reply.tool_calls: - return seal_terminal_reply(reply) - - results = await tools.execute_visible( - calls=reply.tool_calls, - visible=tool_view, - signal=run.signal, - ) - local.append_closed(reply, results) -``` - -Step 是这个算法内部的循环计数,不进入 Core 领域模型。另一个 REACT 插件可以一次模型调用就结束、完全不用 Tool,或使用 plan/execute;Core 都不需要新增分支。 - -### 6.2 Prompt 是不可变贡献 - -`PROMPT_PARTS` 是 `default-react` 依赖的普通 registry,不是 Core 的第三条控制轴。 - -每个贡献是: - -```text -PromptPart = (key, kind, order, content, source_ref, trust) -kind = instruction | context -``` - -规则: - -1. 插件只能返回自己的新 PromptPart,不能取得共享可变 Prompt。 -2. 一次模型调用收集完后整体冻结。 -3. 排序键只保证重放稳定,不证明两个 instruction 在语义上可交换。 -4. 重复 key、互斥 slot、越过字节预算和非法 trust 提升在 candidate Gate fail-loud。 -5. 当前 user Message 永远独立,不被 context 插件改写。 -6. Memory、skill 和检索内容必须带来源与 trust,不能伪装成用户原话。 - -稳定排序可以使用: +Channel 先校验 wire Envelope。MESSAGE_HANDLER 可以拒绝或处理命令;只有 accept 才追加 Session: -```text -(kind_order, order, plugin_id, key) -``` +~~~text +一个 Session append batch +├── turn/open(TurnGrant) → TurnRef +├── projection/policy +├── message/append(U1, Admission { +│ source, reply_target }) → MessageRef +└── run/open(history_cut, Root, + reply_to=U1) → RunRef +~~~ -但 candidate profile 必须额外证明语义兼容。不能用“排序是确定的”替代“组合是合理的”。 +逻辑 `AdmissionKey = (source_owner, source_ref)` 必须包含 owner 所需的 channel/session scope,不能假设裸 client id 全局唯一。持久索引不保存 raw key,而保存 `AdmissionToken = (key_version, HMAC(key_version, "admission" + canonical AdmissionKey))`,由 Session Store 在边界计算并在 append 时强制唯一。固定 domain tag 防止两类 token 互相碰撞。 -### 6.3 `CONTEXT_VIEW` 只产生 prompt history +相同 AdmissionToken 与相同已校验 payload 重投时返回原 MessageRef;同 token 不同 payload fail-loud。Message 已被用户删除时,token tombstone 返回 Gone,不能重新创建正文。不能把可逆 source_ref 偷放进 token。 -输入与输出必须写全名: +key rotation 也属于 identity contract:Store 对 raw AdmissionKey 用 active 与尚未退休的 key versions 计算候选并一次查询;任一候选命中就返回原 MessageRef/Gone,只有全部未命中才用 active version 新建。只要某版本仍有 live token 或 tombstone,其 HMAC key 就必须保留在受备份的 lookup keyring;整个 Session 已进入 SessionGone 且该版本再无引用后才能退休。轮换前后 redelivery 必须命中同一结果,不能借换 key 复活 Message。 -```text -persistent history Session 中完整已提交正文 -runtime history view 当前 Run 可读取的冻结工作视图 -prompt history 本次模型调用真正使用的投影 -``` +若 source transport 必须 durable 地保管 Envelope,handoff 采用前向恢复:先以 AdmissionKey append Session,再把 source attempt 原子替换成 `admitted(MessageRef)` 并 ACK/擦除 Envelope body。若两步之间 crash,重试第一步只返回同一 MessageRef,再完成 cleanup;source copy 在此期间只是不可投影、不可寻址的 transport recovery buffer,不是第二个 Message。它不能在 admitted 后按自己的正文重新驱动 Turn。 -`CONTEXT_VIEW` 只能: +### 5.2 interrupted continuation -- 从 runtime history view 选择完整逻辑单元; -- 做通用 token 预算与 compaction; -- 保留当前 user anchor、成对 Tool 事实与外部效果证据; -- 返回带来源的 prompt history。 - -它不能: - -- UPDATE/DELETE Session Message; -- 改当前输入; -- 改 Prompt instruction; -- 改 ToolGrant 或 ToolView; -- 改模型绑定; -- 用空列表掩盖损坏数据。 - -Akasha 继续是普通 PromptPart/Tool/SessionReceipt consumer,不成为 CONTEXT_VIEW 内部分支。 - -### 6.4 ToolGrant 与 ToolView 完全分开 - -```text -TurnGrant ToolView -───────────────────────── ───────────────────────── -安全能力上界 本次模型调用展示哪些 schema -Turn 建立时冻结 每次模型调用可变化 -只能被交集缩小 可以搜索、排序、分页 -由 admission/policy 拥有 由 TOOL_SELECTOR 拥有 -执行时必须检查 只影响模型可发现性 -``` - -真正执行一项 Tool 必须同时满足: +~~~text +Turn T +├── message U1 +├── Run R1 interrupted +├── message U2 +├── Run R2 interrupted +├── message U3 +├── Run R3 completed +├── message A +└── Turn completed +~~~ -```text -tool ∈ TurnGrant -tool ∈ exact Root catalog -call permit accepts arguments -``` +U2 到达时,在一个 batch 里用精确 RunRef 关闭 R1、在 U2 的 Admission 元数据捕获新 ReplyTarget、在同一个 TurnRef 下追加 U2,并打开 R2。R2 的 `reply_to=U2`;即使 U1、U2 来自不同 thread,也不会到 seal 时猜。新 Message、新 Run,不新建 Turn。 -Tool 是否显示不能授予权限。Tool 没显示但模型凭名字猜到时,`default-react` 返回明确 `tool_not_visible`;这只是模型协议错误,不是安全拒绝。Tool 不在 grant 时返回 `tool_not_allowed`,两者不能混成一句字符串。 +这是 Store invariant,不是调用约定: -ToolSelector 收到的候选集已经是 `TurnGrant ∩ exact Root catalog`。它不能枚举、搜索或泄露 grant 之外的 Tool metadata;`tool_search` 本身也必须同时在 grant 和初始 ToolView 中,profile 缺少这条闭环时在加载期失败。 +- 调用者提交 `expected_current_run`;single writer 在锁内 CAS 当前值。 +- 有 active Step/Run 时,batch 必须先把每个未 terminal ToolCall 变成真实 result 或 canonical `outcome_unknown` result,再合法关闭 exact Step/Run,随后 append 新 Admission,最后 open 后继 Run。 +- 没有 active Run 时,只有 latest Run 已明确 `interrupted` 且 Turn 仍 open,才可声明 `expected_current_run=None + expected_latest_run`,并把 Admission 与后继 `run/open` 同批提交。 +- 后继 Run 的 Turn 必须相同;新 Admission 带 ReplyTarget 时,`reply_to` 必须是这条新 MessageRef。 +- idempotent redelivery 只返回既有 MessageRef,不重复 interrupt/open;两个真正的新输入竞争时由 CAS 串行成 R2、R3。 +- bare Admission、错误 expected ref、只 append 不开后继 Run、或让后继继续 reply_to 旧 Message 都 fail-loud。 +- latest Run 是 failed 时,普通 Admission 不得留在旧 Turn;它走 5.3 fresh,原子 close(superseded) 并创建新 Turn。只有显式 retry 可以在旧 Turn 无新 Message地 open 新 Run。 -### 6.5 ToolSearch 是完整普通插件 +因此 R1 的 ModelHistory 永远不会在运行中突然看到 U2;R1 的旧 callback 也会同时被 Run/Step fence 拒绝。 -`tool-search` 插件拥有: +最终 seal 的一个 batch 追加: -1. `TOOL_SELECTOR` provider; -2. `tool_search` 这个普通 Tool; -3. 搜索索引与排序; -4. 当前 Run 的 selection state; -5. provider schema 上限下的展示策略。 +~~~text +message/append(A, run=R3, step=Sfinal) +run/close(R3, completed) +turn/close(T, completed) +transcript/add(U1) +transcript/add(U2) +transcript/add(U3) +transcript/add(A) +delivery/prepared(A, reply address, exact binding) +~~~ -它不拥有: +整个 batch 成功或失败,不会出现半个聊天 Turn。 -- ToolGrant; -- Tool registry; -- Tool 执行; -- Agent 重启授权; -- Core metadata flag; -- 跨 Run 的隐式已解锁集合。 +### 5.3 retry 与 fresh -`tool_search` 返回选择结果并改变该插件自己的 Run-local selection state。新 Tool 只在下一次模型调用进入 ToolView。卸载它后,可以换成一个 `show-all-selector` 或 `fixed-selector`,不改 Core 和 REACT 接口。 +~~~text +latest Run failed +├── explicit retry(prior_client_message_id, command_id) +│ ├── 不创建 Message +│ └── 同 Turn 追加一个新 run/open +└── ordinary fresh(new source_ref) + ├── 旧 Turn close(superseded) + └── 新 Turn、新 Message、新 Run +~~~ -目标态删除当前这些耦合: +- prior_client_message_id 只在原 channel/session identity 下解析到 AdmissionKey,再解析到最后一个 human MessageRef。 +- unknown、cross-scope、not-last 与 Gone 分别 fail-loud。 +- command_id 只让 retry 命令幂等,不成为 MessageId。 +- 正文相同不等于 retry。 -- `tool_search_enabled`; -- `always_on/preloadable/requires_turn_search` 驱动的 Core 分支; -- Turn ContextVar 中的 search grant; -- ToolSearch 与 `agent_restart` 授权绑定; -- Reasoner 内部 provider schema 挤压特判。 +retry 与 continuation 的区别不靠 `run_reason` enum:两次 Run 之间有新 Message 就是 continuation;有已接纳 retry command 而没有新 Message 就是 retry。 -必要的“默认总要展示哪些 Tool”由 selector 配置表达,不再成为 Tool 的安全属性。 +### 5.4 seal 与迟到完成 -### 6.6 三种 retry 不能混在一起 +RunWork 只能用 current RunRef/StepRef seal。Session Store 在同一个写锁内 fold 当前 TurnState 并检查: -| retry | 身份 | owner | -|---|---|---| -| 同一 HTTP/model call 的网络重试 | 同一 Run、同一模型调用 | Provider plugin | -| context overflow 后重新投影并再次请求 | 同一 Run、新 model call | `default-react` + `CONTEXT_VIEW` | -| failed Run 后用户显式 retry | 同一 Turn、新 Run | ConversationStore/入口协议 | +- Turn 仍 open; +- active Run 正是调用者 RunRef; +- active Step 正是 final Draft 的 StepRef; +- history-reading lane 仍属于该 Run; +- final Draft 合法且只有一个 terminal assistant; +- Session 未进入 deletion state; +- Run 打开时固定的 `reply_to` Message 仍是 Live,且其 ReplyTarget 可由 Delivery owner 解析成 exact binding。 -任何 retry 都不能恢复隐藏思维,也不能重放结果不明的外部 Tool effect。 +seal 请求由 `RunRef + final content digest` 幂等识别。完全相同的重试返回同一 MessageRef;同一 RunRef 带不同 digest fail-loud。新 Run 已开始后,旧 RunRef 永远不能封口。 -### 6.7 不强制永久保存完整模型请求 +所有运行期 append 都走同一条检查:`request/bound`、`tool/call`、Tool result Message、`step/close` 和 final assistant Message 必须携带 RunRef 与 StepRef。Store 验证它们属于同 Session/Turn,且二者在该事件提交前都是 current,或在同一 batch 中合法 close。只带 TurnRef、只带 RunRef或带旧 StepRef 的迟到回调都没有写权限。 -每次模型调用必须留下足以问责的 binding receipt: +任何 RunWork 都可以 append assistant Message 后完成。只有 TurnGrant 含 `finish_without_output` 时,Core 才另发一个窄 `QuietCloser`;它还必须收到已提交领域 decision 的 DomainDecisionPermit,才可完成而不追加 assistant Message。 -```text -RunRef + call_seq -Root logical digest + root instance token -ModelExecution identity -PromptPart refs/digests -prompt history source refs -ToolView names/schema digests -compaction generation -provider outcome/usage/request id -``` +普通 listener 的 return 只结束 listener,不能关闭 Turn。 -完整 Prompt 和完整 provider payload 只在显式诊断开关下保存,并且必须有容量、保留期、权限和脱敏规则。它不是 v4 的永久事实要求。 +### 5.5 crash repair -Root 使用两个身份: - -- logical digest:证明插件拓扑和 catalog 内容相同; -- instance token:区分两个物理 Root 实例,避免把重建后的对象当成原对象。 +- unmatched current Step 先为无 terminal result 的 ToolCall append structured `outcome_unknown` Tool-result Message,再在一个 repair batch 关闭 Step 与 `run/close(interrupted: process_lost)`;不自动重跑模型、Tool 或 provider。 +- conversation Turn 保持 open,等待显式 continuation、retry、fresh 或 abandon。 +- task Turn 的恢复由 source owner 根据自己的 durable decision 选择继续或关闭。 +- 下一 Run 从 Session 按原序重放本 Turn 所有既有 Run 的已闭合 Tool call/result,包括明确标为 unknown 的 result;unresolved-effect fence 与 Tool effect ledger 共同阻止盲重试。 +- crashed Run 的旧 Root 不跨进程复活;下一 Run 绑定当前已提交 generation。 --- -## 七、提交、恢复与外部效果 - -### 7.1 被动 Turn 的正常完成 - -```text -1. Incoming.accept - └── durable Turn/input facts + RunRef;尚未进入 Session - -2. REACT - └── run-local model/tool facts;Session 正文不变 - -3. Delivery Owner 可先 prepare 一个不执行外部调用的 envelope - -4. Run.complete,单个 SessionDB 事务 - ├── 验证 Turn open 且 Run 是当前运行 - ├── 给 session_batch(T) 分配连续 seq - ├── INSERT session_batch(T) messages - ├── 写 Turn completed(reply_message_id | no_reply) - ├── 写 Run succeeded - ├── 更新 session metadata/head - └── session_batch 非空时 INSERT SessionReceipt - -5. 事务提交后发布 Receipt wakeup - -6. Delivery Owner 根据 prepared envelope / Receipt 发送 -``` +## 六、Proactive 从 Core 消失 -第 5 步只是唤醒优化。进程在第 4、5 步之间崩溃,插件和 Delivery Owner 仍能按 `feed_seq` 追上。 +### 6.1 两个 quiet path -如果第 4 步失败: - -- Session 不出现任何本 Turn Message; -- Turn 不变成 completed; -- Draft MessageId 仍只属于 pending/run 事实; -- 外部发送不能开始。 - -pending 正文与 Session 正文不能长期成为两个 owner。目标存储在同一事务中先校验并 INSERT `session_batch(T)`,再把对应 pending slot 收敛为 `MessageId + digest` 引用;事务失败时 pending 原文保持不变。`run-only` Task Message 不做这次 handoff,它的正文与 retention 继续由创建它的来源/Turn 事实 owner 管理。具体表结构与旧数据处理必须在持久化 ADR 中批准。 +~~~text +source timer fires + │ + ├── source ledger 写 attempt + │ + ├── no_due / admission rejected + │ └── 关闭 source attempt;Session 完全不变 + │ + └── due + └── 在目标 Session 追加普通 Turn + plugin-source Message + Run + │ + ├── domain decline / skip + │ ├── source/domain decision event + │ ├── run/close + │ └── turn/close;无 assistant、无 transcript、无 delivery + │ + └── share + ├── assistant Message + ├── turn/close + └── Delivery +~~~ -### 7.2 中断或失败 +第一个 quiet 是“没有开始一次 Session 工作”;第二个 quiet 是“一次工作正常结束,但没有可发布 Message”。它们不需要一个共同 `proactive_skip` 字段。 -中断只原子结束当前 Run: +### 6.2 来源私有的仍归来源 -```text -active Run → interrupted -Turn → open -Session → unchanged -``` +Wake 继续拥有 TimerAttempt、due、watermark、Content pool、Drift 顺序、业务 decision 与 ACK。Scheduler、Spawn 和 message_push 也各自拥有自己的 trigger ledger。 -失败同理。下一条输入如何处理由已接受规则决定: +Core 只看到普通 event vocabulary: -- interrupted 后普通输入:同 Turn、新 Message、新 Run; -- retryable failed 后显式 retry:同 Turn、复用原 Message、新 Run; -- failed 后普通 fresh:旧 Turn `superseded`,新 Turn。 +~~~text +turn/open → message/append → run/open → ... → turn/close +~~~ -Run terminal 必须先落 durable control fact,再释放 Root lease。不能先把内存指针清空后假装已经终结。 +没有 `proactive`、`wake`、`scheduler`、`spawn profile` 或 source enum 改变 Turn/Run 状态机。 -每条 active Run 还记录创建它的 runtime boot identity。进程重启时,ConversationStore 在取得唯一恢复 owner 后,把不存在 live owner 的 active Run 原子终结为 `interrupted(process_lost)`,保持 Turn open,再释放 session lane。它绝不自动重跑模型、Tool 或 Delivery;这些领域分别按自己的 receipt 对账。 +### 6.3 可见、记忆和发送各看自己的事实 -### 7.3 Tool 外部效果 +| 问题 | 唯一依据 | +|---|---| +| 用户是否看见 | transcript/add/remove | +| 模型是否看见 | ModelHistory projection 规则 | +| 是否写长期记忆 | projection/policy + Memory 自己的 eligibility | +| 是否应发送 | delivery/prepared 与 destination contract | +| 为什么开始 | message source → source ledger | -不建立通用 `CommitIntent`。 +这样 assistant-only 主动消息不是特殊 Message。它只是一个没有 human transcript input、但有 assistant transcript/add 的 completed Turn。 -一个可能写外部世界的 Tool 自己保存: +--- -```text -(turn_id, run_seq, call_id, operation_key, state, domain_receipt) -``` +## 七、Delivery 也不携带 Message + +### 7.1 ReplyTarget 与 exact binding + +回复位置也只捕获一次,但它不是 Message 正文,也不需要自己的 ID。Channel Host 或 source adapter 用受限 admission capability,把 opaque ReplyTarget 写进输入 Message 的可擦 privacy slot: + +~~~text +ReplyTarget = { + logical_channel, + logical_address, + credential_handle +} +~~~ + +ReplyTarget 不固定实现 generation,不包含 secret,也不复制输入 Message。v4 的每个 Turn 必须由至少一条 Admission Message 开始;`run/open.reply_to` 只能选择当前 Turn 内、带已授权 ReplyTarget 的 Admission Message。普通 conversation 选择触发本 Run 的最新输入;retry 复用原输入。Task/Wake/Scheduler 也由自己的 source permit 在 Admission Message 上写 target,不能让模型构造地址;没有 `reply_to` 的 Run 只能完成而不发送。 + +seal 前,Delivery owner 从 `reply_to` Message 的 metadata 解析当前可用的 exact target,并取得短 lease;seal batch 才追加: + +~~~text +delivery/prepared = { + turn: TurnRef, + message: MessageRef, + reply_to: MessageRef, + exact_binding_slot: + Live { + exact_artifact, + exact_generation, + adapter_contract_version, + destination_mapping, + credential_handle + } | Gone { redaction: EventRef } +} +~~~ + +DeliveryRef 就是该 event 的 EventRef。调用者不提供 idempotency key;worker 只在 prepared 已提交后,从 `DeliveryRef` 派生 provider operation key 再开始 I/O,因此没有“先知道自己的 event seq”分配环。若 batch 失败就释放临时 lease;提交后 lease 转给 durable Delivery lifecycle。 + +这比 admission 时长期钉住 adapter 更合理:一个 Turn 可以经过多次 Run 和 generation reload;真正产生发送义务时才冻结实现。一旦 prepared,恢复必须用 exact binding,找不到就 fail-loud,不能换成当前 adapter 猜。 + +### 7.2 状态是 delivery events 的 projection + +~~~text +delivery/prepared + ├── delivery/canceled + └── delivery/provider_started + ├── delivery/rejected + ├── delivery/uncertain + │ ├── delivery/rejected(query 证明未发送) + │ ├── delivery/delivered(query 证明已发送) + │ └── delivery/abandoned_uncertain(用户明确接受未知) + └── delivery/delivered + └── transcript/add(U1…Un+A,同一 batch;provider-first 时) + └── delivery/settled +~~~ + +每次 transition 追加一个引用 DeliveryRef 的事件。DeliveryState fold 拒绝非法跳转;Session single writer 和当前 state CAS 防止两个 worker 同时推进。 规则: -1. Tool owner 在调用前固定 operation key。 -2. started/succeeded/rejected/uncertain 由 Tool 自己的 durable store 表达。 -3. Run 重试先查这个 owner 的 receipt,不盲目重放。 -4. Turn 完成后,该 Tool 如需消费 SessionReceipt,使用自己的 cursor。 -5. Core 不理解支付、发信、文件写入、重启或日历事件 payload。 +1. prepared 引用 MessageRef、完整附件集合和 exact target binding,不复制正文。 +2. provider_started 必须在网络 I/O 前提交。 +3. provider 没有幂等键或查询能力时,crash 后只能 uncertain,不能盲发。 +4. delivered 保存去掉正文、preview 和附件内容的 provider receipt;它不能因后续 Session projection 失败而回滚。 +5. provider-first 只有拿到 receipt 后,才在一个 Session batch 追加 delivered 与该 completed Turn 的全部 `transcript/add(U1…Un+A)`;不能先露出半个 Turn。 +6. session-first 在 seal batch 已追加 transcript/add,通知失败不能撤销 Message。 +7. settled 只表示本地与来源 ACK 已前向收口,不改写 provider 历史。 +8. prepared 到 settled/rejected/canceled/abandoned_uncertain 之间,exact target 不能退役。 +9. uncertain 只能由 provider query/recovery 进入 delivered 或 rejected;没有证据时,只有显式用户授权才能追加 abandoned_uncertain。该事件只结束本地等待,审计仍记“外部可能已发生”,不能声称回滚或未发送。 -这既避免外部效果丢失,也避免 Core 变成第二个工作流引擎。 +顺序由 destination contract 决定,不由 passive/proactive 名字决定: -### 7.4 SessionReceipt consumer +~~~text +canonical Akashic client: Session-first +external provider: Provider-first +~~~ -```text -SessionReceipt feed - ├── Akasha cursor/data - ├── Markdown memory cursor/data - ├── FTS/embedding cursor/data - └── UI/notification cursor/data -``` +### 7.3 deletion 与 delivery -每个 consumer 独立: +- prepared 且尚未 I/O:先追加 canceled;同一 maintenance transaction 再把 exact binding slot 转 Gone,之后才删除正文。 +- provider_started 或 uncertain:删除阻塞,直到 provider 查询确认结果,或用户明确授权 `abandoned_uncertain` 并接受外部副本可能仍存在。 +- delivered 但尚未 transcript/add:删除流程追加 `settled(projection_gone)`,保留 delivered 事实,不再把 Message 放进 Transcript。 +- settled/rejected/canceled/abandoned_uncertain:Data Management 把 exact binding privacy slot 转 Gone,只保留无正文、无地址的效果 identity、最小 receipt 和必要 uncertainty audit。 -- 自己决定是否适用; -- 自己保存 cursor; -- 自己保证同 receipt 幂等; -- 自己报告 lag/failure; -- 一个 consumer 失败不回滚 Session,也不阻止其他 consumer。 +Delivery 发现 Message tombstone 时返回 Gone,不得重建或另存正文。 -新装或升级后的 consumer 不从“第一条 Receipt”盲扫全历史。它使用 ConversationStore 的窄只读操作在一个一致性读点取得: +--- -```text -canonical Message snapshot + current feed_seq watermark -``` +## 八、删除与 projection 收口 + +### 8.1 正常路径只追加 + +- Session event envelope、坐标和非内容元数据正常只追加。 +- Projection cache、索引、Mobile Room、Web state 和 compaction 可以随时删除重建。 +- context 裁切、容量优化、插件 reload 和 cache cleanup 无权改写 Session Message。 +- source ledger 与 provider effect ledger 按自己的 retention contract 管理,不由 Session cache cleanup 删除。 + +为同时满足“正常只追加”和“用户可以真正撤销正文”,Session Store 明确定义一个、也只定义一个例外:内容型 event 的 payload 是 Session-owned erasable slot。 + +~~~text +StoredMessageEvent = { + immutable: event_seq + turn + role + producer refs / admission_token, + body_slot: Live(MessageBody) | Gone(redaction_event_seq), + private_slot: Live(SourceRef + ReplyTarget) | Gone(redaction_event_seq) +} +~~~ + +普通 writer 只能创建 `Live`,永远不能 UPDATE。只有 Data Management maintenance transaction 能把 content/privacy slot 的 `Live → Gone`;不能反向恢复,也不能换成另一段正文或地址。`read_events` 和 `get_message` 先解析 slots:历史位置已经 Gone 时,重放从该位置直接得到 tombstoned Message,不会先遇到一个缺 `content` 的坏事件。 + +### 8.2 删除一个 Turn 的 Message + +用户明确撤销后: + +~~~text +建立可验证备份与影响预览 + │ +关闭相关 Run / Delivery;query 或显式覆盖 unknown Tool effect + │ +一个 Session maintenance transaction +├── transcript/remove × N +├── redaction/apply × N +├── turn/redacted +└── 同事务把对应 Message body/private 与 + terminal Delivery exact-binding slots: Live → Gone(redaction seq) + │ +清理 attachment object 与各 durable materializer +并让 source owner 擦除 Envelope/recovery payload + │ +append redaction/verified;全部 ack 后才报告完成 +~~~ + +通用 redaction event 只命名要擦的 owner 与 slot: + +~~~text +redaction/apply = { + targets: [ + { owner: MessageRef, slot: body | private }, + { owner: ToolCallRef, slot: arguments }, + { owner: DeliveryRef, slot: exact_binding }, + { owner: EventRef, slot: declared_dynamic_context } + ], + reason, + audit_principal_token +} +~~~ + +event 不保存旧正文、旧地址、附件内容或默认 content digest。event vocabulary 为每种 event 声明允许擦除的 slot;任意字符串、任意 SQL 或不属于 Data Management scope 的 target 都 fail-loud。`get_message` 在 body Gone 后返回 Gone,不伪装成 NotFound。 + +Session Store 在事务锁内先分配 redaction seq,再同时追加 redaction event 和写入 `Gone(redaction_seq)`。crash 在 commit 前,两者都不存在;crash 在 commit 后,两者都存在。integrity check 强制每个 Gone 指向同 Session、更高 seq、已提交且精确列出 `(owner_ref, slot)` 的 redaction event;反向也强制每个 target slot 已 Gone 且指回该 event。full replay 因而始终确定。 + +原始 event envelope 与非正文 Run/Delivery finality 可以保留;Session content/privacy slot 的 `Live → Gone` 是 append-only 的唯一破坏性例外。raw SourceRef、ReplyTarget、Delivery exact address/credential handle、Tool call 参数、一次性 dynamic context 和其他含用户内容的 event 都使用这种 erasable slot,分别变成 Gone;不存在可供删除的持久 `assistant/chunk` 或 rendered Prompt 副本。防重只保留不可逆 AdmissionToken,不保留 raw AdmissionKey。 + +迟到写不能复活正文:旧 Run 因 RunRef fence 不能 append;同库 projection 被更高 cursor 拒绝;外部 materializer 则按 4.5 先 drain `started/uncertain`、再 cleanup。仅仅推进本地 CAS 不算外部清理完成。 + +### 8.3 删除整个 Session + +`delete_session` 不是循环删行: + +1. 建立 SQLite backup、附件清单与 projection/effect 影响预览。 +2. 关闭新 admission,停止 history-reading lane。 +3. interrupt active Run,等待其 durable close。 +4. cancel 未 I/O Delivery;provider_started/uncertain 明确阻塞,除非 query 收口或用户显式接受 abandoned_uncertain。 +5. 对 delivered Delivery 保存外部 finality,并关闭未完成 transcript projection。 +6. 在 maintenance transaction 追加 session deletion/redaction、全部 transcript remove,并把 Message/Tool argument/dynamic context content slots,以及 SourceRef/ReplyTarget/Delivery binding privacy slots,原子转为 Gone。 +7. 擦除 attachment object、staging object、source Envelope/recovery payload、受控日志副本和其他已盘点用户内容;Request 从未保存 rendered Prompt 或 history body。 +8. 等所有同库 projection cursor 越过 deletion seq,并按各 owner journal 收口外部 materializer cleanup。 +9. append deletion/verified,只保留 SessionId、wire cursor shell、AdmissionToken/LegacyToken Gone 映射、最小非正文 effect finality 与删除 audit。 +10. 校验引用图、cursor high-water、backup retention 与 SQLite integrity 后才报告成功。 + +SessionId、EventRef、AdmissionToken 和 LegacyToken 永不复用。删除后的 retention 窗口内保留无正文 Session shell,使离线客户端收到更高 seq 的 remove 和 terminal `session_gone`;shell 物理 GC 后,最小 Session tombstone 仍让旧 cursor 得到 `SessionGone` 并清空该 Session,而不是当作一个从未存在的新 Session。服务端不能声称已经擦除离线设备无法控制的本地副本。 -插件先从 snapshot 建自己的投影并原子保存 cursor=watermark,再消费更大的 feed_seq。candidate 只能在隔离数据上演练这套过程;正式 promotion 后的新 Fiber 才继续 production cursor。这样插件是否安装、何时更新,与 Session 写入保持正交。 +--- -因此删除同步 `TurnCommitted` 大 fanout 后,Memory 故障不会卡住用户回复的外部 dispatch。 +## 九、公共 API 应该很小 -### 7.5 两种 Delivery 顺序 +### 9.1 Session Store 原子能力 -#### 被动回复与 Akashic/Web/Mobile 主动消息:Session first +~~~python +class SessionWriter(Protocol): + async def append_batch(self, build: EventBatchBuilder) -> CommittedBatch: ... + async def read_events(self, after: int, limit: int) -> list[SessionEvent]: ... +~~~ -```text -prepare envelope → complete Session → send/notify → settle delivery -``` +调用者不能发明 event_seq 或自行构造任何 typed ref。它只能使用 capability 已绑定的 committed ref,或 Builder 为本批较早事件分配的 typed local handle;Store 在锁内分配坐标,提交后返回真正 EventRef。 -发送失败不回滚 Session。Web/Mobile 以 Session head 和连续最大 seq 恢复,通知只负责更快看到更新。 +Store 在边界集中校验 JSON、event vocabulary、所有内部 ref 同 Session且指向正确类型、`turn/open` 初始 batch 至少一条有序 Admission Message且恰有一个后置 run/open、已有 Turn Admission 的 interrupt→append→successor 原子结构、运行产物的 current RunRef/StepRef、Turn/Transcript outcome、`reply_to` 指向本 Turn 的已授权 Admission Message、delivery transition、AdmissionKey uniqueness 和 deletion state。 -#### 外部渠道 proactive:provider first +### 9.2 一个 writer,加一个可选 quiet capability -```text -prepare envelope → provider call → durable provider receipt - → idempotent Session append → settle delivery -``` +~~~python +class RunWork: + async def react(self) -> FinalDraft: ... + async def interrupt(self) -> None: ... + async def seal(self, draft: FinalDraft) -> MessageRef: ... -只有完整 provider success 才追加 Session。部分成功或结果不明停在 Delivery Owner 的结构化状态,不能靠返回文案猜成功,也不能自动重复发送。 +class QuietCloser: + async def finish_without_output(self, decision: DomainDecisionPermit) -> None: ... +~~~ -这两种顺序属于 Delivery 领域的明确分支,不升级成通用 `CommitPlan` 平台。 +它们只是限制合法 append batch 的一次性 capability,不是持久 Turn 类型: -### 7.6 显式删除 +- 默认 RunWork 不能无输出 completed。 +- QuietCloser 只在 TurnGrant 明确允许时签发;没有已提交 decision 也不能使用。 +- 输入或输出是否进入 Transcript 只看 transcript events,不由 writer 类型决定。 +- 模型只产生 Draft,不选择 Session、event seq、message_id 或 destination;target 已由 Run capability 固定。 +- DomainDecisionPermit 只证明 source/domain decision 已提交,不是 Core 的 source enum。 -删除不是普通 Session 生命周期事件。它必须继续满足: +### 9.3 插件边界 -1. 用户主动发起名称明确的撤销/删除操作; -2. 精确到 completed Turn/interaction; -3. 执行前 SQLite online backup 与 `integrity_check`; -4. 在 session lane 和 compaction fence 下串行; -5. messages、embedding、compaction invalidation、audit 与 `interaction_removed` Receipt 原子提交; -6. Message seq 不复用; -7. 派生插件失败时不能继续提供撤销前的陈旧结果。 +默认 REACT 插件拥有 Prompt、Context、LLM、ToolView、ToolSearch、Tool loop、stream 和 Draft。Core 提供 Session append/read、Root lease、TurnGrant、CallPermit 与 writer capability。 -目标态 completed Turn/Run 只保留被删 Message 的 identity、digest 和非正文终态,不保留第二份正文。迁移前必须额外盘点当前 `turns.items/input`、tool trace、delivery envelope 和诊断文件中是否复制了内容;是否随“撤销对话”一起减少要逐类写进数据管理合同,不能由 v4 文档猜测。 +~~~text +ToolView 模型看见哪些 Tool +TurnGrant 这个 Turn 最多可做什么 +CallPermit 这一次调用真正获准什么 +~~~ -`SessionReceipt` 只能通知启用中的 consumer。永久删除某插件的 opaque plugin-data 需要单独的 Data Governance inventory:列出 owner、数据位置、备份、清除动作与证明。v4 不声称一个通用 receipt 能自动发现和清掉所有未来插件数据。 +三者不能合并。隐藏 Tool 不等于撤权,展示 Tool 不等于授权。 --- -## 八、Root、热更新与自我更新 - -### 8.1 一个 Run 一个 exact Root - -Root 在 Handler 开始前选择,并保持到 Run terminal 与短期资源 cleanup 完成。 - -同一个 Turn 的下一 Run 可以使用更新后的 committed Root: - -```text -Turn T -├── Run 1 → Root G10 → interrupted -└── Run 2 → Root G11 → completed -``` - -TurnGrant 不随 Root 更新而扩大。Run 2 的有效能力仍是: - -```text -TurnGrant(T) ∩ capabilities(Root G11) -``` - -如果新 Root 缺少 Turn 继续所需的 Service,admission fail-loud;不能偷偷回退到全局单例或旧插件。 - -### 8.2 candidate 只验证变化面 - -沿用当前增量 candidate 原则: - -1. 以 stable snapshot 为基线。 -2. 从变化插件和它真实依赖的 provider closure 建 candidate Root。 -3. 未变化插件不重启,不复制正式数据库,不运行正式 listener。 -4. candidate 使用只读或隔离的 plugin-data、临时端口和 staged event bus。 -5. 重复 Service、缺依赖、catalog 冲突、manifest 不一致在 latest-ready 前失败。 +## 十、每个事实只有一个 owner -不同变化面跑不同 probe: - -| 变化面 | 最小真实 probe | -|---|---| -| `MESSAGE_HANDLER` | 原始 envelope → accept/ignore/command;无模型假成功 | -| `REACT` | 固定 Message → 真实或可重放 model/tool trace → DraftBatch | -| PromptPart | 最终 part refs、顺序、digest、预算和冲突 | -| Tool/Selector | catalog、grant/view 分离、真实调用、参数拒绝 | -| Delivery | prepared/provider_started/receipt/uncertain 恢复 | -| Memory consumer | SessionReceipt replay、cursor、幂等、删除屏障 | - -“能 import”“manifest healthy”或“plugin-doctor 通过”都不能替代行为 probe。 - -### 8.3 自我安装与 promotion - -推荐流程: - -```text -owner Turn - │ - ├── staged install → immutable artifact → latest-ready - ├── programmatic child 用 exact latest 做真实 probe - ├── owner Turn 回复并完成 Session/Delivery - └── 释放自己的 stable Root lease - │ - ▼ -runtime-owned promotion operation - ├── fence 新 admission - ├── drain 旧 Run leases - ├── 停旧 formal Root - ├── 启新 formal Root - ├── commit stable pointer/snapshot - └── 失败则按 journal 重建旧 stable Root -``` - -不能在安装自己的 Turn 内同步等待旧 Root drain;该 Turn 自己正持有旧 lease,会形成环形等待。 - -### 8.4 drain 与 cleanup - -1. 指针切换后旧 Root 不接新 Run。 -2. 旧 Run 继续使用旧 Root,直到内部 terminal。 -3. Run 内短期资源全部逆序 cleanup 后才释放 lease。 -4. cleanup 失败时 Root 保持 `cleanup_failed/degraded` drain blocker,并保留全部错误。 -5. 不能像当前 `Effect._close()` 一样在 cleanup 报错后仍把 owner 移除并标为 closed。 -6. durable external operation 已交给领域 owner 后,不再阻塞旧 Root;没有 handoff 的 operation 必须阻塞。 - -### 8.5 Python 插件仍是可信代码 - -Root 隔离解决生命周期、状态污染和发布一致性,不是恶意代码沙箱。被安装的 Python 插件仍可执行宿主权限下的代码。 - -因此自我安装还必须保持: - -- 明确来源与 source revision/tree digest; -- immutable artifact; -- 用户或既有授权链; -- candidate 不接正式 secret/write port; -- 安装失败可恢复; -- 正式 promotion 有 journal 和旧代重建证据。 +| Owner | 唯一拥有 | 不拥有 | +|---|---|---| +| Session Store | event log、event_seq、batch 原子性、引用校验、redaction | Prompt、provider、projection 业务 | +| Source plugin | pre-admission attempt、due、cursor、业务 decision | Message body、Turn/Run 状态、Delivery | +| Channel Host / source adapter | wire 校验、source_ref、受限 target capture、ACK | Session truth、exact binding、模型算法、provider finality | +| Run Host | live task、per-Turn lease、history-reading lane、interrupt token、Root lease | Message body、外部 effect | +| Plugin Runtime | artifact、Root、generation、publish、drain | Session/Delivery outcome | +| REACT plugin | Prompt/Context/LLM/Tool 算法与 Draft | Message identity、Session commit、授权授予 | +| Tool plugin | schema、调用、自己的 effect ledger | 全局 loop、任意 Session 写入 | +| Projection Registry | fold 驱动、key、schema version、checkpoint/cursor | Session event、领域 projection 内容 | +| Projection plugin | 自己的 state、wire view、外部 effect journal、cleanup receipt | 原始 Message、Core 全局 claim | +| Delivery owner | delivery event producer、exact binding、provider receipt、恢复 | Message body、source ACK 业务 | +| Data Management | backup、maintenance mode、redaction、tombstone、cleanup 协调 | 正常 compaction、provider 回滚 | + +物理上同一个 SQLite 文件可以让 append batch 原子;逻辑 owner 仍通过窄端口分开。 --- -## 九、从 v3 和 `0902-02` 再做一次减法 +## 十一、v3/v4 再做一次减法 -| 旧概念或提议 | v4 处理 | -|---|---| -| 公共 `Attempt` / `AttemptId` | 删除;保留内部 `RunRef=(TurnId, run_seq)` 和短期 interrupt token | -| `AGENT_LOOP` | 改为普通 `REACT`;Loop 只是默认插件的一种实现 | -| `AgentProgram` / `ProgramOutput` | 删除;与 `react` 重复 | -| `MESSAGE_HANDLER.decide()+handle()` | 压成一次 `handle(Incoming)`;`accept()` 是一次性 capability | -| public `RunLock` | 删除;session lane 由 RunHost 内部拥有 | -| `TurnRules` | 收敛为 immutable `TurnGrant`;不放 model、delivery、memory switch | -| `AgentRun` 大袋子 | 改成固定窄端口;删除 extra、metadata、loop_input、任意 repository | -| Core `TOOL_VIEW` 第三选择轴 | 降为 `default-react` 依赖的普通 `TOOL_SELECTOR` | -| `(TurnId, ordinal)` 取代 MessageId | 拒绝;它只表示成员位置 | -| Session = `Commit/Delete(turn)` fold | 拒绝;Session 仍是 canonical Message rows + seq | -| 通用 `SessionChange` | 收窄为同事务 `SessionReceipt` 引用 feed | -| `DERIVED_STORES` registry | 删除;每个插件拥有自己的 cursor/data/rebuild | -| 通用 `CommitIntent` / evidence bag | 删除;Tool/Delivery/Memory 各自保存领域 pending record | -| 强制永久 `FinalModelRequest` | 删除;只强制 binding receipt,完整 payload 是有界诊断 | -| Fiber Effect 与外部效果同名 | 分开;Effect 只管 Root 资源,外部效果归领域 owner | -| ChannelHost 与 DeliveryHost 都拥有 delivery | 修正;Delivery Owner 持久化,Channel Adapter 执行协议调用 | -| 通用 phase/EventBus 主链 | 迁到具体 Service/registry/Receipt;只保留有独立领域意义的事件 | -| `post_commit` 业务开关袋 | 用来源明确的 Session/Delivery 流程替代;迁移期兼容,目标态删除 | -| passive/schedule/proactive 各有 loop | 来源只创建 Message/Turn;共用 `REACT` | - -### 现有生命周期接线的归宿 - -旧 phase 不能整包换名后继续存在。逐项归位: - -| 当前环节 | v4 owner | +| 旧概念 | 新处理 | |---|---| -| BeforeTurn 的 Session/load/lane | TurnPort + ConversationStore + RunHost | -| 入站过滤、命令与是否进入回复 | 普通 `MESSAGE_HANDLER` 插件 | -| BeforeReasoning 的模型输入准备 | `default-react` 的 PromptParts、ContextView、ToolSelector | -| PromptRender mutable frame | 删除;改成 immutable PromptPart 收集与冻结 | -| BeforeStep/AfterStep | `default-react` 私有循环;有独立领域意义的 Tool/Model 事件留在各自 owner | -| 模型回复 parse/seal | Provider adapter + `default-react` 输出校验 | -| assistant attachment import | Attachment owner 在 complete 前返回 immutable refs;ConversationStore 同事务绑定 | -| AfterReasoning 的 Session 写入 | ConversationStore 的唯一 `complete()` | -| TurnCommitted 同步 fanout | 删除;换成 durable SessionReceipt + wakeup | -| Memory、embedding、presence | 各自的 SessionReceipt consumer | -| AfterTurn dispatch | Delivery Owner;Channel Adapter 只执行协议调用 | -| budget/log/trace | 诊断 owner,不能阻塞 Session 与 Delivery | -| success/failure extras | 回到产生该事实的领域插件;无独立 owner 的删除 | - -迁移原则不是“每个 phase 变成一个新 Service”。只有跨插件需要替换、并且拥有独立规则的能力才成为 Service;其余逻辑内联到真实 owner。 - -### 被动链特殊规则的最终 owner - -| 当前特殊规则 | v4 归宿 | -|---|---| -| command catalog 短路 | default Handler 依赖的 ordinary command registry | -| compaction 水位、完整 Turn 切点、overflow 重试 | `CONTEXT_VIEW`/compaction 插件;ModelPort 只执行已冻结请求 | -| Tool schema 预载、数量上限、`tool_search` 解锁 | `TOOL_SELECTOR` 与 tool-search 插件 | -| 空回复 retry | `default-react` 的模型输出规则;不是 Core retry | -| terminal Tool deadline | Tool 声明终结语义,`default-react` 拥有循环 deadline | -| provider continuation | Provider adapter 给出 typed finish,`default-react` 决定是否再调一次 | -| attachment/media import | Attachment owner;Message 只绑定 immutable ref | -| error response 文案 | default Handler 的 source-facing error mapper;RunHost 只返回 typed error | -| Citation | Citation 插件的 PromptPart/Tool/Receipt consumer,不进 REACT 分支 | -| Meme | Meme 插件的 Tool/PromptPart/独立数据 owner,不进 REACT 分支 | -| Akasha/Markdown post-memory | 各自的 SessionReceipt consumer | -| `agent_restart` | runtime-operation Tool + TurnGrant;与 ToolSearch 展示完全分开 | -| delivery、partial、uncertain、ACK | Delivery Owner 与 Channel Adapter 的明确分工 | -| stream/thinking/status | live StreamPort/UI projection;不能反写 Session 正文 | - -这张表必须在迁移中逐行对账。某项旧逻辑没有新 owner 时不能删;某项已经有 owner 时不能再保留旧 phase 的第二个 writer。 - -### 目标态 Core 中不应出现的产品名字 - -```text -Akasha -ToolSearch -Scheduler -Proactive -Wake -Spawn profile -Telegram / QQ / Mobile -OpenAI / Anthropic / DeepSeek -Markdown memory -Meme / Citation -``` - -它们可以作为普通插件、Provider 或 Channel Adapter 存在,但 Core 不按这些名字分支。 +| 独立 MessageBody store | 删除;Message body slot 是 Session message event 的一部分 | +| SessionEntry relation | 删除;Transcript add/remove 是 Session events | +| SessionReceipt outbox | 删除;Session log 本身就是 cursor/change feed | +| ProjectionGrant table | 删除;policy 是 Session event | +| Core ProjectionClaim/跨插件 lease | 删除;纯 fold 用 cursor,外部写由 owner 的窄 effect journal 收口 | +| Turn table | 删除;TurnState fold turn events | +| Run/Attempt table与随机 ID | 删除;RunState fold,RunRef 是 run/open EventRef | +| ToolFact table | 删除;request/tool/message events 已在 Session | +| Delivery row随机 ID | 删除;DeliveryRef 是 prepared EventRef,state 由 events fold | +| pending body 与 committed body | 删除双份;Envelope/Draft 在提交前不是 Message | +| placement | 删除;Transcript event 是唯一可见性事实 | +| proactive boolean/kind | 删除;source attempt 与普通 Turn 足够 | +| storage=durable/in_memory | 删除全局轴;Session fact durable,cache 可重建 | +| post_commit allow/suppress | 拆成明确 projection keys 的 policy event | +| 额外 fence UUID | 删除;当前 RunRef 就是不可复用 fence | + +仍必须保留的真实轴: + +- SessionId:事实作用域; +- event_seq:Session 内唯一顺序; +- MessageRef:正文身份; +- transcript seq:用户可见变化顺序,直接复用 transcript event_seq; +- ReplyTarget metadata 与 `run.reply_to`:逻辑发送位置在输入处捕获,Run 只选择已有 MessageRef,exact binding 后置; +- source attempt:可能先于 Session 或根本没有 Session; +- exact Root 与 request binding:同一 Turn 的不同 Run 可以独立换代; +- projection policy:相同消息形状可以有不同长期影响许可; +- provider / materializer effect state:外部世界不会跟 Session 事务一起回滚; +- tombstone:删除后的重投不能复活正文; +- Legacy token mapping:旧入站身份必须在不保存 raw id 的前提下确定地解析到唯一 MessageRef/Gone。 + +“foolish and simple”不是把事实硬挤成一个字段,而是让所有事实只沿一条 Session 时间线出现一次,其他东西都能丢掉重算。 --- -## 十、目标架构 - -```text -┌─────────────────────────────────────────────────────────────────────┐ -│ Akashic Core │ -│ │ -│ ┌────────────────┐ ┌────────────────┐ ┌─────────────────────┐ │ -│ │ Plugin Runtime │ │ConversationStore│ │ RunHost │ │ -│ │ Root/Fiber │ │Message/Turn │ │lane/cancel/lease │ │ -│ │ Effect/gens │ │Session/Receipt │ │narrow live ports │ │ -│ └────────────────┘ └────────────────┘ └─────────────────────┘ │ -│ │ -│ Core 只查两个行为 key:MESSAGE_HANDLER、REACT │ -└─────────────────────────────────────────────────────────────────────┘ - │ exact Root │ receipts - ▼ ▼ -┌────────────────────────────────┐ ┌─────────────────────────────┐ -│ ordinary behavior plugins │ │ ordinary state plugins │ -│ │ │ │ -│ passive-message-handler │ │ Akasha │ -│ default-react │ │ Markdown memory │ -│ simple-react / no-tool-react │ │ Embedding / FTS │ -│ prompt-parts │ │ UI projection │ -│ context-view │ │ │ -│ tool-search-selector │ │ each owns cursor + data │ -└────────────────────────────────┘ └─────────────────────────────┘ - │ - ▼ -┌─────────────────────────────────────────────────────────────────────┐ -│ ordinary source/domain plugins │ -│ Channel · Scheduler · Proactive · Spawn · Delivery · Tools · Models│ -└─────────────────────────────────────────────────────────────────────┘ -``` - -### 最小启动证明 - -Core 在没有默认插件时也应能启动: - -```text -Plugin Runtime + ConversationStore + RunHost -``` - -此时收到普通消息应明确返回 `MESSAGE_HANDLER unavailable`,而不是偷偷走旧被动链。 - -装一个十几行的 `echo-message-handler` 后,可以完成: - -```text -Incoming → Turn → "echo" DraftMessage → Session -``` - -它不需要 LLM、Tool、Prompt、Memory 或 `default-react`。这个 Gate 是“被动能力真的不再特权”的最强证明。 +## 十二、目标架构 + +~~~text +┌──────────────────────── Sources ─────────────────────────┐ +│ Channel · Wake · Scheduler · Spawn · message_push │ +│ own: attempt / due / cursor / source_ref │ +└──────────────────────────┬───────────────────────────────┘ + ▼ +┌──────────────────── Session append log ──────────────────┐ +│ turn · message · run · step · request · tool │ +│ transcript · policy · delivery · redaction │ +│ one SessionId + one event_seq line │ +└───────────────┬──────────────────────────┬────────────────┘ + ▼ ▼ +┌──────────────────────────┐ ┌───────────────────────────┐ +│ REACT / ModelHistory │ │ Pure Projection Registry │ +│ reads refs, emits Draft │ │ Turn · Transcript · UI │ +└──────────────────────────┘ └─────────────┬─────────────┘ + ▼ + effectful materializer owner + Memory / index journal + I/O +~~~ + +目标 Core schema/API/test base 不出现: + +~~~text +proactive +wake +content duty +drift duty +scheduler +spawn profile +provider family +memory plugin name +~~~ + +这些词可以留在 owning plugin 的事件和 projection 中,不能改变通用 Turn/Run append rules。 --- -## 十一、迁移路线 - -这不是一次大改。每阶段都必须可回滚,并先保留当前行为 oracle。 +## 十三、迁移路线 -### Phase 0:批准语义变化,冻结证据 +### Phase 0:先批准语义 -先做: +本提案若获批,先同步修改 projectneed 与 accepted decisions: -1. 把本文与 accepted 决策的差异列成正式 ADR:公共 Attempt 降为内部 Run、MessageId 与 seq 解耦、一个 Run 一个 Root。 -2. 录制当前被动、interrupt/retry、ToolSearch、Memory、Delivery、插件自更新 fixtures。 -3. 保存真实 SessionDB、tool trace、delivery ledger、snapshot identity 和 client payload 作为 oracle。 -4. 不改正式 workspace。 +1. Session 从“已完成聊天 rows”提升为完整 append log;Transcript 成为 projection。 +2. Turn/Run 从独立权威 row 改成 Session events 的 projection。 +3. 全局 post_commit 改成 projection policy events。 +4. proactive 从 Core schema/API 删除。 +5. internal MessageRef 只有 EventRef;wire boundary 通过 v4 encoding 或 legacy mapping 解析,不引入随机 v4 MessageId。 +6. v4 不提供 durable Session fork;跨 Session 复制产生新 Message identity。 -回滚点:无代码和数据变化。 +批准前本文只是 proposed target,不能覆盖现行合同。 -### Phase 1:先扶正身份与原子提交 +### Phase 1:建立版本化 Session log -目标:只改 ConversationStore,不改现有 Reasoner 行为。 +1. 用 SQLite backup API 备份 sessions.db,并记录 schema、hash 与 integrity_check。 +2. 新建带 format version 的 Session event storage、erasable content slot、append_batch 和 single-writer handle。 +3. 写 event envelope、同 Session typed refs、Run fence、unknown-event fail-loud、redaction replay 与 projection fold tests。 +4. 先迁移只读 projection,不切生产 writer。 -1. 增加逻辑 Turn 与内部 Run 的明确存储表示。 -2. 新 MessageId 与 seq 解耦;旧 ID 原样保留。 -3. 增加 `SessionReceipt` 表和 global `feed_seq`。 -4. 把 completed batch、Turn outcome、Run terminal、session head 和 Receipt 收进同一事务。 -5. 现有 API 用 adapter 继续返回旧字段,禁止双写漂移。 -6. Realtime/Mobile 同时引入稳定 TurnId 与短期 interrupt token;旧“`turn_id` 实际指一次 attempt”的字段只在客户端迁移期保留,不能继续污染新存储。 +### Phase 2:一次迁移旧事实 -旧 Message 只有在已有 `control_turn_id` 等明确证据时才能建立新 Turn membership。缺少身份的历史行继续由版本化 legacy reader 读取,不能按角色邻接、正文、时间或 seq 距离猜 Turn。历史 backfill 算法要作为单独的数据迁移合同批准。 +1. 每条旧 Message 生成一个 message event,并在同批追加 `identity/legacy-token(message, versioned_hmac(old_id))`;验证后删除 raw old id,LegacyIdResolver 由 token events 重建。 +2. 每条用户可见旧 Message 生成 transcript/add event;新 transcript seq 直接使用新 Session event_seq,不伪造旧 seq 映射。 +3. 每个迁移 Turn 也必须用 `turn/open + 首条 Admission Message` 的初始 batch。旧 assistant-only 工作只有在 durable source evidence 能确定地产生非可见 Admission Message 时才能迁;否则整次迁移 fail-loud,不能造空 Turn。 +4. 只有已有明确 interaction/attempt evidence 时才生成 Turn/Run links;任何 Message 无法确定所属 Turn 时整次迁移 fail-loud,交维护者处理,不能按角色、时间或相邻 seq 猜。 +5. allow/suppress 按持久事实迁成 policy event;非法/冲突/未知值 fail-loud。 +6. 差分验证消息数、legacy resolver 一对一、顺序、正文 hash、附件、删除边界和 projection 结果。 -持久化迁移前必须:完整 SQLite backup、`integrity_check`、row count/digest、隔离恢复 smoke。不得直接复用当前 `turns` 表名改变旧行语义;推荐新表承接 v4,旧表只读保留到 parity Gate 通过。 +迁移完成后删除旧 writer 和旧 schema reader;不长期 dual-write 或双读。Legacy resolver 只接受旧入站值、从不输出 raw old id,是兼容期唯一边界索引,不是第二个内部 Message model。 -回滚点:旧读路径仍是 authority;新表/Receipt 只是 shadow,不能驱动外部效果。 +### Phase 3:切 Turn/Run 与 REACT -### Phase 2:把当前算法整体包进 `REACT` +1. 实现 per-Turn current RunRef/StepRef fence、同 Session ref 校验与 Session history lane。 +2. 用事件 fold 取代 turns/attempt rows。 +3. 把当前 default reasoner 整体包成 REACT,再逐步把 Prompt、Context、Tool 与模型 binding 变成普通依赖。 +4. fixture 比较每个请求的 model-visible messages、Tool facts、final Draft 和 Session events。 -1. 新建普通 `default-react` 插件。 -2. 第一版内部仍调用现有 `DefaultReasoner`,只建立接口边界。 -3. `PassiveTurnPipeline` 通过 exact Root 的 `REACT` 调它。 -4. 比较回复、tool trace、Prompt digest、usage、Session batch 和错误终态。 +### Phase 4:切 Transcript 与客户端 -回滚点:切回旧 Reasoner adapter,不迁数据。 +1. Session 完成 batch 写 transcript/add;provider-first 在 delivered 后写。 +2. Web/Mobile 使用 message_id + 稀疏 transcript event seq + Session high-water cursor。 +3. cursor 做明确断代,不猜映射:旧 cursor 首次请求得到 `reset_required`;客户端用 v4 full snapshot 原子替换该 Session 本地投影,再保存响应的 `cursor_v4=session_high_water`。 +4. shadow 对比旧 Session history 与新 Transcript projection。 +5. 证明 empty delta 也推进 cursor、reconnect、重复 frame、乱序 frame、删除 tombstone 和 interrupted continuation。 -### Phase 3:把默认算法内部拆成普通依赖 +### Phase 5:迁移来源与 Delivery -按这个顺序: +按 Scheduler → Spawn → Wake → message_push 分批: -1. immutable `PROMPT_PARTS`; -2. `CONTEXT_VIEW`; -3. `TOOL_SELECTOR`; -4. ToolSearch 完整插件; -5. model binding 与 model-call receipt; -6. 删除 Reasoner 内对应开关、ContextVar 和 phase。 +- 每批只替换 source admission 和 output handoff; +- Core event vocabulary 不增加来源名; +- Wake 同时证明 no_due 无 Session event、domain skip 有 completed Turn 无 Transcript; +- Delivery 用真实 provider、exact binding 和 crash injection 证明前向恢复。 -每删一条旧路径,先证明新插件能独立卸载、替换、热换并恢复。 +### Phase 6:删除旁路 -回滚点:每个 capability 保留一个短期 adapter,但同一时刻只能有一个 writer。 +只有所有动态 consumer、插件 cache、正式 generation 和真实 DB 差分都通过后,才删除: -### Phase 4:建立 `MESSAGE_HANDLER`,切被动入口 +- MessageBody/SessionEntry 双模型; +- SessionReceipt/outbox; +- turns/attempt 权威 rows; +- global post_commit; +- proactive 字段与分支; +- 旧 schema/cursor 兼容 reader;只保留 legacy wire resolver。 -1. 原始渠道 envelope 在边界校验后进入 exact Root Handler。 -2. `/stop` 旁路 Handler,只调 interrupt。 -3. 默认 passive handler 调 `accept → react → complete`。 -4. 命令、过滤和直接回复迁到 Handler 或各自普通 Service。 -5. 删除 Core 对 `PassiveTurnPipeline` 和 `DefaultReasoner` 的直接构造。 +### 回滚 -回滚点:按 generation 原子切回旧 handler,不能按单请求随机双跑。 +- 每个 schema/data 阶段前建立名称清楚、可校验的 backup。 +- v4 writer 启用后,旧 binary 不得直接打开正式库。 +- 回滚要么运行版本化前向转换,要么恢复切换前 backup 并明确放弃其后的新事实。 +- Git 回滚不能撤销 provider 或 Tool 外部效果;Delivery/effect ledger 继续收口。 -### Phase 5:迁移提交后 consumer 与 Delivery - -1. Akasha、Markdown memory、embedding、UI 改为各自消费 SessionReceipt。 -2. Delivery Owner 用 prepared envelope 消除 commit 后 crash gap。 -3. 外部渠道 proactive 验证 provider-first 恢复。 -4. 同步 `TurnCommitted` fanout 只剩诊断后删除。 -5. `post_commit`、通用 after-turn extra 和旧 response patch 退出。 +--- -回滚点:consumer 可从自己的 cursor 重放;不得回滚 Session 消息。 +## 十四、验收 Gate + +### 14.1 单一真源 + +- [ ] 新 Message 正文只存在于一个 Session message event 的 body slot;durable chunk/request/tool link 不复制它。 +- [ ] Turn、Run、Transcript、ModelHistory、Mobile 与 Memory 可从 Session 重建。 +- [ ] 删除所有 projection cache 后,同一 Session 得到相同视图。 +- [ ] Delivery、Tool ledger、日志和 trace 不复制 Message body。 +- [ ] model-visible Message 与 Tool result 都有 Session event 证据。 + +### 14.2 身份与同步 + +- [ ] 新 MessageId、TurnId、RunId、DeliveryId 不使用独立随机 UUID。 +- [ ] typed refs 都指向正确类型的 EventRef,调用者不能自己填写 seq。 +- [ ] 所有内部 refs 同 Session;v4 无 durable fork 和跨 Session Message graph。 +- [ ] raw legacy message_id 不持久化、不输出;一对一 versioned token 仍把旧入站值解析到原 MessageRef/Gone,Core 内部只有 EventRef。 +- [ ] wire message_id 不透明,授权不依赖不可猜性。 +- [ ] transcript seq 稀疏、单调、不复用;empty delta 也用 next_cursor 推进 Session high-water。 +- [ ] 旧 cursor 只走 reset + full snapshot,不猜新旧 seq 映射。 +- [ ] client_message_id 只做 source-scoped admission/retry。 +- [ ] identity HMAC key rotation 前后,同一 admission redelivery 或 legacy id 命中同一 MessageRef/Gone;有引用的旧 key version 不退休。 + +### 14.3 Turn、Run 与原子性 + +- [ ] U1 → interrupt → U2 → interrupt → U3 → A 是一 Turn、三 Run。 +- [ ] `turn/open` 初始 batch 缺 Admission/Run、或 run/open 排在首条 Admission 前时,Store fail-loud。 +- [ ] retry 是同 Turn 新 Run且无新 Message;fresh 新建 Turn。 +- [ ] existing Turn 的 U2 不能裸 append;必须 CAS exact current Run,并同批 close R1→append U2→open R2(reply_to=U2)。 +- [ ] 两个并发 continuation 由 current Run CAS 排成两个 Run;旧 callback 与只 append 不开 Run 都 fail-loud。 +- [ ] latest Run failed 后 ordinary input 必须 supersede + 新 Turn;只有 explicit retry 可无新 Message留在旧 Turn。 +- [ ] request/tool/运行产物 Message 都强制携带 current RunRef/StepRef,拒绝旧 Run 的迟到 append/seal。 +- [ ] S1 close、S2 open 后,S1 的迟到 Tool result 被拒绝;pending call 只有真实 result 或 canonical outcome_unknown result 后才能 close。 +- [ ] U1→non-idempotent tool started→interrupt/U2→provider unknown 时,R2 history 含 unknown,unresolved-effect fence 阻止自动重复调用。 +- [ ] 同 Session history-reading Run 串行;无 history Run 可以并发。 +- [ ] ModelHistory 取 cut 时 Transcript,并按原序重放当前 Turn 当前 Step 前、跨所有 Run 的 Admission 与 closed Tool pair;不吸入交错 task 私有 Message。 +- [ ] conversation seal 原子追加 final Message、Run/Turn close、全部 transcript/add 和 session-first Delivery。 +- [ ] 每个 Run 固定 exact Root;每个 request 固定 ModelExecution/header。 +- [ ] closed Tool pair 可重放;unknown effect 不盲重试。 + +### 14.4 projection + +- [ ] 每个 projection 是确定 fold,有 state version 和 source cursor。 +- [ ] cache mismatch 直接丢弃重建,不反写 Session。 +- [ ] 同库 materialization 用同事务 cursor;外部 materializer 在 I/O 前写 owner-local effect journal。 +- [ ] started/uncertain 先 query/drain,cleanup 用更高 desired version;本地 CAS 不冒充远端完成。 +- [ ] validation 与普通 conversation 同形状时,policy 仍能禁止长期影响。 +- [ ] policy key 不使用 source/plugin 名字。 +- [ ] Transcript visibility 只读 transcript events,不读 proactive 或 Session 前缀。 + +### 14.5 proactive + +- [ ] no_due/reject 有 source attempt、零 Session event。 +- [ ] due 只使用普通 Turn/Message/Run events。 +- [ ] domain skip 是 completed Turn,无 assistant、Transcript 或 Delivery。 +- [ ] share 只产生一条 final assistant Message,其他模块引用它。 +- [ ] Core schema/API/test base 没有 proactive/wake/source enum。 + +### 14.6 Delivery + +- [ ] prepared 引用 MessageRef,不复制正文。 +- [ ] Run 打开时 `reply_to` 已固定同 Turn、带授权 ReplyTarget 的 Admission Message;U1/U2 不同 target 不会在 seal 时猜。 +- [ ] prepared 冻结 exact target;找不到 exact binding 时 fail-loud。 +- [ ] provider operation key 由 committed DeliveryRef 派生,prepared payload 不自引用未分配 seq。 +- [ ] provider_started 在 I/O 前提交。 +- [ ] crash 后不盲发;uncertain 可查询、可人工收口。 +- [ ] prepared→canceled 与 uncertain→delivered/rejected/abandoned_uncertain 都是合法显式 transition;人工关闭仍保留 unknown audit。 +- [ ] provider-first 只有 delivered 后才 transcript/add。 +- [ ] session-first notification 失败不撤销 Transcript。 +- [ ] target lease 只从 prepared 保持到 terminal,不从 admission 长期钉住。 + +### 14.7 删除 + +- [ ] 删除前有 backup 与影响预览。 +- [ ] redaction/remove 使用更高 Session seq;MessageRef 进入 Gone。 +- [ ] Gone body slot 与更高 redaction event 同事务提交;full replay 不会遇到半个 Message。 +- [ ] raw SourceRef、ReplyTarget 与 terminal Delivery binding 都进入 privacy slot;删除后只留 AdmissionToken 与无正文 finality。 +- [ ] 单 Turn 删除先 cancel 尚未 I/O 的 Delivery,并在同一 maintenance transaction 擦除 canceled exact binding。 +- [ ] source ledger 只留 token/ref/finality,Envelope 与 recovery payload 已擦除;unknown Tool effect 已 query 或由用户明确覆盖。 +- [ ] projection cleanup、started/uncertain materializer 或 uncertain Delivery 未闭合时不报告删除成功。 +- [ ] 迟到 Run、retry、source replay、Delivery 和 projection 都不能复活正文。 +- [ ] delete_session 覆盖 Run、Delivery、附件、Tool result、Tool args、dynamic context、Mobile/Memory projection、cursor shell 与 tombstone。 + +### 14.8 概念 Gate + +- [ ] 每个权威事实只在一个 Session event 或一个明确外部 ledger 中出现。 +- [ ] 每个 cache 删除后可由权威事实重建。 +- [ ] 删除任一留下的字段会破坏一个已命名不变量;否则继续删除。 +- [ ] 没有按来源名复制 Turn/Run/Delivery 状态机。 +- [ ] 没有永久 dual-write、dual-read 或 guessed migration。 -### Phase 6:删桥并收窄 Core +--- -只有全部 Gate 通过后才删除: +## 十五、需要维护者批准的决定 -- 旧 passive phase bundle; -- `DefaultReasoner` 的 Core 接线; -- public Attempt/RunLock/AgentProgram 草案类型; -- ToolSearch Core 开关与授权 ContextVar; -- 通用 CommitIntent/DerivedStore/SessionChange 试验代码; -- 无 consumer 的 lifecycle event 和 DTO。 +推荐整组批准: -删除前再次核对外部插件源码、安装 cache、动态 consumer、测试和运行日志。cache 不是 canonical source,不能因为静态 `rg` 无命中就直接删接入点。 +1. Session append log 是唯一 Message 真源,Transcript 只是 projection。 +2. 完整 Message body 只写入一个 Session-owned erasable slot;durable chunk、rendered Prompt 和第二份 Tool result 不存在。 +3. Turn、Run、Step 和 Delivery identity 由 typed EventRef 派生,不新增随机 ID。 +4. internal MessageRef 只有 `(SessionId, message event seq)`;raw legacy id 不持久化/不输出,只通过 versioned HMAC token 在入站 boundary 解析。 +5. wire seq 使用 transcript add/remove event seq,cursor 是 Session scan high-water;旧 cursor 通过 reset + full snapshot 断代。 +6. Turn/Run 权威 rows、SessionEntry、SessionReceipt、ToolFact 和 Core ProjectionClaim 从目标模型删除。 +7. current RunRef 同时承担 execution fence,不再增加 fence UUID。 +8. projection policy 是 Session event,不是 global boolean 或独立 Grant relation。 +9. proactive 从 Core 完全删除;no-turn 与 completed-without-output 分开。 +10. Delivery state由 Session delivery events fold;provider、Tool 和 effectful materializer 的外部 finality 仍归各 owner journal。 +11. logical ReplyTarget 是 Admission Message 元数据,Run 复用 MessageRef 选择 `reply_to`,exact binding 在 prepared 时冻结;不新增 target ID。 +12. 显式删除是 append-only 的唯一内容 slot 变更,以同事务 Gone/redaction、外部 drain 和高版本 cleanup 收口。 +13. v4 不支持 durable Session fork;跨 Session 复制产生新 MessageRef。 ---- +实现任务仍需单独确定: -## 十二、验收 Gate - -### 12.1 Turn 与身份 - -- [ ] `U1 → interrupt → U2 → interrupt → U3 → A` 只产生一个 Turn、三个内部 Run。 -- [ ] completed Session batch 恰好为 `[U1,U2,U3,A]`,顺序与 Turn ordinal 一致。 -- [ ] schedule/proactive 的 `run-only` Task Message 有稳定身份但没有 seq;目标 Session 只出现最终 A。 -- [ ] failed 后显式 retry 复用原 MessageId;普通 fresh 产生新 MessageId 和新 Turn。 -- [ ] MessageId 不依赖 seq;旧 ID 不被改写。 -- [ ] 同 Session seq 单调且不复用。 -- [ ] `/stop` 和 ignored inbound 不创建 ε Turn。 -- [ ] 同一外部 source_ref 在 accept 前后任意 crash/重投都只产生一个 MessageId、TurnId 和 RunRef。 -- [ ] 同一 Turn 只能完成一次;重复相同 complete 幂等,内容漂移失败。 - -### 12.2 并发与中断 - -- [ ] 同一 Session 同时最多一个 active conversation Turn。 -- [ ] 不同 Session 可以并发。 -- [ ] 热换 generation 不改变 lane key。 -- [ ] interrupt token 只能结束它绑定的当前 Run;旧 token 不能杀新 Run。 -- [ ] active Run 只接受 interrupt,普通输入明确 busy。 -- [ ] 重启把失去 live owner 的 active Run 收束为 `interrupted(process_lost)`,不自动重跑外部效果。 -- [ ] UI 用 TurnId 分组,用 latest_run_state 显示 running/interrupted/failed,不把 RunRef 当 TurnId。 - -### 12.3 Root 与权限 - -- [ ] Handler、REACT、Prompt、Tool schema、Tool execute 和 model binding 来自同一个 exact Root。 -- [ ] Run 1 在 G10 中断、Run 2 在 G11 恢复时,TurnGrant 不扩大。 -- [ ] hidden Tool、not-allowed Tool 和 invalid arguments 有不同结构化错误。 -- [ ] ToolSearch 改 ToolView,不改 TurnGrant。 -- [ ] ToolSelector 无法看到或泄露 grant 之外的 Tool metadata。 -- [ ] candidate Root 不能写正式 Session、Memory、plugin-data 或外部服务。 -- [ ] cleanup 失败保留 drain blocker 和全部错误。 - -### 12.4 Prompt、Context 与 Tool - -- [ ] PromptPart 输入相同则顺序与 digest 相同。 -- [ ] 两个插件不能改写彼此的 PromptPart。 -- [ ] duplicate/exclusive Prompt key 在 candidate Gate 失败。 -- [ ] `persistent history`、`runtime history view`、`prompt history` 在 API 和日志中不混名。 -- [ ] ContextView 不写 Session,也不裁开完整 logical Turn 或 Tool pair。 -- [ ] 卸载 ToolSearch 后换 selector 不改 Core。 -- [ ] 无 Tool 的 REACT 能正常工作。 - -### 12.5 提交与恢复 - -- [ ] 在 complete 事务每个语句前后注入 crash,都只得到“全有”或“全无”。 -- [ ] 非空 batch 的 Message、seq、Turn outcome、Run terminal、session head、SessionReceipt 同事务一致。 -- [ ] run-only Turn 无 Session 变化时不生成假的 SessionReceipt。 -- [ ] commit 后、wakeup 前 crash,consumer 能从 feed_seq 追上。 -- [ ] consumer 失败不回滚 Session、不阻止其他 consumer。 -- [ ] consumer 重放相同 Receipt 幂等,内容漂移失败。 -- [ ] 存储损坏 fail-loud,不变成空 Session 或 cache miss。 - -### 12.6 Delivery 与外部效果 - -- [ ] 被动回复 Session commit 后发送失败,Session 内容仍存在且客户端可补尾。 -- [ ] Akashic/Web/Mobile 通知不产生第二份 durable 正文。 -- [ ] 外部 proactive 只有 provider full success 后才追加 Session。 -- [ ] partial/uncertain 不自动重发,不伪装成功。 -- [ ] Tool effect 在 Run retry 时按 owner receipt 对账,不重复执行。 -- [ ] Run/Root cleanup 不声称回滚已经发生的外部效果。 - -### 12.7 热更新与自我更新 - -- [ ] ordinary run 只租 stable;validation child 可显式租 latest。 -- [ ] candidate 行为 probe 证明真实调用,不只证明 import/manifest。 -- [ ] owner Turn 完成并释放旧 lease 后才开始 promotion drain。 -- [ ] promotion 失败保持 latest candidate 事实并恢复旧 stable formal Root。 -- [ ] 旧 Run 不跨 Root;新 Run 不再进入退休 Root。 -- [ ] durable operation 完成 handoff 后才允许旧 Root drain。 - -### 12.8 复杂度与非特权证明 - -- [ ] Core 不 import `default-react`、ToolSearch、Akasha、Scheduler、Proactive 或具体 Provider。 -- [ ] Core 不按来源名、插件 ID、模型名或渠道名分支。 -- [ ] 无 `MESSAGE_HANDLER` 时明确 unavailable,不回退旧被动链。 -- [ ] echo handler 可在没有 LLM/Tool/Prompt/Memory 时完成 Turn。 -- [ ] `simple-react` 可替换 `default-react`,不修改 Core。 -- [ ] 目标公共词中没有 Attempt、AgentProgram、RunLock、CommitPlan、CommitIntent、DerivedStore。 -- [ ] 新 helper、DTO、event 若没有独立 owner 或第二 consumer,删除或内联。 +- event envelope 与 wire encoding 的具体字节格式; +- SQLite 表、索引、format version 和 append_batch API; +- abandoned/completed-without-output Turn 的 retention; +- 每个外部 provider 的幂等键与查询能力; +- 哪些 durable projections 必须阻塞删除完成。 --- -## 十三、需要批准的决定 +## 十六、证据索引 -### 推荐直接批准 +### DSH 当前源码 -1. **公共 Attempt 改为内部 Run。** 保留内部持久坐标,不保留第二套用户领域身份。 -2. **保留 opaque MessageId,并与 seq 解耦。** 不采用 `(TurnId, ordinal)` 作为身份。 -3. **使用 `REACT`,不引入 `AgentProgram`。** 默认 while loop 整体是普通插件。 -4. **Session 保持 canonical Message rows。** 只增加同事务、无正文的 SessionReceipt feed。 -5. **ToolSearch 只拥有 ToolView。** 权限永远由 TurnGrant 和执行边界拥有。 -6. **一个 Run 一个 exact Root。** 同 Turn 的后续 Run 可以使用新 committed Root。 -7. **外部效果归领域 owner。** 不建立通用 CommitIntent/EffectAttempt 平台。 +检查基线:`/mnt/data/source-code/deepseek-harness` commit `49a606bc5b5934603f22a26957a07dc799ab0291`。以下证据只支持单日志与 pure fold,不证明 Akashic 的异步 I/O、删除、并发或 fork 语义。 -### 实现前仍需单独确认 +- `/mnt/data/source-code/deepseek-harness/docs/architecture.md:74`:Turn 是零个或多个 Step;turn/step/message/tool 都写 Session events。 +- `/mnt/data/source-code/deepseek-harness/docs/architecture.md:103`:Session log 是 model context、fork、resume、transcript、telemetry 和 persistence 的源。 +- `/mnt/data/source-code/deepseek-harness/packages/core/session/src/types.ts:255`:SessionEventMap 是 append-only truth。 +- `/mnt/data/source-code/deepseek-harness/packages/core/session/src/index.ts:628`:seq 等于 log length,append 后才发布。 +- `/mnt/data/source-code/deepseek-harness/packages/core/session/src/index.ts:772`:deriveMessages 从 surface projection 重建。 +- `/mnt/data/source-code/deepseek-harness/packages/core/agent-loop/src/index.ts:55`:turnBoundary 是纯 projection。 +- `/mnt/data/source-code/deepseek-harness/packages/session/session-turn-outline/src/projection.ts:84`:Turn outline 从 committed events fold。 +- `/mnt/data/source-code/deepseek-harness/packages/session/session-projection/src/index.ts:40`:projection unit 是纯同步 fold,并以 state version/source seq checkpoint。 +- `/mnt/data/source-code/deepseek-harness/packages/llm/llm/src/message.ts:130`:同一个 Message representation 供 delivery、history 和 model request 使用。 +- `/mnt/data/source-code/deepseek-harness/packages/llm/llm/src/message.ts:175`:DSH 当前为入 Session 前的 Message 生成 UUID;本设计有意不照抄。 -1. v4 物理表名与旧 `turns` 表怎样长期归档;不能原地解释旧行。 -2. 新 MessageId 的具体编码;本文推荐 UUIDv7,但只要求 opaque 和稳定。 -3. model-call binding receipt 的保留期,以及完整 payload 诊断默认关闭多久。 -4. plugin-data 的全局数据删除 inventory 与阻断语义;它不是本轮 SessionReceipt 的职责。 -5. 外部 proactive、`message_push` 和 BackgroundJobs 各自迁移批次;不能借被动链改造顺手改产品语义。 -6. accepted 决策和 `projectneed` 中 `Attempt`、snapshot execution unit 的措辞如何升级;批准前仍以当前合同为准。 +### Akashic 当前合同与代码 ---- +- `docs/projectneed.md`:SES-001~SES-008、RUN-001~RUN-009、OUT-001~OUT-005、PRO-001~PRO-006。 +- `docs/decisions/0039-react-core-atoms-keep-sources-unprivileged.md`。 +- `docs/decisions/0040-wake-duty-gate-lives-in-scoped-react.md`。 +- `docs/decisions/0041-turn-effects-and-memory-plugins-are-orthogonal.md`。 +- `docs/design/persistence-state-map.md`。 +- `session/store.py:2392`:当前 turns row 仍表达 attempt;目标态改成 Session event projection。 +- `session/store.py:4744`:当前 message_id 由 session_key:seq 生成;证明 derived identity 已有先例。 +- `session/store.py:5385`:当前 completed interaction 删除边界。 +- `infra/mobile_realtime/protocol.py:188`:client_message_id 是 transport/admission identity,不是 Session MessageId。 -## 十四、证据索引 +### 用户要求与运行案例 -### 输入与恢复点 - -| 文件 | SHA-256 | -|---|---| -| `0902-reviewed-v3.md` | `d3e5e9e1ecc09cdea60a532357da31ec87207ffd68f5f6d26f6e913021bd54a0` | -| `0902-02.md` | `7c1dee104a8706f8a2030de1b22b4a00407a8ab5c74e2ef06e5485a7dd0207af` | -| `.0902-reviewed-v3.pre-v4-20260903-111415.bak` | 与 v3 相同 | -| `.0902-02.pre-v4-20260903-111415.bak` | 与 `0902-02.md` 相同 | - -### 用户要求 - -- 2026-09-02 Codex 设计会话;原始记录保留在本地,不随公开 PR 提交。 - -### 当前项目合同 - -- `docs/projectneed.md:371`:不得使用无修饰的 history。 -- `docs/projectneed.md:426`:completed Turn 持久化全有或全无。 -- `docs/projectneed.md:430`:seq 单调且不复用。 -- `docs/projectneed.md:434`:破坏性删除只接受用户显式意图。 -- `docs/projectneed.md:444`:Session 正文正常只追加。 -- `docs/projectneed.md:452`:未完成 logical interaction 的续接规则。 -- `docs/projectneed.md:458`:一个 completed interaction 拥有全部输入和唯一最终回复。 -- `docs/projectneed.md:560`:同 Session 串行,不同 Session 并发。 -- `docs/projectneed.md:564`:active execution 只接受精确中断。 -- `docs/projectneed.md:568`:每个执行单元冻结模型执行绑定。 -- `docs/projectneed.md:638`:被动 Session-first 与 Akashic 主动消息语义。 -- `docs/projectneed.md:648`:外部渠道按完整逻辑消息提交。 -- `docs/projectneed.md:658`:硬终止只关闭 execution attempt。 -- `docs/projectneed.md:664` 起:candidate、snapshot、cleanup 与插件发布不变量。 -- `docs/design/persistence-state-map.md`:持久对象 owner、增改减、备份与恢复边界。 -- 决策 0034:Turn 是逻辑工作单元,Attempt 是当前内部执行概念。 -- 决策 0039:Core 原子能力来源无关,`react` 是唯一控制流。 -- 决策 0041:Turn effect 与 Memory 插件正交,Akasha 是普通插件。 -- 决策 0045:Akashic 主动消息先提交 Session,客户端使用 `message_id + seq`。 -- 决策 0008、0036、0046:Root/generation/snapshot lease、增量 candidate 与 drain。 - -### 当前代码 - -- `agent/looping/core.py:335-369`:Core 直接组 `DefaultReasoner` 与被动 pipeline。 -- `agent/core/passive_turn.py:355-435`:固定 phase 主链。 -- `agent/core/passive_turn.py:958-1075`:Reasoner 内 ToolSearch、Prompt 和 step phase。 -- `agent/lifecycle/phases/after_reasoning.py:257-333`:多 user input 的提交准备。 -- `agent/lifecycle/phases/after_turn.py:243-281`:同步 committed fanout。 -- `session/manager.py:642-738`:Session message 批次事务。 -- `session/store.py:2392` 起:当前 `turns` execution record。 -- `session/store.py:4744`:当前 MessageId 从 session/seq 生成。 -- `session/store.py:5385-5539`:completed interaction 显式删除与恢复证据。 -- `agent/plugins/snapshot.py:876-988`、`1567-1665`:exact snapshot lease 与 drain。 -- `agent/plugin_composition/effect.py:16-148`:Effect setup/rollback/LIFO cleanup。 - -### DeepSeek Harness 参考 - -- `deepseek-harness/docs/architecture.md:11-29`:所有组成件都是插件、profile 与 live patch reload。 -- `deepseek-harness/docs/architecture.md:51-95`:默认 core package、Turn flow 与 durable Session event。 -- `deepseek-harness/packages/core/agent-loop/src/agent.ts:234-359`:默认 loop 直接拥有 pre-step、Turn、Prompt、LLM 与 Tool 驱动。 -- `deepseek-harness/packages/core/system-prompt/src/index.ts:424-536`:Prompt contribution 通过 effect 注册并确定性组装。 -- `deepseek-harness/vendor/cordis/src/fiber.ts:405-560`:effect 注册、逆序 disposer 与卸载行为。 +- 2026-09-02/03 Codex 会话:要求回到基本原理、减少概念、正确解释 Turn、消除 proactive 特判,并质疑新 MessageId。 +- 2026-09-03 修正:当前行为不等于正确设计;Session 应是唯一消息载体,其他消息视图都是 projection,并参考 DSH。 +- hua-home 私有历史只用于 interrupt/retry/Wake 场景覆盖;原始内容不提交,也不定义目标语义。 --- ## 最终判断 -v3 的方向是对的:让 AgentLoop、ToolSearch、Memory 和来源能力回到插件。但它仍把“为了迁移而看得见的运行细节”抬成了长期领域对象,又用通用 Commit、Change、DerivedStore 和 Effect 平台去包住所有失败。 +真正简单的模型不是: + +~~~text +MessageBody → Turn row → SessionEntry → Receipt → ProjectionClaim → UI +~~~ + +而是: -`0902-02.md` 做对了主要减法,却又删掉了不能删的稳定 MessageId,并把 Session 误写成变更日志,还让 Channel 与 Delivery 重复拥有同一个事实。 +~~~text +Session events(唯一事实) + ├── Message 只出现一次 + ├── Turn / Run 只是 fold + ├── Transcript / Model / Mobile / Memory 只是 fold + └── Delivery 只引用 MessageRef +~~~ -v4 的改进不是增加一个更精巧的总框架,而是把边界缩到刚好够用: +新 Message 仍需要一个稳定引用,但不需要一个新的随机 UUID。`(SessionId, message event seq)` 已经足够;wire 继续叫 message_id,只是不再拥有第二套身份。 -```text -Message 组成 Turn -Turn 中该进入 Session 的 Message 整批原子提交 -Run 只是 Turn 的内部执行 -每个 Run 只看一个 Root -REACT 只是普通插件 -外部效果回到自己的 owner -``` +proactive 也不需要被“优化成更聪明的特判”。它应该从 Core 消失:开始前是 source attempt,开始后就是普通 Session events;是否可见、是否学习、是否发送分别由 Transcript、policy 与 Delivery 事实决定。 -如果一个新能力不能用这些积木直接表达,它必须先证明自己拥有新的权威状态、不变量、控制流或生命周期;否则不进入 Core。 +这版比前一版更少,也更正交:少的不是可靠性,而是第二份事实。 From 23160e43c7313b944e7ee46e44509bf6594cbaff Mon Sep 17 00:00:00 2001 From: huashen <2494946808@qq.com> Date: Thu, 3 Sep 2026 14:34:07 +0800 Subject: [PATCH 3/7] =?UTF-8?q?docs:=20=E5=B0=86=20v4=20=E6=94=B6=E6=95=9B?= =?UTF-8?q?=E4=B8=BA=20Session=20Message=20WAL?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/design/0902-reviewed-v4.md | 1488 +++++++++---------------------- 1 file changed, 408 insertions(+), 1080 deletions(-) diff --git a/docs/design/0902-reviewed-v4.md b/docs/design/0902-reviewed-v4.md index a4ae03d8..d7fae4d3 100644 --- a/docs/design/0902-reviewed-v4.md +++ b/docs/design/0902-reviewed-v4.md @@ -1,1274 +1,602 @@ -# Akashic v4:Session 是账本,其他都是投影 +# Akashic v4:只有 Session 与 Message - 文档版本:0902-reviewed-v4 - 日期:2026-09-03 - 状态:设计提案,等待维护者批准 - 当前代码基线:47896b4200731183a54081e2eca77602a0881a0a -- 输入:0902-reviewed-v3.md、0902-02.md、Codex 历史会话、当前项目合同、真实运行案例、DeepSeek Harness +- DSH 参考基线:49a606bc5b5934603f22a26957a07dc799ab0291 - 本文不授权:实现、数据库迁移、正式 workspace 写入、删除、部署或合并 ## 结论 -v4 只设一个消息真源: +Akashic 的对话事实层只保留两个名词: ~~~text -Session = 一份只追加的事实账本 - -一条完整 Message 的正文只在 Session 中记一次 -Turn / Run / Transcript / Model History / Mobile / Memory 都从 Session 折出来 -Delivery 只引用 Message,不复制 Message +Session = 一条只追加的 Message WAL +Message = 一个已经完整产生并被 WAL 接纳的事实 ~~~ -前一版 v4 的 `MessageBody + SessionEntry + SessionReceipt + ProjectionClaim` 仍然造了多条路,应该删除。 +没有第三种权威对象。没有 Turn、Run、Step、Attempt、Delivery row,也没有一层 +`SessionEvent` 再包住 Message。 -最小主线变成: +这里的“只有”限定在**对话事实层**。附件字节、credential、插件 generation、 +scheduler 配置和 provider 外部状态仍由各自边界拥有;它们不是对话实体,也不能复制 +Message。它们若要影响或进入对话,只能被 Message 的类型化内容引用或报告。 ~~~text -Envelope / Draft - │ 只有接纳或封口时才成为 Message - ▼ -Session.append(events) - │ - ├── Turn projection - ├── Run projection - ├── Transcript projection ──▶ Web / Mobile - ├── ModelHistory projection ──▶ REACT - ├── Memory projection - └── Delivery projection ──▶ provider +┌──────────────────── Session ────────────────────┐ +│ seq 1 Message:用户说 U1 │ +│ seq 2 Message:用户补充 U2 │ +│ seq 3 Message:助手请求调用 tool │ +│ seq 4 Message:tool 返回 success │ +│ seq 5 Message:助手回答 A │ +└─────────────────────────────────────────────────┘ + │ │ │ + ▼ ▼ ▼ + 聊天视图 模型上下文 手机增量同步 ~~~ -Core 不认识 passive、proactive、Wake、Scheduler、Spawn、Content、Drift 或插件名。它只保证 Session 事件合法、有序、可恢复,以及当前 Run 才能提交结果。 +网络重试、模型重试和未完成 token 都发生在对应 Message 写入以前。它们没有产出 +Message,因此不进入 Session。重启后可以丢失这些执行过程;不能丢失的只有已经 +提交的 Message。 ### 把我当六岁 -把 Session 想成唯一一本不能偷偷改页码的作业本: - -~~~text -作业本 Session -┌──────────────────────────────────────────┐ -│ 10 这件事开始了 │ -│ 11 花月哥哥说:U1 │ -│ 12 第一次尝试开始 │ -│ 13 第一次尝试被打断 │ -│ 14 花月哥哥补充:U2 │ -│ 15 第二次尝试开始 │ -│ 16 Akashic 回答:A │ -│ 17 第二次尝试完成 │ -│ 18 这件事完成 │ -│ 19 把 U1 放进聊天页 │ -│ 20 把 U2 放进聊天页 │ -│ 21 把 A 放进聊天页 │ -└──────────────────────────────────────────┘ - │ │ │ - ▼ ▼ ▼ - Turn 卡片 手机聊天页 模型上下文 -~~~ - -作业本里的事实只有一份。Turn 卡片、手机聊天页和模型上下文只是用不同彩笔画出的视图,丢了可以再从作业本画出来。 - -- Message 是作业本里真正写下的一句话。 -- Turn 是把“同一件事”的页码圈在一起,不是另一本本子。 -- Run 是这件事实际试了第几次,也不是另一本本子。 -- Transcript 是用户看到的聊天页。 -- Delivery 是邮差的工作状态,只记“送哪一句、送到哪、送成没有”,不再抄一遍信。 - -U1 后被打断,再收到 U2,仍是同一个 Turn,只多一个 Run。Wake 检查后没事,连作业本都不用动;Wake 已经开始工作后决定不说话,则账本里有一个完成但没有可见回复的 Turn。 - -### 本版五个决定 - -| 问题 | v4 决定 | -|---|---| -| Message 在哪里 | 只在 Session 的 `message/append` 事件里一次 | -| Turn 和 Run 是什么 | Session 事件的稳定分组投影,不是第二套正文存储 | -| 是否新增随机 MessageId | 不新增;新 MessageRef 由 SessionId 与 message event seq 派生 | -| 手机如何同步 | 仍只认不透明 `message_id`、单调 `seq` 和已有 cursor | -| proactive 是什么 | 来源插件的一种触发故事,不是 Core 类型、字段或状态机 | - ---- - -## 一、先确定什么是真源 - -### 1.1 唯一消息载体 - -下列规则是本设计的起点: - -1. 还没进入 Session 的输入叫 `Envelope`,不是 Message。 -2. 还没封口的模型输出叫 `Draft`,不是 Message。 -3. Message 一旦创建,身份与元数据不可变;正文只存在于该 Session event 的可擦除 body slot。 -4. 任何其他模块只能保存 `MessageRef` 或可重建 projection,不能保存第二份正文。 -5. 模型看见过的 Message、Tool 结果和请求绑定都必须能从 Session 重建。 -6. 正常运行只追加;只有用户明确删除时,Data Management 才能按受控协议擦除正文。 - -这里的“一次”指一条已经组装完成的 canonical Message。网络分片和模型 token delta 只能短暂直播,不能写入 Session、trace、request 记录或第二张流式表;crash 后丢掉未封口 Draft,不能拿半截 token 冒充 Message。Tool call 参数是另一类事实,只在 `tool/call` 写一次;Tool result 正文只在它对应的 `message/append` 写一次。 - -这与“Session 是用户当前看见的聊天列表”不同。Session 是完整账本;聊天列表只是其中一个 projection。未完成输入、内部 task、Tool 结果和投递状态可以在账本里,但不必出现在聊天页。 - -### 1.2 不是所有事实都是 projection - -“其他都是 projection”只适用于 Message 的各种读法,不能抹掉两个真实外部边界: - -- 来源插件在 admission 前拥有自己的 trigger attempt、due、cursor 和业务去重。`no_due` 时根本没有 Session Message。 -- Tool 或 provider 已经在外部世界产生的效果由对应 owner 的 effect ledger 最终确认。Session 只记录请求与观察到的结果,不能把 Git 回滚或内存回滚说成外部效果没发生。 - -这两类事实都不能复制 Message 正文。它们只在成功进入 Session 后保存稳定 EventRef。 - -### 1.3 当前行为只当反例库 - -hua-home 私有历史里的多 Run、interrupt、retry、Wake `no_due`、model skip 和 shared 只用于构造场景。当前表名、字段和分支不自动升级成正确设计。 - -若当前行为与以下模型冲突,应登记为 migration delta,再由维护者决定修改 accepted 合同或修改本提案。 - ---- - -## 二、从 DSH 借什么,不借什么 +Session 是唯一一本作业本。Message 是已经用墨水写完的一行字。 -DeepSeek Harness 当前源码提供了四个重要证据: +- 小朋友先在草稿纸上写,写错可以重来很多次。 +- 只有一句话写完整,才抄进作业本。 +- 抄进去以后,这一行有自己的编号 `message_id`,也有所在页码 `seq`。 +- 聊天页、模型看到的上下文和“这一轮”的括号,都只是拿彩笔从作业本里画出来。 +- 擦掉彩笔,作业本没有少东西;换一种画法,也不用迁移事实。 -1. `Session` 是 typed `SessionEvent` 的 append-only log,是完整交互历史唯一真源。 -2. 模型历史由 `deriveMessages()` 从 log 的 surface 投影,不单独保存。 -3. `turnBoundary` 和 `turnOutline` 都是对 `turn/start`、`turn/end` 与消息事件的纯 fold。 -4. 纯 projection checkpoint 只保存 `(session, key, version, source seq, value)`;版本不符或越过 log 末尾就丢弃重建。 - -v4 借这条骨架: +所以 U1 后模型断网三次,最后生成 A,作业本仍只有两行: ~~~text -durable Session events - └── pure / ordered folds - ├── runtime state - ├── model surface - └── client views +1 user U1 +2 assistant A ~~~ -但不照抄三个 DSH 选择: - -| DSH 当前选择 | Akashic v4 选择 | 原因 | -|---|---|---| -| `createMessage()` 先生成随机 UUID | Message 进入 Session 时由 event coordinate 得到身份 | Akashic 不需要让未接纳 Envelope 冒充 Message | -| durable inbox splice 可再次携带整条 Message | Message 正文只写一次,后续事件只引用 MessageRef | 避免同一 log 内也复制正文 | -| 单一 turn 顺序足够 | 多个 Turn 可以交错,但每个事件显式引用 TurnRef | message_push/task 不应占住会话的 history-reading lane | -| Session fork 是现成能力 | v4 不引入 durable fork | 避免在未定义 lineage、删除和 retention 前制造跨 Session Message graph | - -如果未来确实需要 durable inbox,入队动作本身就追加 `message/append`;后续 `turn/input` 只引用它,不再携带正文。先证明这个消费者存在,再增加该事件。 - -DSH 在这里是“单日志 + pure fold”的参考,不是 Akashic 多 Turn 并发、异步外部 projection、删除或外部 effect 正确性的证明。尤其 DSH 的 projection contract 是同步纯函数;有网络 I/O 的 materializer 必须另外解决 started-but-unknown,而不能套一个 checkpoint 就宣称安全。 - ---- - -## 三、最小权威模型 - -### 3.1 SessionEvent 是唯一坐标系 - -~~~text -SessionEvent = ( - session_id, - event_seq, - type, - data -) - -EventRef = (SessionId, event_seq) -~~~ - -规则: - -- `event_seq` 在一个 Session 内从 0 连续增加,永不复用。 -- Session Store 只接受原子 `append_batch`;同一批事件先整体校验,再一起提交。 -- 插件可以扩展事件词汇,但必须声明 producer、consumer、重放规则和未知 reader 是否可忽略。 -- 会改变重建语义的未知事件默认 fail-loud,不能静默跳过。 -- EventRef 是位置,不是 capability;读、写、删除仍检查 Session 与调用者权限。 -- 写入某个 Session event 的所有 `TurnRef`、`MessageRef`、`RunRef`、`StepRef`、`ToolCallRef` 和 `DeliveryRef` 必须指向同一个 Session。Store 集中拒绝跨 Session ref。 -- 只有 source/effect ledger 可以从外部保存一个 Session EventRef;Session 内部不会反向引用另一个 Session 的事件。 +那三次断网不是三次对话,也不是三条事实。它们只是草稿纸上的失败。 -所有新领域引用都只是 EventRef 的类型化名字: +## 一、两个权威对象 -~~~text -TurnRef = ref of turn/open event -MessageRef = ref of message/append event -RunRef = ref of run/open event -StepRef = ref of step/open event -ToolCallRef = ref of tool/call event -DeliveryRef = ref of delivery/prepared event -~~~ +### 1.1 Session -因此 v4 不新增随机 TurnId、MessageId、RunId、AttemptId、StepId、ReceiptId 或 DeliveryId。SessionId 已经提供全局作用域,event_seq 提供 Session 内唯一位置。 - -### 3.2 Message 只出现一次 +Session 只拥有身份和按 `seq` 排列的 Message。它不拥有当前执行、轮次、主动模式、 +投递状态或投影视图。 ~~~text -message/append = { - turn: TurnRef, - role: user | assistant, - body: Live { - content: ContentBlock[], - attachments: AttachmentRef[] - } | Gone { redaction: EventRef }, - producer: - Admission { admission_token: AdmissionToken } - | Run { run: RunRef, step: StepRef, - output: model_output | tool_result { call: ToolCallRef } }, - private_slot: - AdmissionOnly( - Live { source: SourceRef, reply_target?: ReplyTarget } - | Gone { redaction: EventRef } - ) | NoneForRun +Session { + session_id + messages: Message[] # 按 seq 连续排列 } ~~~ -MessageRef 就是这条 `message/append` 事件的 EventRef,不在 payload 里再写一次 id。初次 append 只允许 `Live`;`Gone` 只可能由第八章的显式 redaction 原子转换得到。 - -规则: - -- human 输入、插件 task 输入、模型 assistant、Tool result 都用同一个不可变 Message 结构。 -- Admission 只能由 admission capability 写,不能携带 RunRef;它把 source-scoped admission 与可选回复目标固定下来。原始 source/target 是可擦 privacy slot,immutable admission token 只是 keyed digest,不能反解地址。 -- Admission 不是绕过 Run fence 的旁门:初始 Turn 遵守 3.4 的同批规则;已有 open Turn 的新 Admission 只允许作为 5.2 continuation batch 的一部分,不能裸 append。 -- 模型输出与 Tool result 只能由 current Run capability 写,必须携带同 Session、同 Turn 的 RunRef 和 StepRef。Store 在同一写锁内验证 Run 仍 active;旧 Run 的迟到结果没有可绕过的字段。 -- Tool result 对模型仍是 user-role Message,但 `producer.kind=tool_result`,对应 `tool/call` 只保存参数和这条 MessageRef,不再保存 result body。 -- stream chunk、thinking、ACK、Prompt section、turn/run 边界和普通 debug span 不是 Message,也不得持久化 Message 正文。直播 chunk 只存在于当前进程和连接;封口后客户端改读 canonical Message。 -- 输入附件在同一个 append batch 里绑定;输出附件在 Message 提交前只是 staging object。 -- Message 只能属于一个 Session;向该 Session 之外发送由 Delivery 引用它,不复制它。 - -如果相同正文出现两次,就是两条 Message。content hash 不是身份。 - -### 3.3 wire 上仍叫 message_id +这里的 WAL 指领域层的 append-only message log,不是再增加一张 event 表。底层可以 +使用 SQLite WAL,但 `sessions.db/messages` 本身才是产品真源。 -内部 MessageRef 永远只有一种形状: +### 1.2 Message ~~~text -MessageRef = ref of message/append EventRef -~~~ - -wire `message_id` 是边界表示,不是第二种内部身份: - -~~~text -WireMessageIdOut = encode_v4(MessageRef) -WireMessageIdIn = encode_v4(MessageRef) | LegacyWireMessageId - -identity/legacy-token = { - message: MessageRef, - token: (key_version, HMAC(key_version, - "legacy" + session_scope - + canonical LegacyWireMessageId)) +Message { + message_id # 在 append 前生成;全局稳定、不透明 + session_id + seq # WAL 原子提交时分配;Session 内单调连续 + role # system | user | assistant | tool + content[] # 完整、类型明确的内容块 } ~~~ -- 新消息只输出版本化、不透明的 `encode_v4(MessageRef)`,不再有独立随机 UUID。 -- 迁移旧 Message 时,在同一 append batch 追加一条一对一 token mapping event;Session 不保存 raw old id。LegacyIdResolver projection 可由 token events 重建。 -- v4 full snapshot 对旧消息也输出 `encode_v4(MessageRef)`。旧 id 只在 API 入站兼容期被接受:边界用 active/retained identity keys 计算候选 token,解析成 MessageRef;Core 之后看不到 Legacy 类型。 -- resolver 必须先从已经认证的 route/session context 取得 `session_scope`,再计算 token;禁止拿裸 legacy id 做跨 Session 全库查找。 -- 同一 legacy token 映到两个 MessageRef、两个 token 映到同一 MessageRef或跨 Session 使用都 fail-loud;不得按角色、时间、正文或相邻 seq 猜。 -- 映射所用 identity key version 与 AdmissionToken 遵守相同 keyring/退休规则;SessionGone 前仍能把旧 id 解析为原 MessageRef 或 Gone,却无法从 token 反推出 chat identity。 -- 客户端不能拆 message_id 获得权限或业务含义。 -- v4 feedback、附件、删除和引用都用输出的同一 message_id,不再增加 alias;旧链接只走入口 resolver。 -- 来源 transport 的 `client_message_id` 只用于 admission/retry,不是第二个领域 MessageId。 +初始合同不提供通用 `meta` 袋子。以后若要加字段,必须先证明它拥有一个不能由 +`role`、`content`、Session 配置或 projection 表达的独立事实。 -为什么不能只用 wire `seq` 代替 message_id?因为两者回答不同问题: +最小内容块是: ~~~text -message_id 指向 Session 中那一条不可变 Message -seq 指向 Transcript 最近一次 add/remove 变化的顺序 +text { text } +artifact { artifact_ref, media_type } +reply { target_message_id } +tool_call { name, arguments, tool_binding, provider_token? } +tool_result { call_ref, outcome, output } +no_reply {} +delete { target_message_id } ~~~ -删除或重新投影后,MessageRef 不变,但客户端看到变化的 seq 会前进。因此保留两个字段是正交,不是重复。 +这不是所有产品内容的封闭枚举,而是本次设计必须验证的最小集合。新增 image、audio、 +citation 等内容时继续扩展 `content` 的 typed union,不新增平行 Message 表或通用 +metadata 袋子。`artifact_ref` 和 `tool_binding` 指向各自边界已有的不可变对象;它们 +不是新的对话身份。 -v4 不定义 durable Session fork。复制到新 Session 会创建新的 Message event 和新的 MessageRef;可记录非权威 provenance,但不得跨 Session 引用正文。若以后要保留 fork identity,必须先用单独 ADR 定义 lineage、父 Session 删除、retention、权限和 wire 语义,不能暗中塞进 EventRef。 +`outcome` 只有 `success | error | unknown`。失败不是缺一条成功记录,而是一条内容 +明确为 `error` 的完整 tool Message。外部结果无法确认时必须写 `unknown`,不能猜成 +成功,也不能盲目重试。 -### 3.4 Turn 是事件分组,不是 row +`call_ref` 不引入新的随机身份。它由发出 `tool_call` 的 +`(assistant message_id, content block index)` 得到。provider 自己要求的 token 只是 +协议内容,不能升级成 Core 的 ToolCallId。 -~~~text -turn/open ────────────────────────────── turn/close - │ │ │ │ - message run/open message outcome - U1 Run 1 U2/A -~~~ +### 1.3 “Message 存在”究竟证明什么 -`turn/open` 的 EventRef 就是 TurnRef。属于该 Turn 的 Message、Run、Delivery 和 policy event 显式引用 TurnRef,因此不同 Turn 可以安全交错。 +Message 存在只证明两件事: -`turn/open` 不是一个可以空放的壳。Store 强制同一个初始 append batch 按 `turn/open → 至少一条 Admission Message → 一个 run/open` 排序;Message 用 batch local Turn handle,Run 只能引用本批已经验证的 Message。没有 Admission Message/Run 的 Turn、先开 Run 后补输入、以及纯内部空 Turn 都被拒绝。`no_due` 留在 source ledger,不能用空 Turn 代替。 +1. producer 已经产生一个完整 Message; +2. Session WAL 已经持久接纳它。 -Turn projection 只回答: +它不自动证明别的事情: -- 这次逻辑工作有哪些输入和最终输出; -- 它是否仍 open; -- 它最后 completed、superseded、abandoned 或 failed; -- 它包含哪些 Run。 +- assistant Message 存在,不代表手机或邮件已经收到它; +- `tool_call` 存在,不代表工具成功; +- `tool_result(outcome=success)` 才表示工具 owner 确认成功; +- `tool_result(outcome=unknown)` 表示外部效果可能发生,但现在无法确认。 -Turn 不拥有 Message body、Root、Delivery、Session 可见性或 projection cache。 +这样“有没有说出来”和“外部事情有没有做成”是两条正交事实,不再由一个 Run +状态含糊地同时代表。 -`turn/open` 的 TurnGrant 是否包含 `finish_without_output` 也是可重放事实。`turn/close(completed)` 是否需要 final assistant Message 由这个 grant 和事件结构共同决定: +## 二、为什么仍需要 message_id 和 seq -- grant 不含 `finish_without_output` 时必须提供唯一 terminal assistant; -- grant 包含它时,只有先追加一个由 DomainDecisionPermit 授权的 `turn/decision`,才可零 assistant 完成; -- 不保存 `turn_kind` 或 `no_reply` boolean。 +这不是两个 Message 身份,而是两个不同问题的答案: -### 3.5 Run 是一次执行占用 - -`run/open` 的 EventRef 就是 RunRef。一个 Turn 可以有多个 Run,一个 Run 可以有多个 Step;一个 Step 是一次模型请求及其 Tool 调用。 - -~~~text -Turn T -├── Message U1 -├── Run R1 ── interrupted -├── Message U2 -├── Run R2 -│ ├── Step S1 ── tool calls -│ └── Step S2 ── final answer -└── completed -~~~ - -Turn 打开时一次性固定最大 `TurnGrant`;后续 Run 只能缩权,不能重写这个上限。Run 事件只记录不可推导的执行事实: - -- exact plugin Root/generation binding; -- 若读取 Session history,则记录 exact history cut; -- 本 Run 固定的 `reply_to: MessageRef`(没有发送能力时为空); -- close outcome:completed、interrupted 或 failed。 - -同一 Turn 的第一次 history-reading Run 固定 base `TranscriptThrough(cut)`;continuation 和 retry 必须复用这个 cut,只增加本 Turn 的新输入与新 Run facts。这样 background transcript change 不会在 U1 与 U2 之间悄悄改变同一次工作的旧历史。fresh Turn 才取得新的 cut。 - -第一次真实模型请求再追加 exact `request/bound`,并强制引用 current RunRef 与 StepRef。它固定 provider artifact、model、connection、CredentialHandle、roles、CallPermit、prompt artifact refs 与 Tool schema refs,但不复制 history body、rendered Message 或 Tool result。Root 加这些引用必须足以确定地重新构造请求;无法重构的动态 context 必须先作为自己唯一的 Session fact 写一次,再由 request 引用。同一 Run 不漂移;下一 Run 可以使用新的已提交 Root 和 binding。 - -RunRef 本身就是 fence: - -- 每个 Turn 同时最多一个 active Run; -- 同一 Session 同时最多一个带 `history_cut` 的 active Run; -- interrupt、append model-visible facts 和 seal 都必须证明自己仍是当前 RunRef; -- 新 RunRef 永不复用,所以旧 Run 的迟到 seal 必然失败,没有 ABA; -- 不再新增一个内容相同的 fence UUID。 - -StepRef 用同一条规则 fence Run 内的迭代: - -- 每个 active Run 同时最多一个 current StepRef;`step/open` 只在没有 current Step 时成功。 -- `request/bound`、`tool/call`、Tool result Message 和 final assistant Message 必须同时匹配 current RunRef 与 current StepRef。 -- 一个 Step 可以并行发出多个 ToolCallRef,但每个 call 只有一个 terminal Tool-result Message;有 pending/started call 时,必须先得到真实 result,或提交 structured `outcome_unknown` result,才能 `step/close`。外部 effect ledger 的 uncertain 可继续存在。 -- final assistant Message 与 `step/close` 在同一 batch 提交;普通 Tool Step 必须等全部 call terminal 后才能 close。 -- close 后 current Step 清空;S2 打开后,S1 的迟到 callback 即使 Run 仍 active也只能命中已有幂等结果或 fail-loud,不能追加新 Message。 -- `step/open` 的 EventRef 已经不可复用,不增加 Step fence UUID。 - -不读取 history 的 task/message_push Run 可以与 conversation Turn 共存,但所有 Session append 仍由 single-writer 事务串行。 - -### 3.6 Tool 与模型事实也进同一本账 - -下一 Run 恢复所需的内容直接是 Session events: - -~~~text -step/open(S1) -├── request/bound(run, S1, refs only) -├── tool/call(run, S1, arguments once) -├── message/append(tool result, run, S1) -└── step/close(run, S1) -step/open(S2) -├── request/bound(run, S2, refs only) -└── one batch: message/append(final assistant, run, S2) - + step/close(run, S2) -~~~ - -`assistant/chunk` 不在 durable vocabulary 中。Tool call 参数只在 `tool/call`;Tool result 正文只在 Message,其 producer 直接引用 ToolCallRef,不再需要 result-link 或 `ToolFact` 第二张表。ModelHistory projection 只使用完整、已经闭合的 call/result 对;未闭合调用在 crash repair 中变成明确 `outcome_unknown`,不能静默重放。 - -unknown 之后的外部查询只追加无正文状态事实:`tool/effect-resolved { call, happened | not_happened, receipt_ref? }`。显式覆盖则追加 `tool/uncertain-override { call, UserPermitRef }`。二者都不改写原 unknown Message;TurnState fold 用它们清除或受审计地越过 unresolved-effect fence。 - -Tool 插件自己的 effect ledger 仍独占外部副作用 finality。Session 记录模型见到什么,effect ledger 记录外部世界实际发生什么,两者不能互相冒充。 - ---- - -## 四、所有读模型都是 projection - -### 4.1 一个统一 projection 规则 - -每个 projection 都是: - -~~~text -State(n + 1) = apply(State(n), SessionEvent[n + 1]) -~~~ - -每个 projection owner 注册: - -- 稳定 `projection_key`; -- 初始状态; -- 纯、确定性的 `apply`; -- state schema version; -- 可选 wire view; -- 若持久化,最后成功应用的 `source_event_seq`。 - -cache/checkpoint 只是一条捷径: - -~~~text -(session_id, projection_key, state_version, source_event_seq, value) -~~~ - -server-side checkpoint 的 `value` 只保存 refs、状态和必要的 derived value,不保存原始 MessageBody;需要展示正文时按 MessageRef 从 Session 读取。schema 版本不符、cursor 越过 Session 末尾或校验失败时,直接丢弃并从 Session 重建。禁止用 cache 反写 Session。 - -客户端 Room/浏览器状态可以持有用于离线阅读的 materialized body,但它明确是可替换 projection:不能回传覆盖 Session,必须应用 remove/SessionGone。外部 provider 在发送过程中收到正文也是外部效果边界,不因此取得 Akashic Message authority。 - -### 4.2 必需的 projection - -| Projection | 输入 | 输出 | 可否删除重建 | -|---|---|---|---| -| TurnState | turn/run/message events | open Turn、Run、outcome | 是 | -| ModelHistory | transcript cut、当前 Turn message、tool/request events | 下一次模型输入 | 是 | -| Transcript | transcript add/remove + MessageRef | 用户聊天历史 | 是 | -| Mobile/Web | Transcript + TurnState + domain projections | 已提交 wire snapshot/delta | 是 | -| Memory/Embedding | eligible transcript + policy events | 长期检索结构 | 必须可对账、可清理、可重建 | -| DeliveryState | delivery events | pending/uncertain/settled | 是;外部效果仍以 provider receipt 为证 | - -Turn 和 Run 因此仍是重要领域词,但不是独立真源。删掉 projection cache 不会删掉 Turn;重新 fold 同一 Session 会得到同一 Turn。 - -partial Draft 的 token stream 不是 Message projection,也不是 durable history。它是 Run Host 到当前连接的短暂 signal,必须带 RunRef,旧 Run signal 在客户端和服务端都被丢弃。crash 后不能恢复半截 Draft;一旦 seal,所有 UI 都切到 Session 中唯一的 final Message。 - -#### ModelHistory 的选择规则 - -`run/open` 固定一种 `HistorySpec`: - -~~~text -HistorySpec = TranscriptThrough(session_event_seq) | NoPriorTranscript -~~~ - -每个 Step 的模型输入只解析 refs,不复制 body: - -1. `TranscriptThrough(cut)` 取 cut 时已经 visible 且未 remove 的 Message;`NoPriorTranscript` 取空。 -2. 再按 Session event order 重放当前 Turn、当前 Step 之前的 model-visible surface:所有 Admission Message,以及所有更早 Run/Step 已闭合的 `tool/call → tool result Message` 对。 -3. replay 跨 Run 保留原因果顺序。例如 R1 tool result、U2、R2 tool result 必须仍按这个顺序出现,不能把全部输入和全部 Tool 结果分成两堆。 -4. 未闭合 Tool call 在 interrupt/crash repair 时,先由 current Run capability append 一个 canonical structured `outcome_unknown` Tool-result Message,再关闭原 Step/Run;于是 pair 可按原序编入 ModelHistory,模型明确看见“外部可能已发生”。没有 terminal Message 的 pair 不能 close、不能编入历史。 -5. 忽略其他 open Turn 的内部 Message;它们只有先通过 `transcript/add`,并且 event seq 不大于 cut,才会进入后续 history。 - -因此同 Session 的 background task 可以和 conversation 交错,却不会因为“恰好写在前面”污染聊天模型。Task output 是否成为未来 history 只由 `transcript/add` 决定,不由 source 名、role 或 policy 反推。带 `TranscriptThrough` 的 Run 仍独占 Session history-reading lane;`NoPriorTranscript` 的 Run 只读自己 Turn,可以并发。 +| 字段 | 回答什么 | 何时得到 | +|---|---|---| +| `message_id` | 这是不是同一条 Message | append 前 | +| `seq` | 它在这个 Session 的第几个位置 | commit 时 | -TurnState 还从 `outcome_unknown` 派生 unresolved-effect fence。fence 存在时,CallPermit 默认拒绝新的 effectful Tool;无法可靠区分 read-only/effectful 时就拒绝全部 Tool。只有 owner query 后追加明确 resolution,或用户用专门 permit 追加 `tool/uncertain-override`,才可再次产生外部效果。模型自己说“重试”不构成授权。 +只用 `seq` 会遇到一个无法消失的问题:客户端发送 U1,服务端可能已经提交,但 ACK +在网络中丢了。客户端重发时还不知道 U1 的 `seq`。如果没有预先存在的稳定身份, +服务端无法区分“同一条 U1 重试”和“又说了一次 U1”。 -### 4.3 Transcript 才是“聊天页” +因此正确的减法不是删除 `message_id`,而是让它成为唯一身份: -Message 被写入 Session,不等于用户已经看见。可见性由引用型事件表达: +- 客户端创建完整 user Message 时生成 `message_id`;重试始终复用它; +- 模型或工具产生完整内容以后、append 以前生成 `message_id`; +- 删除 `client_message_id`、`retry_of_client_message_id`、TurnId、RunId、StepId 和 + Core DeliveryId; +- 老数据已有的 message ID 原样保留并视为不透明值,不建立 alias 或映射系统; +- 新 ID 的具体编码只是实现选择,合同只要求唯一、稳定、不可从业务含义推断。 -~~~text -transcript/add { message: MessageRef } -transcript/remove { message: MessageRef, reason } -~~~ +如果改成 `(session_id, seq)` 派生 `message_id`,仍要再造一个 pre-commit retry key。 +那会把一个身份重新拆成两个,反而更复杂。 -这些事件不复制正文。每条 transcript 变化各占一个 Session event seq;conversation seal 可以在一个 append batch 中连续写 U1、U2、A 的三个 add。 +## 三、唯一写协议 -Transcript fold 强制每个 Message 只能 `unseen → visible → removed`:不能重复 add、不能 remove unseen、不能在 remove 或 Message Gone 后重新 add。add/remove 与被引用 Message 必须同 Session。 +### 3.1 Append 是 commit 点 -手机和 Web 使用: +所有 producer 都走同一条路: ~~~text -message_id = referenced MessageRef 的不透明编码 -seq = transcript/add 或 transcript/remove 的 event_seq -cursor = 客户端已经扫描完成的 Session event high-water +在内存中产生完整内容 + │ + ▼ +封好不可变内容,并分配 message_id + │ + ▼ +Session.append(message, expected_head_seq?) + │ + ├── 校验 Message 与原子前置条件 + ├── 分配 seq + ├── durable commit + └── commit 后才 ACK / 发布给 projection ~~~ -于是: +WAL 必须保证: -- `changes(after=cursor)` 扫描更高 Session events,只返回其中的 transcript changes,并返回本次扫描到的 `next_cursor`;即使没有 delta,cursor 也能前进; -- transcript `seq` 是稀疏但严格递增的 change watermark,客户端不得要求 `seq + 1` 连续; -- upsert/remove 仍按 message_id; -- U1…Un+A 在同一事务一起发布; -- 删除用更大的 seq 到达,不会被旧 cursor 漏掉; -- 不需要 projection_id、row alias 或第三个持久同步身份。 +1. `message_id` 唯一; +2. 同一 Session 的 `seq` 唯一且连续; +3. 同 ID、同完整内容重试时返回原 `seq`,不再追加; +4. 同 ID、不同内容时 fail-loud,不能覆盖或悄悄归一化; +5. ACK 只能发生在 durable commit 以后; +6. projection 失败不能把已提交 Message 变回未提交。 -### 4.4 Projection policy 留在 Session,不另建 Grant 系统 +append 前的内容与 ID 只是调用参数和内存值;只有 commit 返回的带 `seq` 记录才是 +Session 中的 Message。Message 引用的 artifact 必须已经 durable/ready,不能先写半条 +Message 再补附件。 -“同样的 user + assistant 形状,是否允许写长期记忆”是一个真实独立轴,不能由来源名猜,也不能继续塞进全局 `post_commit` boolean。 +Message 正常路径不可变。唯一例外是用户明确删除后的受控正文擦除,见第九节。 -最小表达是一条 Session event: +### 3.2 模型重试发生在 append 以前 ~~~text -projection/policy = { - turn: TurnRef, - allow: ProjectionKey[] -} +read Session at head H + │ + ▼ +provider 请求 / 断网 / 重试 / token stream ← Message 内容只在内存 + │ + ▼ +完整输出成为 assistant Message M + │ + ▼ +append(M, expected_head_seq=H) ~~~ -- event_seq 本身就是 policy revision,不再生成 grant id。 -- policy owner 在 `turn/open` 同一 batch 写入;来源和模型不能扩权。 -- 未知 key 默认拒绝。 -- 普通 human conversation、内部 validation、user-visible task 可以拥有不同 key 集合,但按“允许什么”表达,不按 Wake/Scheduler 名字表达。 -- 撤销时追加更高 seq 的 policy event;对应 projector 顺序应用并清理。 - -v1 建议只定义当前有真实 consumer 的 key: - -| key | 允许的输出 | -|---|---| -| `message_embedding` | 为已发布 Message 写检索 embedding | -| `long_term_memory` | 把 conversation 纳入长期事实图 | -| `profile_memory` | 更新 SELF/MEMORY 类用户画像 | - -Transcript 本身不受这张表控制;`transcript/add/remove` 已经是明确可见性事实。Compaction 也不受它控制;compaction 只是 ModelHistory 的无损替换 projection,不能删原文。 - -不再需要独立 `ProjectionGrant` relation,也不需要 Core 全局 `ProjectionClaim`。纯 fold 只需 source cursor;真正会向 Session 外写入的 materializer 必须在自己的 effect 边界解决不确定性。 - -### 4.5 异步 projection 如何不迟到写回 +- provider 断网:丢掉未完成输出,重试;Session 不变。 +- token 只生成一半:可以直播给当前界面,但不能 append;崩溃后丢掉。 +- 重试耗尽:产品可以产生一条完整 error Message 再 append;若没有产生 Message, + Session 就不声称助手说过什么。 +- WAL commit 成功但 ACK 丢失:用同一个 `message_id` 重试 append,得到原 `seq`。 -先分两类,不能拿一种协议冒充另一种: +这里不需要 durable attempt、Run 或 Step。若计费和排障需要看失败尝试,telemetry +只记录时间、错误码、用量和 provider request ID 等运行数据,不持久复制未提交正文、 +tool 参数或结果。telemetry 不能决定对话事实,也不能反向补写 Session。 -1. **纯 fold / 同库 materialization**:apply 无 I/O;若结果与 cursor 能在同一个 SQLite transaction 提交,`last_applied_seq` CAS 足够。 -2. **外部 materialization**:embedding、图数据库或远程索引已经越过本地事务边界;owner 必须有窄的 durable effect journal。 +### 3.3 新输入打断旧输出 -每个 effectful projector 自己保存: +假设模型正在根据 U1 生成答案,Session head 是 1;这时 U2 先提交成 seq 2: ~~~text -ProjectionEffect = ( - projection_key, - subject_ref, # same-Session MessageRef / TurnRef, or the SessionId itself - source_event_seq, - desired_version, - effect_key, - state = prepared | started | applied | cleanup_started | cleaned | uncertain -) +seq 1 U1 ──▶ 生成旧草稿 A-old +seq 2 U2 + append(A-old, expected_head_seq=1) ──▶ conflict + 丢掉 A-old,读取 U1 + U2,重新生成 A +seq 3 A ~~~ -这不是 Core 的通用 Claim,也不保存正文。它只回答一个无法从 Session 推导的事实:“远端这次写到底发生了没有”。协议是: +`expected_head_seq` 就是足够小的并发栅栏。两个 worker 同时从同一个 head 生成答案, +也只有第一个能提交;另一个看到 conflict 后丢弃结果。无需保存“现在是哪一个 Run”。 -1. 同一 subject_ref 串行推进 `desired_version`;effect journal 的 `prepared` 必须在网络 I/O 前持久化。 -2. 外部 object key 由 `(projection_key, subject_ref)` 派生;operation key 再加 `desired_version`。provider 必须支持幂等写、按 key 查询或条件版本中的至少一种。 -3. I/O 前写 `started`;crash 后先 query/retry same key。无法判断时进入 `uncertain`,不能假装没写。 -4. 更高 seq 的 remove/revoke/redaction 先提高 desired version,阻止旧 `prepared` 开始;已经 `started` 的旧写必须先被查询或收口,再执行 cleanup。 -5. cleanup 使用相同 object key 和更高 desired version。只有远端确认 cleaned、或查询证明对象不存在,owner 才推进 cleanup cursor。 -6. provider 不提供幂等、查询、条件版本或可靠删除时,不得承载需要可证明删除的 projection;既有 uncertain 会阻塞删除完成并交给人工处置。 -7. Data Management 只有在所有相关 owner 越过 deletion seq,且没有 `started/uncertain` 后才报告完成。 +## 四、工具调用也只是 Message -所以迟到写只有三种可诚实处理的结局:先完成再被 cleanup、被高版本 fence 拒绝,或进入明确 uncertain 并阻塞完成。一个本地 CAS 不能证明远端没有发生;v4 不再作这个错误承诺。 - ---- - -## 五、Turn 怎样工作 - -### 5.1 第一次普通输入 - -Channel 先校验 wire Envelope。MESSAGE_HANDLER 可以拒绝或处理命令;只有 accept 才追加 Session: +### 4.1 正常调用 ~~~text -一个 Session append batch -├── turn/open(TurnGrant) → TurnRef -├── projection/policy -├── message/append(U1, Admission { -│ source, reply_target }) → MessageRef -└── run/open(history_cut, Root, - reply_to=U1) → RunRef +seq 10 assistant tool_call(search, {...}) + │ commit 后才能执行 + ▼ + 调用 / 查询 / 安全重试 + │ + ▼ +seq 11 tool tool_result(call_ref, success, {...}) +seq 12 assistant 根据结果回答 ~~~ -逻辑 `AdmissionKey = (source_owner, source_ref)` 必须包含 owner 所需的 channel/session scope,不能假设裸 client id 全局唯一。持久索引不保存 raw key,而保存 `AdmissionToken = (key_version, HMAC(key_version, "admission" + canonical AdmissionKey))`,由 Session Store 在边界计算并在 append 时强制唯一。固定 domain tag 防止两类 token 互相碰撞。 +顺序是故意的:请求调用工具的 assistant Message 先写入 WAL,外部执行才开始。这样 +崩溃后总能从 `tool_call` 内容算出同一个 `call_ref`。 -相同 AdmissionToken 与相同已校验 payload 重投时返回原 MessageRef;同 token 不同 payload fail-loud。Message 已被用户删除时,token tombstone 返回 Gone,不能重新创建正文。不能把可逆 source_ref 偷放进 token。 +`tool_call` append 前,边界 adapter 必须按本次模型可见的 schema 校验名称与参数, +并封入 exact immutable `tool_binding`。执行时再检查该 binding 仍被当前权限允许;若 +已经撤权,产生 `outcome=error` 的 tool Message。模型看见什么工具是配置的投影, +能否真的调用由工具权限 owner 决定,不能从模型输出反推授权。 -key rotation 也属于 identity contract:Store 对 raw AdmissionKey 用 active 与尚未退休的 key versions 计算候选并一次查询;任一候选命中就返回原 MessageRef/Gone,只有全部未命中才用 active version 新建。只要某版本仍有 live token 或 tombstone,其 HMAC key 就必须保留在受备份的 lookup keyring;整个 Session 已进入 SessionGone 且该版本再无引用后才能退休。轮换前后 redelivery 必须命中同一结果,不能借换 key 复活 Message。 +工具 owner 的规则: -若 source transport 必须 durable 地保管 Envelope,handoff 采用前向恢复:先以 AdmissionKey append Session,再把 source attempt 原子替换成 `admitted(MessageRef)` 并 ACK/擦除 Envelope body。若两步之间 crash,重试第一步只返回同一 MessageRef,再完成 cleanup;source copy 在此期间只是不可投影、不可寻址的 transport recovery buffer,不是第二个 Message。它不能在 admitted 后按自己的正文重新驱动 Turn。 +1. 同一时刻只启动一个该 `call_ref` 的本地 executor;这是可丢失的调度约束,不是 + durable 对话状态; +2. 把 `call_ref` 用作 provider 幂等键; +3. 网络错误时先按 provider 的 query/idempotency 能力确认,再决定是否重试; +4. 得到确定结果以后,产生一个完整 tool Message,再 append; +5. tool Message 的同一 `call_ref` 只能有一个终态结果;重复相同结果幂等成功,冲突 + 结果 fail-loud; +6. 最终 assistant Message 重新读取包含 tool result 的最新 Session,再用 head CAS + 提交。 -### 5.2 interrupted continuation +### 4.2 崩溃与未知外部效果 -~~~text -Turn T -├── message U1 -├── Run R1 interrupted -├── message U2 -├── Run R2 interrupted -├── message U3 -├── Run R3 completed -├── message A -└── Turn completed -~~~ - -U2 到达时,在一个 batch 里用精确 RunRef 关闭 R1、在 U2 的 Admission 元数据捕获新 ReplyTarget、在同一个 TurnRef 下追加 U2,并打开 R2。R2 的 `reply_to=U2`;即使 U1、U2 来自不同 thread,也不会到 seal 时猜。新 Message、新 Run,不新建 Turn。 - -这是 Store invariant,不是调用约定: +`tool_call` 已提交而 tool result 不存在时,“pending”只是从 WAL 算出的视图。 -- 调用者提交 `expected_current_run`;single writer 在锁内 CAS 当前值。 -- 有 active Step/Run 时,batch 必须先把每个未 terminal ToolCall 变成真实 result 或 canonical `outcome_unknown` result,再合法关闭 exact Step/Run,随后 append 新 Admission,最后 open 后继 Run。 -- 没有 active Run 时,只有 latest Run 已明确 `interrupted` 且 Turn 仍 open,才可声明 `expected_current_run=None + expected_latest_run`,并把 Admission 与后继 `run/open` 同批提交。 -- 后继 Run 的 Turn 必须相同;新 Admission 带 ReplyTarget 时,`reply_to` 必须是这条新 MessageRef。 -- idempotent redelivery 只返回既有 MessageRef,不重复 interrupt/open;两个真正的新输入竞争时由 CAS 串行成 R2、R3。 -- bare Admission、错误 expected ref、只 append 不开后继 Run、或让后继继续 reply_to 旧 Message 都 fail-loud。 -- latest Run 是 failed 时,普通 Admission 不得留在旧 Turn;它走 5.3 fresh,原子 close(superseded) 并创建新 Turn。只有显式 retry 可以在旧 Turn 无新 Message地 open 新 Run。 +- provider 能按 `call_ref` 查询:查询真实结果,产生 tool Message; +- provider 支持相同幂等键:可以安全恢复调用; +- 两者都不支持:不能盲目再执行,产生 `outcome=unknown` 的 tool Message。 -因此 R1 的 ModelHistory 永远不会在运行中突然看到 U2;R1 的旧 callback 也会同时被 Run/Step fence 拒绝。 +这承认一个不能被数据模型消灭的边界:Session 回滚不了已经发出的付款、邮件或 Git +操作。正确做法是保留调用意图、复用一个幂等地址并诚实记录 `unknown`,不是再加一套 +Run/effect 状态机假装获得 exactly-once。 -最终 seal 的一个 batch 追加: - -~~~text -message/append(A, run=R3, step=Sfinal) -run/close(R3, completed) -turn/close(T, completed) -transcript/add(U1) -transcript/add(U2) -transcript/add(U3) -transcript/add(A) -delivery/prepared(A, reply address, exact binding) -~~~ +## 五、旧 Turn 和 Run 去哪里 -整个 batch 成功或失败,不会出现半个聊天 Turn。 +它们都不进入新领域模型、schema、公共 API 或持久化合同。 -### 5.3 retry 与 fresh +如果聊天 UI 想把若干行圈成“一次交互”,可以临时画括号: ~~~text -latest Run failed -├── explicit retry(prior_client_message_id, command_id) -│ ├── 不创建 Message -│ └── 同 Turn 追加一个新 run/open -└── ordinary fresh(new source_ref) - ├── 旧 Turn close(superseded) - └── 新 Turn、新 Message、新 Run +┌─ 对话分组视图 ───────────────────────┐ +│ seq 1 user U1 │ +│ seq 2 user U2 │ +│ seq 3 assistant tool_call │ +│ seq 4 tool success │ +│ seq 5 assistant A │ +└──────────────────────────────────────┘ ~~~ -- prior_client_message_id 只在原 channel/session identity 下解析到 AdmissionKey,再解析到最后一个 human MessageRef。 -- unknown、cross-scope、not-last 与 Gone 分别 fail-loud。 -- command_id 只让 retry 命令幂等,不成为 MessageId。 -- 正文相同不等于 retry。 +这个括号是 projection,不叫 Turn 对象,没有 ID,没有 open/seal/abort 状态,也不被 +重试、删除、计费或插件引用。分组算法升级后重新画即可。 -retry 与 continuation 的区别不靠 `run_reason` enum:两次 Run 之间有新 Message 就是 continuation;有已接纳 retry command 而没有新 Message 就是 retry。 +同样,开发者界面可以把当前 token、provider 延迟和重试次数画成“执行中”视图;它读 +内存与 telemetry,不叫 Run,也没有恢复业务语义。进程重启后这个视图消失是允许的。 -### 5.4 seal 与迟到完成 +本设计明确接受以下损失: -RunWork 只能用 current RunRef/StepRef seal。Session Store 在同一个写锁内 fold 当前 TurnState 并检查: +- 无法从 Session 重建失败过几次 provider 请求; +- 无法恢复半截 token stream; +- 无法给一次执行尝试分配稳定身份; +- 无法承诺 UI 的对话分组永远不变。 -- Turn 仍 open; -- active Run 正是调用者 RunRef; -- active Step 正是 final Draft 的 StepRef; -- history-reading lane 仍属于该 Run; -- final Draft 合法且只有一个 terminal assistant; -- Session 未进入 deletion state; -- Run 打开时固定的 `reply_to` Message 仍是 Live,且其 ReplyTarget 可由 Delivery owner 解析成 exact binding。 +这些都不是用户或 Agent 已经说出的 Message,因此不值得污染权威模型。 -seal 请求由 `RunRef + final content digest` 幂等识别。完全相同的重试返回同一 MessageRef;同一 RunRef 带不同 digest fail-loud。新 Run 已开始后,旧 RunRef 永远不能封口。 +### 5.1 用 hua-home 历史反推,而不是照抄现状 -所有运行期 append 都走同一条检查:`request/bound`、`tool/call`、Tool result Message、`step/close` 和 final assistant Message 必须携带 RunRef 与 StepRef。Store 验证它们属于同 Session/Turn,且二者在该事件提交前都是 current,或在同一 batch 中合法 close。只带 TurnRef、只带 RunRef或带旧 StepRef 的迟到回调都没有写权限。 +检查过的私有历史只提供反例场景,原始内容不进入本文,旧字段也不定义目标模型: -任何 RunWork 都可以 append assistant Message 后完成。只有 TurnGrant 含 `finish_without_output` 时,Core 才另发一个窄 `QuietCloser`;它还必须收到已提交领域 decision 的 DomainDecisionPermit,才可完成而不追加 assistant Message。 +- **补充输入**:U1 后生成尚未完成,用户又发 U2。目标态是 `U1, U2, A`;旧草稿 + 因 head 冲突丢弃,不留下 interrupt/attempt 记录。 +- **provider 重试**:一次回复前出现连接错误,后来生成 A。目标态仍是 `U1, A`; + 错误只进短期 telemetry。 +- **Wake 检查**:`no_due` 没有进入对话的内容,因此不写 Message;已经产生输入后, + 模型无话可说则写 `[no_reply]`,有话可说则写普通 assistant text。 +- **tool 不确定**:外部调用 ACK 丢失时,目标态从已提交 `tool_call` 恢复并查询;不能 + 确认时写 `tool_result(unknown)`,不把旧 Run 状态当答案。 -普通 listener 的 return 只结束 listener,不能关闭 Turn。 +这些 case 证明需要的是 WAL、幂等和 CAS,不是 Turn/Run 实体。当前行为若与此冲突, +它是 migration delta,不是保留旧设计的理由。 -### 5.5 crash repair +## 六、proactive 不再是特判 -- unmatched current Step 先为无 terminal result 的 ToolCall append structured `outcome_unknown` Tool-result Message,再在一个 repair batch 关闭 Step 与 `run/close(interrupted: process_lost)`;不自动重跑模型、Tool 或 provider。 -- conversation Turn 保持 open,等待显式 continuation、retry、fresh 或 abandon。 -- task Turn 的恢复由 source owner 根据自己的 durable decision 选择继续或关闭。 -- 下一 Run 从 Session 按原序重放本 Turn 所有既有 Run 的已闭合 Tool call/result,包括明确标为 unknown 的 result;unresolved-effect fence 与 Tool effect ledger 共同阻止盲重试。 -- crashed Run 的旧 Root 不跨进程复活;下一 Run 绑定当前已提交 generation。 - ---- - -## 六、Proactive 从 Core 消失 - -### 6.1 两个 quiet path +Core 不认识 `proactive`、Wake、Scheduler 或某个插件名。 ~~~text -source timer fires - │ - ├── source ledger 写 attempt - │ - ├── no_due / admission rejected - │ └── 关闭 source attempt;Session 完全不变 - │ - └── due - └── 在目标 Session 追加普通 Turn + plugin-source Message + Run - │ - ├── domain decline / skip - │ ├── source/domain decision event - │ ├── run/close - │ └── turn/close;无 assistant、无 transcript、无 delivery - │ - └── share - ├── assistant Message - ├── turn/close - └── Delivery +scheduler 检查 no_due ──▶ 不产生 Message +scheduler 判断需要处理 ──▶ 产生普通 system/user Message +agent 判断 quiet ──▶ assistant Message [no_reply] +agent 生成内容 ──▶ 普通 assistant Message [text/...] ~~~ -第一个 quiet 是“没有开始一次 Session 工作”;第二个 quiet 是“一次工作正常结束,但没有可发布 Message”。它们不需要一个共同 `proactive_skip` 字段。 - -### 6.2 来源私有的仍归来源 +所以主动说话和用户问答走完全相同的 append、模型上下文、客户端同步与投递路径。 +没有 `is_proactive` 字段,也没有 proactive 状态机。 -Wake 继续拥有 TimerAttempt、due、watermark、Content pool、Drift 顺序、业务 decision 与 ACK。Scheduler、Spawn 和 message_push 也各自拥有自己的 trigger ledger。 +一旦输入 Message 已经进入 Session,模型产生的 quiet 判断也必须成为完整 assistant +Message:`content=[no_reply]`。Chat projection 隐藏它,恢复 projection 用它判断此前 +缀已经处理。否则重启只看到一条没有后续的输入,会无限重复同一次判断。这里记录的 +不是一个空 Turn,而是 Agent 确实生成的结果。 -Core 只看到普通 event vocabulary: - -~~~text -turn/open → message/append → run/open → ... → turn/close -~~~ +Scheduler 可以在插件边界拥有自己的时间表和 due cursor;那是调度器的运行配置,不是 +第三种对话事实。若“由计划任务触发”本身必须进入对话历史,就由 producer 把它写成 +普通、类型明确的 Message 内容,而不是给 Core 增加来源分支。 -没有 `proactive`、`wake`、`scheduler`、`spawn profile` 或 source enum 改变 Turn/Run 状态机。 +## 七、其余全部是视图 -### 6.3 可见、记忆和发送各看自己的事实 - -| 问题 | 唯一依据 | +| 视图 | 从 Session Message 得到什么 | |---|---| -| 用户是否看见 | transcript/add/remove | -| 模型是否看见 | ModelHistory projection 规则 | -| 是否写长期记忆 | projection/policy + Memory 自己的 eligibility | -| 是否应发送 | delivery/prepared 与 destination contract | -| 为什么开始 | message source → source ledger | - -这样 assistant-only 主动消息不是特殊 Message。它只是一个没有 human transcript input、但有 assistant transcript/add 的 completed Turn。 - ---- - -## 七、Delivery 也不携带 Message - -### 7.1 ReplyTarget 与 exact binding - -回复位置也只捕获一次,但它不是 Message 正文,也不需要自己的 ID。Channel Host 或 source adapter 用受限 admission capability,把 opaque ReplyTarget 写进输入 Message 的可擦 privacy slot: - -~~~text -ReplyTarget = { - logical_channel, - logical_address, - credential_handle -} -~~~ - -ReplyTarget 不固定实现 generation,不包含 secret,也不复制输入 Message。v4 的每个 Turn 必须由至少一条 Admission Message 开始;`run/open.reply_to` 只能选择当前 Turn 内、带已授权 ReplyTarget 的 Admission Message。普通 conversation 选择触发本 Run 的最新输入;retry 复用原输入。Task/Wake/Scheduler 也由自己的 source permit 在 Admission Message 上写 target,不能让模型构造地址;没有 `reply_to` 的 Run 只能完成而不发送。 - -seal 前,Delivery owner 从 `reply_to` Message 的 metadata 解析当前可用的 exact target,并取得短 lease;seal batch 才追加: - -~~~text -delivery/prepared = { - turn: TurnRef, - message: MessageRef, - reply_to: MessageRef, - exact_binding_slot: - Live { - exact_artifact, - exact_generation, - adapter_contract_version, - destination_mapping, - credential_handle - } | Gone { redaction: EventRef } -} -~~~ - -DeliveryRef 就是该 event 的 EventRef。调用者不提供 idempotency key;worker 只在 prepared 已提交后,从 `DeliveryRef` 派生 provider operation key 再开始 I/O,因此没有“先知道自己的 event seq”分配环。若 batch 失败就释放临时 lease;提交后 lease 转给 durable Delivery lifecycle。 - -这比 admission 时长期钉住 adapter 更合理:一个 Turn 可以经过多次 Run 和 generation reload;真正产生发送义务时才冻结实现。一旦 prepared,恢复必须用 exact binding,找不到就 fail-loud,不能换成当前 adapter 猜。 +| Chat | 选择 user/assistant text,应用 delete | +| Model context | 映射 role/content,裁切只发生在本次请求 | +| 对话分组 | 为 UI 临时圈住相邻 Message | +| Tool status | `tool_call` 加对应 `tool_result` 得到 pending/success/error/unknown | +| Next action | 从末尾 Message 判断执行 pending tool、生成回复或 idle | +| Web/Mobile | 按 `seq > cursor` 返回 Message | +| Memory/Search | 从允许学习的 Message 建索引 | +| Live stream | 展示尚未提交的内存输出;刷新或崩溃可丢失 | -### 7.2 状态是 delivery events 的 projection +projection 可以缓存 `(session_id, projection_version, source_seq, value)`,但缓存必须: -~~~text -delivery/prepared - ├── delivery/canceled - └── delivery/provider_started - ├── delivery/rejected - ├── delivery/uncertain - │ ├── delivery/rejected(query 证明未发送) - │ ├── delivery/delivered(query 证明已发送) - │ └── delivery/abandoned_uncertain(用户明确接受未知) - └── delivery/delivered - └── transcript/add(U1…Un+A,同一 batch;provider-first 时) - └── delivery/settled -~~~ - -每次 transition 追加一个引用 DeliveryRef 的事件。DeliveryState fold 拒绝非法跳转;Session single writer 和当前 state CAS 防止两个 worker 同时推进。 - -规则: +- 可删除、可重建; +- 只读到某个 `source_seq`,不能宣称看见未来; +- 不获得覆盖、重排或补造 Message 的能力; +- 版本不匹配时重建,不迁移成第二份事实; +- 上下文裁切、摘要和 token budget 永不 UPDATE/DELETE Session Message。 -1. prepared 引用 MessageRef、完整附件集合和 exact target binding,不复制正文。 -2. provider_started 必须在网络 I/O 前提交。 -3. provider 没有幂等键或查询能力时,crash 后只能 uncertain,不能盲发。 -4. delivered 保存去掉正文、preview 和附件内容的 provider receipt;它不能因后续 Session projection 失败而回滚。 -5. provider-first 只有拿到 receipt 后,才在一个 Session batch 追加 delivered 与该 completed Turn 的全部 `transcript/add(U1…Un+A)`;不能先露出半个 Turn。 -6. session-first 在 seal batch 已追加 transcript/add,通知失败不能撤销 Message。 -7. settled 只表示本地与来源 ACK 已前向收口,不改写 provider 历史。 -8. prepared 到 settled/rejected/canceled/abandoned_uncertain 之间,exact target 不能退役。 -9. uncertain 只能由 provider query/recovery 进入 delivered 或 rejected;没有证据时,只有显式用户授权才能追加 abandoned_uncertain。该事件只结束本地等待,审计仍记“外部可能已发生”,不能声称回滚或未发送。 - -顺序由 destination contract 决定,不由 passive/proactive 名字决定: +客户端协议只需要: ~~~text -canonical Akashic client: Session-first -external provider: Provider-first +request: session_id, after_seq +response: Message[] +cursor: 最后完整应用的 seq ~~~ -### 7.3 deletion 与 delivery - -- prepared 且尚未 I/O:先追加 canceled;同一 maintenance transaction 再把 exact binding slot 转 Gone,之后才删除正文。 -- provider_started 或 uncertain:删除阻塞,直到 provider 查询确认结果,或用户明确授权 `abandoned_uncertain` 并接受外部副本可能仍存在。 -- delivered 但尚未 transcript/add:删除流程追加 `settled(projection_gone)`,保留 delivered 事实,不再把 Message 放进 Transcript。 -- settled/rejected/canceled/abandoned_uncertain:Data Management 把 exact binding privacy slot 转 Gone,只保留无正文、无地址的效果 identity、最小 receipt 和必要 uncertainty audit。 - -Delivery 发现 Message tombstone 时返回 Gone,不得重建或另存正文。 +客户端用 `message_id` 去重和引用,用 `seq` 排序与追赶。projection 晚到、重复或重建 +都不能改变 WAL。 ---- +恢复时也不读 Run 状态。`Next action` projection 依次判断: -## 八、删除与 projection 收口 +1. 有尚无终态 result 的 `tool_call`:查询或安全恢复该工具; +2. 否则,末尾是需要反应的 user/system/tool Message:从最新 head 生成 assistant; +3. 最新 assistant 没有 unresolved tool call(包括 text 或 `no_reply`):idle。 -### 8.1 正常路径只追加 - -- Session event envelope、坐标和非内容元数据正常只追加。 -- Projection cache、索引、Mobile Room、Web state 和 compaction 可以随时删除重建。 -- context 裁切、容量优化、插件 reload 和 cache cleanup 无权改写 Session Message。 -- source ledger 与 provider effect ledger 按自己的 retention contract 管理,不由 Session cache cleanup 删除。 - -为同时满足“正常只追加”和“用户可以真正撤销正文”,Session Store 明确定义一个、也只定义一个例外:内容型 event 的 payload 是 Session-owned erasable slot。 - -~~~text -StoredMessageEvent = { - immutable: event_seq + turn + role + producer refs / admission_token, - body_slot: Live(MessageBody) | Gone(redaction_event_seq), - private_slot: Live(SourceRef + ReplyTarget) | Gone(redaction_event_seq) -} -~~~ +assistant Message 通过 head CAS 紧跟它实际读取的前缀,因此“哪个前缀已经处理”也能 +由 seq 推出,不需要额外 cursor、Turn outcome 或 Run receipt。 -普通 writer 只能创建 `Live`,永远不能 UPDATE。只有 Data Management maintenance transaction 能把 content/privacy slot 的 `Live → Gone`;不能反向恢复,也不能换成另一段正文或地址。`read_events` 和 `get_message` 先解析 slots:历史位置已经 Gone 时,重放从该位置直接得到 tombstoned Message,不会先遇到一个缺 `content` 的坏事件。 +## 八、投递是 Message 之后的外部效果 -### 8.2 删除一个 Turn 的 Message +canonical Web/Mobile 直接读 Session WAL。邮件、推送或第三方聊天渠道则由投递 worker +把“哪些 assistant Message 需要送出”算成队列视图。 -用户明确撤销后: +这类 retry 发生在 Message append 以后,但它不是“重新生成 Message”: ~~~text -建立可验证备份与影响预览 +assistant Message 已提交 │ -关闭相关 Run / Delivery;query 或显式覆盖 unknown Tool effect - │ -一个 Session maintenance transaction -├── transcript/remove × N -├── redaction/apply × N -├── turn/redacted -└── 同事务把对应 Message body/private 与 - terminal Delivery exact-binding slots: Live → Gone(redaction seq) - │ -清理 attachment object 与各 durable materializer -并让 source owner 擦除 Envelope/recovery payload - │ -append redaction/verified;全部 ack 后才报告完成 + ├── provider 发送成功 + ├── provider 明确失败,可按策略重试同一个 message_id + └── ACK 不明,先查询;不能确认时标记外部状态 unknown ~~~ -通用 redaction event 只命名要擦的 owner 与 slot: - -~~~text -redaction/apply = { - targets: [ - { owner: MessageRef, slot: body | private }, - { owner: ToolCallRef, slot: arguments }, - { owner: DeliveryRef, slot: exact_binding }, - { owner: EventRef, slot: declared_dynamic_context } - ], - reason, - audit_principal_token -} -~~~ - -event 不保存旧正文、旧地址、附件内容或默认 content digest。event vocabulary 为每种 event 声明允许擦除的 slot;任意字符串、任意 SQL 或不属于 Data Management scope 的 target 都 fail-loud。`get_message` 在 body Gone 后返回 Gone,不伪装成 NotFound。 - -Session Store 在事务锁内先分配 redaction seq,再同时追加 redaction event 和写入 `Gone(redaction_seq)`。crash 在 commit 前,两者都不存在;crash 在 commit 后,两者都存在。integrity check 强制每个 Gone 指向同 Session、更高 seq、已提交且精确列出 `(owner_ref, slot)` 的 redaction event;反向也强制每个 target slot 已 Gone 且指回该 event。full replay 因而始终确定。 - -原始 event envelope 与非正文 Run/Delivery finality 可以保留;Session content/privacy slot 的 `Live → Gone` 是 append-only 的唯一破坏性例外。raw SourceRef、ReplyTarget、Delivery exact address/credential handle、Tool call 参数、一次性 dynamic context 和其他含用户内容的 event 都使用这种 erasable slot,分别变成 Gone;不存在可供删除的持久 `assistant/chunk` 或 rendered Prompt 副本。防重只保留不可逆 AdmissionToken,不保留 raw AdmissionKey。 - -迟到写不能复活正文:旧 Run 因 RunRef fence 不能 append;同库 projection 被更高 cursor 拒绝;外部 materializer 则按 4.5 先 drain `started/uncertain`、再 cleanup。仅仅推进本地 CAS 不算外部清理完成。 - -### 8.3 删除整个 Session - -`delete_session` 不是循环删行: +- worker 始终引用原 `message_id`,不复制正文,不生成 DeliveryId; +- queue/cache 只保存 `message_id` 和短期运行数据,发送时从 WAL 读取正文; +- provider 支持幂等键时直接使用 `message_id`; +- provider 不支持幂等或查询时,系统不能承诺 exactly-once; +- receipt、重试次数和延迟属于 provider telemetry/worker cache,不进入 Core schema; +- 若将来产品真的要求“送达结果也成为可回忆事实”,它只能作为一条新 system Message + 进入同一 WAL,不能新建平行账本。 -1. 建立 SQLite backup、附件清单与 projection/effect 影响预览。 -2. 关闭新 admission,停止 history-reading lane。 -3. interrupt active Run,等待其 durable close。 -4. cancel 未 I/O Delivery;provider_started/uncertain 明确阻塞,除非 query 收口或用户显式接受 abandoned_uncertain。 -5. 对 delivered Delivery 保存外部 finality,并关闭未完成 transcript projection。 -6. 在 maintenance transaction 追加 session deletion/redaction、全部 transcript remove,并把 Message/Tool argument/dynamic context content slots,以及 SourceRef/ReplyTarget/Delivery binding privacy slots,原子转为 Gone。 -7. 擦除 attachment object、staging object、source Envelope/recovery payload、受控日志副本和其他已盘点用户内容;Request 从未保存 rendered Prompt 或 history body。 -8. 等所有同库 projection cursor 越过 deletion seq,并按各 owner journal 收口外部 materializer cleanup。 -9. append deletion/verified,只保留 SessionId、wire cursor shell、AdmissionToken/LegacyToken Gone 映射、最小非正文 effect finality 与删除 audit。 -10. 校验引用图、cursor high-water、backup retention 与 SQLite integrity 后才报告成功。 +因此,Message 存在的标准仍是“Agent 已经完整说出并提交”,而不是“每个外部渠道都 +已经收到”。 -SessionId、EventRef、AdmissionToken 和 LegacyToken 永不复用。删除后的 retention 窗口内保留无正文 Session shell,使离线客户端收到更高 seq 的 remove 和 terminal `session_gone`;shell 物理 GC 后,最小 Session tombstone 仍让旧 cursor 得到 `SessionGone` 并清空该 Session,而不是当作一个从未存在的新 Session。服务端不能声称已经擦除离线设备无法控制的本地副本。 +## 九、删除仍只使用 Message ---- +正常路径永远只追加。用户明确撤销一条 Message 时,先追加一条普通 delete Message: -## 九、公共 API 应该很小 - -### 9.1 Session Store 原子能力 - -~~~python -class SessionWriter(Protocol): - async def append_batch(self, build: EventBatchBuilder) -> CommittedBatch: ... - async def read_events(self, after: int, limit: int) -> list[SessionEvent]: ... +~~~text +seq 20 system delete(target_message_id = M7) ~~~ -调用者不能发明 event_seq 或自行构造任何 typed ref。它只能使用 capability 已绑定的 committed ref,或 Builder 为本批较早事件分配的 typed local handle;Store 在锁内分配坐标,提交后返回真正 EventRef。 +所有 projection 读到 seq 20 后都不再展示、送入模型或学习 M7。删除没有 Redaction、 +Tombstone row 或另一套版本号;delete 只是 Message 的一个内容块。 -Store 在边界集中校验 JSON、event vocabulary、所有内部 ref 同 Session且指向正确类型、`turn/open` 初始 batch 至少一条有序 Admission Message且恰有一个后置 run/open、已有 Turn Admission 的 interrupt→append→successor 原子结构、运行产物的 current RunRef/StepRef、Turn/Transcript outcome、`reply_to` 指向本 Turn 的已授权 Admission Message、delivery transition、AdmissionKey uniqueness 和 deletion state。 +delete 的效果单调:普通 delete 不能以“删除 delete Message”的方式恢复旧正文。已删除 +ID 的旧 append 重试只返回原 `seq` 与 gone 结果,永远不比较、覆盖或恢复原 content。 -### 9.2 一个 writer,加一个可选 quiet capability +“撤销刚才那组对话”先让当前 UI projection 解析出一组明确的 `message_id`,再为每个 +目标 append delete Message。执行清单固定后不随分组算法变化,也不需要一个 TurnId。 +删除整个 Session 则是名称明确的 Data Management 操作,不伪装成普通对话写入。 -~~~python -class RunWork: - async def react(self) -> FinalDraft: ... - async def interrupt(self) -> None: ... - async def seal(self, draft: FinalDraft) -> MessageRef: ... +若用户还要求物理擦除正文,Data Management 必须在 delete Message durable 以后: -class QuietCloser: - async def finish_without_output(self, decision: DomainDecisionPermit) -> None: ... -~~~ +1. 建立名称清楚的恢复点; +2. 列出目标 Session、Message 和受影响 projection; +3. 只擦除目标 Message 的 content payload,保留 `message_id`、`session_id` 和 `seq`; +4. 重建所有持久 projection 并做前后完整性检查; +5. 保证旧请求重试不能让 M7 复活。 -它们只是限制合法 append batch 的一次性 capability,不是持久 Turn 类型: +这是 append-only 的唯一例外,必须由用户明确的数据管理操作触发。上下文压缩、容量 +优化、迁移和插件都无权调用。 -- 默认 RunWork 不能无输出 completed。 -- QuietCloser 只在 TurnGrant 明确允许时签发;没有已提交 decision 也不能使用。 -- 输入或输出是否进入 Transcript 只看 transcript events,不由 writer 类型决定。 -- 模型只产生 Draft,不选择 Session、event seq、message_id 或 destination;target 已由 Run capability 固定。 -- DomainDecisionPermit 只证明 source/domain decision 已提交,不是 Core 的 source enum。 +## 十、从 DSH 借什么 -### 9.3 插件边界 +检查的 DSH 基线是 +`/mnt/data/source-code/deepseek-harness@49a606bc5b5934603f22a26957a07dc799ab0291`。 -默认 REACT 插件拥有 Prompt、Context、LLM、ToolView、ToolSearch、Tool loop、stream 和 Draft。Core 提供 Session append/read、Root lease、TurnGrant、CallPermit 与 writer capability。 +值得借用的只有三个原则: -~~~text -ToolView 模型看见哪些 Tool -TurnGrant 这个 Turn 最多可做什么 -CallPermit 这一次调用真正获准什么 -~~~ +1. 一份 ordered Session log 是 history 与 projection 的共同来源; +2. 一个 immutable Message representation 跨 history、model request 和 delivery 复用; +3. Message 在 publication/append 前已经拥有稳定 ID,pure projection 可随时重建。 -三者不能合并。隐藏 Tool 不等于撤权,展示 Tool 不等于授权。 +本设计不照抄 DSH 的 `SessionEvent`、turn/step 事件或完整 runtime。Akashic 再做一步 +减法:Message 自己就是 WAL record,不需要事件壳。 ---- +对应源码证据: -## 十、每个事实只有一个 owner +- `packages/llm/llm/src/message.ts:130`:一个 immutable Message 供 history、model + request 和 delivery 共同使用; +- `packages/llm/llm/src/message.ts:175`:Message 在发布前创建稳定 UUID;本设计只借 + pre-append identity,不规定 UUID 格式; +- `packages/core/session/src/index.ts:628`:Session 以单调 seq append; +- `packages/core/session/src/index.ts:772`:model messages 从 Session surface 派生; +- `packages/session/session-projection/src/index.ts:40`:projection 是按 seq fold 的 + versioned cache。 -| Owner | 唯一拥有 | 不拥有 | -|---|---|---| -| Session Store | event log、event_seq、batch 原子性、引用校验、redaction | Prompt、provider、projection 业务 | -| Source plugin | pre-admission attempt、due、cursor、业务 decision | Message body、Turn/Run 状态、Delivery | -| Channel Host / source adapter | wire 校验、source_ref、受限 target capture、ACK | Session truth、exact binding、模型算法、provider finality | -| Run Host | live task、per-Turn lease、history-reading lane、interrupt token、Root lease | Message body、外部 effect | -| Plugin Runtime | artifact、Root、generation、publish、drain | Session/Delivery outcome | -| REACT plugin | Prompt/Context/LLM/Tool 算法与 Draft | Message identity、Session commit、授权授予 | -| Tool plugin | schema、调用、自己的 effect ledger | 全局 loop、任意 Session 写入 | -| Projection Registry | fold 驱动、key、schema version、checkpoint/cursor | Session event、领域 projection 内容 | -| Projection plugin | 自己的 state、wire view、外部 effect journal、cleanup receipt | 原始 Message、Core 全局 claim | -| Delivery owner | delivery event producer、exact binding、provider receipt、恢复 | Message body、source ACK 业务 | -| Data Management | backup、maintenance mode、redaction、tombstone、cleanup 协调 | 正常 compaction、provider 回滚 | - -物理上同一个 SQLite 文件可以让 append batch 原子;逻辑 owner 仍通过窄端口分开。 - ---- - -## 十一、v3/v4 再做一次减法 - -| 旧概念 | 新处理 | -|---|---| -| 独立 MessageBody store | 删除;Message body slot 是 Session message event 的一部分 | -| SessionEntry relation | 删除;Transcript add/remove 是 Session events | -| SessionReceipt outbox | 删除;Session log 本身就是 cursor/change feed | -| ProjectionGrant table | 删除;policy 是 Session event | -| Core ProjectionClaim/跨插件 lease | 删除;纯 fold 用 cursor,外部写由 owner 的窄 effect journal 收口 | -| Turn table | 删除;TurnState fold turn events | -| Run/Attempt table与随机 ID | 删除;RunState fold,RunRef 是 run/open EventRef | -| ToolFact table | 删除;request/tool/message events 已在 Session | -| Delivery row随机 ID | 删除;DeliveryRef 是 prepared EventRef,state 由 events fold | -| pending body 与 committed body | 删除双份;Envelope/Draft 在提交前不是 Message | -| placement | 删除;Transcript event 是唯一可见性事实 | -| proactive boolean/kind | 删除;source attempt 与普通 Turn 足够 | -| storage=durable/in_memory | 删除全局轴;Session fact durable,cache 可重建 | -| post_commit allow/suppress | 拆成明确 projection keys 的 policy event | -| 额外 fence UUID | 删除;当前 RunRef 就是不可复用 fence | - -仍必须保留的真实轴: - -- SessionId:事实作用域; -- event_seq:Session 内唯一顺序; -- MessageRef:正文身份; -- transcript seq:用户可见变化顺序,直接复用 transcript event_seq; -- ReplyTarget metadata 与 `run.reply_to`:逻辑发送位置在输入处捕获,Run 只选择已有 MessageRef,exact binding 后置; -- source attempt:可能先于 Session 或根本没有 Session; -- exact Root 与 request binding:同一 Turn 的不同 Run 可以独立换代; -- projection policy:相同消息形状可以有不同长期影响许可; -- provider / materializer effect state:外部世界不会跟 Session 事务一起回滚; -- tombstone:删除后的重投不能复活正文; -- Legacy token mapping:旧入站身份必须在不保存 raw id 的前提下确定地解析到唯一 MessageRef/Gone。 - -“foolish and simple”不是把事实硬挤成一个字段,而是让所有事实只沿一条 Session 时间线出现一次,其他东西都能丢掉重算。 - ---- - -## 十二、目标架构 +## 十一、目标写入路径 ~~~text -┌──────────────────────── Sources ─────────────────────────┐ -│ Channel · Wake · Scheduler · Spawn · message_push │ -│ own: attempt / due / cursor / source_ref │ -└──────────────────────────┬───────────────────────────────┘ - ▼ -┌──────────────────── Session append log ──────────────────┐ -│ turn · message · run · step · request · tool │ -│ transcript · policy · delivery · redaction │ -│ one SessionId + one event_seq line │ -└───────────────┬──────────────────────────┬────────────────┘ - ▼ ▼ -┌──────────────────────────┐ ┌───────────────────────────┐ -│ REACT / ModelHistory │ │ Pure Projection Registry │ -│ reads refs, emits Draft │ │ Turn · Transcript · UI │ -└──────────────────────────┘ └─────────────┬─────────────┘ - ▼ - effectful materializer owner - Memory / index journal + I/O +Sources Session Message WAL +────────────────────── ───────────────────────────── +Mobile user Message ───────┐ ┌──────────────────────────┐ +Scheduler Message ─────────┼───▶│ append + seq + durability│ +Assistant complete output ─┤ └────────────┬─────────────┘ +Tool result Message ────────┘ │ + ├──▶ Chat + ├──▶ Model context + ├──▶ Web/Mobile cursor + ├──▶ Memory/Search + └──▶ Delivery queue ~~~ -目标 Core schema/API/test base 不出现: +公开原子能力保持很小: ~~~text -proactive -wake -content duty -drift duty -scheduler -spawn profile -provider family -memory plugin name +SessionStore.append(message, expected_head_seq?) -> committed Message +SessionStore.read(session_id, after_seq?, limit?) -> Message[] +SessionStore.head(session_id) -> seq ~~~ -这些词可以留在 owning plugin 的事件和 projection 中,不能改变通用 Turn/Run append rules。 +工具关联、删除应用、上下文选择和 UI 分组都是对 `read()` 结果的函数。插件只得到完成 +任务所需的窄能力;projection 不持有任意 SQL 或删除权限。 ---- +## 十二、迁移路线 -## 十三、迁移路线 +当前 `projectneed`、代码和数据库仍有 Turn/Run/attempt 合同。本文是替代设计,不允许 +借普通重构偷偷改变线上语义。批准后按以下顺序迁移: -### Phase 0:先批准语义 +### Phase 0:冻结目标合同 -本提案若获批,先同步修改 projectneed 与 accepted decisions: +- 批准“失败 attempt 不进入 Session”的信息损失; +- 批准单一 pre-commit `message_id`; +- 批准 stale assistant output 由 head CAS 丢弃; +- 批准 tool `unknown` 与不支持幂等 provider 的边界; +- 修改 `projectneed` 和相关 decisions,明确旧合同被替代。 -1. Session 从“已完成聊天 rows”提升为完整 append log;Transcript 成为 projection。 -2. Turn/Run 从独立权威 row 改成 Session events 的 projection。 -3. 全局 post_commit 改成 projection policy events。 -4. proactive 从 Core schema/API 删除。 -5. internal MessageRef 只有 EventRef;wire boundary 通过 v4 encoding 或 legacy mapping 解析,不引入随机 v4 MessageId。 -6. v4 不提供 durable Session fork;跨 Session 复制产生新 Message identity。 +### Phase 1:只读转换器 -批准前本文只是 proposed target,不能覆盖现行合同。 +- 从现有 `sessions.db/messages` 和有必要的 tool 数据重建目标 Message WAL; +- 现有 message ID 原样保留;只有缺少 ID 的历史数据才在一次性迁移中分配; +- 对无法证明顺序、角色或 tool outcome 的记录 fail-loud,不能猜默认值; +- 固定真实 Session fixture,比对 Chat、Model context、Tool status 和 Mobile 输出。 -### Phase 1:建立版本化 Session log +### Phase 2:切换唯一 writer -1. 用 SQLite backup API 备份 sessions.db,并记录 schema、hash 与 integrity_check。 -2. 新建带 format version 的 Session event storage、erasable content slot、append_batch 和 single-writer handle。 -3. 写 event envelope、同 Session typed refs、Run fence、unknown-event fail-loud、redaction replay 与 projection fold tests。 -4. 先迁移只读 projection,不切生产 writer。 +- 所有 user、assistant、tool 和 scheduler producer 改走同一个 append; +- 输入 transport 直接复用 `message_id`,删除平行 retry identity; +- 模型 retry 保留在 pre-append 内存流程; +- projection 先 shadow rebuild,证明读取等价后再接管消费者; +- 不长期 dual-write 两套事实。 -### Phase 2:一次迁移旧事实 +### Phase 3:删除旧权威结构 -1. 每条旧 Message 生成一个 message event,并在同批追加 `identity/legacy-token(message, versioned_hmac(old_id))`;验证后删除 raw old id,LegacyIdResolver 由 token events 重建。 -2. 每条用户可见旧 Message 生成 transcript/add event;新 transcript seq 直接使用新 Session event_seq,不伪造旧 seq 映射。 -3. 每个迁移 Turn 也必须用 `turn/open + 首条 Admission Message` 的初始 batch。旧 assistant-only 工作只有在 durable source evidence 能确定地产生非可见 Admission Message 时才能迁;否则整次迁移 fail-loud,不能造空 Turn。 -4. 只有已有明确 interaction/attempt evidence 时才生成 Turn/Run links;任何 Message 无法确定所属 Turn 时整次迁移 fail-loud,交维护者处理,不能按角色、时间或相邻 seq 猜。 -5. allow/suppress 按持久事实迁成 policy event;非法/冲突/未知值 fail-loud。 -6. 差分验证消息数、legacy resolver 一对一、顺序、正文 hash、附件、删除边界和 projection 结果。 +- 在可恢复备份和影响清单上删除旧 Turn/Run/Step/attempt/delivery Core rows 与 API; +- 删除 proactive 分支,让 scheduler 只产生普通 Message; +- 若 seq 发生变化,客户端执行一次明确 snapshot/cursor reset; +- 重建 projection,并核对每个 Session 的 message 数、ID 唯一性、seq 连续性、tool + 配对与 delete 结果。 -迁移完成后删除旧 writer 和旧 schema reader;不长期 dual-write 或双读。Legacy resolver 只接受旧入站值、从不输出 raw old id,是兼容期唯一边界索引,不是第二个内部 Message model。 +迁移前后都不能改写正式 workspace,除非另有明确授权和恢复方案。 -### Phase 3:切 Turn/Run 与 REACT +## 十三、验收 Gate -1. 实现 per-Turn current RunRef/StepRef fence、同 Session ref 校验与 Session history lane。 -2. 用事件 fold 取代 turns/attempt rows。 -3. 把当前 default reasoner 整体包成 REACT,再逐步把 Prompt、Context、Tool 与模型 binding 变成普通依赖。 -4. fixture 比较每个请求的 model-visible messages、Tool facts、final Draft 和 Session events。 +### 13.1 概念 Gate -### Phase 4:切 Transcript 与客户端 +- Core schema 和领域 API 只有 Session、Message; +- 不存在 TurnRef、RunRef、StepRef、DeliveryRef、ProjectionClaim 或 SessionEvent 壳; +- tool call 只用 Message 内可派生的 `call_ref`; +- proactive 不是类型、字段、状态机或特殊 commit 路径; +- 新增任何第三个权威名词时,必须先证明 Session/Message 无法表达其独立不变量。 -1. Session 完成 batch 写 transcript/add;provider-first 在 delivered 后写。 -2. Web/Mobile 使用 message_id + 稀疏 transcript event seq + Session high-water cursor。 -3. cursor 做明确断代,不猜映射:旧 cursor 首次请求得到 `reset_required`;客户端用 v4 full snapshot 原子替换该 Session 本地投影,再保存响应的 `cursor_v4=session_high_water`。 -4. shadow 对比旧 Session history 与新 Transcript projection。 -5. 证明 empty delta 也推进 cursor、reconnect、重复 frame、乱序 frame、删除 tombstone 和 interrupted continuation。 +### 13.2 WAL Gate -### Phase 5:迁移来源与 Delivery +- 同 `message_id` 同内容重试只得到同一 `seq`; +- 同 `message_id` 不同内容 fail-loud; +- commit 前崩溃没有 Message,commit 后 ACK 丢失不会重复 Message; +- 每个 Session 的 seq 唯一连续,projection 失败不影响 commit; +- 正常路径没有 UPDATE/DELETE Message。 -按 Scheduler → Spawn → Wake → message_push 分批: +### 13.3 行为 Gate -- 每批只替换 source admission 和 output handoff; -- Core event vocabulary 不增加来源名; -- Wake 同时证明 no_due 无 Session event、domain skip 有 completed Turn 无 Transcript; -- Delivery 用真实 provider、exact binding 和 crash injection 证明前向恢复。 +- 模型断网和 partial stream 不写 Session; +- U2 抢先提交时,基于旧 head 的 A 被拒绝并重新生成; +- 两个并发 worker 只有一个 assistant Message 能提交; +- tool_call 先提交再执行,crash 后按 `call_ref` 查询或安全恢复; +- 不可确认的外部效果产生 `unknown`,不会盲目重复; +- `no_due` 不产生 Message;quiet 产生普通 `[no_reply]` assistant Message,Chat 隐藏; +- Web/Mobile 只用 `message_id + seq + cursor` 完成重复、断线和追赶; +- delete Message 到达后所有 projection 一致隐藏目标,旧 retry 不会复活它。 -### Phase 6:删除旁路 +### 13.4 明确接受的非目标 -只有所有动态 consumer、插件 cache、正式 generation 和真实 DB 差分都通过后,才删除: +- 不从 Session 审计失败 provider attempt; +- 不恢复半截 token; +- 不给执行尝试稳定 ID; +- 不保证外部 provider 的 exactly-once,除非 provider 提供幂等或查询合同; +- 不让 UI 分组成为持久身份。 -- MessageBody/SessionEntry 双模型; -- SessionReceipt/outbox; -- turns/attempt 权威 rows; -- global post_commit; -- proactive 字段与分支; -- 旧 schema/cursor 兼容 reader;只保留 legacy wire resolver。 +## 十四、对 v3 和前一版 v4 的最终减法 -### 回滚 - -- 每个 schema/data 阶段前建立名称清楚、可校验的 backup。 -- v4 writer 启用后,旧 binary 不得直接打开正式库。 -- 回滚要么运行版本化前向转换,要么恢复切换前 backup 并明确放弃其后的新事实。 -- Git 回滚不能撤销 provider 或 Tool 外部效果;Delivery/effect ledger 继续收口。 - ---- - -## 十四、验收 Gate - -### 14.1 单一真源 - -- [ ] 新 Message 正文只存在于一个 Session message event 的 body slot;durable chunk/request/tool link 不复制它。 -- [ ] Turn、Run、Transcript、ModelHistory、Mobile 与 Memory 可从 Session 重建。 -- [ ] 删除所有 projection cache 后,同一 Session 得到相同视图。 -- [ ] Delivery、Tool ledger、日志和 trace 不复制 Message body。 -- [ ] model-visible Message 与 Tool result 都有 Session event 证据。 - -### 14.2 身份与同步 +| 删除的东西 | 原因 | 现在由什么承担 | +|---|---|---| +| MessageBody / SessionEntry 两层 | 同一正文两种 owner | Message WAL record | +| SessionEvent 壳 | Message 已经是日志记录 | Message | +| Turn / Run / Step | 把临时执行升级成事实 | head CAS + projection | +| Attempt 状态 | 失败重试发生在 append 前 | 内存 + telemetry | +| client/retry message IDs | 同一 Message 多个身份 | 唯一 `message_id` | +| ToolCallId | 可由调用所在位置确定 | `message_id + block index` | +| proactive 分支 | 来源不应改变执行语义 | 普通 input/output Message | +| DeliveryId / delivery rows | 投递不能复制消息身份 | `message_id` + provider 能力 | +| ProjectionClaim | projection 不配拥有事实 | version + source seq cache | -- [ ] 新 MessageId、TurnId、RunId、DeliveryId 不使用独立随机 UUID。 -- [ ] typed refs 都指向正确类型的 EventRef,调用者不能自己填写 seq。 -- [ ] 所有内部 refs 同 Session;v4 无 durable fork 和跨 Session Message graph。 -- [ ] raw legacy message_id 不持久化、不输出;一对一 versioned token 仍把旧入站值解析到原 MessageRef/Gone,Core 内部只有 EventRef。 -- [ ] wire message_id 不透明,授权不依赖不可猜性。 -- [ ] transcript seq 稀疏、单调、不复用;empty delta 也用 next_cursor 推进 Session high-water。 -- [ ] 旧 cursor 只走 reset + full snapshot,不猜新旧 seq 映射。 -- [ ] client_message_id 只做 source-scoped admission/retry。 -- [ ] identity HMAC key rotation 前后,同一 admission redelivery 或 legacy id 命中同一 MessageRef/Gone;有引用的旧 key version 不退休。 - -### 14.3 Turn、Run 与原子性 - -- [ ] U1 → interrupt → U2 → interrupt → U3 → A 是一 Turn、三 Run。 -- [ ] `turn/open` 初始 batch 缺 Admission/Run、或 run/open 排在首条 Admission 前时,Store fail-loud。 -- [ ] retry 是同 Turn 新 Run且无新 Message;fresh 新建 Turn。 -- [ ] existing Turn 的 U2 不能裸 append;必须 CAS exact current Run,并同批 close R1→append U2→open R2(reply_to=U2)。 -- [ ] 两个并发 continuation 由 current Run CAS 排成两个 Run;旧 callback 与只 append 不开 Run 都 fail-loud。 -- [ ] latest Run failed 后 ordinary input 必须 supersede + 新 Turn;只有 explicit retry 可无新 Message留在旧 Turn。 -- [ ] request/tool/运行产物 Message 都强制携带 current RunRef/StepRef,拒绝旧 Run 的迟到 append/seal。 -- [ ] S1 close、S2 open 后,S1 的迟到 Tool result 被拒绝;pending call 只有真实 result 或 canonical outcome_unknown result 后才能 close。 -- [ ] U1→non-idempotent tool started→interrupt/U2→provider unknown 时,R2 history 含 unknown,unresolved-effect fence 阻止自动重复调用。 -- [ ] 同 Session history-reading Run 串行;无 history Run 可以并发。 -- [ ] ModelHistory 取 cut 时 Transcript,并按原序重放当前 Turn 当前 Step 前、跨所有 Run 的 Admission 与 closed Tool pair;不吸入交错 task 私有 Message。 -- [ ] conversation seal 原子追加 final Message、Run/Turn close、全部 transcript/add 和 session-first Delivery。 -- [ ] 每个 Run 固定 exact Root;每个 request 固定 ModelExecution/header。 -- [ ] closed Tool pair 可重放;unknown effect 不盲重试。 - -### 14.4 projection - -- [ ] 每个 projection 是确定 fold,有 state version 和 source cursor。 -- [ ] cache mismatch 直接丢弃重建,不反写 Session。 -- [ ] 同库 materialization 用同事务 cursor;外部 materializer 在 I/O 前写 owner-local effect journal。 -- [ ] started/uncertain 先 query/drain,cleanup 用更高 desired version;本地 CAS 不冒充远端完成。 -- [ ] validation 与普通 conversation 同形状时,policy 仍能禁止长期影响。 -- [ ] policy key 不使用 source/plugin 名字。 -- [ ] Transcript visibility 只读 transcript events,不读 proactive 或 Session 前缀。 - -### 14.5 proactive - -- [ ] no_due/reject 有 source attempt、零 Session event。 -- [ ] due 只使用普通 Turn/Message/Run events。 -- [ ] domain skip 是 completed Turn,无 assistant、Transcript 或 Delivery。 -- [ ] share 只产生一条 final assistant Message,其他模块引用它。 -- [ ] Core schema/API/test base 没有 proactive/wake/source enum。 - -### 14.6 Delivery - -- [ ] prepared 引用 MessageRef,不复制正文。 -- [ ] Run 打开时 `reply_to` 已固定同 Turn、带授权 ReplyTarget 的 Admission Message;U1/U2 不同 target 不会在 seal 时猜。 -- [ ] prepared 冻结 exact target;找不到 exact binding 时 fail-loud。 -- [ ] provider operation key 由 committed DeliveryRef 派生,prepared payload 不自引用未分配 seq。 -- [ ] provider_started 在 I/O 前提交。 -- [ ] crash 后不盲发;uncertain 可查询、可人工收口。 -- [ ] prepared→canceled 与 uncertain→delivered/rejected/abandoned_uncertain 都是合法显式 transition;人工关闭仍保留 unknown audit。 -- [ ] provider-first 只有 delivered 后才 transcript/add。 -- [ ] session-first notification 失败不撤销 Transcript。 -- [ ] target lease 只从 prepared 保持到 terminal,不从 admission 长期钉住。 - -### 14.7 删除 - -- [ ] 删除前有 backup 与影响预览。 -- [ ] redaction/remove 使用更高 Session seq;MessageRef 进入 Gone。 -- [ ] Gone body slot 与更高 redaction event 同事务提交;full replay 不会遇到半个 Message。 -- [ ] raw SourceRef、ReplyTarget 与 terminal Delivery binding 都进入 privacy slot;删除后只留 AdmissionToken 与无正文 finality。 -- [ ] 单 Turn 删除先 cancel 尚未 I/O 的 Delivery,并在同一 maintenance transaction 擦除 canceled exact binding。 -- [ ] source ledger 只留 token/ref/finality,Envelope 与 recovery payload 已擦除;unknown Tool effect 已 query 或由用户明确覆盖。 -- [ ] projection cleanup、started/uncertain materializer 或 uncertain Delivery 未闭合时不报告删除成功。 -- [ ] 迟到 Run、retry、source replay、Delivery 和 projection 都不能复活正文。 -- [ ] delete_session 覆盖 Run、Delivery、附件、Tool result、Tool args、dynamic context、Mobile/Memory projection、cursor shell 与 tombstone。 - -### 14.8 概念 Gate - -- [ ] 每个权威事实只在一个 Session event 或一个明确外部 ledger 中出现。 -- [ ] 每个 cache 删除后可由权威事实重建。 -- [ ] 删除任一留下的字段会破坏一个已命名不变量;否则继续删除。 -- [ ] 没有按来源名复制 Turn/Run/Delivery 状态机。 -- [ ] 没有永久 dual-write、dual-read 或 guessed migration。 - ---- - -## 十五、需要维护者批准的决定 - -推荐整组批准: - -1. Session append log 是唯一 Message 真源,Transcript 只是 projection。 -2. 完整 Message body 只写入一个 Session-owned erasable slot;durable chunk、rendered Prompt 和第二份 Tool result 不存在。 -3. Turn、Run、Step 和 Delivery identity 由 typed EventRef 派生,不新增随机 ID。 -4. internal MessageRef 只有 `(SessionId, message event seq)`;raw legacy id 不持久化/不输出,只通过 versioned HMAC token 在入站 boundary 解析。 -5. wire seq 使用 transcript add/remove event seq,cursor 是 Session scan high-water;旧 cursor 通过 reset + full snapshot 断代。 -6. Turn/Run 权威 rows、SessionEntry、SessionReceipt、ToolFact 和 Core ProjectionClaim 从目标模型删除。 -7. current RunRef 同时承担 execution fence,不再增加 fence UUID。 -8. projection policy 是 Session event,不是 global boolean 或独立 Grant relation。 -9. proactive 从 Core 完全删除;no-turn 与 completed-without-output 分开。 -10. Delivery state由 Session delivery events fold;provider、Tool 和 effectful materializer 的外部 finality 仍归各 owner journal。 -11. logical ReplyTarget 是 Admission Message 元数据,Run 复用 MessageRef 选择 `reply_to`,exact binding 在 prepared 时冻结;不新增 target ID。 -12. 显式删除是 append-only 的唯一内容 slot 变更,以同事务 Gone/redaction、外部 drain 和高版本 cleanup 收口。 -13. v4 不支持 durable Session fork;跨 Session 复制产生新 MessageRef。 - -实现任务仍需单独确定: - -- event envelope 与 wire encoding 的具体字节格式; -- SQLite 表、索引、format version 和 append_batch API; -- abandoned/completed-without-output Turn 的 retention; -- 每个外部 provider 的幂等键与查询能力; -- 哪些 durable projections 必须阻塞删除完成。 - ---- - -## 十六、证据索引 - -### DSH 当前源码 - -检查基线:`/mnt/data/source-code/deepseek-harness` commit `49a606bc5b5934603f22a26957a07dc799ab0291`。以下证据只支持单日志与 pure fold,不证明 Akashic 的异步 I/O、删除、并发或 fork 语义。 - -- `/mnt/data/source-code/deepseek-harness/docs/architecture.md:74`:Turn 是零个或多个 Step;turn/step/message/tool 都写 Session events。 -- `/mnt/data/source-code/deepseek-harness/docs/architecture.md:103`:Session log 是 model context、fork、resume、transcript、telemetry 和 persistence 的源。 -- `/mnt/data/source-code/deepseek-harness/packages/core/session/src/types.ts:255`:SessionEventMap 是 append-only truth。 -- `/mnt/data/source-code/deepseek-harness/packages/core/session/src/index.ts:628`:seq 等于 log length,append 后才发布。 -- `/mnt/data/source-code/deepseek-harness/packages/core/session/src/index.ts:772`:deriveMessages 从 surface projection 重建。 -- `/mnt/data/source-code/deepseek-harness/packages/core/agent-loop/src/index.ts:55`:turnBoundary 是纯 projection。 -- `/mnt/data/source-code/deepseek-harness/packages/session/session-turn-outline/src/projection.ts:84`:Turn outline 从 committed events fold。 -- `/mnt/data/source-code/deepseek-harness/packages/session/session-projection/src/index.ts:40`:projection unit 是纯同步 fold,并以 state version/source seq checkpoint。 -- `/mnt/data/source-code/deepseek-harness/packages/llm/llm/src/message.ts:130`:同一个 Message representation 供 delivery、history 和 model request 使用。 -- `/mnt/data/source-code/deepseek-harness/packages/llm/llm/src/message.ts:175`:DSH 当前为入 Session 前的 Message 生成 UUID;本设计有意不照抄。 - -### Akashic 当前合同与代码 - -- `docs/projectneed.md`:SES-001~SES-008、RUN-001~RUN-009、OUT-001~OUT-005、PRO-001~PRO-006。 -- `docs/decisions/0039-react-core-atoms-keep-sources-unprivileged.md`。 -- `docs/decisions/0040-wake-duty-gate-lives-in-scoped-react.md`。 -- `docs/decisions/0041-turn-effects-and-memory-plugins-are-orthogonal.md`。 -- `docs/design/persistence-state-map.md`。 -- `session/store.py:2392`:当前 turns row 仍表达 attempt;目标态改成 Session event projection。 -- `session/store.py:4744`:当前 message_id 由 session_key:seq 生成;证明 derived identity 已有先例。 -- `session/store.py:5385`:当前 completed interaction 删除边界。 -- `infra/mobile_realtime/protocol.py:188`:client_message_id 是 transport/admission identity,不是 Session MessageId。 - -### 用户要求与运行案例 - -- 2026-09-02/03 Codex 会话:要求回到基本原理、减少概念、正确解释 Turn、消除 proactive 特判,并质疑新 MessageId。 -- 2026-09-03 修正:当前行为不等于正确设计;Session 应是唯一消息载体,其他消息视图都是 projection,并参考 DSH。 -- hua-home 私有历史只用于 interrupt/retry/Wake 场景覆盖;原始内容不提交,也不定义目标语义。 - ---- - -## 最终判断 - -真正简单的模型不是: +最终模型不是“把许多事件放进一份账本”,而是更直接: ~~~text -MessageBody → Turn row → SessionEntry → Receipt → ProjectionClaim → UI -~~~ - -而是: +Session + └── Message + ├── user text + ├── assistant text / tool_call + ├── tool_result + └── delete -~~~text -Session events(唯一事实) - ├── Message 只出现一次 - ├── Turn / Run 只是 fold - ├── Transcript / Model / Mobile / Memory 只是 fold - └── Delivery 只引用 MessageRef +除此以外,Core 内都只是读取这本账的方式。 ~~~ - -新 Message 仍需要一个稳定引用,但不需要一个新的随机 UUID。`(SessionId, message event seq)` 已经足够;wire 继续叫 message_id,只是不再拥有第二套身份。 - -proactive 也不需要被“优化成更聪明的特判”。它应该从 Core 消失:开始前是 source attempt,开始后就是普通 Session events;是否可见、是否学习、是否发送分别由 Transcript、policy 与 Delivery 事实决定。 - -这版比前一版更少,也更正交:少的不是可靠性,而是第二份事实。 From df9467b5720d9186b13e7f2a8cdae75e034f388c Mon Sep 17 00:00:00 2001 From: huashen <2494946808@qq.com> Date: Thu, 3 Sep 2026 14:58:55 +0800 Subject: [PATCH 4/7] =?UTF-8?q?docs:=20=E8=A1=A5=E5=85=A8=20v4=20=E8=A2=AB?= =?UTF-8?q?=E5=8A=A8=E9=93=BE=E8=B7=AF=E6=8F=92=E4=BB=B6=E7=BB=84=E5=90=88?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/design/0902-reviewed-v4.md | 518 +++++++++++++++++++++++++++----- 1 file changed, 439 insertions(+), 79 deletions(-) diff --git a/docs/design/0902-reviewed-v4.md b/docs/design/0902-reviewed-v4.md index d7fae4d3..692650cb 100644 --- a/docs/design/0902-reviewed-v4.md +++ b/docs/design/0902-reviewed-v4.md @@ -1,4 +1,4 @@ -# Akashic v4:只有 Session 与 Message +# Akashic v4:Message WAL 与普通插件组合 - 文档版本:0902-reviewed-v4 - 日期:2026-09-03 @@ -9,6 +9,15 @@ ## 结论 +v4 不是只有一次数据库减法。它同时做两件彼此正交的事: + +1. **事实层做减法**:对话事实只剩 Session 与 Message; +2. **行为层做拆分**:原来固定在 Core/Bootstrap 的被动回复大链路,变成普通插件组合。 + +两件事缺一不可。只做第二件,会让插件继续围着 Turn/Run 等重复状态转;只做第一件, +则只是把新 WAL 塞回旧的 `PassiveMessageWorker → AgentLoop → PassiveTurnPipeline` +巨型流水线,并没有得到可替换的 Agent。 + Akashic 的对话事实层只保留两个名词: ~~~text @@ -40,6 +49,29 @@ Message。它们若要影响或进入对话,只能被 Message 的类型化内 Message,因此不进入 Session。重启后可以丢失这些执行过程;不能丢失的只有已经 提交的 Message。 +被动回复也不是第三个事实对象。它只是一个普通插件提供的函数:读到已提交的 +Message,调用其他普通插件,最后再产生 Message。Core 不认识 passive、proactive、 +Wake 或某一种 Agent 算法。 + +~~~text +对话事实 普通插件行为 +──────────────────── ───────────────────────────── +Session committed Message + └── Message WAL ─────────────▶ MESSAGE_REACTOR + ▲ ├── COMMANDS + │ └── AGENT_PROGRAM + │ ├── Prompt contributions + └──────── append Message ◀──────┼── Context projection + ├── Tool selection + └── Model / Tool ports + +同一份 Message WAL ──▶ Chat / Model context / Memory / Sync / Delivery projection +~~~ + +`MESSAGE_REACTOR`、`AGENT_PROGRAM` 和各项依赖是运行时 capability,不是实体、日志 +记录或另一种消息载体。替换 Agent Program 不改变 Session schema;停用自动回复插件 +也不会删除已经收到的 user Message。 + ### 把我当六岁 Session 是唯一一本作业本。Message 是已经用墨水写完的一行字。 @@ -50,6 +82,13 @@ Session 是唯一一本作业本。Message 是已经用墨水写完的一行字 - 聊天页、模型看到的上下文和“这一轮”的括号,都只是拿彩笔从作业本里画出来。 - 擦掉彩笔,作业本没有少东西;换一种画法,也不用迁移事实。 +旁边还有一个会读作业本、再写新行的机器人,它就是插件: + +- 换一个机器人,只是换回答办法,作业本格式不变; +- 关掉机器人,孩子写下的 user Message 仍在,只是不再自动回答; +- 机器人找模型、挑工具、重试网络,都是它工作时的动作,不是作业本里的新东西; +- 机器人只有把一句话写完整并交给作业本,才算 Agent 真的说过。 + 所以 U1 后模型断网三次,最后生成 A,作业本仍只有两行: ~~~text @@ -351,7 +390,11 @@ Scheduler 可以在插件边界拥有自己的时间表和 due cursor;那是 第三种对话事实。若“由计划任务触发”本身必须进入对话历史,就由 producer 把它写成 普通、类型明确的 Message 内容,而不是给 Core 增加来源分支。 -## 七、其余全部是视图 +## 七、其余对话状态全部是视图 + +“全部是视图”说的是**持久对话状态**:除了 Session 与 Message,不再保存一个平行 +状态来解释对话进行到哪里。插件行为本身不是 projection,也不是事实;它是读取 +Message、调用 capability、再产生 Message 的短命函数。 | 视图 | 从 Session Message 得到什么 | |---|---| @@ -472,131 +515,448 @@ ID 的旧 append 重试只返回原 `seq` 与 gone 结果,永远不比较、 - `packages/session/session-projection/src/index.ts:40`:projection 是按 seq fold 的 versioned cache。 -## 十一、目标写入路径 +## 十一、v4 的另一半:把被动回复大链路变成普通插件 + +### 11.1 现在真正需要替换的链路 + +当前基线的被动回复不是普通插件组合,而是一条固定 owner 链: + +~~~text +PassiveMessageWorker + ├── 入站 custody / attachment / per-session lane + ▼ +ConversationRuntime + ▼ +AgentLoop._react() + ▼ +PassiveTurnPipeline + ├── command short-circuit + ├── BeforeTurn + ├── BeforeReasoning + ├── reasoner + BeforeStep / AfterStep + ├── AfterReasoning:parse + persistence + outbound + └── AfterTurn:事件 + dispatch / ACK +~~~ + +代码证据也显示这些责任仍被固定装配: + +- `bootstrap/passive_worker.py:96`:`PassiveMessageWorker` 拥有消息准入、lane task 和 + 结果 task; +- `agent/looping/core.py:556`:`AgentLoop._react()` 只把请求转给固定 pipeline; +- `agent/core/passive_turn.py:355`:`PassiveTurnPipeline` 构造固定的四段 phase; +- `agent/core/passive_turn.py:440`:command 在 Session/model 准入前走专门短路; +- `agent/core/passive_turn.py:524`:默认被动回复仍由一个固定 `run()` 入口统管。 + +这会让插件只能在旧流水线上挂 hook,而不能真正替换业务。新增语音 Agent、无工具 +Agent、plan/execute Agent 或另一种回复策略时,要么继续给 Core 加分支,要么复制整条 +pipeline。单独换成 Message WAL 并不会消除这个问题。 + +### 11.2 目标:`Message → react → Message` + +默认产品提供一个普通 `passive-conversation` 插件。它与第三方插件经过同一套加载、 +依赖解析、candidate 校验、generation 发布和生命周期清理;Core 不给它后门。 + +这不是再建第二套插件框架。当前代码已经有可复用的骨架: + +- `agent/plugin_composition/model.py:28` 已定义类型化 `ServiceKey`; +- `agent/plugin_composition/context.py:148` 已提供绑定 exact Root 的短命 runtime scope; +- `plugins/models/plugin.py:58` 已用普通 `provide()` 发布 model services; +- `plugins/compaction/plugin.py:468` 已用普通插件发布 provider request projection; +- `plugins/markdown_memory/plugin.py:67` 已把 Prompt 与 post-commit memory 行为接入普通 + plugin lifecycle。 ~~~text -Sources Session Message WAL -────────────────────── ───────────────────────────── -Mobile user Message ───────┐ ┌──────────────────────────┐ -Scheduler Message ─────────┼───▶│ append + seq + durability│ -Assistant complete output ─┤ └────────────┬─────────────┘ -Tool result Message ────────┘ │ - ├──▶ Chat - ├──▶ Model context - ├──▶ Web/Mobile cursor - ├──▶ Memory/Search - └──▶ Delivery queue +passive-conversation plugin + provides MESSAGE_REACTOR + injects SESSION_READ, SESSION_FEED, SESSION_APPEND + COMMANDS, AGENT_PROGRAM + +default-agent plugin + provides AGENT_PROGRAM + injects SESSION_READ, SESSION_APPEND + PROVIDER_REQUEST_PROJECTION + PROMPT_PARTS + TOOL_SELECTOR + ASSISTANT_TRANSFORMS + CHAT_MODELS + TOOL_EXECUTOR + STREAM_PREVIEW ~~~ -公开原子能力保持很小: +这些大写名字都是普通 `ServiceKey`,不是 Core 固定 slot,更不是新领域对象: + +- `SESSION_READ` 只提供 read/head,`SESSION_FEED` 只发布 committed Message, + `SESSION_APPEND` 只签发绑定 Session、role 与 CAS/typed precondition 的 writer;三者 + 共用一个 WAL owner,但权限彼此独立,都没有任意 SQL、原位改写或删除能力; +- `MESSAGE_REACTOR` 读一个已经 committed 的输入 Message,按最新 WAL projection 判断 + `idle / command / respond / recover tool`,再选择 command 或 `AGENT_PROGRAM`;它不 + 保存自己的 outcome; +- `AGENT_PROGRAM` 拥有默认模型/工具算法,包括 provider retry、Tool Search、空回复 + 修正、terminal tool deadline 和继续生成; +- 其他 key 只是 `default-agent` 自己的依赖。不使用工具的 Agent 不需要提供假的 + `TOOL_SELECTOR`,Core 也不维护一张“所有 Agent 都必须有”的选择表。 + +接口只传已有身份,不发明 ReactionId、ProgramId 或通用 context 袋子: ~~~text -SessionStore.append(message, expected_head_seq?) -> committed Message -SessionStore.read(session_id, after_seq?, limit?) -> Message[] -SessionStore.head(session_id) -> seq +MESSAGE_REACTOR.react(session_id, cause_message_id) -> None +AGENT_PROGRAM.respond(session_id, cause_message_id, scoped_messages) -> None ~~~ -工具关联、删除应用、上下文选择和 UI 分组都是对 `read()` 结果的函数。插件只得到完成 -任务所需的窄能力;projection 不持有任意 SQL 或删除权限。 +`None` 只表示函数已经结束;可观察结果只能是 WAL 中新增了哪些 Message。异常、取消、 +provider retry 和临时资源留在当前 Fiber/telemetry。它们不能通过另一个 result record +偷偷变成第二份对话事实。 -## 十二、迁移路线 +不保留 v3 的 `decide() → handle()` 双阶段,也不新增 ReactionPlan。能否继续只由最新 +Message 前缀算出;同一 `cause_message_id` 被重复唤醒时,projection 已经是 idle 就直接 +结束,否则最终仍由 append 的 CAS/typed precondition 仲裁。 -当前 `projectneed`、代码和数据库仍有 Turn/Run/attempt 合同。本文是替代设计,不允许 -借普通重构偷偷改变线上语义。批准后按以下顺序迁移: +`passive-conversation` 自己用普通 Effect 订阅 committed Message,并在自己的 Fiber +里调用 `MESSAGE_REACTOR`。因此 Core 甚至不需要知道这个 ServiceKey。产品 profile +要求自动回复时,candidate 必须恰好解析出一个 provider;依赖缺失或重复 provider +在发布前 fail-loud。停用这个插件以后: -### Phase 0:冻结目标合同 +- 渠道仍可把 user Message 写入 WAL; +- Chat、同步和历史仍正常; +- 不再自动产生 assistant Message; +- UI projection 可以显示“Agent 未启用”,Core 不偷偷启用 legacy fallback。 -- 批准“失败 attempt 不进入 Session”的信息损失; -- 批准单一 pre-commit `message_id`; -- 批准 stale assistant output 由 head CAS 丢弃; -- 批准 tool `unknown` 与不支持幂等 provider 的边界; -- 修改 `projectneed` 和相关 decisions,明确旧合同被替代。 +这里的 `passive` 只是默认插件包名,不是 Message 字段、Session 模式或 Core 分支。 +Scheduler、Wake、Channel 和 subagent 都只产生普通 Message,或者显式依赖同一个 +`AGENT_PROGRAM`;来源不会复制一套执行模型。 -### Phase 1:只读转换器 +### 11.3 固定 phase 不原样搬家 -- 从现有 `sessions.db/messages` 和有必要的 tool 数据重建目标 Message WAL; -- 现有 message ID 原样保留;只有缺少 ID 的历史数据才在一次性迁移中分配; -- 对无法证明顺序、角色或 tool outcome 的记录 fail-loud,不能猜默认值; -- 固定真实 Session fixture,比对 Chat、Model context、Tool status 和 Mobile 输出。 +插件化不是把 `PassiveTurnPipeline` 整块移动到 `plugins/`。现有每项能力先找到唯一 +owner;没有独立不变量或真实消费者的 phase/hook 直接删除: -### Phase 2:切换唯一 writer +| 当前固定行为 | v4 owner | +|---|---| +| channel envelope、附件导入 | Channel/Artifact adapter;artifact ready 后才 append user Message | +| 入站 durable handoff / ACK | Channel adapter;user Message durable 后结算 | +| command catalog 与短路 | `passive-conversation` + 注入的 `COMMANDS` | +| Session/history 准备 | `SESSION_READ` + pure Session projection | +| system prompt、skills、memory、profile | 有序、不可变的 `PROMPT_PARTS` contributions | +| history 裁切、摘要与 compaction retry | `PROVIDER_REQUEST_PROJECTION`;沿用普通 compaction plugin | +| tool schema preload / Tool Search 解锁 | `TOOL_SELECTOR`,由 `default-agent` 使用 | +| Tool 展示 | `TOOL_SELECTOR`;只能缩小当前可见集合 | +| Tool 授权与真实执行 | 受保护的 `TOOL_EXECUTOR`;调用边界重新校验 | +| 默认 ReAct、provider retry、空回复重试、terminal deadline | `AGENT_PROGRAM` | +| model/provider 绑定 | models/provider 插件;从当前 exact Root 注入 | +| Citation、Meme、最终文本/媒体改写 | append 前的有序、不可变 `ASSISTANT_TRANSFORMS` | +| assistant/tool 写入、幂等、seq、CAS | `SESSION_APPEND` 签发的 scoped writer | +| Memory、Akasha、compaction 派生数据 | committed Message observers / projections | +| partial token 展示 | `STREAM_PREVIEW`;可丢失且无权 append 半条 Message | +| error reply、quiet | reactor/program 产生普通 assistant `text` 或 `no_reply` Message | +| continuation、crash 后下一步 | 从 WAL 重建的 `Next action` projection | +| assistant 对外发送、重试、provider ACK | 独立 Delivery projection/effect,只引用 `message_id` | +| generation 固定、取消、资源清理 | 通用 plugin Root lease + Fiber/Effect 生命周期 | + +Command 若只读或只生成回复,可以直接产生 assistant Message;若会付款、发信、改 Git +等产生外部副作用,必须先生成普通 `tool_call` Message,再走同一 Tool 协议。不能因为 +它叫 command,就在 WAL 看不见的短路里执行一次可能重复的外部动作。 + +Prompt 与 assistant transform 的顺序必须确定,但“顺序确定”不表示插件可以任意叠加。 +每个 contribution 不可变,只有 composition owner 能形成最终序列;candidate Gate 要拒绝 +重复 owner、冲突位置和循环依赖。transform 必须在 assistant Message append 前结束; +post-commit observer 无权回来改正文。 + +### 11.4 Tool 可见性不是 Tool 权限 + +`default-agent` 可以用 `TOOL_SELECTOR` 决定本次把哪些 schema 给模型看,但它不能扩大 +当前授权。模型产生 `tool_call` 后: + +1. 按本次可见 schema 校验并把 exact immutable `tool_binding` 写进 assistant Message; +2. `TOOL_EXECUTOR` 在真实副作用边界重新检查当前授权; +3. 已撤权时不执行,append 一个明确的 `tool_result(error)`; +4. 未知外部结果按第四节写 `unknown`,不能由 Agent Program 猜成 success。 + +因此替换 `AGENT_PROGRAM` 只能改变算法,不能绕过工具权限 owner。 + +## 十二、Thin Core 最终保留什么 + +Core 只保留来源无关、产品算法无法安全拥有的原子能力: + +1. **Plugin composition**:`ServiceKey`、`provide/require/inject`、依赖冲突校验、exact + committed Root lease、candidate/stable、Fiber/Effect 清理、health/incident; +2. **Session Message WAL**:append、read、subscribe、head、幂等、seq、CAS 和受限 + writer;用户删除 Session/Message 的 Data Management 是另一个显式管理入口; +3. **短命执行安全**:取消、超时、per-session 串行准入和有界资源 scope;这些可以 + 丢失,不分配持久身份; +4. **真实外部边界**:模型调用、Tool 授权/执行、stream preview、channel ingress/ACK + 和 delivery effect 的窄 port;具体 provider 与策略仍由普通插件提供; +5. **类型化观察**:只发布 committed Message;observer 失败不能回滚、覆盖或补造 + WAL 事实。 + +Core 不认识下面这些产品词: + +~~~text +passive / proactive / Wake / Scheduler / command +compaction / memory / Citation / Meme / Tool Search +某个 model/provider 名称 / 某个 channel 名称 +~~~ + +如果 Core 源码需要按这些名字分支,说明 capability owner 仍没有拆干净。反过来, +`ServiceKey`、Root lease 和 Fiber 也不进入 Session;它们是让插件安全工作的机器零件, +不是对话事实。 + +## 十三、完整目标链路 + +~~~text +Channel adapter + │ 先让 artifact durable + │ append user Message(message_id) + │ durable 后 ACK inbound + ▼ +┌──────────────────────── Session Message WAL ────────────────────────┐ +│ user / system / assistant(tool_call|text|no_reply) / tool / delete │ +└──────────────────────────────┬──────────────────────────────────────┘ + │ committed feed + ▼ + passive-conversation Effect + │ exact Root lease + ▼ + MESSAGE_REACTOR + ┌────────┴────────┐ + known command AGENT_PROGRAM + │ + ┌───────────────────┼────────────────────┐ + ▼ ▼ ▼ + provider request view prompt/tool selection model retry + │ complete + ▼ + append assistant Message + ├── text/no_reply ─────┐ + └── tool_call │ + │ commit first │ + ▼ │ + TOOL_EXECUTOR │ + │ complete │ + ▼ │ + append tool Message │ + └── generate ───┘ + +committed Message feed + ├──▶ Chat / Web / Mobile cursor + ├──▶ Model context / Memory / Search projections + └──▶ Delivery projector ── provider send/retry/ACK +~~~ -- 所有 user、assistant、tool 和 scheduler producer 改走同一个 append; -- 输入 transport 直接复用 `message_id`,删除平行 retry identity; -- 模型 retry 保留在 pre-append 内存流程; -- projection 先 shadow rebuild,证明读取等价后再接管消费者; -- 不长期 dual-write 两套事实。 +这里有两个故意分开的 commit 点: -### Phase 3:删除旧权威结构 +- **输入事实**:channel 在 user Message durable 后就可以 ACK。Agent 后来是否回答, + 不能反过来决定“用户有没有说过”; +- **输出事实**:assistant Message append 表示 Agent 已经说出。外部渠道是否送达由 + Delivery effect 继续处理,不能回滚或复制 assistant Message。 -- 在可恢复备份和影响清单上删除旧 Turn/Run/Step/attempt/delivery Core rows 与 API; -- 删除 proactive 分支,让 scheduler 只产生普通 Message; -- 若 seq 发生变化,客户端执行一次明确 snapshot/cursor reset; -- 重建 projection,并核对每个 Session 的 message 数、ID 唯一性、seq 连续性、tool - 配对与 delete 结果。 +这会有意替换当前将入站 custody、回复终态和 delivery 结算绑在同一大链中的行为。 +它不是漏迁移,而是由“Message 是唯一消息载体”直接推导出的新边界。 -迁移前后都不能改写正式 workspace,除非另有明确授权和恢复方案。 +## 十四、重启、热更新与并发不需要 Run -## 十三、验收 Gate +### 14.1 活着时固定一个 exact Root -### 13.1 概念 Gate +一次 `react()` 调用开始时拿到 exact committed Root lease,到函数完成或取消才释放。 +其中的 model retry、Prompt、tool selection 和 transform 都看同一 Root。热更新可以发布 +新 Root,但不能在半次调用中偷换依赖。 + +Fiber、取消 token、超时和 per-session lane 都是内存资源。U2 到来时可以尽早取消旧 +生成;即使取消来不及,旧输出也会被 `expected_head_seq` 拒绝。正确性来自 WAL CAS, +不是来自一个持久 Run 状态。 + +### 14.2 崩溃后只从 Message 恢复 + +- assistant Message append 前崩溃:没有事实;重启后可用最新 Root 重新生成; +- assistant `tool_call` 已 append:从其 `call_ref` 与 `tool_binding` 查询或恢复; +- tool Message 已 append:下一次从最新 WAL 继续生成 assistant Message; +- assistant text/`no_reply` 已 append:该前缀已经有结果,projection 得到 idle; +- delivery 中崩溃:继续用同一 `message_id` 查询或重试,不重新生成正文。 + +`tool_binding` 中的 generation identity 是 Message 内容的一部分,不需要 RunId。由 WAL +派生的 unresolved-tool projection 为所引用的插件 generation 加 retention lease;进程 +启动时先重建这些 lease,再允许清理旧 generation。若安全 owner 撤销权限,就写明确 +的 tool error Message 并释放 lease,不能执行过期授权。 + +这只保证一段仍活着的函数内部使用同一 Root。崩溃后的新函数可以使用新 Root;唯一 +必须保持的是已提交 tool binding 的执行身份。为了跨崩溃保存整套旧算法而新增 durable +Run,成本大于它保护的事实,本设计明确不做。 + +### 14.3 Candidate 与外部插件边界 + +- 内置插件与外部插件使用同一 loader、manifest、依赖图和生命周期 API; +- 缺少依赖、重复 Service provider、贡献顺序冲突在 candidate 阶段 fail-loud; +- candidate 使用隔离 Session feed,不得订阅生产 Session、发送真实 delivery 或执行 + 高风险 Tool; +- 普通插件不得 import Core 私有模块、获得完整 workspace/repository/SQL 或删除能力; +- generation 下线前必须 drain 自己的 Effects、listeners、tasks 与 leases; +- 一个仓库外 fixture 必须能提供替代 `AGENT_PROGRAM`,不改 Core 就完成真实回复。 + +## 十五、迁移路线:WAL 与插件化一起完成 + +当前 `projectneed`、decisions、代码和数据库仍有 Turn/Run/attempt 合同。本文是有意的 +替代设计;当前行为是调查证据,不是正确性的来源。批准前不实现,批准后也不能把 +旧名词藏进 adapter 永久保留。 + +### Phase 0:批准新合同 + +- 批准 Session/Message 是唯一对话事实,失败 attempt 不进入 Session; +- 批准单一 pre-commit `message_id`、head CAS、tool `unknown` 和外部 exactly-once + 边界; +- 批准 inbound ACK 与 Agent reply/delivery 解耦; +- 以新条款 supersede `projectneed` 和相关 decisions 中的 Turn/Run 合同; +- 建立数据库、附件引用、插件 generation 和客户端 cursor 的可恢复备份与影响清单。 + +### Phase 1:先冻结完整行为账单 + +在改动 owner 前,用真实 Session fixture 和受控 provider/tool 记录当前大链的: + +- 输入与附件、command、Prompt sections、model request、tool schemas 与调用顺序; +- compaction retry、Tool Search 解锁、空回复 retry、terminal tool、continuation; +- Citation/Meme/媒体变换、partial stream、error/no_reply; +- Message 写集、Memory/Akasha 观察、Web/Mobile cursor、delivery 与两侧 ACK; +- 热更新时的 exact generation、取消和资源清理。 + +每个差异必须先标成“保留能力”“按 v4 有意替换”或“已证明的旧 bug”。oracle 不要求 +盲目复制现状;它要求任何消失的能力都有明确决定。旧 phase 名称本身不是能力,没有 +消费者的 hook 不迁移。 + +### Phase 2:建立目标 WAL 与窄 capability + +- 从现有数据只读重建目标 Message WAL;已有 message ID 原样保留; +- 无法证明顺序、角色、tool pairing 或 outcome 的记录 fail-loud; +- 实现正交的 `SESSION_READ`、`SESSION_FEED`、`SESSION_APPEND` 与 scoped CAS writer; +- Chat、Model context、Tool status、Next action、Memory 和 Mobile 先 shadow rebuild; +- 还未切换生产 writer,不改正式 workspace。 + +### Phase 3:先抽出普通 `AGENT_PROGRAM` + +- 把默认 Reasoner/ReAct、provider retry、Tool Search、空回复和 terminal policy 从 + `AgentLoop`/pipeline 抽成 `default-agent` 插件; +- 把 Prompt、context、tool selection 和 assistant transforms 变成普通依赖/contribution; +- 用临时窄 adapter 接回旧入口,比对 Phase 1 oracle;adapter 只存在于迁移期并登记 + 删除 commit; +- 用一个无工具替代 Agent Program 证明 Core 与默认算法已解耦。 + +### Phase 4:接入 `passive-conversation` 并 shadow + +- 插件订阅 shadow Message feed,调用 `MESSAGE_REACTOR → AGENT_PROGRAM`; +- command、附件、error/no_reply、tool loop、continuation 与输出 transform 逐项对账; +- shadow 禁止真实 append、Tool 副作用和 delivery,只比较计划产生的 Message 与 + 外部调用; +- candidate/stable 切换期间验证 exact Root 和所有 Effect 均可排空。 + +### Phase 5:一次切换唯一 writer + +- Channel 先 append user Message,再 ACK inbound; +- user、assistant、tool、scheduler producer 全部只走同一个 WAL append; +- 只启用 `passive-conversation` 的生产 subscriber,旧 worker 变为不可达; +- Delivery projector 只消费 committed assistant Message; +- projection 完成 cursor reset/shadow 对账后接管读取;不长期 dual-write 两套事实。 + +### Phase 6:删除旧链和旧权威结构 + +- 删除 `PassiveMessageWorker → ConversationRuntime → AgentLoop._react → + PassiveTurnPipeline` 固定业务链; +- 删除 Before/After phase DAG、proactive/source 分支和无消费者的兼容 hook; +- 删除旧 Turn/Run/Step/attempt/delivery Core rows、API 与双重 message identity; +- 卸掉所有迁移 adapter,确认没有插件 cache、动态消费者或恢复任务仍引用它们; +- 重建 projection,核对每个 Session 的 Message 数、ID、seq、tool pairing、delete、 + plugin generation retention 与客户端 cursor。 + +迁移前后都不能改写正式 workspace,除非另有明确授权、恢复点和执行前后完整性检查。 + +## 十六、验收 Gate + +### 16.1 概念 Gate - Core schema 和领域 API 只有 Session、Message; -- 不存在 TurnRef、RunRef、StepRef、DeliveryRef、ProjectionClaim 或 SessionEvent 壳; +- 不存在 TurnRef、RunRef、StepRef、AttemptRef、DeliveryRef、ProjectionClaim 或 + SessionEvent 壳; +- `MESSAGE_REACTOR`、`AGENT_PROGRAM`、Root、Fiber 和 projection 不可序列化成第三种 + 对话事实; - tool call 只用 Message 内可派生的 `call_ref`; - proactive 不是类型、字段、状态机或特殊 commit 路径; -- 新增任何第三个权威名词时,必须先证明 Session/Message 无法表达其独立不变量。 +- 没有 `metadata/context/intent` 通用可变袋子绕过 typed Message 与 capability。 -### 13.2 WAL Gate +### 16.2 WAL Gate - 同 `message_id` 同内容重试只得到同一 `seq`; - 同 `message_id` 不同内容 fail-loud; - commit 前崩溃没有 Message,commit 后 ACK 丢失不会重复 Message; -- 每个 Session 的 seq 唯一连续,projection 失败不影响 commit; -- 正常路径没有 UPDATE/DELETE Message。 - -### 13.3 行为 Gate - -- 模型断网和 partial stream 不写 Session; -- U2 抢先提交时,基于旧 head 的 A 被拒绝并重新生成; -- 两个并发 worker 只有一个 assistant Message 能提交; -- tool_call 先提交再执行,crash 后按 `call_ref` 查询或安全恢复; +- 每个 Session 的 seq 唯一连续,projection/observer 失败不影响 commit; +- 正常路径没有 UPDATE/DELETE Message;受控物理擦除只走 Data Management; +- U2 抢先提交时,基于旧 head 的 A 被拒绝;两个 worker 也只有一个能提交。 + +### 16.3 插件组合 Gate + +- Core/Bootstrap 不再 import 或构造 `PassiveMessageWorker`、`AgentLoop` 默认算法和 + `PassiveTurnPipeline`; +- Core 源码不按 passive/proactive/Wake/Scheduler/Citation/Meme/compaction/provider + 名称分支; +- 停用 `passive-conversation` 后,输入 Message、历史与同步仍工作,只停止自动回答; +- 替换 `AGENT_PROGRAM` fixture 无需修改 Core、WAL schema 或 channel adapter; +- 缺失/重复 Service 和 contribution 冲突在 candidate 发布前失败; +- Prompt 与 transform 次序确定,post-commit 插件不能修改 Message; +- 仓库外测试插件经正式安装链提供 `AGENT_PROGRAM`,不使用 Core 私有 import; +- generation 下线后无遗留 task、listener、subscription、Root lease 或 tool binding lease。 + +### 16.4 行为 Gate + +- command short-circuit、附件、Prompt、compaction、Tool Search 和模型绑定都有目标 owner; +- provider 断网、retry 与 partial stream 不写 Session; +- `tool_call` 先提交再执行,crash 后按 `call_ref/tool_binding` 查询或安全恢复; +- Tool 可见性不能扩大真实授权,撤权产生明确 tool error; - 不可确认的外部效果产生 `unknown`,不会盲目重复; -- `no_due` 不产生 Message;quiet 产生普通 `[no_reply]` assistant Message,Chat 隐藏; +- 空回复 retry、terminal deadline、continuation、error reply 和 transform 有受控 fixture; +- `no_due` 不产生 Message;quiet 产生普通 `no_reply` assistant Message,Chat 隐藏; +- user Message durable 后即可 ACK,reply/delivery 失败不会抹掉输入; +- assistant Message durable 后 Delivery 独立重试同一 `message_id`,不会重新生成正文; - Web/Mobile 只用 `message_id + seq + cursor` 完成重复、断线和追赶; - delete Message 到达后所有 projection 一致隐藏目标,旧 retry 不会复活它。 -### 13.4 明确接受的非目标 +### 16.5 迁移 Gate + +- Phase 1 每项行为都有 preserve/replace/bug 分类和可复跑 fixture; +- WAL shadow 重建与旧读取逐 Session 对账;无法转换的数据有显式阻塞清单; +- 切换点只有一个生产 writer、一个自动回复 subscriber,没有窗口式 dual-write; +- 数据迁移、客户端 cursor reset 和 generation retention 均从备份做过恢复演练; +- 删除清单逐项证明静态 import、插件源码/cache、测试、数据库、日志和运行进程都无消费者。 + +### 16.6 明确接受的非目标 - 不从 Session 审计失败 provider attempt; - 不恢复半截 token; -- 不给执行尝试稳定 ID; -- 不保证外部 provider 的 exactly-once,除非 provider 提供幂等或查询合同; +- 不给执行尝试分配稳定 ID; +- 不跨崩溃固定整套旧 Agent Program;只保留已提交 tool binding; +- 不保证外部 provider exactly-once,除非 provider 提供幂等或查询合同; - 不让 UI 分组成为持久身份。 -## 十四、对 v3 和前一版 v4 的最终减法 +## 十七、对 v3 和前一版 v4 的最终减法 | 删除的东西 | 原因 | 现在由什么承担 | |---|---|---| | MessageBody / SessionEntry 两层 | 同一正文两种 owner | Message WAL record | | SessionEvent 壳 | Message 已经是日志记录 | Message | -| Turn / Run / Step | 把临时执行升级成事实 | head CAS + projection | -| Attempt 状态 | 失败重试发生在 append 前 | 内存 + telemetry | +| Turn / Run / Step / Attempt | 把短命执行升级成事实 | head CAS + Fiber + telemetry + projection | | client/retry message IDs | 同一 Message 多个身份 | 唯一 `message_id` | | ToolCallId | 可由调用所在位置确定 | `message_id + block index` | -| proactive 分支 | 来源不应改变执行语义 | 普通 input/output Message | -| DeliveryId / delivery rows | 投递不能复制消息身份 | `message_id` + provider 能力 | +| proactive/source 执行分支 | 来源不应改变执行语义 | 普通 input Message + 同一 reactor | +| DeliveryId / conversation delivery rows | 投递不能复制消息身份 | `message_id` + provider projection/effect | | ProjectionClaim | projection 不配拥有事实 | version + source seq cache | +| Core 中的默认 AgentLoop | 产品算法不可替换 | 普通 `default-agent` 插件 | +| `PassiveTurnPipeline` 固定 phase DAG | 把业务、持久化和外部效果绑死 | `MESSAGE_REACTOR` + 普通依赖/contributions | +| `PassiveMessageWorker` 大 owner | 同时拥有 ingress、reply 和 delivery | Channel adapter + plugin Effect + Delivery projector | +| Citation/Meme/ToolSearch 等 Core 特判 | 产品能力侵入基础设施 | 普通插件 contribution/service | +| 通用 metadata/context 袋子 | 隐藏第二套协议和 owner | typed Message content + 窄 capability | -最终模型不是“把许多事件放进一份账本”,而是更直接: +最终不是“只做 WAL”,也不是“把旧 pipeline 移进插件目录”,而是三句可以独立验证的 +规则: ~~~text -Session - └── Message - ├── user text - ├── assistant text / tool_call - ├── tool_result - └── delete - -除此以外,Core 内都只是读取这本账的方式。 +事实:Session = ordered Message WAL +行为:ordinary plugins read Message and append Message +视图:projection = fold(Messages up to source_seq) ~~~ + +Core 只保护 append、权限、外部边界和插件生命周期。它不再决定 Agent 怎样回答, +也不再为回答过程发明另一套可持久化名词。 From 998ff5c4067d9aa82be4054cec03ad3176af4582 Mon Sep 17 00:00:00 2001 From: huashen <2494946808@qq.com> Date: Thu, 3 Sep 2026 15:22:10 +0800 Subject: [PATCH 5/7] =?UTF-8?q?docs:=20=E8=A1=A5=E9=BD=90=20v4=20=E5=9B=A0?= =?UTF-8?q?=E6=9E=9C=E4=B8=8E=E5=A4=96=E9=83=A8=E8=BE=B9=E7=95=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/design/0902-reviewed-v4.md | 397 +++++++++++++++++++++++++++----- 1 file changed, 345 insertions(+), 52 deletions(-) diff --git a/docs/design/0902-reviewed-v4.md b/docs/design/0902-reviewed-v4.md index 692650cb..d682191b 100644 --- a/docs/design/0902-reviewed-v4.md +++ b/docs/design/0902-reviewed-v4.md @@ -25,8 +25,10 @@ Session = 一条只追加的 Message WAL Message = 一个已经完整产生并被 WAL 接纳的事实 ~~~ -没有第三种权威对象。没有 Turn、Run、Step、Attempt、Delivery row,也没有一层 -`SessionEvent` 再包住 Message。 +没有第三种权威对话对象。没有 Turn、Run、Step、Attempt、DeliveryId 或一层 +`SessionEvent` 再包住 Message。外部 Channel 为 crash recovery 保存的 +`DeliveryEffect[message_id]` 是 provider effect projection,不复制正文、不成为消息载体, +见第八节。 这里的“只有”限定在**对话事实层**。附件字节、credential、插件 generation、 scheduler 配置和 provider 外部状态仍由各自边界拥有;它们不是对话实体,也不能复制 @@ -155,6 +157,36 @@ metadata 袋子。`artifact_ref` 和 `tool_binding` 指向各自边界已有的 `(assistant message_id, content block index)` 得到。provider 自己要求的 token 只是 协议内容,不能升级成 Core 的 ToolCallId。 +#### `reply` 是 Message 之间的因果确认 + +head CAS 只证明输出读取过哪个 Session 前缀,不能证明它回答了其中哪几条输入。因此 +每个由 reactor/program 产生的 assistant `text`、`no_reply` 或 `tool_call` Message 都 +必须带一个或多个 `reply` block: + +~~~text +reply { target_message_id } +~~~ + +每个 block 只确认**明确写出的那一条** prior Message,不隐含“此前全部已处理”。同一 +assistant Message 可以用多个 block 表示合并回答: + +~~~text +seq 1 user U1 +seq 2 user U2 +seq 3 assistant reply(U1), reply(U2), text(A) +~~~ + +append owner 必须验证 target 与新 Message 属于同一 Session、target `seq` 更小、没有 +重复或前向/跨 Session 引用,而且 target 是当前 reactor 允许反应的 typed Message。 +v4 初始合同只允许 assistant role 携带 `reply`;它不是 UI 引用/引用回复的万能字段。 +这条边使用已有 `message_id`,没有 AckId、ReactionId 或处理 cursor。 + +`tool_result.call_ref` 是另一条类型化因果边:它只确认对应的 `tool_call`。如果 assistant +先用 `reply(U1), tool_call(...)` 推进 U1,恢复时先完成 unresolved tool;tool Message +落盘后,它又成为需要 Agent 回应的 cause。即使 terminal tool 不再请求模型, +`AGENT_PROGRAM` 也必须根据确定的 tool result 生成一个带 `reply(tool_message_id)` 的 +assistant `text` 或 `no_reply` Message,不能用消失的执行状态表示“结束”。 + ### 1.3 “Message 存在”究竟证明什么 Message 存在只证明两件事: @@ -188,6 +220,9 @@ Message 存在只证明两件事: 因此正确的减法不是删除 `message_id`,而是让它成为唯一身份: - 客户端创建完整 user Message 时生成 `message_id`;重试始终复用它; +- 外部 Channel 有稳定 provider message ID 时由 adapter 确定性映射;没有时,adapter 在 + pre-WAL staging 先持久分配同一个 `message_id`。若 provider 既无身份又会任意重投, + 系统必须承认无法保证入站 exactly-once,不能再发明第二个业务 ID 掩盖它; - 模型或工具产生完整内容以后、append 以前生成 `message_id`; - 删除 `client_message_id`、`retry_of_client_message_id`、TurnId、RunId、StepId 和 Core DeliveryId; @@ -273,6 +308,40 @@ seq 3 A `expected_head_seq` 就是足够小的并发栅栏。两个 worker 同时从同一个 head 生成答案, 也只有第一个能提交;另一个看到 conflict 后丢弃结果。无需保存“现在是哪一个 Run”。 +### 3.4 append 必须带短命写权限 + +`role` 会改变模型对内容的信任,不能让任意插件自己填字符串。`SESSION_APPEND` 不暴露 +裸 `SessionStore`,只签发一个不可伪造、不可序列化、用后即失效的 +`MessageWriteGrant`。它不是 Message 字段或第三种事实,没有稳定 ID;它只是一枚运行时 +object capability。 + +每份 grant 至少绑定: + +~~~text +session_id +caller = exact plugin Root generation 或具体 host adapter +allowed role + allowed content variants +allowed reply targets / call_ref +observed_head_seq 或另一项 typed atomic precondition +expiry + one-shot append budget +~~~ + +谁能写什么由真实信任边界决定: + +| caller | 可签发的写入 | +|---|---| +| 已认证 Channel adapter | 当前 Session 的 `user` text/artifact;不能伪装 system/assistant/tool | +| `MESSAGE_REACTOR` / `AGENT_PROGRAM` | 当前 cause set 的 `assistant` reply/text/no_reply/tool_call | +| Tool executor | exact `call_ref` 的一个 `tool` result;不能写任意 assistant 或其他调用结果 | +| Scheduler/Wake/Drift 等 source plugin | 获授权 Session 的 typed `system` input;不能写 `user` | +| Data Management | 已预览 closure 的 `system delete`;普通插件拿不到 | +| 一次性 migration | 只在离线恢复流程写固定清单;runtime 永不复用 | + +Session owner 在同一个 append 事务中校验 grant、Message 结构、同 Session 因果边和 CAS。 +跨 Session、越权 role/content、错误 cause/call、过期或已撤销 generation 都 fail-loud。 +generation 在外部 effect 完成后被撤销时,Tool owner只能用受限 completion grant 记录 +`error/unknown`,不能借旧授权再次执行副作用。 + ## 四、工具调用也只是 Message ### 4.1 正常调用 @@ -373,13 +442,13 @@ Core 不认识 `proactive`、Wake、Scheduler 或某个插件名。 ~~~text scheduler 检查 no_due ──▶ 不产生 Message -scheduler 判断需要处理 ──▶ 产生普通 system/user Message +scheduler 判断需要处理 ──▶ 产生普通 system Message agent 判断 quiet ──▶ assistant Message [no_reply] agent 生成内容 ──▶ 普通 assistant Message [text/...] ~~~ 所以主动说话和用户问答走完全相同的 append、模型上下文、客户端同步与投递路径。 -没有 `is_proactive` 字段,也没有 proactive 状态机。 +没有 `is_proactive` 字段,Core 也没有 proactive 状态机。 一旦输入 Message 已经进入 Session,模型产生的 quiet 判断也必须成为完整 assistant Message:`content=[no_reply]`。Chat projection 隐藏它,恢复 projection 用它判断此前 @@ -390,6 +459,33 @@ Scheduler 可以在插件边界拥有自己的时间表和 due cursor;那是 第三种对话事实。若“由计划任务触发”本身必须进入对话历史,就由 producer 把它写成 普通、类型明确的 Message 内容,而不是给 Core 增加来源分支。 +### 6.1 去掉 Core 特判,不等于删除 source plugin 的事实 + +Session/Message 只统一**进入对话以后**的语义。Scheduler、Wake、Drift 和 Channel 在 +决定是否产生 Message 以前,各自仍有不能从 Session 反推的配置与连续性事实;它们由 +普通插件拥有,不复制对话正文,也不升级成 Session 实体: + +| source owner | 自己保留的 durable state | 进入对话的唯一边界 | 结算与恢复 | +|---|---|---|---| +| Channel adapter | inbound provider ref/handoff、immutable `SessionRoute`、临时 pre-WAL payload | 已认证输入 append `user` Message | Message durable 后 ACK;回复与 ACK 解耦;按同一 `message_id` 恢复 | +| Scheduler plugin | `schedules.json` job、enabled/next fire/missed-tick;candidate 原子保存 | due 时 append typed `system` Message;`no_due` 不写 | stop 回收 tick;cancel 才物理移除 job;损坏不能当空任务集 | +| EventMail plugin | Content/Alert/Context immutable envelope 与 transition | 被 Wake/Agent 选中的 material 变成 system `text/artifact` Message | supersede/expiry/selection/ack/settlement 仍在 EventMail;不能共用万能 status | +| Wake plugin | watermark、实际 tick receipt、reservoir、hazard timer、pending ACK、dedupe/cooldown | due 时 append system Message;模型 skip append assistant `no_reply` | 只有 Delivery delivered 后推进 ACK/dedupe/cooldown;unknown 保持可恢复 | +| Drift plugin | `drift/drift.db` cursor、journal、continuum 和下一轮选择 | 需要 Agent 时 append system Message | cursor/journal 按插件合同恢复;日志 retention 不得删连续性行 | +| `message_push` adapter | 不拥有正文或第二个 queue message | 只接受已 committed assistant `message_id` 交给 Delivery | 旧 raw-body caller 必须先通过 system cause → reactor 产生 Message | +| subagent plugin | 自己的受控 job/process continuity(若确有外部效果) | parent assistant `tool_call` → subagent → `tool_result` | 按 `call_ref` 恢复;不能建立子 Turn 消息载体 | + +`proactive.db`、`wake_proactive.db`、`drift/drift.db`、`schedules.json`、 +`proactive_quota.json`、既有 pending/ACK/hazard/reservoir state 和 EventMail transitions +都必须在 Phase 1 逐项标成 preserve、replace 或 retire。owner handoff 完成前继续阻止新 +插件 activation;不能因为 Core 不再认识 proactive 就清库。普通插件最终只从通用 +`TIMERS`、`SESSION_APPEND`、Tool 与 Delivery ports 获得能力。 + +这份清单来自当前 `docs/projectneed.md:834` 的 Scheduler 合同、 +`docs/projectneed.md:841` 起的 PRO 合同,以及 +`docs/design/persistence-state-map.md:95` 起的真实持久状态清单。它们约束迁移不能丢数据, +但其中的 Turn/attempt 说法仍要由 Phase 0 的新合同明确替换。 + ## 七、其余对话状态全部是视图 “全部是视图”说的是**持久对话状态**:除了 Session 与 Message,不再保存一个平行 @@ -402,7 +498,7 @@ Message、调用 capability、再产生 Message 的短命函数。 | Model context | 映射 role/content,裁切只发生在本次请求 | | 对话分组 | 为 UI 临时圈住相邻 Message | | Tool status | `tool_call` 加对应 `tool_result` 得到 pending/success/error/unknown | -| Next action | 从末尾 Message 判断执行 pending tool、生成回复或 idle | +| Next action | fold reply/call edges,得到 unresolved tool、unacknowledged cause 或 idle | | Web/Mobile | 按 `seq > cursor` 返回 Message | | Memory/Search | 从允许学习的 Message 建索引 | | Live stream | 展示尚未提交的内存输出;刷新或崩溃可丢失 | @@ -426,19 +522,97 @@ cursor: 最后完整应用的 seq 客户端用 `message_id` 去重和引用,用 `seq` 排序与追赶。projection 晚到、重复或重建 都不能改变 WAL。 -恢复时也不读 Run 状态。`Next action` projection 依次判断: +恢复时也不读 Run 状态。`Next action` projection 对整份 WAL 做一次因果 fold: -1. 有尚无终态 result 的 `tool_call`:查询或安全恢复该工具; -2. 否则,末尾是需要反应的 user/system/tool Message:从最新 head 生成 assistant; -3. 最新 assistant 没有 unresolved tool call(包括 text 或 `no_reply`):idle。 +1. 校验并收集所有 `reply(target_message_id)` 与 `tool_result.call_ref` 边; +2. 有尚无任何确定 result 的 `tool_call`,先查询或安全恢复; +3. 否则选出尚未被 `reply` 明确认领、且按当前 reactor policy 可反应的 user/system/tool + Message;delete Message 永不触发回复,command 默认单独选择,普通连续输入可以 + 显式组成 cause set; +4. reactor 把这个 cause set 交给 command 或 Agent Program;产出的 assistant Message + 必须逐个写入对应的 `reply` block; +5. 没有 unresolved tool call 或 unacknowledged cause 才是 idle。 -assistant Message 通过 head CAS 紧跟它实际读取的前缀,因此“哪个前缀已经处理”也能 -由 seq 推出,不需要额外 cursor、Turn outcome 或 Run receipt。 +被多个 feed callback 重复唤醒时,它们会算出同一个 pending set;只有一个输出能通过 +observed head CAS。失败者重新 fold,看到 reply 边后结束。这里没有处理 cursor,也不 +从“最后一条是什么 role”猜是否完成。 + +两个容易出错的例子: + +~~~text +# 合并回答:A 明确认领两条输入 +U1, U2, A(reply U1, reply U2) → idle + +# U1 是 command,U2 是普通补充:不能把 command A 当成整个前缀的回答 +U1(command), U2, A-command(reply U1) → U2 pending +U1(command), U2, A-command(reply U1), A(reply U2) → idle +~~~ + +如果 U2 在输出 append 前到来,CAS 先失败,再由 reactor 重新选择 cause set。读过 U2 +但只回答 U1 的 command 结果仍只写 `reply(U1)`,所以 U2 不会丢失。 ## 八、投递是 Message 之后的外部效果 canonical Web/Mobile 直接读 Session WAL。邮件、推送或第三方聊天渠道则由投递 worker -把“哪些 assistant Message 需要送出”算成队列视图。 +消费 committed assistant Message。它不能复制正文,也不能让发送结果决定 Message +是否存在;但 provider I/O 的确需要自己的 durable external-effect projection。 + +v4 初始合同规定:一个外部 Channel Session 绑定一个 immutable route;多收件人广播要 +拆成不同 Session/Message,不在一条 Message 下发明 DeliveryId。Channel plugin 在接纳 +首条 user Message 前持久保存: + +~~~text +SessionRoute[session_id] = { + channel/provider generation, + account + recipient, + inbound provider message reference mapping +} +~~~ + +这里不保存 user/assistant 正文。入站 payload 若必须在 WAL append 前 crash-recover, +只能在 Channel-owned staging 中短暂保存;同一 `message_id` append 成功并 ACK 后就清除 +payload,只保留 provider reference 与 route。 + +每条需要外发的 assistant Message 最多有一条记录,以原 `message_id` 作为唯一主键: + +~~~text +DeliveryEffect[message_id] = { + source_seq, + immutable route/binding/reply reference, + message_digest, + state: prepared | provider_started | delivered | rejected | unknown | suppressed | settled, + provider_receipt? +} +~~~ + +这是把现有能力换 owner,不是删除恢复保证:当前 route/binding 由 +`bootstrap/passive_worker.py:440` 起的 envelope settlement 传递, +`agent/plugin_composition/durable_deliveries.py:121` 起已经在 provider I/O 前后保存 +durable 状态。v4 删除它们对 Turn logical id 和正文副本的依赖,保留必须的 effect +recovery。 + +这不是新的消息身份或对话事实。它是 Channel/Delivery plugin 对“一条既有 Message 的 +一次外部效果”保存的可恢复视图;发送时必须按 `message_id` 从 WAL 读取正文并核对 +digest。正常状态只单调前进: + +~~~text +prepared ──fsync──▶ provider_started ──▶ delivered ──▶ settled + │ ├─────────▶ rejected + │ └─────────▶ unknown + └──────────────────────────────────▶ suppressed +~~~ + +- `prepared` 在任何 provider I/O 前冻结 route 与 exact plugin binding; +- `provider_started` 必须先 durable,随后才可调用 provider; +- crash/timeout 后先用 `message_id` 幂等键或 receipt query;能确认才写 `delivered`; +- provider 明确拒绝写 `rejected`;可能已经发生但无法确认写 `unknown`;两者都不盲重发; +- delete 在 provider I/O 前把 `prepared` 单调推进为 `suppressed`;已经 + `provider_started` 只能查询真实结果,不能倒退成未发送; +- `delivered` 后,source plugin 才推进自己的 ACK/dedupe/cooldown,再写 `settled`; +- observer 在 Message commit 后、`prepared` 前崩溃时,恢复扫描按 SessionRoute 补建缺失 + record;同 `message_id`/同 binding 幂等,binding 冲突 fail-loud; +- unresolved record 固定对应 channel generation;恢复先重建 retention lease,再清理 + 旧 generation。 这类 retry 发生在 Message append 以后,但它不是“重新生成 Message”: @@ -451,41 +625,84 @@ assistant Message 已提交 ~~~ - worker 始终引用原 `message_id`,不复制正文,不生成 DeliveryId; -- queue/cache 只保存 `message_id` 和短期运行数据,发送时从 WAL 读取正文; - provider 支持幂等键时直接使用 `message_id`; - provider 不支持幂等或查询时,系统不能承诺 exactly-once; -- receipt、重试次数和延迟属于 provider telemetry/worker cache,不进入 Core schema; +- receipt、route 和 state 属于 Channel/Delivery plugin 的 external-effect projection; + 重试次数、延迟和错误栈仍只是 telemetry;它们都不进入 conversation schema; - 若将来产品真的要求“送达结果也成为可回忆事实”,它只能作为一条新 system Message 进入同一 WAL,不能新建平行账本。 因此,Message 存在的标准仍是“Agent 已经完整说出并提交”,而不是“每个外部渠道都 已经收到”。 +`SessionRoute` 与 `DeliveryEffect` 必须和对应 plugin-data、SQLite WAL/SHM 一起做 online +backup 与 `integrity_check`。unsettled、`unknown`、仍被 source continuity 引用或其 Message +仍可能重投的记录当前不得自动删除。以后若加 retention,只能用名称明确的 Delivery +Data Management 操作预览目标、备份、验证 provider/source 已不再依赖,再物理减少; +删除 effect record 不得删除 Session Message。 + ## 九、删除仍只使用 Message -正常路径永远只追加。用户明确撤销一条 Message 时,先追加一条普通 delete Message: +正常路径永远只追加。用户明确撤销 Message 时,Data Management 先在 Session head `H` +上冻结一份因果 closure,而不是只删界面当前点中的一行。 + +计算前先取得该 Session 的短命独占管理 barrier,停止签发新的 reactor/tool write grant。 +closure 中若有 unresolved tool call:尚未开始外部 I/O 的先取消;已经可能发生的先按 +第四节查询,并 append 确定 result 或 `unknown`,再从新 head 重算。不能一边删除调用 +意图,一边让 executor 在看不见的地方继续执行。 + +closure 从明确目标开始,沿已有 typed edge 只向后展开: + +1. assistant `reply(target_message_id)` 指向 closure 中 Message,则加入该 assistant; +2. closure 中 assistant 的 `tool_call` 对应的 tool result 加入; +3. reply 该 tool result 的后续 assistant 继续加入,直到没有新后代; +4. 上游 cause 不自动加入;“撤销整组”由 UI 在请求中明确给出需要一起作为 seed 的 + user/system Message。 + +然后用 Data Management 专属 grant 和 `expected_head_seq=H` 一次 append **一条**普通 +delete Message,其中按 seq 排列多个 delete block: ~~~text -seq 20 system delete(target_message_id = M7) +seq 20 system delete(M7), delete(M8), delete(M10), delete(M11) ~~~ -所有 projection 读到 seq 20 后都不再展示、送入模型或学习 M7。删除没有 Redaction、 -Tombstone row 或另一套版本号;delete 只是 Message 的一个内容块。 +这条 Message 本身就是 immutable closure manifest,不再创建 DeleteOperation、TurnId、 +Redaction row 或另一套版本号。若 head 在计算期间变化,append 失败并从新 head 重算; +不会留下半个 closure。 + +各 consumer 对同一个 manifest 的职责是: + +| owner | 必须做什么 | +|---|---| +| Chat / Model context | 隐藏全部目标和依赖输出,不留下孤立 tool result | +| `Next action` | deleted input/call/result 永不重新进入 pending;被删除 assistant 的旧 reply 边仍证明上游曾被处理,不能自动重跑 command/effect | +| Memory / Search / Akasha | 按目标 `message_id` 撤销派生项,或从 WAL 重建到 manifest 的 `source_seq` | +| Delivery | `prepared` 停止发送;`provider_started` 先查询并记 delivered/unknown;已经 delivered 只能尝试 provider 明确支持的 recall,不能伪装成未发送 | +| Artifact owner | 重新计算引用;只有引用归零且另有物理删除授权时才减少字节 | + +因此删 U1 会同时隐藏 reply U1 的 assistant、其 tool call/result 和最终回答;删一个 +tool_call 也不会留下无主 tool result。只删 assistant A 时,上游 U1 可以继续显示,但 +不会因 A 被隐藏而自动触发第二次回复;用户要重新问,必须再 append 一条新 user +Message。 delete 的效果单调:普通 delete 不能以“删除 delete Message”的方式恢复旧正文。已删除 ID 的旧 append 重试只返回原 `seq` 与 gone 结果,永远不比较、覆盖或恢复原 content。 -“撤销刚才那组对话”先让当前 UI projection 解析出一组明确的 `message_id`,再为每个 -目标 append delete Message。执行清单固定后不随分组算法变化,也不需要一个 TurnId。 -删除整个 Session 则是名称明确的 Data Management 操作,不伪装成普通对话写入。 +“撤销刚才那组对话”先让当前 UI projection 列出 seed 与推导 closure 供用户预览,提交 +时由服务端按同一算法和最新 head 重新验证。删除整个 Session 则是名称明确的 Data +Management 操作,不伪装成普通对话写入。v4 的 causal closure 会有意替代当前按 +interaction 原子撤销的合同,必须在 Phase 0 单独批准。 若用户还要求物理擦除正文,Data Management 必须在 delete Message durable 以后: 1. 建立名称清楚的恢复点; -2. 列出目标 Session、Message 和受影响 projection; -3. 只擦除目标 Message 的 content payload,保留 `message_id`、`session_id` 和 `seq`; -4. 重建所有持久 projection 并做前后完整性检查; -5. 保证旧请求重试不能让 M7 复活。 +2. 等所有持久 projection 至少应用到 delete Message 的 `seq`; +3. 列出 closure 中的 Message、artifact 和 external-effect record; +4. 擦除目标 Message 的 text、tool arguments/output 和 artifact payload reference;保留 + `message_id`、`session_id`、`seq`、block position、`reply/call_ref` 因果骨架以及 + delete manifest,避免删除后重启又执行旧 cause; +5. 按各 owner 的独立减少合同处理 artifact/effect,重建 projection 并做完整性检查; +6. 保证旧请求重试不能让任一 closure Message 复活或让工具/投递再次发生。 这是 append-only 的唯一例外,必须由用户明确的数据管理操作触发。上下文压缩、容量 优化、迁移和插件都无权调用。 @@ -599,17 +816,24 @@ default-agent plugin 接口只传已有身份,不发明 ReactionId、ProgramId 或通用 context 袋子: ~~~text -MESSAGE_REACTOR.react(session_id, cause_message_id) -> None -AGENT_PROGRAM.respond(session_id, cause_message_id, scoped_messages) -> None +MESSAGE_REACTOR.react(session_id) -> None +AGENT_PROGRAM.respond(session_id, cause_message_ids[]) -> None ~~~ `None` 只表示函数已经结束;可观察结果只能是 WAL 中新增了哪些 Message。异常、取消、 provider retry 和临时资源留在当前 Fiber/telemetry。它们不能通过另一个 result record 偷偷变成第二份对话事实。 +`AGENT_PROGRAM` 不接收自由传入的 `Message[]`、`scoped_messages` 或旧 context 快照。 +它必须自己从 `SESSION_READ.read_at_head(session_id)` 取得不可变 read view 与 +`observed_head_seq`,再要求 `SESSION_APPEND` 签发绑定同一 Session、cause set 和 observed +head 的 writer。snapshot 只引用 WAL Message 并标明 `source_seq`,不可修改、不可持久化, +也没有 append 方法。cause 不在 snapshot、已被其他 reply 确认或当前 head 已变化时, +签发/append 都 fail-loud,program 重新读取。 + 不保留 v3 的 `decide() → handle()` 双阶段,也不新增 ReactionPlan。能否继续只由最新 -Message 前缀算出;同一 `cause_message_id` 被重复唤醒时,projection 已经是 idle 就直接 -结束,否则最终仍由 append 的 CAS/typed precondition 仲裁。 +Message 因果图算出;同一个 Session 被重复唤醒时,projection 已经是 idle 就直接结束, +否则最终仍由 append 的 CAS/typed precondition 仲裁。 `passive-conversation` 自己用普通 Effect 订阅 committed Message,并在自己的 Fiber 里调用 `MESSAGE_REACTOR`。因此 Core 甚至不需要知道这个 ServiceKey。产品 profile @@ -705,7 +929,7 @@ compaction / memory / Citation / Meme / Tool Search ~~~text Channel adapter │ 先让 artifact durable - │ append user Message(message_id) + │ Channel grant → append user Message(message_id) │ durable 后 ACK inbound ▼ ┌──────────────────────── Session Message WAL ────────────────────────┐ @@ -717,6 +941,7 @@ Channel adapter │ exact Root lease ▼ MESSAGE_REACTOR + causal fold → cause set ┌────────┴────────┐ known command AGENT_PROGRAM │ @@ -725,7 +950,9 @@ Channel adapter provider request view prompt/tool selection model retry │ complete ▼ + Agent grant + head CAS append assistant Message + reply(each cause) ├── text/no_reply ─────┐ └── tool_call │ │ commit first │ @@ -734,12 +961,12 @@ Channel adapter │ complete │ ▼ │ append tool Message │ - └── generate ───┘ + └── reply(tool) ┘ committed Message feed ├──▶ Chat / Web / Mobile cursor ├──▶ Model context / Memory / Search projections - └──▶ Delivery projector ── provider send/retry/ACK + └──▶ durable DeliveryEffect[message_id] ── provider send/retry/ACK ~~~ 这里有两个故意分开的 commit 点: @@ -783,13 +1010,31 @@ Run,成本大于它保护的事实,本设计明确不做。 ### 14.3 Candidate 与外部插件边界 -- 内置插件与外部插件使用同一 loader、manifest、依赖图和生命周期 API; +“普通插件”表示内置与外部插件使用同一 loader、manifest、ServiceKey、依赖图、candidate +和生命周期 API,不表示任意 Python 代码天然安全。当前 `PluginRuntime.workspace` +(`agent/plugin_composition/model.py:106`) 仍暴露 workspace 路径,而同进程 Python 即使 +没有这个属性也能直接 import `os` 或打开绝对路径。 + +因此 v4 把信任声明写死: + +- in-process plugin 是用户在 install 时授权的**可信代码**;object capability 能减少 + 误用和稳定 API,不能充当恶意代码 sandbox; +- 不可信第三方能力必须放进受 OS 文件系统/网络/进程权限约束的 MCP、Workload 或其他 + out-of-process host,只通过窄协议进入 composition; +- 在宣称“只给声明文件”以前,先盘点消费者并从公共 `PluginRuntime` 移除/隐藏 broad + `workspace`,只保留 `data_root`、`workspace_root(name)`、`workspace_file(name)`; +- Session append、Tool、Delivery 和删除不向插件暴露 raw store/SQL,只接受本设计的 + grant/port。即使可信插件写错,也会在真实 owner 处被拒绝; - 缺少依赖、重复 Service provider、贡献顺序冲突在 candidate 阶段 fail-loud; - candidate 使用隔离 Session feed,不得订阅生产 Session、发送真实 delivery 或执行 高风险 Tool; -- 普通插件不得 import Core 私有模块、获得完整 workspace/repository/SQL 或删除能力; -- generation 下线前必须 drain 自己的 Effects、listeners、tasks 与 leases; -- 一个仓库外 fixture 必须能提供替代 `AGENT_PROGRAM`,不改 Core 就完成真实回复。 +- generation 下线前必须 drain 自己的 Effects、listeners、tasks 与 leases。 + +正式 Gate 必须从仓库外源码走 install → candidate → stable → hot reload → uninstall, +用替代 `AGENT_PROGRAM` 完成真实回复,并分别尝试跨 Session append、role/delete/tool +result 越权、未声明 workspace path、raw SQL 和 candidate real effect。对 private import +与任意文件访问的测试只能证明 API/policy;若未使用进程 sandbox,报告必须明确写 +“trusted in-process”,不能声称抵抗恶意插件。 ## 十五、迁移路线:WAL 与插件化一起完成 @@ -800,11 +1045,17 @@ Run,成本大于它保护的事实,本设计明确不做。 ### Phase 0:批准新合同 - 批准 Session/Message 是唯一对话事实,失败 attempt 不进入 Session; -- 批准单一 pre-commit `message_id`、head CAS、tool `unknown` 和外部 exactly-once - 边界; +- 批准单一 pre-commit `message_id`、显式 reply cause set、head CAS、tool `unknown` 和 + 外部 exactly-once 边界; - 批准 inbound ACK 与 Agent reply/delivery 解耦; +- 批准 `message_id`-keyed durable DeliveryEffect、单 external route/Session 和 causal + delete closure; +- 分别批准 Scheduler、Wake、Drift、EventMail、Channel、subagent 与 `message_push` 的 + preserve/replace/retire 选择,不能用一句“删除 proactive”代替; +- 批准 in-process plugin 是可信代码;若目标是恶意插件隔离,另立 out-of-process 合同; - 以新条款 supersede `projectneed` 和相关 decisions 中的 Turn/Run 合同; -- 建立数据库、附件引用、插件 generation 和客户端 cursor 的可恢复备份与影响清单。 +- 建立 Session、Delivery、source plugin DB/JSON、附件引用、插件 generation 和客户端 + cursor 的可恢复备份与 schema lineage 清单。 ### Phase 1:先冻结完整行为账单 @@ -815,6 +1066,10 @@ Run,成本大于它保护的事实,本设计明确不做。 - Citation/Meme/媒体变换、partial stream、error/no_reply; - Message 写集、Memory/Akasha 观察、Web/Mobile cursor、delivery 与两侧 ACK; - 热更新时的 exact generation、取消和资源清理。 +- Scheduler schedule/missed-tick、EventMail transitions、Wake tick/reservoir/hazard/ACK/ + cooldown、Drift cursor/journal、`message_push` raw-body caller 和 subagent recovery; +- `runtime/deliveries/settlements.sqlite` 的 route、provider_started、receipt、unknown 与 + settle 恢复窗口,以及 Channel handoff 的 exact binding。 每个差异必须先标成“保留能力”“按 v4 有意替换”或“已证明的旧 bug”。oracle 不要求 盲目复制现状;它要求任何消失的能力都有明确决定。旧 phase 名称本身不是能力,没有 @@ -823,9 +1078,13 @@ Run,成本大于它保护的事实,本设计明确不做。 ### Phase 2:建立目标 WAL 与窄 capability - 从现有数据只读重建目标 Message WAL;已有 message ID 原样保留; -- 无法证明顺序、角色、tool pairing 或 outcome 的记录 fail-loud; -- 实现正交的 `SESSION_READ`、`SESSION_FEED`、`SESSION_APPEND` 与 scoped CAS writer; +- 无法证明顺序、role、reply cause、tool pairing 或 outcome 的记录 fail-loud;旧 + assistant Message 只有在唯一因果前缀可证明时才补 reply,歧义数据进入人工阻塞清单; +- 实现正交的 `SESSION_READ`、`SESSION_FEED`、`SESSION_APPEND`、短命 + `MessageWriteGrant` 与 scoped CAS writer; - Chat、Model context、Tool status、Next action、Memory 和 Mobile 先 shadow rebuild; +- 用 U1 command + U2、U1/U2 合并回答、tool continuation、replay、并发 CAS 和 delete + closure fixture 验证因果 fold; - 还未切换生产 writer,不改正式 workspace。 ### Phase 3:先抽出普通 `AGENT_PROGRAM` @@ -833,6 +1092,8 @@ Run,成本大于它保护的事实,本设计明确不做。 - 把默认 Reasoner/ReAct、provider retry、Tool Search、空回复和 terminal policy 从 `AgentLoop`/pipeline 抽成 `default-agent` 插件; - 把 Prompt、context、tool selection 和 assistant transforms 变成普通依赖/contribution; +- `AGENT_PROGRAM` 只接收 cause Message IDs,自行读取带 `source_seq` 的 immutable view, + 不接收另一个 Message 容器; - 用临时窄 adapter 接回旧入口,比对 Phase 1 oracle;adapter 只存在于迁移期并登记 删除 commit; - 用一个无工具替代 Agent Program 证明 Core 与默认算法已解耦。 @@ -844,24 +1105,32 @@ Run,成本大于它保护的事实,本设计明确不做。 - shadow 禁止真实 append、Tool 副作用和 delivery,只比较计划产生的 Message 与 外部调用; - candidate/stable 切换期间验证 exact Root 和所有 Effect 均可排空。 +- 完成 broad `PluginRuntime.workspace` 消费者盘点与移除;仓库外 fixture 走正式安装链, + 同时验证其可信进程边界和所有可执行的 capability 拒绝。 ### Phase 5:一次切换唯一 writer - Channel 先 append user Message,再 ACK inbound; -- user、assistant、tool、scheduler producer 全部只走同一个 WAL append; +- user、assistant、tool 与各 source producer 全部只走带正确 grant 的同一个 WAL append; - 只启用 `passive-conversation` 的生产 subscriber,旧 worker 变为不可达; -- Delivery projector 只消费 committed assistant Message; +- Channel/Delivery plugin 先完成 `SessionRoute` 和 `DeliveryEffect[message_id]` handoff,再 + 让新 projector 消费 committed assistant Message; +- Scheduler/Wake/Drift/EventMail 连续性 owner 全部 handoff 或显式 retire 后,才允许新 + generation activation;`message_push` 只接收 `message_id`; - projection 完成 cursor reset/shadow 对账后接管读取;不长期 dual-write 两套事实。 ### Phase 6:删除旧链和旧权威结构 - 删除 `PassiveMessageWorker → ConversationRuntime → AgentLoop._react → PassiveTurnPipeline` 固定业务链; -- 删除 Before/After phase DAG、proactive/source 分支和无消费者的兼容 hook; -- 删除旧 Turn/Run/Step/attempt/delivery Core rows、API 与双重 message identity; +- 删除 Before/After phase DAG、Core proactive/source 分支和无消费者的兼容 hook;source + plugin 已确认保留的 DB/JSON 不随之删除; +- 删除旧 conversation Turn/Run/Step/attempt rows、API、双重 message identity 和旧 + Delivery logical ID;保留并迁移 `message_id`-keyed external-effect recovery state; - 卸掉所有迁移 adapter,确认没有插件 cache、动态消费者或恢复任务仍引用它们; - 重建 projection,核对每个 Session 的 Message 数、ID、seq、tool pairing、delete、 - plugin generation retention 与客户端 cursor。 + reply graph、DeliveryEffect、source owner handoff、plugin generation retention 与客户端 + cursor。 迁移前后都不能改写正式 workspace,除非另有明确授权、恢复点和执行前后完整性检查。 @@ -875,7 +1144,10 @@ Run,成本大于它保护的事实,本设计明确不做。 - `MESSAGE_REACTOR`、`AGENT_PROGRAM`、Root、Fiber 和 projection 不可序列化成第三种 对话事实; - tool call 只用 Message 内可派生的 `call_ref`; -- proactive 不是类型、字段、状态机或特殊 commit 路径; +- `MessageWriteGrant` 没有 ID、序列化或恢复语义;`DeliveryEffect` 只能以既有 + `message_id` 寻址且不复制正文; +- proactive 不是 Message 类型、字段或 Core commit 路径;source plugin state 仍由各自 + owner 持有; - 没有 `metadata/context/intent` 通用可变袋子绕过 typed Message 与 capability。 ### 16.2 WAL Gate @@ -884,6 +1156,9 @@ Run,成本大于它保护的事实,本设计明确不做。 - 同 `message_id` 不同内容 fail-loud; - commit 前崩溃没有 Message,commit 后 ACK 丢失不会重复 Message; - 每个 Session 的 seq 唯一连续,projection/observer 失败不影响 commit; +- reply 只能指向同 Session 的 prior reactable Message;跨 Session、前向、重复或缺失 + cause fail-loud; +- Agent Program snapshot 与 cause/head 不匹配时不能取得可写 grant; - 正常路径没有 UPDATE/DELETE Message;受控物理擦除只走 Data Management; - U2 抢先提交时,基于旧 head 的 A 被拒绝;两个 worker 也只有一个能提交。 @@ -897,7 +1172,12 @@ Run,成本大于它保护的事实,本设计明确不做。 - 替换 `AGENT_PROGRAM` fixture 无需修改 Core、WAL schema 或 channel adapter; - 缺失/重复 Service 和 contribution 冲突在 candidate 发布前失败; - Prompt 与 transform 次序确定,post-commit 插件不能修改 Message; -- 仓库外测试插件经正式安装链提供 `AGENT_PROGRAM`,不使用 Core 私有 import; +- 仓库外测试插件经正式安装链提供 `AGENT_PROGRAM`;报告明确区分 trusted in-process + 与 out-of-process sandbox,不把 API 检查冒充恶意代码隔离; +- 公共 `PluginRuntime` 不再暴露 broad workspace;未声明 path、raw SQL、candidate + production feed/effect 都在实际 owner 处拒绝; +- cross-session、user/system/assistant/tool/delete role spoof 与 revoked-generation append + 均被 `MessageWriteGrant` 边界拒绝; - generation 下线后无遗留 task、listener、subscription、Root lease 或 tool binding lease。 ### 16.4 行为 Gate @@ -909,17 +1189,27 @@ Run,成本大于它保护的事实,本设计明确不做。 - 不可确认的外部效果产生 `unknown`,不会盲目重复; - 空回复 retry、terminal deadline、continuation、error reply 和 transform 有受控 fixture; - `no_due` 不产生 Message;quiet 产生普通 `no_reply` assistant Message,Chat 隐藏; +- U1 command + U2 普通输入只确认 U1,U2 仍 pending;合并回答显式 reply U1/U2; +- tool_call reply、tool result 与最终 assistant reply 在 crash/replay 后形成同一条因果链; - user Message durable 后即可 ACK,reply/delivery 失败不会抹掉输入; -- assistant Message durable 后 Delivery 独立重试同一 `message_id`,不会重新生成正文; +- assistant Message durable 后,Delivery 从 immutable SessionRoute 建立 + `DeliveryEffect[message_id]`;prepared/provider_started/delivered/unknown/rejected/ + suppressed/settled 每个 crash 窗口都有恢复 fixture; - Web/Mobile 只用 `message_id + seq + cursor` 完成重复、断线和追赶; -- delete Message 到达后所有 projection 一致隐藏目标,旧 retry 不会复活它。 +- causal delete manifest 原子覆盖 reply/tool descendants;Chat、Model、Memory、Delivery + 与 Next action 一致处理,旧 retry 不会复活或重放 effect。 ### 16.5 迁移 Gate - Phase 1 每项行为都有 preserve/replace/bug 分类和可复跑 fixture; - WAL shadow 重建与旧读取逐 Session 对账;无法转换的数据有显式阻塞清单; +- Scheduler/Wake/Drift/EventMail/Channel/subagent/`message_push` 各有 source state owner、 + handoff receipt、backup/restore 和 preserve/replace/retire 结论; +- 旧 delivery ledger 的每个非终态都能迁到同 `message_id` effect record,route/binding/ + receipt 不漂移; - 切换点只有一个生产 writer、一个自动回复 subscriber,没有窗口式 dual-write; -- 数据迁移、客户端 cursor reset 和 generation retention 均从备份做过恢复演练; +- 数据迁移、delete closure、客户端 cursor reset、source continuity、delivery 与 generation + retention 均从备份做过恢复演练; - 删除清单逐项证明静态 import、插件源码/cache、测试、数据库、日志和运行进程都无消费者。 ### 16.6 明确接受的非目标 @@ -941,8 +1231,10 @@ Run,成本大于它保护的事实,本设计明确不做。 | client/retry message IDs | 同一 Message 多个身份 | 唯一 `message_id` | | ToolCallId | 可由调用所在位置确定 | `message_id + block index` | | proactive/source 执行分支 | 来源不应改变执行语义 | 普通 input Message + 同一 reactor | -| DeliveryId / conversation delivery rows | 投递不能复制消息身份 | `message_id` + provider projection/effect | +| DeliveryId / Turn-bound delivery body | 投递不能复制消息身份或正文 | `DeliveryEffect[message_id]` + route/receipt | | ProjectionClaim | projection 不配拥有事实 | version + source seq cache | +| handled-prefix cursor | 会和消息因果关系漂移 | typed `reply(message_id)` graph | +| `scoped_messages` / 第二份 history 参数 | 会绕过 WAL freshness 与 CAS | `SESSION_READ` immutable view + observed head | | Core 中的默认 AgentLoop | 产品算法不可替换 | 普通 `default-agent` 插件 | | `PassiveTurnPipeline` 固定 phase DAG | 把业务、持久化和外部效果绑死 | `MESSAGE_REACTOR` + 普通依赖/contributions | | `PassiveMessageWorker` 大 owner | 同时拥有 ingress、reply 和 delivery | Channel adapter + plugin Effect + Delivery projector | @@ -956,6 +1248,7 @@ Run,成本大于它保护的事实,本设计明确不做。 事实:Session = ordered Message WAL 行为:ordinary plugins read Message and append Message 视图:projection = fold(Messages up to source_seq) +外部效果:effect record = existing message_id + provider observation ~~~ Core 只保护 append、权限、外部边界和插件生命周期。它不再决定 Agent 怎样回答, From acf866b33054ff4f233c50eb918d9ad5a1c11991 Mon Sep 17 00:00:00 2001 From: huashen <2494946808@qq.com> Date: Thu, 3 Sep 2026 15:44:03 +0800 Subject: [PATCH 6/7] =?UTF-8?q?docs:=20=E9=97=AD=E5=90=88=20v4=20=E5=A4=96?= =?UTF-8?q?=E9=83=A8=E6=95=88=E6=9E=9C=E6=81=A2=E5=A4=8D=E8=BE=B9=E7=95=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/design/0902-reviewed-v4.md | 278 ++++++++++++++++++++++---------- 1 file changed, 195 insertions(+), 83 deletions(-) diff --git a/docs/design/0902-reviewed-v4.md b/docs/design/0902-reviewed-v4.md index d682191b..4c20973a 100644 --- a/docs/design/0902-reviewed-v4.md +++ b/docs/design/0902-reviewed-v4.md @@ -176,10 +176,11 @@ seq 2 user U2 seq 3 assistant reply(U1), reply(U2), text(A) ~~~ -append owner 必须验证 target 与新 Message 属于同一 Session、target `seq` 更小、没有 -重复或前向/跨 Session 引用,而且 target 是当前 reactor 允许反应的 typed Message。 -v4 初始合同只允许 assistant role 携带 `reply`;它不是 UI 引用/引用回复的万能字段。 -这条边使用已有 `message_id`,没有 AckId、ReactionId 或处理 cursor。 +Session owner 必须验证 target 与新 Message 属于同一 Session、target `seq` 更小、没有 +重复或前向/跨 Session 引用,并且 target set 与短命 grant 完全相等。当前 reactor policy +是否允许反应由 grant issuer 在签发前判断,不复制进 Core。v4 初始合同只允许 assistant +role 携带 `reply`;它不是 UI 引用/引用回复的万能字段。这条边使用已有 `message_id`, +没有 AckId、ReactionId 或处理 cursor。 `tool_result.call_ref` 是另一条类型化因果边:它只确认对应的 `tool_call`。如果 assistant 先用 `reply(U1), tool_call(...)` 推进 U1,恢复时先完成 unresolved tool;tool Message @@ -319,13 +320,18 @@ object capability。 ~~~text session_id -caller = exact plugin Root generation 或具体 host adapter +caller = 由 runtime scope 绑定的 exact plugin Root generation 或具体 host adapter allowed role + allowed content variants -allowed reply targets / call_ref +required reply target set / exact call_ref observed_head_seq 或另一项 typed atomic precondition expiry + one-shot append budget ~~~ +`caller` 不是调用者自己传入的字符串;composition host 从当前 scope 注入。assistant +Message 的 `reply` target set 必须与 grant 绑定的 cause set **完全相等**。如果 reactor +决定分别回答 U1 和 U2,它必须先明确选择 `{U1}`,再为这一小组签一份 grant;不能拿 +`{U1, U2}` 的宽权限只确认其中一条。 + 谁能写什么由真实信任边界决定: | caller | 可签发的写入 | @@ -337,9 +343,11 @@ expiry + one-shot append budget | Data Management | 已预览 closure 的 `system delete`;普通插件拿不到 | | 一次性 migration | 只在离线恢复流程写固定清单;runtime 永不复用 | -Session owner 在同一个 append 事务中校验 grant、Message 结构、同 Session 因果边和 CAS。 -跨 Session、越权 role/content、错误 cause/call、过期或已撤销 generation 都 fail-loud。 -generation 在外部 effect 完成后被撤销时,Tool owner只能用受限 completion grant 记录 +Session owner 在同一个 append 事务中只校验 grant、Message 结构、同 Session 因果边和 +CAS;“哪些输入应当一起回答”由 reactor 选择,再由 exact cause set grant 固定,Core 不 +复制这项产品策略。跨 Session、越权 role/content、错误 cause/call、过期或已撤销 +generation 都 fail-loud。generation 在外部 effect 完成后被撤销时,Tool owner 只能用 +受限 completion grant 记录 `error/unknown`,不能借旧授权再次执行副作用。 ## 四、工具调用也只是 Message @@ -389,6 +397,58 @@ seq 12 assistant 根据结果回答 操作。正确做法是保留调用意图、复用一个幂等地址并诚实记录 `unknown`,不是再加一套 Run/effect 状态机假装获得 exactly-once。 +### 4.3 正常执行和崩溃恢复共用一个 Tool 入口 + +已提交的 `tool_call` 不能依赖“碰巧仍活着的 Agent Program”才能完成。Tool runtime 通过 +普通 composition 提供一项受保护的窄服务: + +~~~text +TOOL_EXECUTOR.execute_or_recover(call_ref) -> None +~~~ + +它没有 execution ID,也不返回另一份结果对象:成功、失败或不确定的唯一可观察结果, +仍是 exact `call_ref` 的一条 tool Message。`session_id`、参数和 exact binding 都从 +`call_ref` 指向的既有 assistant Message 读取,不让调用者再传一份可能漂移的副本。它 +同时承担两条入口: + +1. `AGENT_PROGRAM` 提交 `tool_call` 后立即调用; +2. Tool runtime 的普通 Effect 在启动时扫描、运行时订阅 committed Message feed,对每个 + 未删除且没有终态 result 的 call 调用同一接口。 + +两条入口可以重复到达,但 exact `call_ref` 的本地互斥、provider query/idempotency 与 +result append 幂等会把它们收敛成一次结果。执行前必须重新校验 unresolved 状态、exact +binding、当前授权和 delete projection;随后取得 binding generation retention lease, +在真实副作用边界查询或执行,并用 Tool completion grant append 结果。 + +停用 `MESSAGE_REACTOR` 或 `AGENT_PROGRAM` 只是不再生成新的回复,不能遗弃已经 committed +的 `tool_call`;Tool recovery Effect 仍会把它结算,之后的 result 留在 WAL,等待未来的 +reactor。若管理员要停用 Tool runtime,candidate 必须先拒绝新的 tool call,并 drain +所有 unresolved call。exact generation 意外丢失时写 `unknown` 并报 incident,不能换用 +一个“差不多”的新 generation,更不能盲重试。 + +### 4.4 删除与外部副作用共用一个短命闸门 + +工具和 Delivery 在真正开始外部 I/O 前,都必须取得该 Session 的短命 shared effect +lease;Data Management append delete 前取得同一闸门的 exclusive lease。lease 只存在于 +内存,没有 ID、表或恢复语义,因此不是第三种对话事实。 + +shared 路径在 lease 内重新读取最新 delete graph。若目标已删除,工具拒绝开始,Delivery +直接进入 `suppressed`;否则工具保持 lease 直到 append result/`unknown`,Delivery 保持 +lease 直到 durable `delivered/rejected/unknown`。超时或取消也必须先查询并写出 +`unknown`,不能释放后假装副作用没有发生。 + +exclusive 路径会阻止新的 shared lease,并等待已经开始的 effect 得到 result 或 +`unknown`;然后从最新 head 重算 closure、append delete,再释放。于是只有两个结果: + +~~~text +effect 先取得 shared lease ──▶ delete 等它结算,再把结果纳入 closure +delete 先取得 exclusive lease ──▶ 新 effect 看见 delete,永不开始 +~~~ + +进程崩溃会丢掉 lease,所以启动顺序必须固定:先从完整 WAL 重建 delete/tool settlement +projection 和 generation lease,再启动 Tool/Delivery worker。恢复仍只依赖既有 Message、 +provider query/idempotency 和 `unknown`,不会恢复一个 Run。 + ## 五、旧 Turn 和 Run 去哪里 它们都不进入新领域模型、schema、公共 API 或持久化合同。 @@ -522,10 +582,14 @@ cursor: 最后完整应用的 seq 客户端用 `message_id` 去重和引用,用 `seq` 排序与追赶。projection 晚到、重复或重建 都不能改变 WAL。 -恢复时也不读 Run 状态。`Next action` projection 对整份 WAL 做一次因果 fold: +恢复时也不读 Run 状态。`Next action` projection 对整份 WAL 做一次因果 fold。它先从 +**完整历史**收集 reply 与 call/result settlement 边,再应用 delete manifest 决定哪些 +内容可见、哪些 call 不再可执行;隐藏一条旧 result 绝不能把已经结算的 call 变回 pending: 1. 校验并收集所有 `reply(target_message_id)` 与 `tool_result.call_ref` 边; -2. 有尚无任何确定 result 的 `tool_call`,先查询或安全恢复; +2. 有尚无终态 result、且未被 delete 覆盖的 `tool_call`,输出 `await_tool(call_ref)`; + Tool recovery Effect 调用 `TOOL_EXECUTOR.execute_or_recover(call_ref)`,projection 本身不执行 + 外部动作; 3. 否则选出尚未被 `reply` 明确认领、且按当前 reactor policy 可反应的 user/system/tool Message;delete Message 永不触发回复,command 默认单独选择,普通连续输入可以 显式组成 cause set; @@ -534,7 +598,8 @@ cursor: 最后完整应用的 seq 5. 没有 unresolved tool call 或 unacknowledged cause 才是 idle。 被多个 feed callback 重复唤醒时,它们会算出同一个 pending set;只有一个输出能通过 -observed head CAS。失败者重新 fold,看到 reply 边后结束。这里没有处理 cursor,也不 +observed head CAS。失败者重新 fold,看到 reply 边后结束。`await_tool` 只会唤醒幂等的 +Tool executor;`MESSAGE_REACTOR` 自己返回等待,不偷做恢复。这里没有处理 cursor,也不 从“最后一条是什么 role”猜是否完成。 两个容易出错的例子: @@ -597,20 +662,25 @@ digest。正常状态只单调前进: ~~~text prepared ──fsync──▶ provider_started ──▶ delivered ──▶ settled - │ ├─────────▶ rejected + │ ├─────────▶ rejected (terminal) │ └─────────▶ unknown └──────────────────────────────────▶ suppressed ~~~ - `prepared` 在任何 provider I/O 前冻结 route 与 exact plugin binding; -- `provider_started` 必须先 durable,随后才可调用 provider; +- 取得 shared effect lease 并重读 delete graph 后,先把 `provider_started` durable, + 随后才可调用 provider;直到结果状态 durable 前不释放 lease; - crash/timeout 后先用 `message_id` 幂等键或 receipt query;能确认才写 `delivered`; -- provider 明确拒绝写 `rejected`;可能已经发生但无法确认写 `unknown`;两者都不盲重发; +- provider 的永久拒绝写 `rejected`,它是终态,永不重试;只有 provider 能证明 + 没有产生外部效果时,才能保持 `provider_started` 并用同一 `message_id`/binding + 安全重试;无法证明就写 `unknown`,不盲重发; - delete 在 provider I/O 前把 `prepared` 单调推进为 `suppressed`;已经 `provider_started` 只能查询真实结果,不能倒退成未发送; - `delivered` 后,source plugin 才推进自己的 ACK/dedupe/cooldown,再写 `settled`; - observer 在 Message commit 后、`prepared` 前崩溃时,恢复扫描按 SessionRoute 补建缺失 - record;同 `message_id`/同 binding 幂等,binding 冲突 fail-loud; + record;它必须先应用 delete manifest,若 Message 已删除就直接建 + `suppressed`,不先建 `prepared`;同 `message_id`/同 binding 幂等,binding 冲突 + fail-loud; - unresolved record 固定对应 channel generation;恢复先重建 retention lease,再清理 旧 generation。 @@ -620,7 +690,8 @@ prepared ──fsync──▶ provider_started ──▶ delivered ──▶ set assistant Message 已提交 │ ├── provider 发送成功 - ├── provider 明确失败,可按策略重试同一个 message_id + ├── provider 证明未产生效果,可用同一 message_id 安全重试 + ├── provider 永久拒绝,写 rejected 后结束 └── ACK 不明,先查询;不能确认时标记外部状态 unknown ~~~ @@ -643,20 +714,25 @@ Data Management 操作预览目标、备份、验证 provider/source 已不再 ## 九、删除仍只使用 Message -正常路径永远只追加。用户明确撤销 Message 时,Data Management 先在 Session head `H` -上冻结一份因果 closure,而不是只删界面当前点中的一行。 - -计算前先取得该 Session 的短命独占管理 barrier,停止签发新的 reactor/tool write grant。 -closure 中若有 unresolved tool call:尚未开始外部 I/O 的先取消;已经可能发生的先按 -第四节查询,并 append 确定 result 或 `unknown`,再从新 head 重算。不能一边删除调用 -意图,一边让 executor 在看不见的地方继续执行。 - -closure 从明确目标开始,沿已有 typed edge 只向后展开: - -1. assistant `reply(target_message_id)` 指向 closure 中 Message,则加入该 assistant; -2. closure 中 assistant 的 `tool_call` 对应的 tool result 加入; -3. reply 该 tool result 的后续 assistant 继续加入,直到没有新后代; -4. 上游 cause 不自动加入;“撤销整组”由 UI 在请求中明确给出需要一起作为 seed 的 +正常路径永远只追加。用户明确撤销 Message 时,Data Management 先取得第 +4.4 节同一个 per-session exclusive lease。它会停止签发新 reactor grant,并阻止 +新的 Tool/Delivery effect 开始。 + +已经取得 shared lease 的 effect 先写出 result 或 `unknown`。尚未取得 shared lease +的 unresolved tool call 可以明确 append `tool_result(error: deleted_before_start)`,不调用 +provider。启动时遗留的不确定 call 必须在 Data Management 对外 ready 前已按第 +4.3 节查询或写 `unknown`,不能把“进程里没有 task”误当成“从未开始”。已有 +Delivery record 按第八节结算;尚未建 record 且目标将被删除时,只建 +`suppressed`。这些完成后才在最新 Session head `H` 上计算 closure。 + +先归一化 seed,再沿已有 typed edge 向后展开: + +1. 若 seed 是 tool result Message,同时加入包含它所引用 `tool_call` 的 assistant + Message。Message 不可拆,所以该 assistant 内其他 call 的 result 也会在后续闭包中加入; +2. assistant `reply(target_message_id)` 指向 closure 中 Message,则加入该 assistant; +3. closure 中 assistant 的 `tool_call` 对应的 tool result 加入; +4. reply 该 tool result 的后续 assistant 继续加入,直到没有新后代; +5. 上游 cause 不自动加入;“撤销整组”由 UI 在请求中明确给出需要一起作为 seed 的 user/system Message。 然后用 Data Management 专属 grant 和 `expected_head_seq=H` 一次 append **一条**普通 @@ -675,13 +751,14 @@ Redaction row 或另一套版本号。若 head 在计算期间变化,append | owner | 必须做什么 | |---|---| | Chat / Model context | 隐藏全部目标和依赖输出,不留下孤立 tool result | -| `Next action` | deleted input/call/result 永不重新进入 pending;被删除 assistant 的旧 reply 边仍证明上游曾被处理,不能自动重跑 command/effect | +| `Next action` | 先用完整历史的 reply/result 边结算,再应用可见性;deleted input/call/result 永不重新进入 pending,被删除 result 也不会让旧 call 复活 | | Memory / Search / Akasha | 按目标 `message_id` 撤销派生项,或从 WAL 重建到 manifest 的 `source_seq` | | Delivery | `prepared` 停止发送;`provider_started` 先查询并记 delivered/unknown;已经 delivered 只能尝试 provider 明确支持的 recall,不能伪装成未发送 | | Artifact owner | 重新计算引用;只有引用归零且另有物理删除授权时才减少字节 | -因此删 U1 会同时隐藏 reply U1 的 assistant、其 tool call/result 和最终回答;删一个 -tool_call 也不会留下无主 tool result。只删 assistant A 时,上游 U1 可以继续显示,但 +因此删 U1 会同时隐藏 reply U1 的 assistant、其 tool call/result 和最终回答;从 +tool result 开始删也会先纳入它的 call Message,不会留下一个看似 pending 的 +call。只删 assistant A 时,上游 U1 可以继续显示,但 不会因 A 被隐藏而自动触发第二次回复;用户要重新问,必须再 append 一条新 user Message。 @@ -798,6 +875,11 @@ default-agent plugin CHAT_MODELS TOOL_EXECUTOR STREAM_PREVIEW + +tool-runtime plugin + provides TOOL_EXECUTOR + recovery Effect + injects SESSION_READ, SESSION_FEED, SESSION_APPEND + exact tool bindings + shared effect lease ~~~ 这些大写名字都是普通 `ServiceKey`,不是 Core 固定 slot,更不是新领域对象: @@ -806,10 +888,12 @@ default-agent plugin `SESSION_APPEND` 只签发绑定 Session、role 与 CAS/typed precondition 的 writer;三者 共用一个 WAL owner,但权限彼此独立,都没有任意 SQL、原位改写或删除能力; - `MESSAGE_REACTOR` 读一个已经 committed 的输入 Message,按最新 WAL projection 判断 - `idle / command / respond / recover tool`,再选择 command 或 `AGENT_PROGRAM`;它不 - 保存自己的 outcome; + `idle / command / respond / await tool`,再选择 command 或 `AGENT_PROGRAM`;它不执行 + Tool,也不保存自己的 outcome; - `AGENT_PROGRAM` 拥有默认模型/工具算法,包括 provider retry、Tool Search、空回复 修正、terminal tool deadline 和继续生成; +- `TOOL_EXECUTOR` 只结算 committed `tool_call`;即时调用和 crash recovery 走同一个 + `execute_or_recover()`,因此它不依赖某次 Agent Program 仍然活着; - 其他 key 只是 `default-agent` 自己的依赖。不使用工具的 Agent 不需要提供假的 `TOOL_SELECTOR`,Core 也不维护一张“所有 Agent 都必须有”的选择表。 @@ -818,6 +902,7 @@ default-agent plugin ~~~text MESSAGE_REACTOR.react(session_id) -> None AGENT_PROGRAM.respond(session_id, cause_message_ids[]) -> None +TOOL_EXECUTOR.execute_or_recover(call_ref) -> None ~~~ `None` 只表示函数已经结束;可观察结果只能是 WAL 中新增了哪些 Message。异常、取消、 @@ -849,6 +934,10 @@ Message 因果图算出;同一个 Session 被重复唤醒时,projection 已 Scheduler、Wake、Channel 和 subagent 都只产生普通 Message,或者显式依赖同一个 `AGENT_PROGRAM`;来源不会复制一套执行模型。 +Tool recovery 是另一条普通 Effect:它订阅同一 feed,只把 unresolved call 交给 +`TOOL_EXECUTOR`。它不塞进 `passive-conversation`,所以关掉自动回复也不会让已经提交 +的外部意图悬空。这是两个正交生命周期,不是 proactive/passive 的新特判。 + ### 11.3 固定 phase 不原样搬家 插件化不是把 `PassiveTurnPipeline` 整块移动到 `plugins/`。现有每项能力先找到唯一 @@ -864,7 +953,7 @@ owner;没有独立不变量或真实消费者的 phase/hook 直接删除: | history 裁切、摘要与 compaction retry | `PROVIDER_REQUEST_PROJECTION`;沿用普通 compaction plugin | | tool schema preload / Tool Search 解锁 | `TOOL_SELECTOR`,由 `default-agent` 使用 | | Tool 展示 | `TOOL_SELECTOR`;只能缩小当前可见集合 | -| Tool 授权与真实执行 | 受保护的 `TOOL_EXECUTOR`;调用边界重新校验 | +| Tool 授权、执行与恢复 | `tool-runtime` 的受保护 `TOOL_EXECUTOR` + recovery Effect;调用边界重新校验 | | 默认 ReAct、provider retry、空回复重试、terminal deadline | `AGENT_PROGRAM` | | model/provider 绑定 | models/provider 插件;从当前 exact Root 注入 | | Citation、Meme、最终文本/媒体改写 | append 前的有序、不可变 `ASSISTANT_TRANSFORMS` | @@ -874,6 +963,7 @@ owner;没有独立不变量或真实消费者的 phase/hook 直接删除: | error reply、quiet | reactor/program 产生普通 assistant `text` 或 `no_reply` Message | | continuation、crash 后下一步 | 从 WAL 重建的 `Next action` projection | | assistant 对外发送、重试、provider ACK | 独立 Delivery projection/effect,只引用 `message_id` | +| delete 与外部 effect 竞态 | 通用 per-session shared/exclusive lease;短命、无 ID、不落盘 | | generation 固定、取消、资源清理 | 通用 plugin Root lease + Fiber/Effect 生命周期 | Command 若只读或只生成回复,可以直接产生 assistant Message;若会付款、发信、改 Git @@ -891,9 +981,12 @@ post-commit observer 无权回来改正文。 当前授权。模型产生 `tool_call` 后: 1. 按本次可见 schema 校验并把 exact immutable `tool_binding` 写进 assistant Message; -2. `TOOL_EXECUTOR` 在真实副作用边界重新检查当前授权; -3. 已撤权时不执行,append 一个明确的 `tool_result(error)`; -4. 未知外部结果按第四节写 `unknown`,不能由 Agent Program 猜成 success。 +2. `TOOL_EXECUTOR` 固定 exact binding lease,取得 shared effect lease,并在真实副作用 + 边界重新检查当前授权与 delete graph; +3. 能证明尚未开始且已撤权时不执行,append 一个明确的 `tool_result(error)`; +4. 已经可能开始但无法确认时按第四节写 `unknown`,不能由 Agent Program 猜成 success; +5. 正常调用、feed replay 和启动扫描都走同一个 `execute_or_recover()`,不会因换入口而 + 绕过上述检查。 因此替换 `AGENT_PROGRAM` 只能改变算法,不能绕过工具权限 owner。 @@ -905,8 +998,8 @@ Core 只保留来源无关、产品算法无法安全拥有的原子能力: committed Root lease、candidate/stable、Fiber/Effect 清理、health/incident; 2. **Session Message WAL**:append、read、subscribe、head、幂等、seq、CAS 和受限 writer;用户删除 Session/Message 的 Data Management 是另一个显式管理入口; -3. **短命执行安全**:取消、超时、per-session 串行准入和有界资源 scope;这些可以 - 丢失,不分配持久身份; +3. **短命执行安全**:取消、超时、per-session 串行准入、effect shared/exclusive lease + 和有界资源 scope;这些可以丢失,不分配持久身份; 4. **真实外部边界**:模型调用、Tool 授权/执行、stream preview、channel ingress/ACK 和 delivery effect 的窄 port;具体 provider 与策略仍由普通插件提供; 5. **类型化观察**:只发布 committed Message;observer 失败不能回滚、覆盖或补造 @@ -932,41 +1025,34 @@ Channel adapter │ Channel grant → append user Message(message_id) │ durable 后 ACK inbound ▼ -┌──────────────────────── Session Message WAL ────────────────────────┐ -│ user / system / assistant(tool_call|text|no_reply) / tool / delete │ -└──────────────────────────────┬──────────────────────────────────────┘ +┌──────────────────────── Session Message WAL ─────────────────────────┐ +│ user / system / assistant(tool_call|text|no_reply) / tool / delete │ +└──────────────────────────────┬───────────────────────────────────────┘ │ committed feed - ▼ - passive-conversation Effect - │ exact Root lease - ▼ - MESSAGE_REACTOR - causal fold → cause set - ┌────────┴────────┐ - known command AGENT_PROGRAM - │ - ┌───────────────────┼────────────────────┐ - ▼ ▼ ▼ - provider request view prompt/tool selection model retry - │ complete - ▼ - Agent grant + head CAS - append assistant Message - reply(each cause) - ├── text/no_reply ─────┐ - └── tool_call │ - │ commit first │ - ▼ │ - TOOL_EXECUTOR │ - │ complete │ - ▼ │ - append tool Message │ - └── reply(tool) ┘ - -committed Message feed - ├──▶ Chat / Web / Mobile cursor - ├──▶ Model context / Memory / Search projections - └──▶ durable DeliveryEffect[message_id] ── provider send/retry/ACK + ┌─────────────────┼──────────────────┬──────────────────┐ + ▼ ▼ ▼ ▼ + passive-conversation Tool recovery Chat / Model Delivery projector + Effect Effect / Memory message_id only + │ │ view │ + ▼ │ ▼ + MESSAGE_REACTOR └──────────────┐ DeliveryEffect record + causal fold → cause set │ │ + ┌───────┴────────┐ │ shared effect lease + known command AGENT_PROGRAM │ ▼ + │ │ provider send/ACK + prompt/tool selection + model retry │ + │ complete │ + ▼ │ + exact-cause grant + head CAS │ + append assistant Message ─────────┼──────────────────────▶ WAL + ├── text/no_reply │ + └── tool_call ── commit first ────┤ + ▼ + TOOL_EXECUTOR + shared effect lease + │ result/unknown + ▼ + completion grant → tool Message ──▶ WAL ~~~ 这里有两个故意分开的 commit 点: @@ -994,15 +1080,19 @@ Fiber、取消 token、超时和 per-session lane 都是内存资源。U2 到来 ### 14.2 崩溃后只从 Message 恢复 - assistant Message append 前崩溃:没有事实;重启后可用最新 Root 重新生成; -- assistant `tool_call` 已 append:从其 `call_ref` 与 `tool_binding` 查询或恢复; +- assistant `tool_call` 已 append:独立 Tool recovery Effect 从其 `call_ref` 与 + `tool_binding` 调用同一个 `execute_or_recover()`,不要求旧 Agent Program 复活; - tool Message 已 append:下一次从最新 WAL 继续生成 assistant Message; - assistant text/`no_reply` 已 append:该前缀已经有结果,projection 得到 idle; -- delivery 中崩溃:继续用同一 `message_id` 查询或重试,不重新生成正文。 +- delivery 中崩溃:继续用同一 `message_id` 查询或安全重试,不重新生成正文;若 Message + 已删除且 effect record 缺失,直接补 `suppressed`。 `tool_binding` 中的 generation identity 是 Message 内容的一部分,不需要 RunId。由 WAL 派生的 unresolved-tool projection 为所引用的插件 generation 加 retention lease;进程 -启动时先重建这些 lease,再允许清理旧 generation。若安全 owner 撤销权限,就写明确 -的 tool error Message 并释放 lease,不能执行过期授权。 +启动时先应用全部 delete manifest,再重建这些 lease,并在开放 reactor、delivery 和 +Data Management 前结算崩溃遗留的 unresolved call。若安全 owner 在可证明尚未开始时 +撤销权限,就写明确的 tool error Message;若是否开始已不可知就写 `unknown`。两者都 +释放 lease,不能执行过期授权。 这只保证一段仍活着的函数内部使用同一 Root。崩溃后的新函数可以使用新 Root;唯一 必须保持的是已提交 tool binding 的执行身份。为了跨崩溃保存整套旧算法而新增 durable @@ -1050,6 +1140,8 @@ result 越权、未声明 workspace path、raw SQL 和 candidate real effect。 - 批准 inbound ACK 与 Agent reply/delivery 解耦; - 批准 `message_id`-keyed durable DeliveryEffect、单 external route/Session 和 causal delete closure; +- 批准 `rejected` 是不可重试终态,以及 delete/effect 以短命 shared/exclusive lease + 线性化;lease 不进入 schema; - 分别批准 Scheduler、Wake、Drift、EventMail、Channel、subagent 与 `message_push` 的 preserve/replace/retire 选择,不能用一句“删除 proactive”代替; - 批准 in-process plugin 是可信代码;若目标是恶意插件隔离,另立 out-of-process 合同; @@ -1065,11 +1157,13 @@ result 越权、未声明 workspace path、raw SQL 和 candidate real effect。 - compaction retry、Tool Search 解锁、空回复 retry、terminal tool、continuation; - Citation/Meme/媒体变换、partial stream、error/no_reply; - Message 写集、Memory/Akasha 观察、Web/Mobile cursor、delivery 与两侧 ACK; -- 热更新时的 exact generation、取消和资源清理。 +- 热更新时的 exact generation、取消和资源清理; - Scheduler schedule/missed-tick、EventMail transitions、Wake tick/reservoir/hazard/ACK/ cooldown、Drift cursor/journal、`message_push` raw-body caller 和 subagent recovery; - `runtime/deliveries/settlements.sqlite` 的 route、provider_started、receipt、unknown 与 - settle 恢复窗口,以及 Channel handoff 的 exact binding。 + settle 恢复窗口,以及 Channel handoff 的 exact binding; +- delete 分别早于/晚于 Tool 与 Delivery effect start、只删 tool result、effect record + 尚未建立和永久 rejected 的受控竞态。 每个差异必须先标成“保留能力”“按 v4 有意替换”或“已证明的旧 bug”。oracle 不要求 盲目复制现状;它要求任何消失的能力都有明确决定。旧 phase 名称本身不是能力,没有 @@ -1082,6 +1176,8 @@ result 越权、未声明 workspace path、raw SQL 和 candidate real effect。 assistant Message 只有在唯一因果前缀可证明时才补 reply,歧义数据进入人工阻塞清单; - 实现正交的 `SESSION_READ`、`SESSION_FEED`、`SESSION_APPEND`、短命 `MessageWriteGrant` 与 scoped CAS writer; +- 实现不落盘的 per-session shared/exclusive effect lease,以及统一 + `TOOL_EXECUTOR.execute_or_recover(call_ref)`; - Chat、Model context、Tool status、Next action、Memory 和 Mobile 先 shadow rebuild; - 用 U1 command + U2、U1/U2 合并回答、tool continuation、replay、并发 CAS 和 delete closure fixture 验证因果 fold; @@ -1104,7 +1200,7 @@ result 越权、未声明 workspace path、raw SQL 和 candidate real effect。 - command、附件、error/no_reply、tool loop、continuation 与输出 transform 逐项对账; - shadow 禁止真实 append、Tool 副作用和 delivery,只比较计划产生的 Message 与 外部调用; -- candidate/stable 切换期间验证 exact Root 和所有 Effect 均可排空。 +- candidate/stable 切换期间验证 exact Root 和所有 Effect 均可排空; - 完成 broad `PluginRuntime.workspace` 消费者盘点与移除;仓库外 fixture 走正式安装链, 同时验证其可信进程边界和所有可执行的 capability 拒绝。 @@ -1113,6 +1209,8 @@ result 越权、未声明 workspace path、raw SQL 和 candidate real effect。 - Channel 先 append user Message,再 ACK inbound; - user、assistant、tool 与各 source producer 全部只走带正确 grant 的同一个 WAL append; - 只启用 `passive-conversation` 的生产 subscriber,旧 worker 变为不可达; +- 启用独立 Tool recovery subscriber;它先应用 delete graph、重建 binding lease 并结算 + crash 遗留 call,再把 reactor、Delivery 和 Data Management 标成 ready; - Channel/Delivery plugin 先完成 `SessionRoute` 和 `DeliveryEffect[message_id]` handoff,再 让新 projector 消费 committed assistant Message; - Scheduler/Wake/Drift/EventMail 连续性 owner 全部 handoff 或显式 retire 后,才允许新 @@ -1146,6 +1244,7 @@ result 越权、未声明 workspace path、raw SQL 和 candidate real effect。 - tool call 只用 Message 内可派生的 `call_ref`; - `MessageWriteGrant` 没有 ID、序列化或恢复语义;`DeliveryEffect` 只能以既有 `message_id` 寻址且不复制正文; +- effect shared/exclusive lease 没有 ID、持久状态或对话语义; - proactive 不是 Message 类型、字段或 Core commit 路径;source plugin state 仍由各自 owner 持有; - 没有 `metadata/context/intent` 通用可变袋子绕过 typed Message 与 capability。 @@ -1158,6 +1257,8 @@ result 越权、未声明 workspace path、raw SQL 和 candidate real effect。 - 每个 Session 的 seq 唯一连续,projection/observer 失败不影响 commit; - reply 只能指向同 Session 的 prior reactable Message;跨 Session、前向、重复或缺失 cause fail-loud; +- assistant 的 reply target set 必须与 grant 的 exact cause set 相等;需要拆答时先由 + reactor 选择更小 cause set,不能消费一部分宽授权; - Agent Program snapshot 与 cause/head 不匹配时不能取得可写 grant; - 正常路径没有 UPDATE/DELETE Message;受控物理擦除只走 Data Management; - U2 抢先提交时,基于旧 head 的 A 被拒绝;两个 worker 也只有一个能提交。 @@ -1169,6 +1270,8 @@ result 越权、未声明 workspace path、raw SQL 和 candidate real effect。 - Core 源码不按 passive/proactive/Wake/Scheduler/Citation/Meme/compaction/provider 名称分支; - 停用 `passive-conversation` 后,输入 Message、历史与同步仍工作,只停止自动回答; +- 停用 `MESSAGE_REACTOR`/`AGENT_PROGRAM` 后,独立 Tool recovery Effect 仍会结算已提交 + call;停用 Tool runtime 前必须 drain unresolved call; - 替换 `AGENT_PROGRAM` fixture 无需修改 Core、WAL schema 或 channel adapter; - 缺失/重复 Service 和 contribution 冲突在 candidate 发布前失败; - Prompt 与 transform 次序确定,post-commit 插件不能修改 Message; @@ -1185,6 +1288,8 @@ result 越权、未声明 workspace path、raw SQL 和 candidate real effect。 - command short-circuit、附件、Prompt、compaction、Tool Search 和模型绑定都有目标 owner; - provider 断网、retry 与 partial stream 不写 Session; - `tool_call` 先提交再执行,crash 后按 `call_ref/tool_binding` 查询或安全恢复; +- 即时执行、feed replay 与启动恢复都调用同一个 + `TOOL_EXECUTOR.execute_or_recover(call_ref)`; - Tool 可见性不能扩大真实授权,撤权产生明确 tool error; - 不可确认的外部效果产生 `unknown`,不会盲目重复; - 空回复 retry、terminal deadline、continuation、error reply 和 transform 有受控 fixture; @@ -1195,9 +1300,16 @@ result 越权、未声明 workspace path、raw SQL 和 candidate real effect。 - assistant Message durable 后,Delivery 从 immutable SessionRoute 建立 `DeliveryEffect[message_id]`;prepared/provider_started/delivered/unknown/rejected/ suppressed/settled 每个 crash 窗口都有恢复 fixture; +- `rejected` 永不重试;只有 provider 证明未产生效果时,`provider_started` 才能用原 + `message_id`/binding 安全重试; +- delete 先取得 exclusive lease 时 Tool/Delivery 不开始;effect 先取得 shared lease 时 + delete 等 result/unknown 后重算 closure; +- recovery scanner 看见已删除且缺失 DeliveryEffect 的 Message 时只建 `suppressed`; - Web/Mobile 只用 `message_id + seq + cursor` 完成重复、断线和追赶; - causal delete manifest 原子覆盖 reply/tool descendants;Chat、Model、Memory、Delivery 与 Next action 一致处理,旧 retry 不会复活或重放 effect。 +- 只以 tool result 为 seed 时 closure 自动纳入对应 call Message;即使读取旧的不完整 + delete manifest,历史 result edge 也不会让 call 再执行。 ### 16.5 迁移 Gate From 51f1467456881e7302abf76a931e9dfe698fef6c Mon Sep 17 00:00:00 2001 From: huashen <2494946808@qq.com> Date: Thu, 3 Sep 2026 16:57:08 +0800 Subject: [PATCH 7/7] =?UTF-8?q?docs:=20=E4=BF=AE=E6=AD=A3=20v4=20Message?= =?UTF-8?q?=20=E4=B8=8E=20Turn=20=E8=AF=AD=E4=B9=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/design/0902-reviewed-v4.md | 2055 ++++++++++++++----------------- 1 file changed, 948 insertions(+), 1107 deletions(-) diff --git a/docs/design/0902-reviewed-v4.md b/docs/design/0902-reviewed-v4.md index 4c20973a..167bdbd2 100644 --- a/docs/design/0902-reviewed-v4.md +++ b/docs/design/0902-reviewed-v4.md @@ -1,4 +1,4 @@ -# Akashic v4:Message WAL 与普通插件组合 +# Akashic v4:Message WAL、Turn Projection 与普通插件组合 - 文档版本:0902-reviewed-v4 - 日期:2026-09-03 @@ -9,104 +9,58 @@ ## 结论 -v4 不是只有一次数据库减法。它同时做两件彼此正交的事: +v4 同时完成两件彼此正交的事: -1. **事实层做减法**:对话事实只剩 Session 与 Message; -2. **行为层做拆分**:原来固定在 Core/Bootstrap 的被动回复大链路,变成普通插件组合。 +1. **对话事实只剩 Session 与 Message。** Session 是一条只追加的 Message WAL;用户输入、 + Agent 输出、Tool 调用和 Tool 结果只要被接纳,就立即成为 Message,不再等一个 Turn + 完成后批量补写。 +2. **对话行为由普通插件组合。** 当前固定的被动回复大链被拆成 Turn projection、 + `passive-conversation`、可替换 `AGENT_PROGRAM`、受保护 `ToolHost` 和普通行为插件; + Core 不认识 passive、proactive、Wake、Scheduler 或某个 Agent 算法。 -两件事缺一不可。只做第二件,会让插件继续围着 Turn/Run 等重复状态转;只做第一件, -则只是把新 WAL 塞回旧的 `PassiveMessageWorker → AgentLoop → PassiveTurnPipeline` -巨型流水线,并没有得到可替换的 Agent。 +`Turn` 仍然是有用的用户概念,但它只是一个普通插件从 Message 因果图算出的 projection: +不落库、没有稳定 TurnId,也不决定 Message 能否存在。不同 Turn 插件可以改变分组规则, +而不迁移 Session WAL。 -Akashic 的对话事实层只保留两个名词: +最重要的例子是: ~~~text -Session = 一条只追加的 Message WAL -Message = 一个已经完整产生并被 WAL 接纳的事实 -~~~ +真实提交顺序 -没有第三种权威对话对象。没有 Turn、Run、Step、Attempt、DeliveryId 或一层 -`SessionEvent` 再包住 Message。外部 Channel 为 crash recovery 保存的 -`DeliveryEffect[message_id]` 是 provider effect projection,不复制正文、不成为消息载体, -见第八节。 +seq 1 Human U1 + interrupt # 只取消短命执行,不写 Message +seq 2 Human U2 + interrupt +seq 3 Agent P # 与 U1/U2 无关的 proactive A +seq 4 Agent A1 responds_to(U1, U2) -这里的“只有”限定在**对话事实层**。附件字节、credential、插件 generation、 -scheduler 配置和 provider 外部状态仍由各自边界拥有;它们不是对话实体,也不能复制 -Message。它们若要影响或进入对话,只能被 Message 的类型化内容引用或报告。 +Turn projection -~~~text -┌──────────────────── Session ────────────────────┐ -│ seq 1 Message:用户说 U1 │ -│ seq 2 Message:用户补充 U2 │ -│ seq 3 Message:助手请求调用 tool │ -│ seq 4 Message:tool 返回 success │ -│ seq 5 Message:助手回答 A │ -└─────────────────────────────────────────────────┘ - │ │ │ - ▼ ▼ ▼ - 聊天视图 模型上下文 手机增量同步 +Turn H = {U1, U2, A1} +Turn P = {P} ~~~ -网络重试、模型重试和未完成 token 都发生在对应 Message 写入以前。它们没有产出 -Message,因此不进入 Session。重启后可以丢失这些执行过程;不能丢失的只有已经 -提交的 Message。 - -被动回复也不是第三个事实对象。它只是一个普通插件提供的函数:读到已提交的 -Message,调用其他普通插件,最后再产生 Message。Core 不认识 passive、proactive、 -Wake 或某一种 Agent 算法。 - -~~~text -对话事实 普通插件行为 -──────────────────── ───────────────────────────── -Session committed Message - └── Message WAL ─────────────▶ MESSAGE_REACTOR - ▲ ├── COMMANDS - │ └── AGENT_PROGRAM - │ ├── Prompt contributions - └──────── append Message ◀──────┼── Context projection - ├── Tool selection - └── Model / Tool ports - -同一份 Message WAL ──▶ Chat / Model context / Memory / Sync / Delivery projection -~~~ - -`MESSAGE_REACTOR`、`AGENT_PROGRAM` 和各项依赖是运行时 capability,不是实体、日志 -记录或另一种消息载体。替换 Agent Program 不改变 Session schema;停用自动回复插件 -也不会删除已经收到的 user Message。 +`P` 插在物理时间线中间,不会让 A1 失效,也不会把自己塞进 Turn H。A1 的正确性取决于 +它选择的 causes 是否仍未被结算,而不是 Session head 是否完全没变。 ### 把我当六岁 -Session 是唯一一本作业本。Message 是已经用墨水写完的一行字。 - -- 小朋友先在草稿纸上写,写错可以重来很多次。 -- 只有一句话写完整,才抄进作业本。 -- 抄进去以后,这一行有自己的编号 `message_id`,也有所在页码 `seq`。 -- 聊天页、模型看到的上下文和“这一轮”的括号,都只是拿彩笔从作业本里画出来。 -- 擦掉彩笔,作业本没有少东西;换一种画法,也不用迁移事实。 - -旁边还有一个会读作业本、再写新行的机器人,它就是插件: - -- 换一个机器人,只是换回答办法,作业本格式不变; -- 关掉机器人,孩子写下的 user Message 仍在,只是不再自动回答; -- 机器人找模型、挑工具、重试网络,都是它工作时的动作,不是作业本里的新东西; -- 机器人只有把一句话写完整并交给作业本,才算 Agent 真的说过。 - -所以 U1 后模型断网三次,最后生成 A,作业本仍只有两行: +Session 是唯一一本作业本,Message 是已经用墨水写下的一行。 -~~~text -1 user U1 -2 assistant A -~~~ +- U1 写进作业本以后,按下“停”只让机器人停笔,不会把 U1 撕掉。 +- 后来写入 U2,它也马上留在本子里。 +- 机器人最后写 A1,并明确说“我在回答 U1 和 U2”。 +- 中间另一个机器人写了一条主动消息 P,也不会把 U1、U2 或 A1 挤走。 +- Turn 插件只是拿彩笔画圈:把 U1、U2、A1 圈在一起,把 P 单独圈起来。 +- 换一个画圈插件,圈法可以改变;本子里的字和顺序完全不变。 -那三次断网不是三次对话,也不是三条事实。它们只是草稿纸上的失败。 +模型重试、半截 token、取消信号和正在运行的 Python object 都像草稿纸:可以丢失。 +只有完整 Message append 成功以后,才算真正写进了作业本。 -## 一、两个权威对象 +## 一、唯一事实模型 ### 1.1 Session -Session 只拥有身份和按 `seq` 排列的 Message。它不拥有当前执行、轮次、主动模式、 -投递状态或投影视图。 - ~~~text Session { session_id @@ -114,1254 +68,1141 @@ Session { } ~~~ -这里的 WAL 指领域层的 append-only message log,不是再增加一张 event 表。底层可以 -使用 SQLite WAL,但 `sessions.db/messages` 本身才是产品真源。 +这里的 WAL 是领域层的 append-only Message log,不是再套一张 `SessionEvent` 表。底层 +可以使用 SQLite WAL,但 `sessions.db/messages` 才是产品真源。 + +Session 不拥有当前 Turn、Run、Attempt、投递状态、模型请求或投影缓存。 ### 1.2 Message ~~~text Message { - message_id # 在 append 前生成;全局稳定、不透明 + message_id # append 前由可信边界产生;全局稳定、不透明 session_id - seq # WAL 原子提交时分配;Session 内单调连续 - role # system | user | assistant | tool - content[] # 完整、类型明确的内容块 + seq # WAL commit 时分配;Session 内单调且不复用 + author # Human | Agent | Tool | Source(plugin_key) + responds_to[] # 结构化 cause 边;不是正文 block + content[] # 完整、类型明确的正文或治理内容 } ~~~ -初始合同不提供通用 `meta` 袋子。以后若要加字段,必须先证明它拥有一个不能由 -`role`、`content`、Session 配置或 projection 表达的独立事实。 - -最小内容块是: +初始内容 union 只保留当前设计必须证明的类型: ~~~text -text { text } -artifact { artifact_ref, media_type } -reply { target_message_id } -tool_call { name, arguments, tool_binding, provider_token? } -tool_result { call_ref, outcome, output } -no_reply {} -delete { target_message_id } +text { text } +artifact { artifact_ref, media_type } +tool_call { name, arguments, tool_binding } +tool_result { resolves: call_message_id, outcome, output } +no_reply {} +tombstone { seed_message_ids[] } ~~~ -这不是所有产品内容的封闭枚举,而是本次设计必须验证的最小集合。新增 image、audio、 -citation 等内容时继续扩展 `content` 的 typed union,不新增平行 Message 表或通用 -metadata 袋子。`artifact_ref` 和 `tool_binding` 指向各自边界已有的不可变对象;它们 -不是新的对话身份。 - -`outcome` 只有 `success | error | unknown`。失败不是缺一条成功记录,而是一条内容 -明确为 `error` 的完整 tool Message。外部结果无法确认时必须写 `unknown`,不能猜成 -成功,也不能盲目重试。 +新增 image、audio、citation 等能力时扩展 typed content,不新增平行 Message 表,也不加 +通用 `meta/context/intent` 袋子。 -`call_ref` 不引入新的随机身份。它由发出 `tool_call` 的 -`(assistant message_id, content block index)` 得到。provider 自己要求的 token 只是 -协议内容,不能升级成 Core 的 ToolCallId。 +`responds_to` 不放进 `content[]`,因为它不是聊天正文,而是这条 Agent Message 结算了 +哪些 cause。它没有 block 顺序,不参与 citation span,也不会与渠道的原生 reply-to 混淆。 +默认只有 Agent Message 可以写 `responds_to`;Tool result 用 `resolves` 结算 exact +`tool_call`。所有 cause/ref 必须指向同一 Session 中更早、真实存在的 Message/block;重复、 +跨 Session、未来引用或错误 block 类型都在 append 边界 fail-loud,因此因果图天然无环。 -#### `reply` 是 Message 之间的因果确认 +### 1.3 持久 author 不是 provider role -head CAS 只证明输出读取过哪个 Session 前缀,不能证明它回答了其中哪几条输入。因此 -每个由 reactor/program 产生的 assistant `text`、`no_reply` 或 `tool_call` Message 都 -必须带一个或多个 `reply` block: +旧的 `role=system|user|assistant|tool` 把两个不同问题绑在了一起: ~~~text -reply { target_message_id } +谁产生了这条事实? +模型应当给它多高的指令权限? ~~~ -每个 block 只确认**明确写出的那一条** prior Message,不隐含“此前全部已处理”。同一 -assistant Message 可以用多个 block 表示合并回答: +v4 只持久化 author。模型请求 projection 再做映射: -~~~text -seq 1 user U1 -seq 2 user U2 -seq 3 assistant reply(U1), reply(U2), text(A) -~~~ +| Message author/content | provider request view | +|---|---| +| `Human` text/artifact | user input | +| `Agent` text/tool_call | assistant | +| `Agent` no_reply | 不产生 provider body;只保留已生成的 cause settlement | +| `Tool` tool_result | tool | +| `Source(plugin_key)` | untrusted task/context frame,绝不自动成为 system instruction | +| tombstone | 不进入模型请求 | -Session owner 必须验证 target 与新 Message 属于同一 Session、target `seq` 更小、没有 -重复或前向/跨 Session 引用,并且 target set 与短命 grant 完全相等。当前 reactor policy -是否允许反应由 grant issuer 在签发前判断,不复制进 Core。v4 初始合同只允许 assistant -role 携带 `reply`;它不是 UI 引用/引用回复的万能字段。这条边使用已有 `message_id`, -没有 AckId、ReactionId 或处理 cursor。 +system/developer instruction 只来自 Agent Program 拥有的 Prompt assembler。Scheduler、 +Wake 或第三方 source plugin 即使能向 Session 写 Source Message,也不能借 `system` role +绕过 Prompt 权限。 -`tool_result.call_ref` 是另一条类型化因果边:它只确认对应的 `tool_call`。如果 assistant -先用 `reply(U1), tool_call(...)` 推进 U1,恢复时先完成 unresolved tool;tool Message -落盘后,它又成为需要 Agent 回应的 cause。即使 terminal tool 不再请求模型, -`AGENT_PROGRAM` 也必须根据确定的 tool result 生成一个带 `reply(tool_message_id)` 的 -assistant `text` 或 `no_reply` Message,不能用消失的执行状态表示“结束”。 +### 1.4 `message_id` 与 `seq` 仍然都需要 -### 1.3 “Message 存在”究竟证明什么 +两者不是重复身份: -Message 存在只证明两件事: +| 字段 | 唯一职责 | +|---|---| +| `message_id` | 识别“是不是同一条 Message”、幂等重试和引用 | +| `seq` | 表示一个 Session 内的提交顺序和客户端 cursor | -1. producer 已经产生一个完整 Message; -2. Session WAL 已经持久接纳它。 +如果只用 seq,ACK 丢失的 producer 在 commit 前不知道最终序号,无法证明重试的是同一条 +Message。如果只用 message_id,客户端仍不知道顺序和缺口。 -它不自动证明别的事情: +但 canonical `message_id` 不能直接信任外部客户端。可信 Channel boundary 必须: -- assistant Message 存在,不代表手机或邮件已经收到它; -- `tool_call` 存在,不代表工具成功; -- `tool_result(outcome=success)` 才表示工具 owner 确认成功; -- `tool_result(outcome=unknown)` 表示外部效果可能发生,但现在无法确认。 +- 从已认证 account/device/session namespace 与 provider inbound reference 确定性映射;或 +- 在发送前向客户端签发/验证属于该 namespace 的最终 ID。 -这样“有没有说出来”和“外部事情有没有做成”是两条正交事实,不再由一个 Run -状态含糊地同时代表。 +外部 nonce/provider ref 只是 Channel staging 的传输输入,不进入 conversation schema, +也不恢复 `client_message_id + message_id` 双重身份。 -## 二、为什么仍需要 message_id 和 seq +Agent/Tool/Source/Tombstone 路径则由对应可信 writer 在完整内容 sealed 后、append 前一次性 +mint server-namespace message_id;append ACK 丢失时复用它,进程在 append 前崩溃则什么 +事实都没有留下。 -这不是两个 Message 身份,而是两个不同问题的答案: +初始 v4 每条 Agent Message 最多包含一个 tool_call,因此那条 Message 的 `message_id` 就是 +调用身份。Tool result 直接 `resolves: call_message_id`,不再增加 ToolCallId、`call_ref` 或 +block-index identity。将来若要并行 tool_call,应先证明值得扩大这个合同。 -| 字段 | 回答什么 | 何时得到 | -|---|---|---| -| `message_id` | 这是不是同一条 Message | append 前 | -| `seq` | 它在这个 Session 的第几个位置 | commit 时 | +## 二、唯一写协议 -只用 `seq` 会遇到一个无法消失的问题:客户端发送 U1,服务端可能已经提交,但 ACK -在网络中丢了。客户端重发时还不知道 U1 的 `seq`。如果没有预先存在的稳定身份, -服务端无法区分“同一条 U1 重试”和“又说了一次 U1”。 +### 2.1 输入先写 WAL,再开始 Agent -因此正确的减法不是删除 `message_id`,而是让它成为唯一身份: +任何被接纳的 Human 输入都立即 append: -- 客户端创建完整 user Message 时生成 `message_id`;重试始终复用它; -- 外部 Channel 有稳定 provider message ID 时由 adapter 确定性映射;没有时,adapter 在 - pre-WAL staging 先持久分配同一个 `message_id`。若 provider 既无身份又会任意重投, - 系统必须承认无法保证入站 exactly-once,不能再发明第二个业务 ID 掩盖它; -- 模型或工具产生完整内容以后、append 以前生成 `message_id`; -- 删除 `client_message_id`、`retry_of_client_message_id`、TurnId、RunId、StepId 和 - Core DeliveryId; -- 老数据已有的 message ID 原样保留并视为不透明值,不建立 alias 或映射系统; -- 新 ID 的具体编码只是实现选择,合同只要求唯一、稳定、不可从业务含义推断。 +~~~text +Channel 收到 U1 + │ 认证、附件 durable、canonical message_id + ▼ +append Human U1 ── durable ──▶ ACK inbound + │ + └──▶ 唤醒普通插件 +~~~ + +因此 `U1 → interrupt → U2 → interrupt → A1` 的 Session 从来不会暂存 U1/U2 到某个 +Attempt row,最后再批量复制正文。interrupt 只取消当前内存 Reaction;U1/U2 已经是事实, +不能因为模型没答完而丢失。 -如果改成 `(session_id, seq)` 派生 `message_id`,仍要再造一个 pre-commit retry key。 -那会把一个身份重新拆成两个,反而更复杂。 +这里 interrupt 只表示“停掉这张草稿纸”,不表示“永远不要回答这条 Message”。后者是 +持久产品意图,必须由一条被接纳的 Message 表达,再由行为插件依照 Turn policy 结算, +不能藏在已消失的 cancel flag 里。 -## 三、唯一写协议 +这会有意替换当前 SES-001、SES-007、SES-008 和 decision 0025 中“最终 A 出现时再批量 +提交完整 transcript”的合同。旧合同是迁移原点,不是 v4 的正确性来源;只有 Phase 0 +批准并勘误长期条款后才能实现。 -### 3.1 Append 是 commit 点 +写入事务保持最小: -所有 producer 都走同一条路: +1. 验证 typed writer 与 Message 结构; +2. 若同 `message_id` 已存在,比较不可变内容:完全相同则返回原 receipt,不同则 + fail-loud; +3. 检查该 writer 的 typed precondition; +4. 分配下一个 seq 并 INSERT; +5. commit 后发布 feed。observer 失败不能回滚 Message。 + +### 2.2 Cause CAS 是基础;head CAS 只是可选隔离级别 + +先把不可变事实和可替换 policy 分开: ~~~text -在内存中产生完整内容 - │ - ▼ -封好不可变内容,并分配 message_id - │ - ▼ -Session.append(message, expected_head_seq?) - │ - ├── 校验 Message 与原子前置条件 - ├── 分配 seq - ├── durable commit - └── commit 后才 ACK / 发布给 projection +Settled(m, S) = 存在 committed Agent Message a,且 m ∈ a.responds_to + +CauseOpen(m, S) = + m 存在于 S and not Settled(m, S) and not Hidden(m, S) + +PendingCause_T(S) = { + m | TurnPolicy_T.Reactable(m, S) and CauseOpen(m, S) +} ~~~ -WAL 必须保证: +`CauseOpen` 是 Session owner 能在 append 临界区检查的结构事实;`Reactable` 和怎样把 open +cause 分组属于可替换 Turn plugin。这样换 Turn policy 不会把任意插件代码塞进 WAL 事务。 -1. `message_id` 唯一; -2. 同一 Session 的 `seq` 唯一且连续; -3. 同 ID、同完整内容重试时返回原 `seq`,不再追加; -4. 同 ID、不同内容时 fail-loud,不能覆盖或悄悄归一化; -5. ACK 只能发生在 durable commit 以后; -6. projection 失败不能把已提交 Message 变回未提交。 +Agent 输出 A 针对明确 cause set C 时,基础提交条件是: -append 前的内容与 ID 只是调用参数和内存值;只有 commit 返回的带 `seq` 记录才是 -Session 中的 Message。Message 引用的 artifact 必须已经 durable/ready,不能先写半条 -Message 再补附件。 +~~~text +CanAppend(A, C, S) = + C 是 writer 绑定的 exact cause set + and C 中每个 Message 仍满足 CauseOpen(m, S) + and writer authority 仍有效 + and 可选 read condition 仍成立 +~~~ + +AgentReplyWriter 只能由受保护 factory 针对 `PendingCause_T` 中一个 projected group 的 +**全部 open causes** 签发; +普通插件不能自己拼 cause ID。Session 任意 append 不再自动使 A 失效。调用它的行为插件 +再按工作语义选择一个很小的 typed read condition: -Message 正常路径不可变。唯一例外是用户明确删除后的受控正文擦除,见第九节。 +| 条件 | 用途 | +|---|---| +| 无额外 read condition | command、独立 cause;只使用必选 CauseOpen CAS | +| `NoNewHumanInputAfter(source_seq)` | 默认被动回答;新 Human 对话输入加入本组,tombstone/Source/主动 Agent 不干扰 | +| `HeadEquals(source_seq)` | 极少数确实要求读取最新完整 Session 前缀的算法 | + +这些条件由 Session owner 在同一个 append 临界区检查,不持久化 token/revision。首版只 +提供封闭的 typed 条件,不接受插件传任意 predicate 或可变配置袋。 +`HumanInput` 由 author + conversation content 的结构类型判断,不把 Human tombstone 当成 +新对话输入。 -### 3.2 模型重试发生在 append 以前 +#### U2 会打断旧草稿,proactive P 不会 ~~~text -read Session at head H - │ - ▼ -provider 请求 / 断网 / 重试 / token stream ← Message 内容只在内存 - │ - ▼ -完整输出成为 assistant Message M - │ - ▼ -append(M, expected_head_seq=H) +seq 1 Human U1 + Reaction R0 读取到 source_seq=1,writer 条件 NoNewHumanInputAfter(1) + +seq 2 Human U2 + R0 append A-old ──▶ condition conflict;A-old 丢弃 + + Reaction R1 causes={U1,U2},source_seq=2 +seq 3 Agent P,独立主动消息 + R1 append A1(responds_to U1,U2) ──▶ success +seq 4 Agent A1 ~~~ -- provider 断网:丢掉未完成输出,重试;Session 不变。 -- token 只生成一半:可以直播给当前界面,但不能 append;崩溃后丢掉。 -- 重试耗尽:产品可以产生一条完整 error Message 再 append;若没有产生 Message, - Session 就不声称助手说过什么。 -- WAL commit 成功但 ACK 丢失:用同一个 `message_id` 重试 append,得到原 `seq`。 +如果 A-old 的事务先于 U2 commit,它就是对 U1 的合法完整回答,随后 U2 成为新 pending +cause;如果 U2 先 commit,A-old 必须失败。这是唯一需要定义的竞态顺序。 -这里不需要 durable attempt、Run 或 Step。若计费和排障需要看失败尝试,telemetry -只记录时间、错误码、用量和 provider request ID 等运行数据,不持久复制未提交正文、 -tool 参数或结果。telemetry 不能决定对话事实,也不能反向补写 Session。 +feed 收到 P 可以触发一次无害 reconcile,但 `passive-conversation` 只在 projected cause +group 改变时取消当前 Reaction;P 没有改变 `{U1,U2}`,所以无需判断 `is_proactive`,也不 +取消 R1。 -### 3.3 新输入打断旧输出 +### 2.3 不公开通用 `MessageWriteGrant` -假设模型正在根据 U1 生成答案,Session head 是 1;这时 U2 先提交成 seq 2: +一个不断增加 `role/mode/variant/cause/call/expiry` 字段的通用 grant 会变成权限配置袋。 +v4 让类型直接表达用途: ~~~text -seq 1 U1 ──▶ 生成旧草稿 A-old -seq 2 U2 - append(A-old, expected_head_seq=1) ──▶ conflict - 丢掉 A-old,读取 U1 + U2,重新生成 A -seq 3 A +InboundWriter.append_human(content) # exact Session + authenticated boundary +AgentReplyWriter.append(content) # exact Session + causes + read condition + Root +ToolResultWriter.complete(outcome, output) # exact call Message +SourceWriter.append(content) # exact Session + Source(plugin_key) +StandaloneAgentWriter.append(content) # exact source job + target Session, no causes +TombstoneWriter.hide(seed_message_ids) # exact user-approved management request ~~~ -`expected_head_seq` 就是足够小的并发栅栏。两个 worker 同时从同一个 head 生成答案, -也只有第一个能提交;另一个看到 conflict 后丢弃结果。无需保存“现在是哪一个 Run”。 +每个 writer 都是不可伪造、不可序列化、短命且窄用途的 object capability: -### 3.4 append 必须带短命写权限 +- 调用者不能再传 `author`、另一个 Session、另一个 cause set 或另一个 call Message; +- `AgentReplyWriter` 的 `responds_to` 自动等于 exact causes,不能只消费宽授权的一部分; +- `ToolResultWriter` 只能完成一个 call;相同结果幂等,冲突结果 fail-loud; +- `SourceWriter` 只能写绑定 plugin_key 的低信任输入;`StandaloneAgentWriter` 只能为绑定 + source job 写无 cause 的完整 Agent Message; +- `TombstoneWriter` 只能写纯 tombstone Message,不能混入聊天正文; +- 底层仍只有一个 Session WAL owner 和一个 internal append,不形成多个事实 owner。 -`role` 会改变模型对内容的信任,不能让任意插件自己填字符串。`SESSION_APPEND` 不暴露 -裸 `SessionStore`,只签发一个不可伪造、不可序列化、用后即失效的 -`MessageWriteGrant`。它不是 Message 字段或第三种事实,没有稳定 ID;它只是一枚运行时 -object capability。 +### 2.4 短命 receipt 不会创造第二份真相 -每份 grant 至少绑定: +“durable 结果只看 WAL”不等于所有函数都必须返回 `None`。调用者和测试可以得到只引用 +既有事实的短命 receipt: ~~~text -session_id -caller = 由 runtime scope 绑定的 exact plugin Root generation 或具体 host adapter -allowed role + allowed content variants -required reply target set / exact call_ref -observed_head_seq 或另一项 typed atomic precondition -expiry + one-shot append budget +AppendReceipt(message_id, seq, already_existed) + +ReactionReceipt = + Appended(message_id) + | Conflict + +ToolReceipt = + InFlight(call_message_id) + | Result(result_message_id, already_existed) ~~~ -`caller` 不是调用者自己传入的字符串;composition host 从当前 scope 注入。assistant -Message 的 `reply` target set 必须与 grant 绑定的 cause set **完全相等**。如果 reactor -决定分别回答 U1 和 U2,它必须先明确选择 `{U1}`,再为这一小组签一份 grant;不能拿 -`{U1, U2}` 的宽权限只确认其中一条。 +receipt 不落盘、不携带正文、不授权后续写入,也不能覆盖 WAL。丢掉它以后重新 fold +Session 仍得到相同答案。`InFlight` 只描述当前进程眼前的 task,随时可能过期;`Result` +只引用已经存在的 terminal Tool Message。ToolReceipt 不是可恢复 Tool 状态,恢复仍只 fold +call/result Message。 -谁能写什么由真实信任边界决定: +### 2.5 模型 retry 发生在输出 append 前 -| caller | 可签发的写入 | -|---|---| -| 已认证 Channel adapter | 当前 Session 的 `user` text/artifact;不能伪装 system/assistant/tool | -| `MESSAGE_REACTOR` / `AGENT_PROGRAM` | 当前 cause set 的 `assistant` reply/text/no_reply/tool_call | -| Tool executor | exact `call_ref` 的一个 `tool` result;不能写任意 assistant 或其他调用结果 | -| Scheduler/Wake/Drift 等 source plugin | 获授权 Session 的 typed `system` input;不能写 `user` | -| Data Management | 已预览 closure 的 `system delete`;普通插件拿不到 | -| 一次性 migration | 只在离线恢复流程写固定清单;runtime 永不复用 | +~~~text +读取 immutable Reaction view + │ + ├── provider 断网:重试,Session 不变 + ├── partial token:只给当前 UI preview,崩溃可丢 + └── 完整输出 A + │ + ▼ + AgentReplyWriter.append(A) +~~~ + +重试耗尽时,产品可以生成一条完整 Agent error Message;没有生成 Message,就不声称 +Agent 说过。commit 成功但 ACK 丢失时,用同一个 canonical `message_id` 重试并拿到原 seq。 -Session owner 在同一个 append 事务中只校验 grant、Message 结构、同 Session 因果边和 -CAS;“哪些输入应当一起回答”由 reactor 选择,再由 exact cause set grant 固定,Core 不 -复制这项产品策略。跨 Session、越权 role/content、错误 cause/call、过期或已撤销 -generation 都 fail-loud。generation 在外部 effect 完成后被撤销时,Tool owner 只能用 -受限 completion grant 记录 -`error/unknown`,不能借旧授权再次执行副作用。 +## 三、Turn 是普通插件拥有的 projection -## 四、工具调用也只是 Message +### 3.1 为什么保留 Turn 这个词 -### 4.1 正常调用 +用户、历史窗口、Akasha 和 UI 都需要“哪些 Message 属于一件完整事情”的视图。因此 +公开一个有真实多消费者的普通 capability: ~~~text -seq 10 assistant tool_call(search, {...}) - │ commit 后才能执行 - ▼ - 调用 / 查询 / 安全重试 - │ - ▼ -seq 11 tool tool_result(call_ref, success, {...}) -seq 12 assistant 根据结果回答 +turn-projection plugin + provides TURN_VIEW + injects SESSION_READ ~~~ -顺序是故意的:请求调用工具的 assistant Message 先写入 WAL,外部执行才开始。这样 -崩溃后总能从 `tool_call` 内容算出同一个 `call_ref`。 +`TURN_VIEW` 可以返回 `member_message_ids`、完成/等待的派生状态和 UI 所需顺序,但: -`tool_call` append 前,边界 adapter 必须按本次模型可见的 schema 校验名称与参数, -并封入 exact immutable `tool_binding`。执行时再检查该 binding 仍被当前权限允许;若 -已经撤权,产生 `outcome=error` 的 tool Message。模型看见什么工具是配置的投影, -能否真的调用由工具权限 owner 决定,不能从模型输出反推授权。 +- 不创建 Turn row、TurnId、open/seal/abort 状态; +- 不拥有 Message commit; +- 不被 retry、Tool 或 Delivery 当作寻址身份; +- 缓存只能带 `source_seq + projection_version`,可删除、可重建; +- UI 需要 key 时复用 root/first `message_id`,不生成 ProjectionId。 -工具 owner 的规则: +这让 Turn 插件可以真正“自己定义 Turn”。替换它会改变分组、上下文切点和学习样本, +但不会迁移 Message。 + +### 3.2 默认分组使用因果图,不使用连续行 + +默认 Turn projection 使用两类不可变边: + +~~~text +cause Message ──responds_to──▶ Agent Message +tool_call block ──resolves───▶ Tool result Message +~~~ -1. 同一时刻只启动一个该 `call_ref` 的本地 executor;这是可丢失的调度约束,不是 - durable 对话状态; -2. 把 `call_ref` 用作 provider 幂等键; -3. 网络错误时先按 provider 的 query/idempotency 能力确认,再决定是否重试; -4. 得到确定结果以后,产生一个完整 tool Message,再 append; -5. tool Message 的同一 `call_ref` 只能有一个终态结果;重复相同结果幂等成功,冲突 - 结果 fail-loud; -6. 最终 assistant Message 重新读取包含 tool result 的最新 Session,再用 head CAS - 提交。 +它先形成因果连通分量,再应用以下默认 policy: -### 4.2 崩溃与未知外部效果 +默认 `Reactable` 只有 Human conversation input、Source input 和 terminal Tool result;Agent +输出与 tombstone 不会仅因“排在最后”而自动变成新 cause。 -`tool_call` 已提交而 tool result 不存在时,“pending”只是从 WAL 算出的视图。 +1. 当前尚未被回答的有序 Human Message 组成一个 pending human group;interrupt 不切断它; +2. Agent A 明确 `responds_to(U1,U2,...)` 后,A 与这些 Human cause 属于同一 Turn; +3. Agent tool_call、对应 Tool result、继续回答通过边的传递闭包归入同一 Turn; +4. 一个 Human Turn 尚有 unresolved call 时,后来进入的 Human input 先成为该 Turn 的 + deferred open cause;TURN_VIEW 只标记 deferred,`passive-conversation` 在 Tool result + 到达前不得为它单独申请 Reaction; +5. Source 触发的 Agent 工作单独成 Turn,不与同时未完成的 Human group 混合; +6. 没有 Human cause 的 standalone Agent Message,例如直接 proactive/message_push 输出, + 自己成为一个 Turn; +7. tombstone 等治理 Message 不成为聊天 Turn。 -- provider 能按 `call_ref` 查询:查询真实结果,产生 tool Message; -- provider 支持相同幂等键:可以安全恢复调用; -- 两者都不支持:不能盲目再执行,产生 `outcome=unknown` 的 tool Message。 +因此 Turn member 不要求 seq 连续: -这承认一个不能被数据模型消灭的边界:Session 回滚不了已经发出的付款、邮件或 Git -操作。正确做法是保留调用意图、复用一个幂等地址并诚实记录 `unknown`,不是再加一套 -Run/effect 状态机假装获得 exactly-once。 +~~~text +seq 10 Human U1 ┐ +seq 11 Human U2 ├── Turn H +seq 12 Agent P │ Turn P = {P} +seq 13 Agent A1 ┘ responds_to(U1,U2) +~~~ -### 4.3 正常执行和崩溃恢复共用一个 Tool 入口 +这里 Turn H 是 `{seq10, seq11, seq13}`。投影不能因为 seq12 插入就拆开 H,也不能因为 H +跨三次短命 Reaction 就增加 Attempt identity。 -已提交的 `tool_call` 不能依赖“碰巧仍活着的 Agent Program”才能完成。Tool runtime 通过 -普通 composition 提供一项受保护的窄服务: +Tool 也不制造第二种 Turn 规则。默认插件可以把等待 Tool 期间到来的新 Human cause 接到 +同一条因果链: ~~~text -TOOL_EXECUTOR.execute_or_recover(call_ref) -> None +seq 20 Human U1 ┐ +seq 21 Agent M1 responds_to(U1), tool_call C1 │ +seq 22 Human U2 │ Turn H +seq 23 Agent P Turn P │ +seq 24 Tool T1 resolves(C1) │ +seq 25 Agent A2 responds_to(T1,U2) ┘ ~~~ -它没有 execution ID,也不返回另一份结果对象:成功、失败或不确定的唯一可观察结果, -仍是 exact `call_ref` 的一条 tool Message。`session_id`、参数和 exact binding 都从 -`call_ref` 指向的既有 assistant Message 读取,不让调用者再传一份可能漂移的副本。它 -同时承担两条入口: +这里 M1 已结算 U1;最终 continuation 只结算仍 open 的 T1 与 U2,传递闭包仍得到 +`{U1,M1,U2,T1,A2}`。如果另一个 Turn plugin 选择把 U2 分开,它可以改变这个 projection, +但不能改 WAL 或伪造另一条 Message。 + +中间快照必须明确:到 seq22 时 group 是 +`members={U1,M1,U2}, unresolved={C1}, deferred={U2}`,所以不签发 `Reaction(U2)`;seq24 +到达后才得到 `open_causes={T1,U2}` 并签发唯一 continuation。否则先写 A(U2) 会把 U2 +结算掉,破坏上图承诺的默认 grouping。 -1. `AGENT_PROGRAM` 提交 `tool_call` 后立即调用; -2. Tool runtime 的普通 Effect 在启动时扫描、运行时订阅 committed Message feed,对每个 - 未删除且没有终态 result 的 call 调用同一接口。 +### 3.3 Turn 插件只 fold 视图,不执行 action -两条入口可以重复到达,但 exact `call_ref` 的本地互斥、provider query/idempotency 与 -result append 幂等会把它们收敛成一次结果。执行前必须重新校验 unresolved 状态、exact -binding、当前授权和 delete projection;随后取得 binding generation retention lease, -在真实副作用边界查询或执行,并用 Tool completion grant append 结果。 +Turn 插件对完整 WAL 计算: -停用 `MESSAGE_REACTOR` 或 `AGENT_PROGRAM` 只是不再生成新的回复,不能遗弃已经 committed -的 `tool_call`;Tool recovery Effect 仍会把它结算,之后的 result 留在 WAL,等待未来的 -reactor。若管理员要停用 Tool runtime,candidate 必须先拒绝新的 tool call,并 drain -所有 unresolved call。exact generation 意外丢失时写 `unknown` 并报 incident,不能换用 -一个“差不多”的新 generation,更不能盲重试。 +1. 先收集全部 `responds_to` 与 tool `resolves` settlement 边; +2. 再应用 tombstone 得到 Hidden set;隐藏旧回复不能让已结算 cause 复活; +3. 标出未结算 tool_call 及等待它的 Turn; +4. 按 policy 投影 exact pending cause groups;默认不混合 Human 与 Source group; +5. 没有 pending cause 或 call 的 Turn 标为 complete。 -### 4.4 删除与外部副作用共用一个短命闸门 +projection 不决定哪个 group 先执行,不取消 Reaction,不执行 Tool、不调用模型、不 append。 +`passive-conversation` 等行为插件只能从完整 projected group 申请 Reaction;ToolHost 则直接从 +WAL 找 unresolved call。重复 fold 必须得到相同结果。 -工具和 Delivery 在真正开始外部 I/O 前,都必须取得该 Session 的短命 shared effect -lease;Data Management append delete 前取得同一闸门的 exclusive lease。lease 只存在于 -内存,没有 ID、表或恢复语义,因此不是第三种对话事实。 +#### 分组不改写时间顺序 -shared 路径在 lease 内重新读取最新 delete graph。若目标已删除,工具拒绝开始,Delivery -直接进入 `suppressed`;否则工具保持 lease 直到 append result/`unknown`,Delivery 保持 -lease 直到 durable `delivered/rejected/unknown`。超时或取消也必须先查询并写出 -`unknown`,不能释放后假装副作用没有发生。 +`TURN_VIEW` 返回的只是 `root_message_id → member_message_ids[]`。root 复用该组最早的 +Message ID,不是新的 TurnId。WAL 顺序仍只由 seq 决定:时间线 UI 仍按 `U1,U2,P,A1` +显示,并给 U1/U2/A1 标相同 group;它不能为了做一张连续 Turn card,把 A1 移到 P 前面。 +需要整组样本的 Akasha/compaction 可以按 member IDs 收集非连续成员。 + +### 3.4 其他全部是 Message projection + +| projection | 从 Session Message 得到什么 | +|---|---| +| Chat | 按 seq 的可见内容与 Turn group 标记 | +| Model context | author/content 到 provider request 的有界映射 | +| Tool status | call + result 得到 pending/success/error/unknown | +| Web/Mobile | `seq > cursor` 的 Message 与按需 Turn view | +| Memory/Search/Akasha | 按 TURN_VIEW 与允许学习规则建立派生项 | +| Live stream | 尚未 commit 的内存 preview;刷新可丢 | -exclusive 路径会阻止新的 shared lease,并等待已经开始的 effect 得到 result 或 -`unknown`;然后从最新 head 重算 closure、append delete,再释放。于是只有两个结果: +客户端最小同步仍是: ~~~text -effect 先取得 shared lease ──▶ delete 等它结算,再把结果纳入 closure -delete 先取得 exclusive lease ──▶ 新 effect 看见 delete,永不开始 +request: session_id, after_seq +response: Message[] +cursor: 最后完整应用的 seq ~~~ -进程崩溃会丢掉 lease,所以启动顺序必须固定:先从完整 WAL 重建 delete/tool settlement -projection 和 generation lease,再启动 Tool/Delivery worker。恢复仍只依赖既有 Message、 -provider query/idempotency 和 `unknown`,不会恢复一个 Run。 +客户端用 message_id 去重、seq 排序。Turn plugin 是分组规则的唯一 owner;服务端按 +`source_seq + projection_version` 返回可丢的 group membership。客户端离线时退化为纯 seq +时间线,不复制一套可能漂移的 Turn 算法,也不要求另一条权威 cursor。 -## 五、旧 Turn 和 Run 去哪里 +## 四、被动回复大链变成普通插件组合 -它们都不进入新领域模型、schema、公共 API 或持久化合同。 +### 4.1 当前真正需要替换的固定链 -如果聊天 UI 想把若干行圈成“一次交互”,可以临时画括号: +当前基线仍是: ~~~text -┌─ 对话分组视图 ───────────────────────┐ -│ seq 1 user U1 │ -│ seq 2 user U2 │ -│ seq 3 assistant tool_call │ -│ seq 4 tool success │ -│ seq 5 assistant A │ -└──────────────────────────────────────┘ +PassiveMessageWorker + ├── inbound custody / attachment / per-session lane + ▼ +ConversationRuntime + ▼ +AgentLoop._react() + ▼ +PassiveTurnPipeline + ├── command short-circuit + ├── BeforeTurn / BeforeReasoning + ├── reasoner + BeforeStep / AfterStep + ├── AfterReasoning:parse + persistence + outbound + └── AfterTurn:event + dispatch / ACK ~~~ -这个括号是 projection,不叫 Turn 对象,没有 ID,没有 open/seal/abort 状态,也不被 -重试、删除、计费或插件引用。分组算法升级后重新画即可。 +代码证据: -同样,开发者界面可以把当前 token、provider 延迟和重试次数画成“执行中”视图;它读 -内存与 telemetry,不叫 Run,也没有恢复业务语义。进程重启后这个视图消失是允许的。 +- `bootstrap/passive_worker.py:96`:worker 拥有准入、lane 与结果 task; +- `agent/looping/core.py:556`:`AgentLoop._react()` 转入固定 pipeline; +- `agent/core/passive_turn.py:355`:固定四段 phase; +- `agent/core/passive_turn.py:440`:command 专门短路; +- `agent/core/passive_turn.py:524`:默认被动回复由固定 `run()` 统管。 -本设计明确接受以下损失: +只换成 Message WAL 而保留这条大链,仍然不能替换 Agent 算法,也会继续给 Core 加 +Citation、Meme、Tool Search、proactive 等分支。 -- 无法从 Session 重建失败过几次 provider 请求; -- 无法恢复半截 token stream; -- 无法给一次执行尝试分配稳定身份; -- 无法承诺 UI 的对话分组永远不变。 +### 4.2 目标拓扑 -这些都不是用户或 Agent 已经说出的 Message,因此不值得污染权威模型。 +~~~text +turn-projection plugin + provides TURN_VIEW + +passive-conversation plugin + owns startup reconcile + SESSION_FEED subscription + private reconcile(session_id) + injects TURN_VIEW, COMMANDS, AGENT_PROGRAM, REACTION_FACTORY -### 5.1 用 hua-home 历史反推,而不是照抄现状 +default-agent plugin + provides AGENT_PROGRAM + injects PROMPT_PARTS, CONTEXT_VIEW, TOOL_SELECTOR, CHAT_MODELS -检查过的私有历史只提供反例场景,原始内容不进入本文,旧字段也不定义目标模型: +protected substrate + Session WAL + typed writers + ReactionFactory + exact Root lease + ToolHost + effect-start gate + Channel ingress + Delivery effect port +~~~ -- **补充输入**:U1 后生成尚未完成,用户又发 U2。目标态是 `U1, U2, A`;旧草稿 - 因 head 冲突丢弃,不留下 interrupt/attempt 记录。 -- **provider 重试**:一次回复前出现连接错误,后来生成 A。目标态仍是 `U1, A`; - 错误只进短期 telemetry。 -- **Wake 检查**:`no_due` 没有进入对话的内容,因此不写 Message;已经产生输入后, - 模型无话可说则写 `[no_reply]`,有话可说则写普通 assistant text。 -- **tool 不确定**:外部调用 ACK 丢失时,目标态从已提交 `tool_call` 恢复并查询;不能 - 确认时写 `tool_result(unknown)`,不把旧 Run 状态当答案。 +不再公开 `MESSAGE_REACTOR`。它只有 `passive-conversation` 自己一个 consumer,没有独立 +owner 或生命周期,所以只是插件内部函数。真正可替换的公共行为边界只有: -这些 case 证明需要的是 WAL、幂等和 CAS,不是 Turn/Run 实体。当前行为若与此冲突, -它是 migration delta,不是保留旧设计的理由。 +~~~text +AGENT_PROGRAM.react(reaction) -> ReactionReceipt +~~~ -## 六、proactive 不再是特判 +### 4.3 Reaction 只是短命 capability,不是 Run -Core 不认识 `proactive`、Wake、Scheduler 或某个插件名。 +`passive-conversation` 从 TURN_VIEW 选择 exact causes 后,让受保护 factory 创建: ~~~text -scheduler 检查 no_due ──▶ 不产生 Message -scheduler 判断需要处理 ──▶ 产生普通 system Message -agent 判断 quiet ──▶ assistant Message [no_reply] -agent 生成内容 ──▶ 普通 assistant Message [text/...] +Reaction { + causes: immutable MessageView[] + history: immutable SessionView(source_seq) + model: scoped ModelPort + tools: scoped ToolPort + reply: AgentReplyWriter + stream: StreamPreview + cancellation + resources +} ~~~ -所以主动说话和用户问答走完全相同的 append、模型上下文、客户端同步与投递路径。 -没有 `is_proactive` 字段,Core 也没有 proactive 状态机。 +Reaction 没有 ID、表、序列化、恢复或状态机;函数结束就失效。它只是把当前调用可用的 +几扇小门绑在一起: -一旦输入 Message 已经进入 Session,模型产生的 quiet 判断也必须成为完整 assistant -Message:`content=[no_reply]`。Chat projection 隐藏它,恢复 projection 用它判断此前 -缀已经处理。否则重启只看到一条没有后续的输入,会无限重复同一次判断。这里记录的 -不是一个空 Turn,而是 Agent 确实生成的结果。 +- exact Root lease 在一次 `react()` 和其中的模型 retry 内不变; +- Agent Program 看不到任意 Session repository、裸 SQL 或通用 append; +- `reply` 已绑定 exact Session、cause set、author、read condition 和一次性预算; +- `tools` 只能把已 commit 的 call Message ID 交给受保护 ToolHost; +- 崩溃后重新 fold WAL,用新 Reaction 恢复,不恢复旧 Reaction。 -Scheduler 可以在插件边界拥有自己的时间表和 due cursor;那是调度器的运行配置,不是 -第三种对话事实。若“由计划任务触发”本身必须进入对话历史,就由 producer 把它写成 -普通、类型明确的 Message 内容,而不是给 Core 增加来源分支。 +一次 Reaction 最多 append 一条 Agent Message。若它包含 tool_call,Reaction 在 commit 后 +结束;ToolHost 独立结算并 append Tool result,feed 再为该 result 创建一张新 Reaction。 +所以 continuation 靠 Message 因果边恢复,不靠跨 Tool 等待存活的 Run 或 Reaction;新 +Reaction 可以在升级后使用新的 Root,已提交 call 的 exact Tool binding 仍保持不变。 -### 6.1 去掉 Core 特判,不等于删除 source plugin 的事实 +### 4.4 `SESSION_FEED` 只唤醒,但启动必须无缺口 -Session/Message 只统一**进入对话以后**的语义。Scheduler、Wake、Drift 和 Channel 在 -决定是否产生 Message 以前,各自仍有不能从 Session 反推的配置与连续性事实;它们由 -普通插件拥有,不复制对话正文,也不升级成 Session 实体: +仅订阅未来 callback 会漏掉“Message commit 后、subscriber 收到前进程崩溃”的 Session。 +每个必须追赶 WAL 的 owner 都使用同一个 snapshot-to-feed handoff: -| source owner | 自己保留的 durable state | 进入对话的唯一边界 | 结算与恢复 | -|---|---|---|---| -| Channel adapter | inbound provider ref/handoff、immutable `SessionRoute`、临时 pre-WAL payload | 已认证输入 append `user` Message | Message durable 后 ACK;回复与 ACK 解耦;按同一 `message_id` 恢复 | -| Scheduler plugin | `schedules.json` job、enabled/next fire/missed-tick;candidate 原子保存 | due 时 append typed `system` Message;`no_due` 不写 | stop 回收 tick;cancel 才物理移除 job;损坏不能当空任务集 | -| EventMail plugin | Content/Alert/Context immutable envelope 与 transition | 被 Wake/Agent 选中的 material 变成 system `text/artifact` Message | supersede/expiry/selection/ack/settlement 仍在 EventMail;不能共用万能 status | -| Wake plugin | watermark、实际 tick receipt、reservoir、hazard timer、pending ACK、dedupe/cooldown | due 时 append system Message;模型 skip append assistant `no_reply` | 只有 Delivery delivered 后推进 ACK/dedupe/cooldown;unknown 保持可恢复 | -| Drift plugin | `drift/drift.db` cursor、journal、continuum 和下一轮选择 | 需要 Agent 时 append system Message | cursor/journal 按插件合同恢复;日志 retention 不得删连续性行 | -| `message_push` adapter | 不拥有正文或第二个 queue message | 只接受已 committed assistant `message_id` 交给 Delivery | 旧 raw-body caller 必须先通过 system cause → reactor 产生 Message | -| subagent plugin | 自己的受控 job/process continuity(若确有外部效果) | parent assistant `tool_call` → subagent → `tool_result` | 按 `call_ref` 恢复;不能建立子 Turn 消息载体 | - -`proactive.db`、`wake_proactive.db`、`drift/drift.db`、`schedules.json`、 -`proactive_quota.json`、既有 pending/ACK/hazard/reservoir state 和 EventMail transitions -都必须在 Phase 1 逐项标成 preserve、replace 或 retire。owner handoff 完成前继续阻止新 -插件 activation;不能因为 Core 不再认识 proactive 就清库。普通插件最终只从通用 -`TIMERS`、`SESSION_APPEND`、Tool 与 Delivery ports 获得能力。 - -这份清单来自当前 `docs/projectneed.md:834` 的 Scheduler 合同、 -`docs/projectneed.md:841` 起的 PRO 合同,以及 -`docs/design/persistence-state-map.md:95` 起的真实持久状态清单。它们约束迁移不能丢数据, -但其中的 Turn/attempt 说法仍要由 Phase 0 的新合同明确替换。 - -## 七、其余对话状态全部是视图 - -“全部是视图”说的是**持久对话状态**:除了 Session 与 Message,不再保存一个平行 -状态来解释对话进行到哪里。插件行为本身不是 projection,也不是事实;它是读取 -Message、调用 capability、再产生 Message 的短命函数。 - -| 视图 | 从 Session Message 得到什么 | -|---|---| -| Chat | 选择 user/assistant text,应用 delete | -| Model context | 映射 role/content,裁切只发生在本次请求 | -| 对话分组 | 为 UI 临时圈住相邻 Message | -| Tool status | `tool_call` 加对应 `tool_result` 得到 pending/success/error/unknown | -| Next action | fold reply/call edges,得到 unresolved tool、unacknowledged cause 或 idle | -| Web/Mobile | 按 `seq > cursor` 返回 Message | -| Memory/Search | 从允许学习的 Message 建索引 | -| Live stream | 展示尚未提交的内存输出;刷新或崩溃可丢失 | +~~~text +1. 打开 subscription,取得短命 feed watermark W +2. 在一致 snapshot 中枚举截至 W 的 Session heads +3. 对每个 Session fold/reconcile +4. 消费 W 之后的通知;扫描与通知重复时按 message_id/因果 fold 去重 +5. 追到当前 head 后才把该 consumer 标为 ready +~~~ -projection 可以缓存 `(session_id, projection_version, source_seq, value)`,但缓存必须: +watermark 是 feed 实现游标,不进入 Session schema,也不成为恢复身份。崩溃后从头重复 +上述 handoff。`passive-conversation`、ToolHost recovery 和 Delivery projector 都必须遵守; +feed 丢一条唤醒不能让已 committed Message 永久无人处理。 +若 subscription buffer 溢出或 watermark 不再可读,consumer 必须放弃 ready、从新 W 重做 +handoff,不能用“可能追上了”继续运行。 -- 可删除、可重建; -- 只读到某个 `source_seq`,不能宣称看见未来; -- 不获得覆盖、重排或补造 Message 的能力; -- 版本不匹配时重建,不迁移成第二份事实; -- 上下文裁切、摘要和 token budget 永不 UPDATE/DELETE Session Message。 +### 4.5 固定 phase 的能力去向 -客户端协议只需要: +| 当前固定行为 | v4 owner | +|---|---| +| channel envelope、附件导入 | Channel/Artifact adapter;artifact ready 后使用 InboundWriter | +| inbound handoff / ACK | Channel adapter;Human Message durable 后结算 | +| Turn 分组、pending causes | 普通 `turn-projection` 插件 | +| command catalog 与短路 | `passive-conversation` 私有 reconcile + `COMMANDS` | +| Session/history 准备 | immutable SessionView + `CONTEXT_VIEW` | +| policy、identity、task、context Prompt | Agent Program assembler + 封闭 `PROMPT_PARTS` slots | +| context 裁切、摘要、compaction retry | `CONTEXT_VIEW` / provider request projection | +| tool schema preload / Tool Search | `TOOL_SELECTOR`,只决定模型可见集合 | +| Tool 授权、唯一执行、恢复 | 受保护 `ToolHost` | +| Tool schema/query/execute | 普通 Tool 插件的 exact generation | +| 默认 ReAct、provider retry、empty reply、terminal policy | `AGENT_PROGRAM` | +| model/provider binding | 普通 model/provider 插件,通过 Reaction scope 冻结 | +| Citation | Agent Program 根据 Tool/Prompt 直接产生 typed citation content | +| Meme/媒体 | 普通 Tool 产生 artifact;Agent Program 决定是否写入最终 Message | +| Agent/Tool 写入、幂等、cause CAS | typed writers + Session WAL owner | +| Memory、Akasha、compaction 派生项 | committed Message/Turn observers | +| partial stream | `StreamPreview`,可丢且不能 append 半条 Message | +| error/no_reply | Agent Program 产生普通完整 Message | +| delivery/ACK | Delivery effect 与 source state 各自拥有,见第七节 | +| generation、取消、资源清理 | Root lease + Reaction resource scope | + +Command 若消费了一个 cause group,也必须通过同一个 AgentReplyWriter append text、artifact +或 no_reply;插件内部的 `handled=true`、日志或返回值不能把 Message 结算。带持久或外部 +副作用的 command 还必须调用拥有该状态的 typed domain/effect port,不能借 Turn projection +藏一套恢复状态。 + +### 4.6 删除通用 `ASSISTANT_TRANSFORMS` + +v4 不把旧 `PassiveTurnPipeline` 换皮成一串共享正文 mutator。最终输出只有: ~~~text -request: session_id, after_seq -response: Message[] -cursor: 最后完整应用的 seq +Agent Program 产生 final typed content + ──▶ validate + ──▶ AgentReplyWriter.append ~~~ -客户端用 `message_id` 去重和引用,用 `seq` 排序与追赶。projection 晚到、重复或重建 -都不能改变 WAL。 +Citation、Meme 或媒体必须通过 Prompt、Tool 和 typed content 显式进入 Agent Program 的 +决定;不允许插件 A 改文字、插件 B 改附件、插件 C 再声明必须排在 A 后面。append 后的 +observer 更无权回来改 Message。 -恢复时也不读 Run 状态。`Next action` projection 对整份 WAL 做一次因果 fold。它先从 -**完整历史**收集 reply 与 call/result settlement 边,再应用 delete manifest 决定哪些 -内容可见、哪些 call 不再可执行;隐藏一条旧 result 绝不能把已经结算的 call 变回 pending: +Prompt contribution 也不是靠 plugin_id、数字 priority 或词法排序解决语义冲突。首版只 +提供 assembler 定义的封闭层: -1. 校验并收集所有 `reply(target_message_id)` 与 `tool_result.call_ref` 边; -2. 有尚无终态 result、且未被 delete 覆盖的 `tool_call`,输出 `await_tool(call_ref)`; - Tool recovery Effect 调用 `TOOL_EXECUTOR.execute_or_recover(call_ref)`,projection 本身不执行 - 外部动作; -3. 否则选出尚未被 `reply` 明确认领、且按当前 reactor policy 可反应的 user/system/tool - Message;delete Message 永不触发回复,command 默认单独选择,普通连续输入可以 - 显式组成 cause set; -4. reactor 把这个 cause set 交给 command 或 Agent Program;产出的 assistant Message - 必须逐个写入对应的 `reply` block; -5. 没有 unresolved tool call 或 unacknowledged cause 才是 idle。 +~~~text +policy → identity → task → context +~~~ -被多个 feed callback 重复唤醒时,它们会算出同一个 pending set;只有一个输出能通过 -observed head CAS。失败者重新 fold,看到 reply 边后结束。`await_tool` 只会唤醒幂等的 -Tool executor;`MESSAGE_REACTOR` 自己返回等待,不偷做恢复。这里没有处理 cursor,也不 -从“最后一条是什么 role”猜是否完成。 +每个层的 owner、是否允许多 contribution 和合并规则由 Agent Program 明确声明;重复 +exclusive owner、循环依赖或无法解释的冲突在 candidate 阶段 fail-loud。 -两个容易出错的例子: +## 五、Tool:意图和结果是 Message,安全 owner 不是普通插件 -~~~text -# 合并回答:A 明确认领两条输入 -U1, U2, A(reply U1, reply U2) → idle +### 5.1 正常链路 -# U1 是 command,U2 是普通补充:不能把 command A 当成整个前缀的回答 -U1(command), U2, A-command(reply U1) → U2 pending -U1(command), U2, A-command(reply U1), A(reply U2) → idle +~~~text +Agent Message M10 + responds_to(U1,U2) + tool_call(search, args, exact binding) + │ commit 后 ToolHost 才能执行 + ▼ +Tool Message M11 + tool_result(resolves=M10, success, output) + │ + ▼ +Agent Message M12 + responds_to(M11) + text(final answer) ~~~ -如果 U2 在输出 append 前到来,CAS 先失败,再由 reactor 重新选择 cause set。读过 U2 -但只回答 U1 的 command 结果仍只写 `reply(U1)`,所以 U2 不会丢失。 +`outcome` 只有 `success | error | unknown`。Tool 调用可能已经发生但无法确认时必须写 +`unknown`,不能猜成功,也不能盲重试。 + +初始 v4 的 Agent Message 最多包含一个 tool_call,且同一 Turn 有 +unresolved call 时不启动 continuation。于是等待期间进入的 U2 可以在 result 到达后与 +该 result 一起成为下一张 Reaction 的全部 open causes,不需要为了并行 Tool 再引入 work +identity。未来若要并行 tool_call,必须先给出同组 late result 的 typed read condition, +不能退回全局 head CAS。 +provider 一次返回多个 call 时,Agent Program 在 append 前请求修正或报错;不能偷偷拆成 +多条已经“说过”的 Message。 + +### 5.2 Protected ToolHost 与普通 Tool plugin 分开 + +受保护 `ToolHost` 独占这些不变量: -## 八、投递是 Message 之后的外部效果 +- 从 call Message ID 读取原 Agent Message,验证唯一 tool_call 和 exact binding; +- 重新检查 Tool 权限;模型可见性不能扩大真实授权; +- 为 exact call 签发 `ToolResultWriter`; +- 保证一个 call 只有一个 terminal result; +- 管理 effect-start gate、运行中 generation lease、恢复扫描和 `unknown`; +- 决定旧 generation artifact 何时可以物理回收。 -canonical Web/Mobile 直接读 Session WAL。邮件、推送或第三方聊天渠道则由投递 worker -消费 committed assistant Message。它不能复制正文,也不能让发送结果决定 Message -是否存在;但 provider I/O 的确需要自己的 durable external-effect projection。 +普通 Tool 插件只拥有: -v4 初始合同规定:一个外部 Channel Session 绑定一个 immutable route;多收件人广播要 -拆成不同 Session/Message,不在一条 Message 下发明 DeliveryId。Channel plugin 在接纳 -首条 user Message 前持久保存: +- schema 与参数解析; +- 领域校验; +- provider query/idempotency; +- 实际执行与领域结果格式。 + +因此 Agent Program 使用 `reaction.tools.execute(call_message_id)`;它不能注入一个可由任意普通 +插件替换安全 owner 的全局 `TOOL_EXECUTOR`。替换 Tool plugin 可以改变领域能力,不能 +绕过 grant、binding、唯一结果或删除闸门。 + +### 5.3 即时执行和 crash recovery 是同一条 ToolHost 路径 + +两种唤醒都调用: ~~~text -SessionRoute[session_id] = { - channel/provider generation, - account + recipient, - inbound provider message reference mapping -} +ToolHost.execute_or_recover(call_message_id) -> ToolReceipt ~~~ -这里不保存 user/assistant 正文。入站 payload 若必须在 WAL append 前 crash-recover, -只能在 Channel-owned staging 中短暂保存;同一 `message_id` append 成功并 ACK 后就清除 -payload,只保留 provider reference 与 route。 +1. Agent Program commit tool_call 后立即请求; +2. ToolHost 按第 4.4 节启动扫描并订阅 feed,发现 unresolved call 后请求。 + +重复请求以 call Message ID、本地互斥和 result append 幂等收敛。provider 能 query 就先 query; +支持相同幂等键才可安全重试;两者都不支持就写 `unknown`。 + +停用 `passive-conversation` 或 `AGENT_PROGRAM` 不会遗弃 committed call。ToolHost 仍结算, +result 留在 WAL;以后启用的 Turn 插件会看到它。管理员停用某个 Tool generation 前必须 +先拒绝新 binding,并处理全部 WAL 引用。 -每条需要外发的 assistant Message 最多有一条记录,以原 `message_id` 作为唯一主键: +### 5.4 generation 的持久可达性来自 WAL + +内存 lease 只能保护当前进程正在执行的调用,不能保护“tool_call 已 commit、observer +尚未运行就崩溃”的窗口。generation GC 必须直接读取 WAL projection: ~~~text -DeliveryEffect[message_id] = { - source_seq, - immutable route/binding/reply reference, - message_digest, - state: prepared | provider_started | delivered | rejected | unknown | suppressed | settled, - provider_receipt? -} +CanRetire(g) = + 不存在任何尚无 terminal tool_result 的 call c, + 且 BindingGeneration(c) == g ~~~ -这是把现有能力换 owner,不是删除恢复保证:当前 route/binding 由 -`bootstrap/passive_worker.py:440` 起的 envelope settlement 传递, -`agent/plugin_composition/durable_deliveries.py:121` 起已经在 provider I/O 前后保存 -durable 状态。v4 删除它们对 Turn logical id 和正文副本的依赖,保留必须的 effect -recovery。 +即使 call 已被 tombstone 隐藏,只要它仍可能已经执行而尚无 result/unknown,generation +仍不可删除。启动时先从完整 WAL 重建这些引用,再允许清理旧 artifact。exact generation +意外缺失时记录 incident 并写 `unknown`,绝不换用“差不多”的新 generation。 -这不是新的消息身份或对话事实。它是 Channel/Delivery plugin 对“一条既有 Message 的 -一次外部效果”保存的可恢复视图;发送时必须按 `message_id` 从 WAL 读取正文并核对 -digest。正常状态只单调前进: +### 5.5 Tombstone 与 effect start 只有一个先后顺序 + +Tool 和 Delivery 在跨过真实外部 I/O 起点前取得 per-session shared **start permit**; +TombstoneWriter commit 前取得同一闸门的 exclusive permit。permit 没有 ID、不落盘,只 +负责把“允许开始”与“治理 Message 先提交”排成先后。 ~~~text -prepared ──fsync──▶ provider_started ──▶ delivered ──▶ settled - │ ├─────────▶ rejected (terminal) - │ └─────────▶ unknown - └──────────────────────────────────▶ suppressed +effect 先拿 shared permit + └── 在 permit 内重查 pending/Hidden/authority,固定 generation 并真正发起 I/O + └── tombstone 随后可提交;effect 继续结算,晚到 result 自动被 projection 隐藏 + +tombstone 先拿 exclusive permit + └── commit seed + └── 后来的 effect 看见 Hidden,不发起 I/O + ├── Tool append terminal tool_result(error: hidden_before_start) + └── Delivery 写 suppressed ~~~ -- `prepared` 在任何 provider I/O 前冻结 route 与 exact plugin binding; -- 取得 shared effect lease 并重读 delete graph 后,先把 `provider_started` durable, - 随后才可调用 provider;直到结果状态 durable 前不释放 lease; -- crash/timeout 后先用 `message_id` 幂等键或 receipt query;能确认才写 `delivered`; -- provider 的永久拒绝写 `rejected`,它是终态,永不重试;只有 provider 能证明 - 没有产生外部效果时,才能保持 `provider_started` 并用同一 `message_id`/binding - 安全重试;无法证明就写 `unknown`,不盲重发; -- delete 在 provider I/O 前把 `prepared` 单调推进为 `suppressed`;已经 - `provider_started` 只能查询真实结果,不能倒退成未发送; -- `delivered` 后,source plugin 才推进自己的 ACK/dedupe/cooldown,再写 `settled`; -- observer 在 Message commit 后、`prepared` 前崩溃时,恢复扫描按 SessionRoute 补建缺失 - record;它必须先应用 delete manifest,若 Message 已删除就直接建 - `suppressed`,不先建 `prepared`;同 `message_id`/同 binding 幂等,binding 冲突 - fail-loud; -- unresolved record 固定对应 channel generation;恢复先重建 retention lease,再清理 - 旧 generation。 +permit 不需要等整个 provider 调用完成,只保持到请求已被 provider 接纳,或本地 effect +已经完成;如果 adapter 无法把 start 与 await-result 分开,必须在有界 timeout 内保持。 +运行中 exact generation lease 则持续到 result/unknown。 + +Tool 没有另建 durable `started` row。若进程在 start 之后、result 之前崩溃,重启从 call +Message query/idempotency/unknown。若此时已有 tombstone,绝不新执行,只 query;不能确认 +就写 unknown。这是没有 RunId 时必须诚实接受的保守边界。 + +## 六、Proactive 与其他 source 不再是 Core 特判 -这类 retry 发生在 Message append 以后,但它不是“重新生成 Message”: +Core 不认识 proactive、Wake、Scheduler、Drift、`message_push` 或 subagent。source plugin +先在自己的边界决定是否有内容真正进入对话: ~~~text -assistant Message 已提交 - │ - ├── provider 发送成功 - ├── provider 证明未产生效果,可用同一 message_id 安全重试 - ├── provider 永久拒绝,写 rejected 后结束 - └── ACK 不明,先查询;不能确认时标记外部状态 unknown +no_due / 未选择 / 尚未生成完整输出 ──▶ 不写目标 Session Message +需要可恢复的 Agent 工作输入 ──▶ Source Message +Agent 生成完整主动输出 ──▶ Agent Message +模型对已进入 Session 的 cause quiet ──▶ Agent no_reply Message ~~~ -- worker 始终引用原 `message_id`,不复制正文,不生成 DeliveryId; -- provider 支持幂等键时直接使用 `message_id`; -- provider 不支持幂等或查询时,系统不能承诺 exactly-once; -- receipt、route 和 state 属于 Channel/Delivery plugin 的 external-effect projection; - 重试次数、延迟和错误栈仍只是 telemetry;它们都不进入 conversation schema; -- 若将来产品真的要求“送达结果也成为可回忆事实”,它只能作为一条新 system Message - 进入同一 WAL,不能新建平行账本。 - -因此,Message 存在的标准仍是“Agent 已经完整说出并提交”,而不是“每个外部渠道都 -已经收到”。 - -`SessionRoute` 与 `DeliveryEffect` 必须和对应 plugin-data、SQLite WAL/SHM 一起做 online -backup 与 `integrity_check`。unsettled、`unknown`、仍被 source continuity 引用或其 Message -仍可能重投的记录当前不得自动删除。以后若加 retention,只能用名称明确的 Delivery -Data Management 操作预览目标、备份、验证 provider/source 已不再依赖,再物理减少; -删除 effect record 不得删除 Session Message。 - -## 九、删除仍只使用 Message - -正常路径永远只追加。用户明确撤销 Message 时,Data Management 先取得第 -4.4 节同一个 per-session exclusive lease。它会停止签发新 reactor grant,并阻止 -新的 Tool/Delivery effect 开始。 - -已经取得 shared lease 的 effect 先写出 result 或 `unknown`。尚未取得 shared lease -的 unresolved tool call 可以明确 append `tool_result(error: deleted_before_start)`,不调用 -provider。启动时遗留的不确定 call 必须在 Data Management 对外 ready 前已按第 -4.3 节查询或写 `unknown`,不能把“进程里没有 task”误当成“从未开始”。已有 -Delivery record 按第八节结算;尚未建 record 且目标将被删除时,只建 -`suppressed`。这些完成后才在最新 Session head `H` 上计算 closure。 - -先归一化 seed,再沿已有 typed edge 向后展开: - -1. 若 seed 是 tool result Message,同时加入包含它所引用 `tool_call` 的 assistant - Message。Message 不可拆,所以该 assistant 内其他 call 的 result 也会在后续闭包中加入; -2. assistant `reply(target_message_id)` 指向 closure 中 Message,则加入该 assistant; -3. closure 中 assistant 的 `tool_call` 对应的 tool result 加入; -4. reply 该 tool result 的后续 assistant 继续加入,直到没有新后代; -5. 上游 cause 不自动加入;“撤销整组”由 UI 在请求中明确给出需要一起作为 seed 的 - user/system Message。 - -然后用 Data Management 专属 grant 和 `expected_head_seq=H` 一次 append **一条**普通 -delete Message,其中按 seq 排列多个 delete block: +Source Message 永远是低信任 context,不是 system instruction。一个 source-triggered +工作可形成 `{Source, tool_call, tool_result, Agent A}` 的 Turn projection;Chat 可以只显示 +A。直接 `message_push` 的 standalone Agent A 自己形成一个 Turn。 + +各 source 仍有不能从 Session 反推的 durable state: + +| source owner | 自己保留的 state | 进入 Session 的边界 | 结算 | +|---|---|---|---| +| Channel adapter | inbound provider ref/staging、认证 route | `InboundWriter` 写 Human Message | Message durable 后 ACK | +| Scheduler | schedule、enabled、next fire、missed tick | 需要 Agent 时写 Source Message;无事不写 | job state 自己推进 | +| EventMail | Content/Alert/Context envelope 与各自 transition | 被选择且需要对话时写 Source Message | supersede/expiry/selection 仍归 EventMail | +| Wake | watermark、receipt、reservoir、hazard、pending ACK、cooldown | 真正开始 Agent 工作时写 Source Message | 观察 Delivery receipt 后推进自己的 ACK | +| Drift | cursor、journal、continuum、下一轮选择 | 需要 Agent 时写 Source Message | cursor/journal 自己恢复 | +| `message_push` | parent call 的 Tool 意图/结果 | target Session 写 standalone Agent Message | target Delivery 与 parent Tool result 分开 | +| subagent | job/process continuity 与 parent call | parent Session 写 tool_call/result;必要时 child 用独立 Session | 按 call Message 恢复 | + +`proactive.db`、`wake_proactive.db`、`drift/drift.db`、`schedules.json`、quota、EventMail +transition 和 pending/ACK/hazard/reservoir state 不能因为 Core 不再识别 proactive 就清除。 +迁移前逐项标记 preserve、replace 或 retire,并提供 owner handoff 和恢复证据。 + +### 6.1 用户指出的交错 case ~~~text -seq 20 system delete(M7), delete(M8), delete(M10), delete(M11) +seq 1 Human U1 + stop R1 +seq 2 Human U2 + stop R2 +seq 3 Agent proactive P +seq 4 Agent A1 responds_to(U1,U2) ~~~ -这条 Message 本身就是 immutable closure manifest,不再创建 DeleteOperation、TurnId、 -Redaction row 或另一套版本号。若 head 在计算期间变化,append 失败并从新 head 重算; -不会留下半个 closure。 +必须同时满足: -各 consumer 对同一个 manifest 的职责是: - -| owner | 必须做什么 | -|---|---| -| Chat / Model context | 隐藏全部目标和依赖输出,不留下孤立 tool result | -| `Next action` | 先用完整历史的 reply/result 边结算,再应用可见性;deleted input/call/result 永不重新进入 pending,被删除 result 也不会让旧 call 复活 | -| Memory / Search / Akasha | 按目标 `message_id` 撤销派生项,或从 WAL 重建到 manifest 的 `source_seq` | -| Delivery | `prepared` 停止发送;`provider_started` 先查询并记 delivered/unknown;已经 delivered 只能尝试 provider 明确支持的 recall,不能伪装成未发送 | -| Artifact owner | 重新计算引用;只有引用归零且另有物理删除授权时才减少字节 | - -因此删 U1 会同时隐藏 reply U1 的 assistant、其 tool call/result 和最终回答;从 -tool result 开始删也会先纳入它的 call Message,不会留下一个看似 pending 的 -call。只删 assistant A 时,上游 U1 可以继续显示,但 -不会因 A 被隐藏而自动触发第二次回复;用户要重新问,必须再 append 一条新 user -Message。 - -delete 的效果单调:普通 delete 不能以“删除 delete Message”的方式恢复旧正文。已删除 -ID 的旧 append 重试只返回原 `seq` 与 gone 结果,永远不比较、覆盖或恢复原 content。 - -“撤销刚才那组对话”先让当前 UI projection 列出 seed 与推导 closure 供用户预览,提交 -时由服务端按同一算法和最新 head 重新验证。删除整个 Session 则是名称明确的 Data -Management 操作,不伪装成普通对话写入。v4 的 causal closure 会有意替代当前按 -interaction 原子撤销的合同,必须在 Phase 0 单独批准。 - -若用户还要求物理擦除正文,Data Management 必须在 delete Message durable 以后: - -1. 建立名称清楚的恢复点; -2. 等所有持久 projection 至少应用到 delete Message 的 `seq`; -3. 列出 closure 中的 Message、artifact 和 external-effect record; -4. 擦除目标 Message 的 text、tool arguments/output 和 artifact payload reference;保留 - `message_id`、`session_id`、`seq`、block position、`reply/call_ref` 因果骨架以及 - delete manifest,避免删除后重启又执行旧 cause; -5. 按各 owner 的独立减少合同处理 artifact/effect,重建 projection 并做完整性检查; -6. 保证旧请求重试不能让任一 closure Message 复活或让工具/投递再次发生。 - -这是 append-only 的唯一例外,必须由用户明确的数据管理操作触发。上下文压缩、容量 -优化、迁移和插件都无权调用。 - -## 十、从 DSH 借什么 +- U1、U2 在各自 ingress 时已经 durable,不因 stop 或失败模型请求消失; +- P 没有 Human cause,不能触发 `NoNewHumanInputAfter` 冲突; +- A1 cause CAS 只结算 U1/U2; +- TURN_VIEW 输出 `{U1,U2,A1}` 和 `{P}` 两个 Turn,即使成员不连续; +- Akasha、compaction、history 和 UI 使用同一个 TURN_VIEW 版本,不能各自按邻接角色猜。 -检查的 DSH 基线是 -`/mnt/data/source-code/deepseek-harness@49a606bc5b5934603f22a26957a07dc799ab0291`。 +这里没有 `proactive=true`、ProactiveTurn 或特殊 writer。P 之所以单独成组,只因为它没有 +U1/U2 的 cause 边;相同结构的 command push、subagent notice 或普通 standalone Agent +Message 得到同样结果。 -值得借用的只有三个原则: +### 6.2 hua-home 历史只提供 fixture,不提供正确答案 -1. 一份 ordered Session log 是 history 与 projection 的共同来源; -2. 一个 immutable Message representation 跨 history、model request 和 delivery 复用; -3. Message 在 publication/append 前已经拥有稳定 ID,pure projection 可随时重建。 +2026-09-03 对 hua-home 私有 `sessions.db` 的只读窄查询只取 role 和布尔标记,不取正文、 +Session ID 或私人时间戳:共看到 6 条 interrupted marker、分布在 2 个 Session;有 15 条 +proactive row 位于“此前 50 seq 内至少有两次 interrupt”的窗口。脱敏相对序列也确实呈现 +`interrupt → 后续 user → interrupt → 后续 user → proactive → 后续普通 assistant` 的 +交错。它只能证明这种压力形态真实发生过,不能证明旧写法正确。 -本设计不照抄 DSH 的 `SessionEvent`、turn/step 事件或完整 runtime。Akashic 再做一步 -减法:Message 自己就是 WAL record,不需要事件壳。 +当前 `session/manager.py:190-230` 仍按**连续** `control_turn_id` 分组,并在 +`session/manager.py:213-226` 显式检查 `proactive`;当前 +`agent/lifecycle/phases/after_reasoning.py:257-332,413-447` 仍在 reasoning 后把暂存 Human +与 Agent rows 批量 append。v4 有意同时替换这两点:每个 U 在 ingress 单独 durable,P +不再有特判,专门的 Turn plugin 只按 Message 因果与自己的 policy 投影。 -对应源码证据: +这段历史应转成脱敏 fixture:输入只有 `U1/interrupt/U2/interrupt/P/A1`,断言 WAL 是六个 +动作中的四条完整 Message、Turn groups 是 `{U1,U2,A1}` 与 `{P}`;旧 marker、attempt 和 +`control_turn_id` 都不是 expected output。 -- `packages/llm/llm/src/message.ts:130`:一个 immutable Message 供 history、model - request 和 delivery 共同使用; -- `packages/llm/llm/src/message.ts:175`:Message 在发布前创建稳定 UUID;本设计只借 - pre-append identity,不规定 UUID 格式; -- `packages/core/session/src/index.ts:628`:Session 以单调 seq append; -- `packages/core/session/src/index.ts:772`:model messages 从 Session surface 派生; -- `packages/session/session-projection/src/index.ts:40`:projection 是按 seq fold 的 - versioned cache。 +## 七、Delivery 是按 Message 与 sink 建立的外部效果 projection -## 十一、v4 的另一半:把被动回复大链路变成普通插件 +一条 Message 可以同时送到 Telegram、邮件和手机通知,不能为了不同目的地复制三条 +对话 Message。Delivery key 是: -### 11.1 现在真正需要替换的链路 +~~~text +DeliveryKey = (message_id, sink_id) +~~~ -当前基线的被动回复不是普通插件组合,而是一条固定 owner 链: +Delivery scan 不表示“看到 Message 就发送”。初始 eligibility 是纯 projection: ~~~text -PassiveMessageWorker - ├── 入站 custody / attachment / per-session lane - ▼ -ConversationRuntime - ▼ -AgentLoop._react() - ▼ -PassiveTurnPipeline - ├── command short-circuit - ├── BeforeTurn - ├── BeforeReasoning - ├── reasoner + BeforeStep / AfterStep - ├── AfterReasoning:parse + persistence + outbound - └── AfterTurn:事件 + dispatch / ACK +Deliverable(m, S) = + m.author == Agent + and m 有 user-visible final content + and m 不含 tool_call/no_reply/tombstone + and not Hidden(m, S) ~~~ -代码证据也显示这些责任仍被固定装配: +因此 Human、Source、Tool result、tombstone、no_reply 和带 tool_call 的中间 Agent Message +都不创建 DeliveryEffect;command reply、user-visible error、standalone proactive P 与普通 +最终 Agent reply 使用同一规则。这里没有 `deliverable/proactive` Message 字段。具体送到 +哪些 sink 再由 Delivery plugin 的 route/user setting projection 决定;没有 sink 就没有 +effect record。 -- `bootstrap/passive_worker.py:96`:`PassiveMessageWorker` 拥有消息准入、lane task 和 - 结果 task; -- `agent/looping/core.py:556`:`AgentLoop._react()` 只把请求转给固定 pipeline; -- `agent/core/passive_turn.py:355`:`PassiveTurnPipeline` 构造固定的四段 phase; -- `agent/core/passive_turn.py:440`:command 在 Session/model 准入前走专门短路; -- `agent/core/passive_turn.py:524`:默认被动回复仍由一个固定 `run()` 入口统管。 +`sink_id` 是 Delivery plugin 自己的稳定地址引用,不是对话身份,也不是随机 DeliveryId。 +每个 effect record 不保存正文: -这会让插件只能在旧流水线上挂 hook,而不能真正替换业务。新增语音 Agent、无工具 -Agent、plan/execute Agent 或另一种回复策略时,要么继续给 Core 加分支,要么复制整条 -pipeline。单独换成 Message WAL 并不会消除这个问题。 +~~~text +DeliveryEffect[(message_id, sink_id)] = { + immutable sink/provider binding, + state: prepared | provider_started | delivered | rejected | unknown | suppressed, + provider_receipt? +} +~~~ -### 11.2 目标:`Message → react → Message` +发送时只按 message_id 从不可变 Session WAL 读取正文;effect record 不复制 seq、正文或 +digest。Message 缺失/损坏是 incident,不能用 effect 中的副本降级。状态单调: -默认产品提供一个普通 `passive-conversation` 插件。它与第三方插件经过同一套加载、 -依赖解析、candidate 校验、generation 发布和生命周期清理;Core 不给它后门。 +~~~text +prepared ──fsync──▶ provider_started ──▶ delivered + │ ├─────────▶ rejected + │ ├─────────▶ unknown + │ └─────────▶ suppressed # query 证明尚未产生效果且已 tombstoned + └──────────────────────────────────▶ suppressed +~~~ -这不是再建第二套插件框架。当前代码已经有可复用的骨架: +- `provider_started` 必须在 provider I/O 前 durable,并在 shared start permit 内提交; +- 永久拒绝写 `rejected`,它是终态,永不重试; +- 只有 provider 能证明未产生效果时,才可在同一 record、同一 message_id/sink/binding 上 + 安全重试;无法确认就写 `unknown`; +- Message 已 tombstoned 且 record 尚不存在时,recovery 直接创建 `suppressed`,不经过 + `prepared`; +- effect 已先开始时,tombstone 不伪装成撤回;worker query 后写 delivered/unknown,或在 + 证明未发送时写 suppressed; +- unresolved record 固定 exact channel generation;GC 同样受 durable effect 引用约束。 -- `agent/plugin_composition/model.py:28` 已定义类型化 `ServiceKey`; -- `agent/plugin_composition/context.py:148` 已提供绑定 exact Root 的短命 runtime scope; -- `plugins/models/plugin.py:58` 已用普通 `provide()` 发布 model services; -- `plugins/compaction/plugin.py:468` 已用普通插件发布 provider request projection; -- `plugins/markdown_memory/plugin.py:67` 已把 Prompt 与 post-commit memory 行为接入普通 - plugin lifecycle。 +Delivery 不再有 `settled`。`delivered` 只说明 provider effect;Wake 的 ACK、dedupe、 +cooldown 或 Scheduler 的 run state 是另一个 owner 的状态: ~~~text -passive-conversation plugin - provides MESSAGE_REACTOR - injects SESSION_READ, SESSION_FEED, SESSION_APPEND - COMMANDS, AGENT_PROGRAM - -default-agent plugin - provides AGENT_PROGRAM - injects SESSION_READ, SESSION_APPEND - PROVIDER_REQUEST_PROJECTION - PROMPT_PARTS - TOOL_SELECTOR - ASSISTANT_TRANSFORMS - CHAT_MODELS - TOOL_EXECUTOR - STREAM_PREVIEW - -tool-runtime plugin - provides TOOL_EXECUTOR + recovery Effect - injects SESSION_READ, SESSION_FEED, SESSION_APPEND - exact tool bindings + shared effect lease +DeliveryEffect.delivered + │ typed receipt + ▼ +Wake 自己推进 ACK/cooldown ~~~ -这些大写名字都是普通 `ServiceKey`,不是 Core 固定 slot,更不是新领域对象: +Wake 失败不能倒退或改写 Delivery,Delivery 也不解释 Wake 是否完成。 -- `SESSION_READ` 只提供 read/head,`SESSION_FEED` 只发布 committed Message, - `SESSION_APPEND` 只签发绑定 Session、role 与 CAS/typed precondition 的 writer;三者 - 共用一个 WAL owner,但权限彼此独立,都没有任意 SQL、原位改写或删除能力; -- `MESSAGE_REACTOR` 读一个已经 committed 的输入 Message,按最新 WAL projection 判断 - `idle / command / respond / await tool`,再选择 command 或 `AGENT_PROGRAM`;它不执行 - Tool,也不保存自己的 outcome; -- `AGENT_PROGRAM` 拥有默认模型/工具算法,包括 provider retry、Tool Search、空回复 - 修正、terminal tool deadline 和继续生成; -- `TOOL_EXECUTOR` 只结算 committed `tool_call`;即时调用和 crash recovery 走同一个 - `execute_or_recover()`,因此它不依赖某次 Agent Program 仍然活着; -- 其他 key 只是 `default-agent` 自己的依赖。不使用工具的 Agent 不需要提供假的 - `TOOL_SELECTOR`,Core 也不维护一张“所有 Agent 都必须有”的选择表。 +`SessionRoute` 可以是 Channel 的默认 sink discovery 输入,但不是“一 Session 只能有一个 +目的地”的限制。广播增加 sink effect,不增加 Message。 -接口只传已有身份,不发明 ReactionId、ProgramId 或通用 context 袋子: +## 八、逻辑删除仍由 Message 承载,但只保存 seed + +本设计不增加 `SessionRecord = Message | Tombstone` 第三种权威记录。用户明确要求隐藏时, +`TombstoneWriter` append 一条只有 tombstone content 的治理 Message: ~~~text -MESSAGE_REACTOR.react(session_id) -> None -AGENT_PROGRAM.respond(session_id, cause_message_ids[]) -> None -TOOL_EXECUTOR.execute_or_recover(call_ref) -> None +seq 20 Human tombstone(seed_message_ids=[M7]) ~~~ -`None` 只表示函数已经结束;可观察结果只能是 WAL 中新增了哪些 Message。异常、取消、 -provider retry 和临时资源留在当前 Fiber/telemetry。它们不能通过另一个 result record -偷偷变成第二份对话事实。 +它不是聊天气泡,也不进入模型上下文;但它确实是“用户向 Session 提交了一项治理要求”, +因此仍由唯一 Message carrier 保存。writer 的认证和权限证明不放进 Message 字段。 -`AGENT_PROGRAM` 不接收自由传入的 `Message[]`、`scoped_messages` 或旧 context 快照。 -它必须自己从 `SESSION_READ.read_at_head(session_id)` 取得不可变 read view 与 -`observed_head_seq`,再要求 `SESSION_APPEND` 签发绑定同一 Session、cause set 和 observed -head 的 writer。snapshot 只引用 WAL Message 并标明 `source_seq`,不可修改、不可持久化, -也没有 append 方法。cause 不在 snapshot、已被其他 reply 确认或当前 head 已变化时, -签发/append 都 fail-loud,program 重新读取。 +不再把完整 descendant closure 展开进 tombstone。projection 每次从不可变因果边计算: -不保留 v3 的 `decide() → handle()` 双阶段,也不新增 ReactionPlan。能否继续只由最新 -Message 因果图算出;同一个 Session 被重复唤醒时,projection 已经是 idle 就直接结束, -否则最终仍由 append 的 CAS/typed precondition 仲裁。 +~~~text +Hidden(S) = DescendantClosure(NormalizeSeeds(S), CausalEdges(S)) +~~~ -`passive-conversation` 自己用普通 Effect 订阅 committed Message,并在自己的 Fiber -里调用 `MESSAGE_REACTOR`。因此 Core 甚至不需要知道这个 ServiceKey。产品 profile -要求自动回复时,candidate 必须恰好解析出一个 provider;依赖缺失或重复 provider -在发布前 fail-loud。停用这个插件以后: +边包括 `responds_to` 与 `resolves`。seed 归一化只有一个向上规则:如果用户点中 Tool +result,server 把 seed 改成包含对应 tool_call 的 Agent Message;然后只向后展开。这样 +不会留下看似 pending 的 call,也不需要保存所有未来 result ID。 -- 渠道仍可把 user Message 写入 WAL; -- Chat、同步和历史仍正常; -- 不再自动产生 assistant Message; -- UI projection 可以显示“Agent 未启用”,Core 不偷偷启用 legacy fallback。 +归一化可能扩大可见影响,所以顺序必须是:可信 Data Management 边界先算 normalized +seeds 和当前可见影响预览,用户确认这组 exact seeds 后,才签发一次性 TombstoneWriter。 +append 只保存确认过的 seeds;预览和当时的 closure 都不成为第二份持久真相。 +seed 只能指向 conversation Message;指向 tombstone、未来/跨 Session Message 或重复 seed +都在签发前拒绝。 -这里的 `passive` 只是默认插件包名,不是 Message 字段、Session 模式或 Core 分支。 -Scheduler、Wake、Channel 和 subagent 都只产生普通 Message,或者显式依赖同一个 -`AGENT_PROGRAM`;来源不会复制一套执行模型。 +晚到的 Tool result 或 Agent continuation 会自然成为已隐藏 call 的 descendant。Next +action 先用完整历史 settlement 边判断 cause/call 已处理,再应用 Hidden;删除可见回复 +不会让旧输入或 Tool 复活。 -Tool recovery 是另一条普通 Effect:它订阅同一 feed,只把 unresolved call 交给 -`TOOL_EXECUTOR`。它不塞进 `passive-conversation`,所以关掉自动回复也不会让已经提交 -的外部意图悬空。这是两个正交生命周期,不是 proactive/passive 的新特判。 +Tombstone 与 effect start 按第 5.5 节只争夺一个短临界区,不等待整个远程 effect 才让 UI +隐藏。若 effect 已先开始,它诚实结算并被 projection 隐藏;若 tombstone 先提交,effect +永不开始。 -### 11.3 固定 phase 不原样搬家 +### 8.1 物理擦除不属于初始 v4 -插件化不是把 `PassiveTurnPipeline` 整块移动到 `plugins/`。现有每项能力先找到唯一 -owner;没有独立不变量或真实消费者的 phase/hook 直接删除: +初始 v4 只提供 append-only logical tombstone,不 UPDATE/DELETE 既有 Message body。 +物理 purge 需要另外回答 projection 枚举、artifact 引用、外部 receipt、备份、恢复、旧 +重试和法规证明,不能作为被动链插件化的阻塞依赖,也不能在本文假装已经闭合。 -| 当前固定行为 | v4 owner | -|---|---| -| channel envelope、附件导入 | Channel/Artifact adapter;artifact ready 后才 append user Message | -| 入站 durable handoff / ACK | Channel adapter;user Message durable 后结算 | -| command catalog 与短路 | `passive-conversation` + 注入的 `COMMANDS` | -| Session/history 准备 | `SESSION_READ` + pure Session projection | -| system prompt、skills、memory、profile | 有序、不可变的 `PROMPT_PARTS` contributions | -| history 裁切、摘要与 compaction retry | `PROVIDER_REQUEST_PROJECTION`;沿用普通 compaction plugin | -| tool schema preload / Tool Search 解锁 | `TOOL_SELECTOR`,由 `default-agent` 使用 | -| Tool 展示 | `TOOL_SELECTOR`;只能缩小当前可见集合 | -| Tool 授权、执行与恢复 | `tool-runtime` 的受保护 `TOOL_EXECUTOR` + recovery Effect;调用边界重新校验 | -| 默认 ReAct、provider retry、空回复重试、terminal deadline | `AGENT_PROGRAM` | -| model/provider 绑定 | models/provider 插件;从当前 exact Root 注入 | -| Citation、Meme、最终文本/媒体改写 | append 前的有序、不可变 `ASSISTANT_TRANSFORMS` | -| assistant/tool 写入、幂等、seq、CAS | `SESSION_APPEND` 签发的 scoped writer | -| Memory、Akasha、compaction 派生数据 | committed Message observers / projections | -| partial token 展示 | `STREAM_PREVIEW`;可丢失且无权 append 半条 Message | -| error reply、quiet | reactor/program 产生普通 assistant `text` 或 `no_reply` Message | -| continuation、crash 后下一步 | 从 WAL 重建的 `Next action` projection | -| assistant 对外发送、重试、provider ACK | 独立 Delivery projection/effect,只引用 `message_id` | -| delete 与外部 effect 竞态 | 通用 per-session shared/exclusive lease;短命、无 ID、不落盘 | -| generation 固定、取消、资源清理 | 通用 plugin Root lease + Fiber/Effect 生命周期 | - -Command 若只读或只生成回复,可以直接产生 assistant Message;若会付款、发信、改 Git -等产生外部副作用,必须先生成普通 `tool_call` Message,再走同一 Tool 协议。不能因为 -它叫 command,就在 WAL 看不见的短路里执行一次可能重复的外部动作。 - -Prompt 与 assistant transform 的顺序必须确定,但“顺序确定”不表示插件可以任意叠加。 -每个 contribution 不可变,只有 composition owner 能形成最终序列;candidate Gate 要拒绝 -重复 owner、冲突位置和循环依赖。transform 必须在 assistant Message append 前结束; -post-commit observer 无权回来改正文。 - -### 11.4 Tool 可见性不是 Tool 权限 - -`default-agent` 可以用 `TOOL_SELECTOR` 决定本次把哪些 schema 给模型看,但它不能扩大 -当前授权。模型产生 `tool_call` 后: - -1. 按本次可见 schema 校验并把 exact immutable `tool_binding` 写进 assistant Message; -2. `TOOL_EXECUTOR` 固定 exact binding lease,取得 shared effect lease,并在真实副作用 - 边界重新检查当前授权与 delete graph; -3. 能证明尚未开始且已撤权时不执行,append 一个明确的 `tool_result(error)`; -4. 已经可能开始但无法确认时按第四节写 `unknown`,不能由 Agent Program 猜成 success; -5. 正常调用、feed replay 和启动扫描都走同一个 `execute_or_recover()`,不会因换入口而 - 绕过上述检查。 - -因此替换 `AGENT_PROGRAM` 只能改变算法,不能绕过工具权限 owner。 - -## 十二、Thin Core 最终保留什么 - -Core 只保留来源无关、产品算法无法安全拥有的原子能力: - -1. **Plugin composition**:`ServiceKey`、`provide/require/inject`、依赖冲突校验、exact - committed Root lease、candidate/stable、Fiber/Effect 清理、health/incident; -2. **Session Message WAL**:append、read、subscribe、head、幂等、seq、CAS 和受限 - writer;用户删除 Session/Message 的 Data Management 是另一个显式管理入口; -3. **短命执行安全**:取消、超时、per-session 串行准入、effect shared/exclusive lease - 和有界资源 scope;这些可以丢失,不分配持久身份; -4. **真实外部边界**:模型调用、Tool 授权/执行、stream preview、channel ingress/ACK - 和 delivery effect 的窄 port;具体 provider 与策略仍由普通插件提供; -5. **类型化观察**:只发布 committed Message;observer 失败不能回滚、覆盖或补造 - WAL 事实。 - -Core 不认识下面这些产品词: +以后若批准 purge,必须另立 Data Purge 设计和操作 Gate;上下文裁切、容量优化、插件 +卸载或普通 migration 永远无权触发。 + +## 九、Thin Core 与信任边界 + +Core/受保护 substrate 只保留来源无关且不能安全交给产品算法的原子能力: + +1. **Plugin composition**:ServiceKey、provide/require/inject、candidate/stable、exact Root + lease、Fiber/Effect 清理、health/incident; +2. **Session Message WAL**:read/feed/internal append、message identity、seq、幂等、cause + precondition 与 typed writers; +3. **短命执行安全**:Reaction factory、取消、timeout、有界资源、per-session admission 和 + effect-start permit; +4. **真实外部边界**:Model transport、ToolHost、Channel ingress 与 Delivery effect port; +5. **类型化观察**:只发布 committed Message 与短命 receipt,observer 无权修改事实。 + +Core 不出现: ~~~text passive / proactive / Wake / Scheduler / command compaction / memory / Citation / Meme / Tool Search -某个 model/provider 名称 / 某个 channel 名称 +某个 Agent、model、provider、tool、channel 或 plugin 名称 ~~~ -如果 Core 源码需要按这些名字分支,说明 capability owner 仍没有拆干净。反过来, -`ServiceKey`、Root lease 和 Fiber 也不进入 Session;它们是让插件安全工作的机器零件, -不是对话事实。 +Turn projection、Agent Program、Prompt policy、source state 和 delivery selection 都由普通 +插件组合。 + +“普通插件”不表示任意 Python 代码天然安全。当前 in-process plugin 是用户 install 时 +授权的可信代码;object capability 减少误用,不能充当恶意代码 sandbox。不可信第三方 +能力必须进入受 OS 文件系统、网络和进程权限限制的 MCP/Workload/out-of-process host。 + +公共 `PluginRuntime` 还需在迁移前盘点并移除 broad workspace/私有 store/SQL 暴露。正式 +Gate 必须从仓库外源码走 install → candidate → stable → hot reload → uninstall,并测试 +typed writer、ToolHost、workspace path 和 candidate production effect 拒绝。 -## 十三、完整目标链路 +## 十、完整目标链路 ~~~text Channel adapter - │ 先让 artifact durable - │ Channel grant → append user Message(message_id) - │ durable 后 ACK inbound + │ artifact durable + authenticated canonical message_id ▼ -┌──────────────────────── Session Message WAL ─────────────────────────┐ -│ user / system / assistant(tool_call|text|no_reply) / tool / delete │ -└──────────────────────────────┬───────────────────────────────────────┘ - │ committed feed - ┌─────────────────┼──────────────────┬──────────────────┐ - ▼ ▼ ▼ ▼ - passive-conversation Tool recovery Chat / Model Delivery projector - Effect Effect / Memory message_id only - │ │ view │ - ▼ │ ▼ - MESSAGE_REACTOR └──────────────┐ DeliveryEffect record - causal fold → cause set │ │ - ┌───────┴────────┐ │ shared effect lease - known command AGENT_PROGRAM │ ▼ - │ │ provider send/ACK - prompt/tool selection + model retry │ - │ complete │ - ▼ │ - exact-cause grant + head CAS │ - append assistant Message ─────────┼──────────────────────▶ WAL - ├── text/no_reply │ - └── tool_call ── commit first ────┤ - ▼ - TOOL_EXECUTOR - shared effect lease - │ result/unknown - ▼ - completion grant → tool Message ──▶ WAL +InboundWriter ───────────────▶ Session Message WAL ───────────────┐ + │ committed feed │ + ┌────────────────┼───────────────┐ │ + ▼ ▼ ▼ │ + TURN_VIEW fold ToolHost scan Delivery scan │ + │ │ │ │ + ▼ │ ▼ │ + passive-conversation │ Effect[message,sink] │ + private reconcile │ │ │ + │ │ provider I/O │ + ▼ │ │ + create short Reaction │ │ + │ │ │ + ▼ │ │ + AGENT_PROGRAM │ │ + prompt / model / tool choice│ │ + │ │ │ + ▼ │ │ + AgentReplyWriter ────────┴────────────────────────────▶│ + │ tool_call committed │ + ▼ │ + scoped ToolPort │ + ▼ │ + protected ToolHost │ + │ ToolResultWriter │ + └──────────────────────────────────────────────▶│ + ▼ + next committed feed ~~~ -这里有两个故意分开的 commit 点: - -- **输入事实**:channel 在 user Message durable 后就可以 ACK。Agent 后来是否回答, - 不能反过来决定“用户有没有说过”; -- **输出事实**:assistant Message append 表示 Agent 已经说出。外部渠道是否送达由 - Delivery effect 继续处理,不能回滚或复制 assistant Message。 - -这会有意替换当前将入站 custody、回复终态和 delivery 结算绑在同一大链中的行为。 -它不是漏迁移,而是由“Message 是唯一消息载体”直接推导出的新边界。 - -## 十四、重启、热更新与并发不需要 Run - -### 14.1 活着时固定一个 exact Root - -一次 `react()` 调用开始时拿到 exact committed Root lease,到函数完成或取消才释放。 -其中的 model retry、Prompt、tool selection 和 transform 都看同一 Root。热更新可以发布 -新 Root,但不能在半次调用中偷换依赖。 - -Fiber、取消 token、超时和 per-session lane 都是内存资源。U2 到来时可以尽早取消旧 -生成;即使取消来不及,旧输出也会被 `expected_head_seq` 拒绝。正确性来自 WAL CAS, -不是来自一个持久 Run 状态。 - -### 14.2 崩溃后只从 Message 恢复 - -- assistant Message append 前崩溃:没有事实;重启后可用最新 Root 重新生成; -- assistant `tool_call` 已 append:独立 Tool recovery Effect 从其 `call_ref` 与 - `tool_binding` 调用同一个 `execute_or_recover()`,不要求旧 Agent Program 复活; -- tool Message 已 append:下一次从最新 WAL 继续生成 assistant Message; -- assistant text/`no_reply` 已 append:该前缀已经有结果,projection 得到 idle; -- delivery 中崩溃:继续用同一 `message_id` 查询或安全重试,不重新生成正文;若 Message - 已删除且 effect record 缺失,直接补 `suppressed`。 - -`tool_binding` 中的 generation identity 是 Message 内容的一部分,不需要 RunId。由 WAL -派生的 unresolved-tool projection 为所引用的插件 generation 加 retention lease;进程 -启动时先应用全部 delete manifest,再重建这些 lease,并在开放 reactor、delivery 和 -Data Management 前结算崩溃遗留的 unresolved call。若安全 owner 在可证明尚未开始时 -撤销权限,就写明确的 tool error Message;若是否开始已不可知就写 `unknown`。两者都 -释放 lease,不能执行过期授权。 - -这只保证一段仍活着的函数内部使用同一 Root。崩溃后的新函数可以使用新 Root;唯一 -必须保持的是已提交 tool binding 的执行身份。为了跨崩溃保存整套旧算法而新增 durable -Run,成本大于它保护的事实,本设计明确不做。 - -### 14.3 Candidate 与外部插件边界 - -“普通插件”表示内置与外部插件使用同一 loader、manifest、ServiceKey、依赖图、candidate -和生命周期 API,不表示任意 Python 代码天然安全。当前 `PluginRuntime.workspace` -(`agent/plugin_composition/model.py:106`) 仍暴露 workspace 路径,而同进程 Python 即使 -没有这个属性也能直接 import `os` 或打开绝对路径。 - -因此 v4 把信任声明写死: - -- in-process plugin 是用户在 install 时授权的**可信代码**;object capability 能减少 - 误用和稳定 API,不能充当恶意代码 sandbox; -- 不可信第三方能力必须放进受 OS 文件系统/网络/进程权限约束的 MCP、Workload 或其他 - out-of-process host,只通过窄协议进入 composition; -- 在宣称“只给声明文件”以前,先盘点消费者并从公共 `PluginRuntime` 移除/隐藏 broad - `workspace`,只保留 `data_root`、`workspace_root(name)`、`workspace_file(name)`; -- Session append、Tool、Delivery 和删除不向插件暴露 raw store/SQL,只接受本设计的 - grant/port。即使可信插件写错,也会在真实 owner 处被拒绝; -- 缺少依赖、重复 Service provider、贡献顺序冲突在 candidate 阶段 fail-loud; -- candidate 使用隔离 Session feed,不得订阅生产 Session、发送真实 delivery 或执行 - 高风险 Tool; -- generation 下线前必须 drain 自己的 Effects、listeners、tasks 与 leases。 - -正式 Gate 必须从仓库外源码走 install → candidate → stable → hot reload → uninstall, -用替代 `AGENT_PROGRAM` 完成真实回复,并分别尝试跨 Session append、role/delete/tool -result 越权、未声明 workspace path、raw SQL 和 candidate real effect。对 private import -与任意文件访问的测试只能证明 API/policy;若未使用进程 sandbox,报告必须明确写 -“trusted in-process”,不能声称抵抗恶意插件。 - -## 十五、迁移路线:WAL 与插件化一起完成 - -当前 `projectneed`、decisions、代码和数据库仍有 Turn/Run/attempt 合同。本文是有意的 -替代设计;当前行为是调查证据,不是正确性的来源。批准前不实现,批准后也不能把 -旧名词藏进 adapter 永久保留。 - -### Phase 0:批准新合同 - -- 批准 Session/Message 是唯一对话事实,失败 attempt 不进入 Session; -- 批准单一 pre-commit `message_id`、显式 reply cause set、head CAS、tool `unknown` 和 - 外部 exactly-once 边界; -- 批准 inbound ACK 与 Agent reply/delivery 解耦; -- 批准 `message_id`-keyed durable DeliveryEffect、单 external route/Session 和 causal - delete closure; -- 批准 `rejected` 是不可重试终态,以及 delete/effect 以短命 shared/exclusive lease - 线性化;lease 不进入 schema; -- 分别批准 Scheduler、Wake、Drift、EventMail、Channel、subagent 与 `message_push` 的 - preserve/replace/retire 选择,不能用一句“删除 proactive”代替; -- 批准 in-process plugin 是可信代码;若目标是恶意插件隔离,另立 out-of-process 合同; -- 以新条款 supersede `projectneed` 和相关 decisions 中的 Turn/Run 合同; -- 建立 Session、Delivery、source plugin DB/JSON、附件引用、插件 generation 和客户端 - cursor 的可恢复备份与 schema lineage 清单。 - -### Phase 1:先冻结完整行为账单 - -在改动 owner 前,用真实 Session fixture 和受控 provider/tool 记录当前大链的: - -- 输入与附件、command、Prompt sections、model request、tool schemas 与调用顺序; -- compaction retry、Tool Search 解锁、空回复 retry、terminal tool、continuation; -- Citation/Meme/媒体变换、partial stream、error/no_reply; -- Message 写集、Memory/Akasha 观察、Web/Mobile cursor、delivery 与两侧 ACK; -- 热更新时的 exact generation、取消和资源清理; -- Scheduler schedule/missed-tick、EventMail transitions、Wake tick/reservoir/hazard/ACK/ - cooldown、Drift cursor/journal、`message_push` raw-body caller 和 subagent recovery; -- `runtime/deliveries/settlements.sqlite` 的 route、provider_started、receipt、unknown 与 - settle 恢复窗口,以及 Channel handoff 的 exact binding; -- delete 分别早于/晚于 Tool 与 Delivery effect start、只删 tool result、effect record - 尚未建立和永久 rejected 的受控竞态。 - -每个差异必须先标成“保留能力”“按 v4 有意替换”或“已证明的旧 bug”。oracle 不要求 -盲目复制现状;它要求任何消失的能力都有明确决定。旧 phase 名称本身不是能力,没有 -消费者的 hook 不迁移。 - -### Phase 2:建立目标 WAL 与窄 capability - -- 从现有数据只读重建目标 Message WAL;已有 message ID 原样保留; -- 无法证明顺序、role、reply cause、tool pairing 或 outcome 的记录 fail-loud;旧 - assistant Message 只有在唯一因果前缀可证明时才补 reply,歧义数据进入人工阻塞清单; -- 实现正交的 `SESSION_READ`、`SESSION_FEED`、`SESSION_APPEND`、短命 - `MessageWriteGrant` 与 scoped CAS writer; -- 实现不落盘的 per-session shared/exclusive effect lease,以及统一 - `TOOL_EXECUTOR.execute_or_recover(call_ref)`; -- Chat、Model context、Tool status、Next action、Memory 和 Mobile 先 shadow rebuild; -- 用 U1 command + U2、U1/U2 合并回答、tool continuation、replay、并发 CAS 和 delete - closure fixture 验证因果 fold; -- 还未切换生产 writer,不改正式 workspace。 - -### Phase 3:先抽出普通 `AGENT_PROGRAM` - -- 把默认 Reasoner/ReAct、provider retry、Tool Search、空回复和 terminal policy 从 - `AgentLoop`/pipeline 抽成 `default-agent` 插件; -- 把 Prompt、context、tool selection 和 assistant transforms 变成普通依赖/contribution; -- `AGENT_PROGRAM` 只接收 cause Message IDs,自行读取带 `source_seq` 的 immutable view, - 不接收另一个 Message 容器; -- 用临时窄 adapter 接回旧入口,比对 Phase 1 oracle;adapter 只存在于迁移期并登记 - 删除 commit; -- 用一个无工具替代 Agent Program 证明 Core 与默认算法已解耦。 - -### Phase 4:接入 `passive-conversation` 并 shadow - -- 插件订阅 shadow Message feed,调用 `MESSAGE_REACTOR → AGENT_PROGRAM`; -- command、附件、error/no_reply、tool loop、continuation 与输出 transform 逐项对账; -- shadow 禁止真实 append、Tool 副作用和 delivery,只比较计划产生的 Message 与 - 外部调用; -- candidate/stable 切换期间验证 exact Root 和所有 Effect 均可排空; -- 完成 broad `PluginRuntime.workspace` 消费者盘点与移除;仓库外 fixture 走正式安装链, - 同时验证其可信进程边界和所有可执行的 capability 拒绝。 - -### Phase 5:一次切换唯一 writer - -- Channel 先 append user Message,再 ACK inbound; -- user、assistant、tool 与各 source producer 全部只走带正确 grant 的同一个 WAL append; -- 只启用 `passive-conversation` 的生产 subscriber,旧 worker 变为不可达; -- 启用独立 Tool recovery subscriber;它先应用 delete graph、重建 binding lease 并结算 - crash 遗留 call,再把 reactor、Delivery 和 Data Management 标成 ready; -- Channel/Delivery plugin 先完成 `SessionRoute` 和 `DeliveryEffect[message_id]` handoff,再 - 让新 projector 消费 committed assistant Message; -- Scheduler/Wake/Drift/EventMail 连续性 owner 全部 handoff 或显式 retire 后,才允许新 - generation activation;`message_push` 只接收 `message_id`; -- projection 完成 cursor reset/shadow 对账后接管读取;不长期 dual-write 两套事实。 - -### Phase 6:删除旧链和旧权威结构 - -- 删除 `PassiveMessageWorker → ConversationRuntime → AgentLoop._react → - PassiveTurnPipeline` 固定业务链; -- 删除 Before/After phase DAG、Core proactive/source 分支和无消费者的兼容 hook;source - plugin 已确认保留的 DB/JSON 不随之删除; -- 删除旧 conversation Turn/Run/Step/attempt rows、API、双重 message identity 和旧 - Delivery logical ID;保留并迁移 `message_id`-keyed external-effect recovery state; -- 卸掉所有迁移 adapter,确认没有插件 cache、动态消费者或恢复任务仍引用它们; -- 重建 projection,核对每个 Session 的 Message 数、ID、seq、tool pairing、delete、 - reply graph、DeliveryEffect、source owner handoff、plugin generation retention 与客户端 - cursor。 - -迁移前后都不能改写正式 workspace,除非另有明确授权、恢复点和执行前后完整性检查。 - -## 十六、验收 Gate - -### 16.1 概念 Gate - -- Core schema 和领域 API 只有 Session、Message; -- 不存在 TurnRef、RunRef、StepRef、AttemptRef、DeliveryRef、ProjectionClaim 或 - SessionEvent 壳; -- `MESSAGE_REACTOR`、`AGENT_PROGRAM`、Root、Fiber 和 projection 不可序列化成第三种 - 对话事实; -- tool call 只用 Message 内可派生的 `call_ref`; -- `MessageWriteGrant` 没有 ID、序列化或恢复语义;`DeliveryEffect` 只能以既有 - `message_id` 寻址且不复制正文; -- effect shared/exclusive lease 没有 ID、持久状态或对话语义; -- proactive 不是 Message 类型、字段或 Core commit 路径;source plugin state 仍由各自 - owner 持有; -- 没有 `metadata/context/intent` 通用可变袋子绕过 typed Message 与 capability。 - -### 16.2 WAL Gate - -- 同 `message_id` 同内容重试只得到同一 `seq`; -- 同 `message_id` 不同内容 fail-loud; -- commit 前崩溃没有 Message,commit 后 ACK 丢失不会重复 Message; -- 每个 Session 的 seq 唯一连续,projection/observer 失败不影响 commit; -- reply 只能指向同 Session 的 prior reactable Message;跨 Session、前向、重复或缺失 - cause fail-loud; -- assistant 的 reply target set 必须与 grant 的 exact cause set 相等;需要拆答时先由 - reactor 选择更小 cause set,不能消费一部分宽授权; -- Agent Program snapshot 与 cause/head 不匹配时不能取得可写 grant; -- 正常路径没有 UPDATE/DELETE Message;受控物理擦除只走 Data Management; -- U2 抢先提交时,基于旧 head 的 A 被拒绝;两个 worker 也只有一个能提交。 - -### 16.3 插件组合 Gate - -- Core/Bootstrap 不再 import 或构造 `PassiveMessageWorker`、`AgentLoop` 默认算法和 - `PassiveTurnPipeline`; -- Core 源码不按 passive/proactive/Wake/Scheduler/Citation/Meme/compaction/provider - 名称分支; -- 停用 `passive-conversation` 后,输入 Message、历史与同步仍工作,只停止自动回答; -- 停用 `MESSAGE_REACTOR`/`AGENT_PROGRAM` 后,独立 Tool recovery Effect 仍会结算已提交 - call;停用 Tool runtime 前必须 drain unresolved call; -- 替换 `AGENT_PROGRAM` fixture 无需修改 Core、WAL schema 或 channel adapter; -- 缺失/重复 Service 和 contribution 冲突在 candidate 发布前失败; -- Prompt 与 transform 次序确定,post-commit 插件不能修改 Message; -- 仓库外测试插件经正式安装链提供 `AGENT_PROGRAM`;报告明确区分 trusted in-process - 与 out-of-process sandbox,不把 API 检查冒充恶意代码隔离; -- 公共 `PluginRuntime` 不再暴露 broad workspace;未声明 path、raw SQL、candidate - production feed/effect 都在实际 owner 处拒绝; -- cross-session、user/system/assistant/tool/delete role spoof 与 revoked-generation append - 均被 `MessageWriteGrant` 边界拒绝; -- generation 下线后无遗留 task、listener、subscription、Root lease 或 tool binding lease。 - -### 16.4 行为 Gate - -- command short-circuit、附件、Prompt、compaction、Tool Search 和模型绑定都有目标 owner; -- provider 断网、retry 与 partial stream 不写 Session; -- `tool_call` 先提交再执行,crash 后按 `call_ref/tool_binding` 查询或安全恢复; -- 即时执行、feed replay 与启动恢复都调用同一个 - `TOOL_EXECUTOR.execute_or_recover(call_ref)`; -- Tool 可见性不能扩大真实授权,撤权产生明确 tool error; -- 不可确认的外部效果产生 `unknown`,不会盲目重复; -- 空回复 retry、terminal deadline、continuation、error reply 和 transform 有受控 fixture; -- `no_due` 不产生 Message;quiet 产生普通 `no_reply` assistant Message,Chat 隐藏; -- U1 command + U2 普通输入只确认 U1,U2 仍 pending;合并回答显式 reply U1/U2; -- tool_call reply、tool result 与最终 assistant reply 在 crash/replay 后形成同一条因果链; -- user Message durable 后即可 ACK,reply/delivery 失败不会抹掉输入; -- assistant Message durable 后,Delivery 从 immutable SessionRoute 建立 - `DeliveryEffect[message_id]`;prepared/provider_started/delivered/unknown/rejected/ - suppressed/settled 每个 crash 窗口都有恢复 fixture; -- `rejected` 永不重试;只有 provider 证明未产生效果时,`provider_started` 才能用原 - `message_id`/binding 安全重试; -- delete 先取得 exclusive lease 时 Tool/Delivery 不开始;effect 先取得 shared lease 时 - delete 等 result/unknown 后重算 closure; -- recovery scanner 看见已删除且缺失 DeliveryEffect 的 Message 时只建 `suppressed`; -- Web/Mobile 只用 `message_id + seq + cursor` 完成重复、断线和追赶; -- causal delete manifest 原子覆盖 reply/tool descendants;Chat、Model、Memory、Delivery - 与 Next action 一致处理,旧 retry 不会复活或重放 effect。 -- 只以 tool result 为 seed 时 closure 自动纳入对应 call Message;即使读取旧的不完整 - delete manifest,历史 result edge 也不会让 call 再执行。 - -### 16.5 迁移 Gate - -- Phase 1 每项行为都有 preserve/replace/bug 分类和可复跑 fixture; -- WAL shadow 重建与旧读取逐 Session 对账;无法转换的数据有显式阻塞清单; -- Scheduler/Wake/Drift/EventMail/Channel/subagent/`message_push` 各有 source state owner、 - handoff receipt、backup/restore 和 preserve/replace/retire 结论; -- 旧 delivery ledger 的每个非终态都能迁到同 `message_id` effect record,route/binding/ - receipt 不漂移; -- 切换点只有一个生产 writer、一个自动回复 subscriber,没有窗口式 dual-write; -- 数据迁移、delete closure、客户端 cursor reset、source continuity、delivery 与 generation - retention 均从备份做过恢复演练; -- 删除清单逐项证明静态 import、插件源码/cache、测试、数据库、日志和运行进程都无消费者。 - -### 16.6 明确接受的非目标 - -- 不从 Session 审计失败 provider attempt; +这条链只有 Message commit 能改变对话事实。Turn、Reaction、stream、feed watermark 和 +receipt 都可以丢失或重建。 + +## 十一、从 DSH 借什么 + +检查的 DSH 基线是 +`/mnt/data/source-code/deepseek-harness@49a606bc5b5934603f22a26957a07dc799ab0291`。 + +只借三条原则: + +1. 一份 ordered Session log 是 history 与 projection 的共同来源; +2. immutable Message representation 跨 history、model request 和 delivery 复用; +3. runtime scope、service injection、Fiber/Effect 是短命执行结构,不进入 Session。 + +对应源码证据: + +- `packages/llm/llm/src/message.ts:130`:immutable Message 跨消费者复用; +- `packages/llm/llm/src/message.ts:175`:Message 在 publication 前有稳定 ID; +- `packages/core/session/src/index.ts:628`:Session 单调 append; +- `packages/core/session/src/index.ts:772`:model messages 从 Session surface 派生; +- `packages/session/session-projection/src/index.ts:40`:projection 按 seq fold。 + +不照抄 DSH 的 SessionEvent/turn/step runtime。Akashic 的 Message 自己就是 WAL record。 + +## 十二、迁移按纵向切片,不一次切全部 owner + +本文是总设计,不是一个实现 PR。正式实现必须拆成独立任务合同;物理 purge 另立设计。 +每一刀都保持一个生产 writer,不长期 dual-write。 + +### Phase 0:批准破坏性语义 + +- 将“每条 Human 输入立即进入 Session”取代 completed transcript batch; +- 将 Turn 从持久 identity 改成普通 `TURN_VIEW` projection; +- 批准 author + responds_to schema、cause CAS 与默认 `NoNewHumanInputAfter`; +- 批准 typed writers、ephemeral Reaction/receipt 和 trusted message ID namespace; +- 批准 protected ToolHost、WAL generation reachability 与 multi-sink Delivery key; +- 批准 tombstone seed-only closure,物理 purge 延后; +- 分别批准各 source state 的 preserve/replace/retire; +- 逐项勘误或 supersede projectneed、0025、0034、0039、0050;核对 0052 的 compaction/ + Markdown ordinary-plugin owner 与新 TURN_VIEW/CONTEXT_VIEW 边界,不用“等”掩盖冲突; +- 建立全量备份与 schema lineage。 + +### Slice A:新 Message WAL 在旧 Agent 算法下运行 + +- 新 schema 支持 author、responds_to、typed content 和 canonical message_id; +- Channel 用 InboundWriter 立即 append Human U;旧 pipeline adapter 只读 WAL,不再批量补写 U; +- 旧 Agent 输出通过 AgentReplyWriter append;同一时刻只有新 writer 生效; +- 用 `U1/stop/U2/stop/P/A1` fixture 证明物理顺序与正文不丢。 + +### Slice B:Turn projection shadow 接管所有分组消费者 + +- `TURN_VIEW` 从 responds_to/resolves 构图,并 shadow 对比 UI、history、compaction、Markdown + 和 Akasha; +- 新数据不再依赖 control_turn_id;旧数据只在迁移 adapter 中读取已有 ID; +- 非连续 `{U1,U2,A1}`、standalone P、tool chain 与 incomplete group 均有 fixture; +- 全部消费者切换后删除各自的邻接角色/control ID 猜测。 + +### Slice C:ToolHost 与 Message recovery + +- tool_call 先 commit,ToolHost 后执行;result 只由 ToolResultWriter 写; +- startup snapshot-to-feed、provider query/idempotency/unknown 和 generation reachability + 先通过 crash fixture; +- 再删除旧 Attempt/tool item 作为恢复真源的路径。 + +### Slice D:被动大链插件化 + +- 先冻结 command、Prompt、compaction retry、Tool Search、empty reply、terminal tool、 + continuation、Citation/Meme、stream、Memory、delivery/ACK 的行为 oracle; +- 抽出普通 AGENT_PROGRAM 与短命 Reaction; +- 接入 `passive-conversation` 的 private reconcile 和 no-gap feed handoff; +- 用仓库外 Agent Program 完成真实回复,证明无需修改 Core/WAL/Channel; +- 最后删除 `PassiveMessageWorker → ConversationRuntime → AgentLoop._react → + PassiveTurnPipeline` 的固定业务编排和无消费者 phase/hook;原链拥有的 ingress custody、 + attachment durable、per-session admission、cancel 和资源清理由前三个 slice 明确迁到 + Channel/受保护 substrate 后才可删,不能随类名一起丢掉。 + +### Slice E:Delivery 与 source handoff + +- 迁移到 `(message_id,sink_id)` effect,不复制正文、不保留 settled; +- 冻结 terminal/command/error/proactive eligibility oracle;证明 Human/Source/Tool/no_reply/ + tombstone/tool-call intermediate 都不被默认投递; +- Channel、Scheduler、Wake、Drift、EventMail、message_push、subagent 逐项 handoff; +- 只有旧 owner 的 pending/ACK/cursor/journal 全部可恢复后才发布新 generation; +- 真实 provider 验证多 sink、ACK loss、unknown、rejected 和 tombstone race。 + +### Slice F:逻辑 tombstone + +- TombstoneWriter 只保存 normalized seeds; +- Chat、TURN_VIEW、Context、Memory、Delivery 与 ToolHost shadow 计算同一 Hidden closure; +- 切换后删除旧 interaction-delete 依赖,但不物理擦除正文; +- Data Purge 等维护者另行批准后再设计、实现和演练。 + +## 十三、验收 Gate + +### 13.1 事实与 schema Gate + +- Core conversation schema 只有 Session 与 Message; +- 没有 TurnId、RunId、AttemptId、SessionEvent、ReactionId、DeliveryId 或 ProjectionId; +- Message 使用 author,不持久化 provider `system` role; +- responds_to 是结构字段,不在 content blocks 中; +- tool_call 身份直接复用所在 Agent message_id,没有 ToolCallId/call_ref/block-index identity; +- canonical message_id 由认证 namespace 产生/验证,跨 Session/账号碰撞 fail-loud; +- 正常路径只 INSERT Message;初始 v4 没有物理 purge。 + +### 13.2 并发与 Turn Gate + +- U1、U2 在 ingress 时立即 durable,interrupt 不减少或延后它们; +- U2 先 commit 时,`NoNewHumanInputAfter` 拒绝只读过 U1 的旧 A; +- proactive P 先 commit 时,不拒绝 A1(responds_to U1,U2); +- TURN_VIEW 把非连续 U1/U2/A1 投影成一个 Turn,把 P 投影成独立 Turn; +- 时间线仍按 seq 显示 U1/U2/P/A1,Turn grouping 不重排 Message; +- Tool 等待期间插入 U2/P 时,默认 projection 得到 `{U1,M1,U2,T1,A2}` 与 `{P}`; +- seq22 的 U2 在 C1 unresolved 时是 deferred cause,不会先得到 A(U2);T1 后只签发 + `Reaction(T1,U2)`; +- cause 已被另一 Agent Message 结算时,旧 writer conflict; +- 只有显式选择 `HeadEquals` 的算法才因任意 head 变化重算; +- 替换 Turn plugin 不迁移或改写任何 Message。 + +### 13.3 Capability 与插件 Gate + +- 没有公开 `MESSAGE_REACTOR` 或通用 MessageWriteGrant 配置袋; +- Agent Program 只拿短命 Reaction,不拿裸 SessionStore、author、cause 或任意 append; +- Inbound/AgentReply/ToolResult/Source/StandaloneAgent/Tombstone writer 的越权调用均 + fail-loud; +- receipt、Reaction、Root lease、watermark 和 permit 均无 ID、不可序列化; +- ToolReceipt 只短命引用 call/result Message,不含正文或可恢复 Tool 状态; +- Core/Bootstrap 不再固定构造默认 Agent 算法或 PassiveTurnPipeline; +- Core 不按 passive/proactive/Wake/Scheduler/Citation/Meme/provider/plugin 名称分支; +- 仓库外 Agent Program 经正式 install/candidate/stable 链完成真实回复; +- trusted in-process 与 out-of-process sandbox 在报告中明确区分。 + +### 13.4 Feed 与恢复 Gate + +- Message commit 后、subscriber callback 前 crash,重启扫描仍会 reconcile 该 Session; +- scan 与实时 feed 重叠只重复唤醒,不重复 Message/effect; +- consumer 追到 snapshot watermark 和当前 head 前不报告 ready; +- ToolHost 从 WAL 恢复 unresolved call,不依赖 Agent Program 存活; +- generation GC 在 call commit、observer 前 crash 时仍拒绝删除 exact binding; +- provider 无 query/idempotency 时写 unknown,不盲重试。 + +### 13.5 Prompt、Tool 与行为 Gate + +- Source Message 永不映射为 system/developer instruction; +- Prompt slots 封闭,重复 owner/语义冲突 fail-loud,不用任意 priority 排序; +- 不存在通用 ASSISTANT_TRANSFORMS;Citation/Meme/媒体由 Agent Program + Tool 显式产生; +- Tool visibility 不能扩大 ToolHost authorization; +- 初始 v4 每条 Agent Message 至多一个 tool_call,unresolved 时不启动 continuation; +- command、attachment、compaction retry、Tool Search、empty reply、terminal、continuation、 + stream、error/no_reply 与 Memory 都有目标 owner 和 fixture; +- 停用 passive/Agent Program 后输入同步仍工作,committed Tool 仍由 ToolHost 结算。 + +### 13.6 Delivery、source 与 tombstone Gate + +- 同一 Message 可向两个 sink 发送而不复制正文或 message_id; +- `(message_id,sink_id)` 同 binding 幂等,binding 冲突 fail-loud; +- Delivery eligibility 只由 author/content/Hidden 投影;中间和非 Agent Message 不发送; +- rejected 永不重试,unknown 不伪装 exactly-once; +- Delivery delivered 后不等待或写 Wake settled;Wake 自己推进 ACK/cooldown; +- tombstone 先于 effect start 时 provider 不被调用;effect 先行时结果诚实结算并被隐藏; +- 只选 Tool result 时 seed 归一化到 call Message;晚到 result 自动进入 Hidden closure; +- 隐藏 Agent reply/tool result 不会让旧 cause/call 重跑; +- Scheduler/Wake/Drift/EventMail/Channel/subagent/message_push 的 state 都有 owner、备份和 + handoff 证明。 + +### 13.7 迁移 Gate + +- 每个 slice 只有一个正式 Message writer,没有窗口式 dual-write; +- Phase 1 行为账单逐项标成 preserve、intentional replacement 或 proven old bug; +- 旧 control_turn_id/attempt/delivery ledger 的每条数据都有转换、保留或人工阻塞结论; +- WAL、Turn view、ToolHost、Delivery、source state、client cursor 和 generation artifact + 都从备份完成恢复演练; +- 删除旧链前核对静态 import、动态插件、cache、测试、数据库、日志和运行进程消费者; +- Gate 证据绑定 exact clean commit/tree;concept Gate 的 must-fix 清零。 + +## 十四、明确接受的代价与仍需批准的问题 + +v4 明确接受: + +- Session 不审计失败 provider attempt; - 不恢复半截 token; -- 不给执行尝试分配稳定 ID; -- 不跨崩溃固定整套旧 Agent Program;只保留已提交 tool binding; -- 不保证外部 provider exactly-once,除非 provider 提供幂等或查询合同; -- 不让 UI 分组成为持久身份。 +- interrupt/cancel 不拥有持久身份; +- Turn grouping 可以随 Turn plugin 版本改变; +- 无幂等/query 的外部效果只能得到 unknown; +- 初始 v4 只有逻辑 tombstone,没有物理擦除; +- in-process Python plugin 是可信代码,不冒充 sandbox。 + +维护者仍需决定: -## 十七、对 v3 和前一版 v4 的最终减法 +- 默认 Turn plugin 是否永远不混合 Human 与 Source causes; +- multi-sink 的 sink discovery、用户配置 owner 与未来 eligibility 扩展流程; +- 各 source plugin state 的 preserve/replace/retire 清单; +- author 是否在未来多 Agent/多人 Session 中增加 stable principal ref;当前 v4 不预建。 -| 删除的东西 | 原因 | 现在由什么承担 | +## 十五、相对 v3 与前一版 v4 的最终减法 + +| 删除/替换 | 原因 | v4 归宿 | |---|---|---| -| MessageBody / SessionEntry 两层 | 同一正文两种 owner | Message WAL record | -| SessionEvent 壳 | Message 已经是日志记录 | Message | -| Turn / Run / Step / Attempt | 把短命执行升级成事实 | head CAS + Fiber + telemetry + projection | -| client/retry message IDs | 同一 Message 多个身份 | 唯一 `message_id` | -| ToolCallId | 可由调用所在位置确定 | `message_id + block index` | -| proactive/source 执行分支 | 来源不应改变执行语义 | 普通 input Message + 同一 reactor | -| DeliveryId / Turn-bound delivery body | 投递不能复制消息身份或正文 | `DeliveryEffect[message_id]` + route/receipt | -| ProjectionClaim | projection 不配拥有事实 | version + source seq cache | -| handled-prefix cursor | 会和消息因果关系漂移 | typed `reply(message_id)` graph | -| `scoped_messages` / 第二份 history 参数 | 会绕过 WAL freshness 与 CAS | `SESSION_READ` immutable view + observed head | -| Core 中的默认 AgentLoop | 产品算法不可替换 | 普通 `default-agent` 插件 | -| `PassiveTurnPipeline` 固定 phase DAG | 把业务、持久化和外部效果绑死 | `MESSAGE_REACTOR` + 普通依赖/contributions | -| `PassiveMessageWorker` 大 owner | 同时拥有 ingress、reply 和 delivery | Channel adapter + plugin Effect + Delivery projector | -| Citation/Meme/ToolSearch 等 Core 特判 | 产品能力侵入基础设施 | 普通插件 contribution/service | -| 通用 metadata/context 袋子 | 隐藏第二套协议和 owner | typed Message content + 窄 capability | - -最终不是“只做 WAL”,也不是“把旧 pipeline 移进插件目录”,而是三句可以独立验证的 -规则: +| Turn/Run/Attempt 持久实体 | 重复 Message 已能表达的事实与恢复 | 普通 TURN_VIEW + 短命 Reaction | +| completed transcript batch | 会让 U1/U2 在完成前不属于 canonical Session | ingress 立即 append 每条 Human Message | +| 全局 mandatory head CAS | 把无关 append 与回答有效性绑死 | cause CAS + typed optional read condition | +| 公开 MESSAGE_REACTOR | 只有插件自己一个 consumer | `passive-conversation` 私有 reconcile | +| 裸 `respond(session_id,cause_ids)` | 反复传可漂移身份并给程序宽能力 | `AGENT_PROGRAM.react(Reaction)` | +| 通用 MessageWriteGrant | 会长成 mode/role/variant 配置袋 | 分型短命 writer | +| persistent system role | 混淆来源与模型指令权限 | author + provider request projection | +| reply content block | 混淆正文与 cause settlement | Message.responds_to | +| client_message_id + message_id | 两份身份会让重试和引用分叉 | 认证边界产生一个 canonical message_id;seq 只管顺序 | +| ToolCallId / `(message_id, block index)` call_ref | 每条 Agent Message 只有一个 call 时是重复身份 | 直接复用 call Message 的 message_id | +| 普通插件拥有 TOOL_EXECUTOR 安全边界 | grant/binding/唯一结果不可随算法替换 | protected ToolHost + ordinary Tool plugins | +| ASSISTANT_TRANSFORMS | 共享可变正文和顺序耦合 | Agent Program 直接产出 final typed content | +| DeliveryEffect[message_id] 单 route | 对话事实与投递拓扑耦合 | DeliveryEffect[(message_id,sink_id)] | +| Delivery 中复制 seq/body digest | immutable Message 已拥有顺序和正文 | effect 只引用 message_id | +| Delivery settled | 跨 owner 解释 Wake/Scheduler 是否完成 | Delivery terminal receipt → source 自己结算 | +| 完整 delete closure manifest | 阻塞删除且遗漏未来 descendant | tombstone normalized seeds + dynamic closure | +| v4 内物理 purge | projection/artifact/恢复 owner 尚未闭合 | 独立 Data Purge 设计 | + +最终只剩四句话: ~~~text -事实:Session = ordered Message WAL -行为:ordinary plugins read Message and append Message -视图:projection = fold(Messages up to source_seq) -外部效果:effect record = existing message_id + provider observation +Session = Ordered(Message) +Pending = Fold(Messages) +Turn = PluginProjection(Messages) +Commit = CausesStillOpen + AuthorityValid + OptionalReadCondition ~~~ - -Core 只保护 append、权限、外部边界和插件生命周期。它不再决定 Agent 怎样回答, -也不再为回答过程发明另一套可持久化名词。