# 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`。创建与显式恢复会激活目标；暂停、完成、阻塞和清除都会解除激活。编辑保留激活态及阻塞原因；恢复和完成会清除该原因。持久快照绝不包含激活态。

### 持久记录与回放

每次非清除变更都通过 `Agent.inject()` 追加一条模型可见的 `context/message`，其中包含带版本的完整快照；会话会将其内容原样投射给模型。清除操作追加带修订号的墓碑。上下文来源为 `{ kind: 'goal', goalId, revision, round: 0 }`；元数据必须与渲染后的 `<goal_state>...</goal_state>` 内容完全一致。这个描述性分隔符沿用了仓库已有的 `<workspace_context>` 约定，也符合 [Anthropic 关于用一致且描述明确的 XML 标签组织混合提示词内容的公开指南](https://platform.claude.com/docs/en/build-with-claude/prompt-engineering/claude-prompting-best-practices#structure-prompts-with-xml-tags)。这是公开的模型体验先例，并非对任何提供方专有训练语料的推断。会话日志是唯一的持久事实来源，因此持久化和 fork 会继承目标记录，而无需另设数据库或头字段。

回放折叠会校验 JSON 形状、来源归属、渲染内容、新 id、修订连续性、生命周期转换、计数器以及单个目标内单调递增的时间戳。目标回合是当前活跃修订上带正数且连续编号的 `user/message` 来源，且不能超过 `maxGoalRounds`；普通会话轮次不会影响该计数器。当前格式的畸形记录会使回放失败，而不会被忽略或修复。

当 `Agent.inject()` 在活跃工具批次中延迟变更时，服务会在进程内叠加已接受的载荷，使后续变更可以使用新的修订号。FIFO 追加可见后，协调过程只移除完全匹配的载荷；重入的追加观察器对每次变更只投影一次。增量回放会在每个有效事件后推进游标，并停留在首个损坏事件处，因此后续读取会报告同一个持久故障。重启后仍以持久日志为准。

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

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

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

### 服务边界

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

## 测试

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

## 考虑过的替代方案

- **把目标存入独立数据库或会话头**——不予采纳，因为会话日志已经提供顺序、持久化、fork 前缀与可重建性；第二份存储会引入原子性和谱系问题。
- **使用模型不可见的纯日志事件**——不予采纳，因为会改变后续模型行为的持久状态必须满足仓库日志不变量，保持模型可见且可重建。
- **持久化激活态并自动重启**——不予采纳，因为打开或恢复会话时必须等待人类输入；持久阶段记录状态，而不是再次消耗资源的授权。
- **把所有会话轮次都计为目标回合**——不予采纳，因为同一会话可以包含人类澄清、检查和无关工作；只有归属于目标的继续执行轮次才消耗该预算。
- **向 `dsh-agent-loop` 添加目标状态或通用循环抽象**——不予采纳，因为状态与继续执行策略可以通过现有插件、`Agent` 动词和事件组合，而无需赋予默认循环实现特权。

## 后果

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

## 已知限制与延期工作

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