# Conflicts: # .agents/notes/implemented/architecture/2026-06-11-content-block-vocabulary.i18n.yaml # .agents/notes/implemented/architecture/2026-06-11-content-block-vocabulary.zh.md # .agents/notes/implemented/architecture/2026-06-11-event-sourced-sessions.i18n.yaml # .agents/notes/implemented/architecture/2026-06-11-microkernel-event-taxonomy.i18n.yaml # .agents/notes/implemented/architecture/2026-06-11-microkernel-event-taxonomy.zh.md # .agents/notes/implemented/architecture/2026-06-18-agent-lifecycle-and-ownership-seams.i18n.yaml # .agents/notes/implemented/architecture/2026-06-18-session-surface.i18n.yaml # .agents/notes/implemented/architecture/2026-06-21-bounded-llm-request-recovery.i18n.yaml # .agents/notes/implemented/architecture/2026-06-21-bounded-llm-request-recovery.zh.md # .agents/notes/implemented/architecture/2026-06-30-event-domain-semantics.i18n.yaml # .agents/notes/implemented/architecture/2026-06-30-event-domain-semantics.zh.md # .agents/notes/implemented/architecture/2026-07-05-reconstructable-requests.i18n.yaml # .agents/notes/implemented/architecture/2026-07-05-reconstructable-requests.zh.md # .agents/notes/implemented/architecture/2026-07-10-after-call-compaction-pressure-and-overflow-recovery.i18n.yaml # .agents/notes/implemented/architecture/2026-07-10-after-call-compaction-pressure-and-overflow-recovery.zh.md # .agents/notes/implemented/architecture/2026-07-10-single-file-executable-sdk-runtime-distribution.i18n.yaml # .agents/notes/implemented/architecture/2026-07-10-single-file-executable-sdk-runtime-distribution.zh.md # .agents/notes/implemented/architecture/2026-07-14-provider-routed-llm-adapters.i18n.yaml # .agents/notes/implemented/architecture/2026-07-15-replay-token-meter-service.i18n.yaml # .agents/notes/implemented/architecture/2026-07-16-explicit-turn-cancellation.i18n.yaml # .agents/notes/implemented/architecture/2026-07-22-unified-send-and-coalesced-user-messages.i18n.yaml # .agents/notes/implemented/architecture/2026-07-22-unified-send-and-coalesced-user-messages.zh.md # .agents/notes/implemented/architecture/2026-07-24-separate-context-injection-from-turn-execution.i18n.yaml # .agents/notes/implemented/architecture/2026-07-24-separate-context-injection-from-turn-execution.zh.md # .agents/notes/implemented/architecture/2026-07-25-web-client-session-scope-and-provide-channel.i18n.yaml # .agents/notes/implemented/architecture/2026-07-25-web-client-session-scope-and-provide-channel.zh.md # .agents/notes/implemented/architecture/2026-07-28-identified-immutable-message-values.i18n.yaml # .agents/notes/implemented/bug-fix/2026-07-21-semantic-session-checkpoints.i18n.yaml # .agents/notes/implemented/bug-fix/2026-07-21-semantic-session-checkpoints.zh.md # .agents/notes/implemented/feature/2026-06-18-compaction-capability-seam.i18n.yaml # .agents/notes/implemented/feature/2026-06-18-compaction-capability-seam.zh.md # .agents/notes/implemented/feature/2026-06-24-workspace-context.i18n.yaml # .agents/notes/implemented/feature/2026-06-24-workspace-context.zh.md # .agents/notes/implemented/feature/2026-06-30-hook-bridges.i18n.yaml # .agents/notes/implemented/feature/2026-06-30-hook-protocol-lib.i18n.yaml # .agents/notes/implemented/feature/2026-06-30-hook-protocol-lib.zh.md # .agents/notes/implemented/feature/2026-06-30-interception-seams.i18n.yaml # .agents/notes/implemented/feature/2026-06-30-interception-seams.zh.md # .agents/notes/implemented/feature/2026-07-06-sandbox.i18n.yaml # .agents/notes/implemented/feature/2026-07-06-sandbox.zh.md # .agents/notes/implemented/feature/2026-07-16-durable-per-step-time-context.i18n.yaml # .agents/notes/implemented/feature/2026-07-16-durable-per-step-time-context.zh.md # .agents/notes/implemented/feature/2026-07-16-harness-level-loop.i18n.yaml # .agents/notes/implemented/feature/2026-07-16-harness-level-loop.zh.md # .agents/notes/implemented/feature/2026-07-19-human-goal-command.i18n.yaml # .agents/notes/implemented/feature/2026-07-19-model-facing-goal-tools.i18n.yaml # .agents/notes/implemented/feature/2026-07-19-persisted-same-session-goal-domain.i18n.yaml # .agents/notes/implemented/feature/2026-07-19-persisted-same-session-goal-domain.zh.md # .agents/notes/implemented/feature/2026-07-19-plugin-command-registration.i18n.yaml # .agents/notes/implemented/feature/2026-07-19-same-session-goal-round-driver.i18n.yaml # .agents/notes/implemented/feature/2026-07-19-same-session-goal-round-driver.zh.md # .agents/notes/implemented/feature/2026-07-21-cross-session-references.i18n.yaml # .agents/notes/implemented/feature/2026-07-21-cross-session-references.zh.md # .agents/notes/implemented/feature/2026-07-27-tmux-location-context.i18n.yaml # .agents/notes/implemented/simplification/2026-06-20-public-agent-stop-surface.i18n.yaml # .agents/notes/implemented/simplification/2026-07-17-one-send-one-turn.i18n.yaml # .agents/notes/implemented/simplification/2026-07-17-one-send-one-turn.zh.md # .agents/notes/implemented/simplification/2026-07-20-unwrap-injected-content-envelopes.i18n.yaml # .agents/notes/implemented/simplification/2026-07-22-plan-specific-collaboration-state.i18n.yaml # .agents/notes/implemented/simplification/2026-07-22-plan-specific-collaboration-state.zh.md # .agents/notes/implemented/simplification/2026-07-24-agent-loop-observable-state-machine.i18n.yaml # .agents/notes/implemented/simplification/2026-07-27-request-error-retry-action.i18n.yaml # docs/core-data-structures/compaction.i18n.yaml # docs/core-data-structures/goal.i18n.yaml # docs/core-data-structures/goal.zh.md # docs/core-data-structures/llm-streaming.i18n.yaml # docs/core-data-structures/session.i18n.yaml # docs/core-data-structures/session.zh.md # docs/core-data-structures/skills.i18n.yaml # docs/core-data-structures/skills.zh.md # docs/core-data-structures/system-prompt.i18n.yaml # docs/defensive-patterns.i18n.yaml # docs/defensive-patterns.zh.md # docs/user/develop/framework/events.i18n.yaml # docs/user/develop/framework/events.zh.md # packages/acp/acp/README.i18n.yaml # packages/client/ui-goal/README.i18n.yaml # packages/compact/compact-basic/README.i18n.yaml # packages/compact/compact/README.i18n.yaml # packages/context/time-context/README.i18n.yaml # packages/context/tmux-context/README.i18n.yaml # packages/core/agent-loop/README.i18n.yaml # packages/core/agent-loop/README.zh.md # packages/core/agent/README.i18n.yaml # packages/core/agent/README.zh.md # packages/core/session/README.i18n.yaml # packages/core/session/README.zh.md # packages/core/system-prompt/README.i18n.yaml # packages/core/system-prompt/README.zh.md # packages/examples/cli-demo/README.i18n.yaml # packages/goal/command-goal/README.i18n.yaml # packages/goal/goal-session/README.i18n.yaml # packages/goal/goal/README.i18n.yaml # packages/goal/tool-goal/README.i18n.yaml # packages/goal/tool-goal/README.zh.md # packages/guard/README.i18n.yaml # packages/guard/README.zh.md # packages/guard/repeat-tool-guard/README.i18n.yaml # packages/hooks/hooks-claude/README.i18n.yaml # packages/hooks/hooks-codex/README.i18n.yaml # packages/host/apiproxy/README.i18n.yaml # packages/host/apiproxy/README.zh.md # packages/llm/llm/README.i18n.yaml # packages/plan/plan-mode/README.i18n.yaml # packages/plan/plan-mode/README.zh.md # packages/sdk/sdk-client/README.i18n.yaml # packages/sdk/sdk-client/README.zh.md # packages/sdk/sdk-protocol/README.i18n.yaml # packages/session-persistence/session-persistence/README.i18n.yaml # packages/session-persistence/session-persistence/README.zh.md # packages/skill/tool-skill/README.i18n.yaml # packages/subagent/subagent-dsh-sdk/README.i18n.yaml # packages/subagent/subagent-inprocess/README.i18n.yaml # packages/subagent/subagent-inprocess/README.zh.md # packages/ui/jsonrpc/README.i18n.yaml
7.5 KiB
Agent Note: 会话 surface:事件日志上的有序投影
Status: implemented
English | 中文
问题
事件日志是权威数据源,但历史操纵此前没有持久化的共享机制。如果没有这样的机制,上下文压缩(context compaction)等插件只能通过顺序敏感的监听器改写派生请求,不留溯源信息,且每次新增操纵都要反复修改 deriveMessages()。
决策
新增一个 surface:事件 seq 的派生、缓存有序投影(即产出 LLM(大语言模型)消息的事件子集),通过事件日志中的 surfaceOp 标记维护。
SessionEvent 新增两个顶层字段
每个 SessionEvent 获得两个可选字段(结构性元数据,与 seq/time 同级):
sourceEventSeqs?: number[]:作为溯源来源的事件 seq 编号(例如构成assistant/message的各assistant/chunk的 seq,或被压缩标记遮蔽的 surface 节点)。出现的[]只在assistant/message上有效,表示已知为空的提供方流;在该事件上省略字段表示旧数据或未记录的溯源。其他 surface 事件一旦出现此字段,就必须是非空列表。溯源是核心设计原则;没有它,replace-range 操作在回放时无法被验证。surfaceOp?: SurfaceOp:该事件如何进入 surface。非 surface 事件不携带此字段。
SurfaceOp:两种操作
export type SurfaceOp =
| 'append' // normal tail append
| { op: 'replace'; start: number; end: number } // shadow [start, end] inclusive
-
Append:在尾部追加新事件的 seq。
user/message、assistant/message、tool/result、context/message使用此操作。agent loop(智能体循环)在所有此类追加上传入surfaceOp: 'append',并在适用时记录sourceEventSeqs:每个成功的assistant/message都记录完整的assistant/chunk来源集合(包括[]),而tool/result记录其tool/call来源。 -
Replace:移除从
start到end(两端包含)的条目,并在其位置插入新事件的 seq。start和end都必须存在于当前 surface;start === end表示替换单个条目。该事件的sourceEventSeqs必须包含所有被遮蔽的 surface seq。被遮蔽的事件仍留在日志中,但不再出现在 surface 上。
SurfaceManager:基于增量,而非全量重建
一个 Session 拥有一个 SurfaceManager,后者维护事件 seq 的有序 number[]。管理器会在提交前校验每个种子或追加候选项而不应用它,然后只处理上次同步之后已经提交的事件,而不重新扫描整个日志。Session.surface 通过只读的 SessionSurface 契约暴露同一个管理器,因此接纳、派生历史、压缩与工作区上下文共享同一份增量状态。Replace 按数组位置定位两个端点(均包含在范围内),并把替换 seq splice 到该范围;不会用第二个管理器、链接对象或 seq 到节点的 map 来重复表达顺序。
无新事件时增量处理为 O(1),有新事件到达时为 O(新事件数)。
deriveMessages() 在存在 surface 标记时使用 surface,对没有标记的会话回退到既有的线性扫描(向后兼容)。
持久化
新字段作为顶层 JSON 属性序列化。JSONL 后端无需任何改动:JSON.stringify/JSON.parse 透明地保留一切。SQLite 后端的 events 表新增两个可空 TEXT 列(source_event_seqs、surface_op)。磁盘上的 SCHEMA_VERSION 递增以反映列集变化,并且按照预发布的 bump-and-reject 策略,由其他构建写入的数据库在打开时被拒绝而非迁移(没有需要升级的持久化用户数据)。会话格式 version 固定为 SESSION_FORMAT_VERSION = 0(「不稳定/预发布」立场):可选的 surface 字段被吸收而不递增版本号。
崩溃恢复
repair.ts 模块在崩溃后为孤立的工具调用合成 tool/result 闭合事件。这些闭合事件携带 surfaceOp: 'append' 和指向孤立 tool/call 事件的 sourceEventSeqs,确保重建的 surface 有效。
不变式
Session 在始终启用的 seed/append 边界校验 sourceEventSeqs 与 surfaceOp:只有 assistant/message 可以使用空的溯源列表;引用必须唯一、更早且已知;替换端点必须存在于 surface 顺序中;溯源必须覆盖每个被遮蔽的节点。这些是单记录接纳与存储投影规则,不是由可选的不变式服务提供的规则。
每个可进入 surface 的事件都必须携带 surfaceOp,否则它将从派生历史中消失。类型化的 append 重载对字面事件类型强制执行此规则;append 和种子构造函数中的运行时检查覆盖宽化联合类型和加载的日志。按照预发布格式策略,无效的种子被拒绝而非升级。
曾考虑的替代方案
- 逐插件的
agent/request包装(surface 之前的历史操纵模式):监听器排序脆弱、无法持久记录改动内容,且每种新操纵都迫使核心deriveMessages()再次修改。 - 半开区间
[start, endExclusive)的 replace 范围:否决。端点由 surface 事件 seq 命名,单条目替换(start === end)在闭区间语义下读起来更自然。 - 链接节点对象加 seq map:否决。生产代码不读取前驱链接,唯一的后继用途就是数组中的下一个位置,而替换本来就需要线性
indexOf查找。单个 seq 数组在保留相同渐进复杂度的同时,只留下一个需要校验的表示。 - 脏标记后全量重建替代增量处理:在会话生命周期内为 O(N²),每次单事件追加都要重新扫描所有先前事件。
后果
packages/core/session:surface.ts(SurfaceManager)维护一个用于候选接纳和实时投影的有序 seq 数组;SessionSurface是其只读公共视图。SurfaceOp/SurfaceIntent与顶层会话事件字段记录条目如何加入它。append()要求 surface 事件携带SurfaceIntent,deriveMessages()以遍历 surface 作为唯一派生路径,repair.ts则发出 surface 感知的闭合事件。种子构造函数拒绝缺少surfaceOp标记的可进入 surface 的种子事件(见「不变式」一节)。packages/core/agent-loop:所有涉及 surface 事件的追加操作都传入 surface 选项。收集分片 seq 用于assistant/message溯源;捕获tool/callseq 用于tool/result溯源。packages/session-persistence/session-persistence-sqlite:events表新增两个可空 TEXT 列(source_event_seqs、surface_op);SCHEMA_VERSION递增(bump-and-reject,无迁移)。packages/session-persistence/session-persistence-jsonl:无需改动。packages/session-persistence/session-persistence:抽象接口不变。
Surface 是未来历史操纵的基础。压缩或 tool-result-prune 插件追加一个既有的消息产出事件类型(例如一条携带摘要的 user/message),附带 surfaceOp: { op: 'replace', start, end } 和覆盖被遮蔽条目的 sourceEventSeqs——新事件在 surface 上取代该范围的位置,而插件自身的 trace 事件(如 compaction/start、compaction/end)不进入 surface。回放以确定性方式保留该决策。
一次 tool/result 替换只能改写当前的一个 tool/result,并且必须保留除 content 以外的每个数据字段。Session 接纳会与位置范围和溯源校验一起强制这条规则,不依赖可选的诊断插件。