# Agent Note: 持久化的同会话目标领域

Status: implemented

[English](2026-07-19-persisted-same-session-goal-domain.md) | 中文

## 问题

长时间运行的目标会跨越单个提示词、轮次或模型请求。若把该目标视为内存中的循环变量，进程重启时就会丢失；若只存放在 UI 状态中，又无法重建模型行为。若把会话中的每个轮次都视为目标进度，与自动工作无关的人类消息也会消耗预算。

持久化的生命周期与继续执行的权限是两个不同事实。会话在重启或 fork（派生）后可以保留活跃目标，但用户打开会话时静默启动工作并不符合直觉。该领域需要可回放的状态，却不能持久化自动执行权限；它还必须作为公共 agent（智能体）与会话服务上的插件存在，而不是具体循环中的特例。

## 决策

位于 `packages/goal/goal/` 的 `@deepseek-ai/dsh-goal` 通过 `ctx.goals` 管理一个当前的同会话目标。目标包含带品牌的 id、目标描述、持久化阶段、比较并交换修订号和 `maxGoalRounds`。`defaultMaxGoalRounds` 是经过校验的部署配置，默认值为 `256`；`create()` 在变更前于内部将其解析为完整值，而不会把解析过程暴露为额外的服务动词。

持久阶段包括 `active`、`paused`、`blocked` 和 `complete`。阻塞快照包含由策略定义的全小写 kebab-case 代码和规范化自由文本消息，因此用量限制、回合上限、执行失败和等待人工输入可以共享一个生命周期状态而不丢失原因。独立的实时激活态为 `armed` 或 `disarmed`。创建与显式恢复会激活目标；暂停、完成、阻塞和清除都会解除激活。编辑保留激活态及阻塞原因；恢复和完成会清除该原因。持久快照绝不包含激活态。

### 持久记录与回放

每次变更都会追加带版本的 `goal/change` 会话事件，其中包含完整快照；清除时则包含带修订号的墓碑。会话日志是唯一的持久真源，因此持久化和 fork 会继承目标记录，而无需另设数据库或头字段。[目标自有持久事件决策](../architecture/2026-07-31-goal-owned-durable-events.md)负责将目标持久事件与收件箱状态和模型上下文分离。

回放折叠只从 `goal/change` 派生生命周期变更，并校验 JSON 形状、新 id、修订连续性、生命周期转换、计数器以及单个目标内单调递增的时间戳。只有当前活跃修订上带正数且连续编号、已接纳的 `user/message` 来源才会推进 Goal Round，且不能超过 `maxGoalRounds`；普通会话轮次不会影响该计数器。当前格式的畸形记录会使回放失败，而不会被忽略或修复。

增量回放会在每个有效事件后推进游标，并停留在首个损坏事件处，因此后续读取会报告同一个持久故障。重启后仍以持久日志为准。

### 生命周期与实时激活态

最多只有一个当前目标。创建要求不存在未完成的当前目标，并始终生成该会话此前未使用过、修订号为一的 id；已完成目标可以被替换。其他每次变更都携带预期的 `GoalRef`，陈旧的 id 或修订号会被拒绝。仅当回合上限仍有余量时，暂停或阻塞阶段以及已解除激活的活跃目标才能恢复。领域层校验阻塞原因的形状，但会把原因代码和是否阻塞的决策留给策略消费者。

从任何种子构建的缓存都以未激活状态开始，每次 `agent/session-start` 边沿也会再次解除激活。`GoalService.disarm(agent)` 还允许生命周期所有者移除进程内权限，而不写入会话事件、不改变修订号，也不发出 `goal/changed` 通知。因此，会话恢复、fork 和继续执行驱动器替换都会保留持久化目标与历史，但绝不会自行启动工作。后续人类提示词可由模型解释，其策略表面可以显式调用恢复操作并激活目标。

### 服务边界

服务只接受在对应 id 下注册的同一个实时 `Agent` 对象。变更提交后，它会发出带作用域的 `goal/changed` 事件，并隔离监听器失败。策略消费者通过本服务、公共 `Agent` 接口和 `agent/*` 事件工作；目标领域既不导入也不修改 `dsh-agent-loop`。

## 测试

单元测试固定创建默认值、精确实时 agent 校验、比较并交换拒绝、所有生命周期转换、阻塞原因校验与保留、恢复时的上限执行、清除与替换、种子回放和 `SessionStore.fork()` 继承、会话启动与生命周期所有者解除激活、活跃目标重新激活、持久事件折叠、收件箱独立性、损坏事件的稳定回放、服务与监听器销毁、监听器隔离、挂钟后退钳制、严格记录解码、生命周期连续性，以及连续 Goal Round 归属。无密钥 Loader/stdio 进程测试通过测试专用 `cordis.yml` 挂载服务与生命周期消费方，再从外部读取持久 JSONL，以验证目标记录以及不存在未经请求的 Goal Round。包源码受仓库逐文件 100% 覆盖率门禁约束。

## 考虑过的替代方案

- **把目标存入独立数据库或会话头**——不予采纳，因为会话日志已经提供顺序、持久化、fork 前缀与可重建性；第二份存储会引入原子性和谱系问题。
- **把每次持久变更与排队的模型上下文绑定。** 后续的 [Goal 自有持久事件决策](../architecture/2026-07-31-goal-owned-durable-events.md)不采用这一方案：目标工具与已调度的继续执行提示词会在需要时暴露状态，而领域持久化不依赖队列结果。
- **持久化激活态并自动重启**——不予采纳，因为打开或恢复会话时必须等待人类输入；持久阶段记录状态，而不是再次消耗资源的授权。
- **把所有会话轮次都计为 Goal Round**——不予采纳，因为同一会话可以包含人类澄清、检查和无关工作；只有归属于目标的继续执行轮次才消耗该预算。
- **向 `dsh-agent-loop` 添加目标状态或通用循环抽象**——不予采纳，因为状态与继续执行策略可以通过现有插件、`Agent` 动词和事件组合，而无需赋予默认循环实现特权。

## 后果

- 目标历史作为普通会话数据，在持久化、恢复、无关节点压缩和会话 fork 后继续保留。
- 恢复与 fork 会暴露同一持久阶段，但在显式恢复变更激活目标前不会执行任何操作。
- 完整快照便于检查、严格回放与 last-wins 投影，且不会向模型历史添加只用于变更的消息。
- 修订号与生命周期校验会尽早拒绝遭篡改、部分写入或生产者不一致的目标记录。
- 回合上限只约束继续执行次数；当回合、token、费用、时间或提供方限制停止工作时，策略消费者会把它们映射为不同的阻塞原因。

## 已知限制与暂缓事项

- 本领域记录状态，但不调度 Goal Round、不取消活跃轮次，也不分类异常停止。
- 记录 `complete` 或 `blocked` 的参与者具有最终权威；独立评估器或完成证书延期到策略消费者中实现。
- 每个会话只有一个当前目标；不存在并行目标图和跨会话目标存储。
- 插件共享同一个受信任的进程边界。直接写入会话的插件可以伪造目标记录；严格回放会检测不一致并在违规记录处使目标访问失败，但不会隔离插件或修复日志。
- `GOAL_CHANGE_VERSION` 在首次发布前不承诺兼容性，也不提供迁移路径。
