Persisted same-session goal domain
持久化的同会话目标领域
A long-running objective outlives one prompt, turn, or model request. Treating that objective as an in-memory loop variable loses it on process restart, while putting it only in UI state makes model behavior impossible to reconstruct. Treating every session turn as progress also charges unrelated human messages against an automatic-work budget. Durable lifecycle and permission to continue are different facts. A sessi
English
Problem
A long-running objective outlives one prompt, turn, or model request. Treating that objective as an in-memory loop variable loses it on process restart, while putting it only in UI state makes model behavior impossible to reconstruct. Treating every session turn as progress also charges unrelated human messages against an automatic-work budget.
Durable lifecycle and permission to continue are different facts. A session may retain an active objective after restart or fork, but silently starting work when a user opens that session is surprising. The domain needs replayable state without persisted auto-execution authority, and it must remain a plugin on the public agent and session services rather than a special case in the concrete loop.
Decision
@deepseek-ai/dsh-goal in packages/goal/goal/ owns one current same-session goal through ctx.goals. A goal has a branded id, objective, durable phase, compare-and-set revision, and maxGoalRounds. defaultMaxGoalRounds is a validated deployment setting with default 256; create() materializes it internally before mutation rather than exposing resolution as another service verb.
The durable phases are active, paused, blocked, and complete. A blocked snapshot includes a policy-owned lower-kebab-case code and a normalized free-form message, so usage limits, round caps, execution failures, and human-input dependencies share one lifecycle state without losing their cause. A separate live activation is armed or disarmed. Creation and explicit resume arm activation; pause, completion, blocking, and clear disarm it. Edits preserve activation and any blocker reason; resume and completion clear that reason. Activation is never part of the persisted snapshot.
Durable record and replay
Every mutation appends a versioned goal/change session event containing a full snapshot or, for clear, a revisioned tombstone. The session log is the only durable source of truth, so persistence and fork inherit goal records without another database or header field. The goal-owned durable event decision owns the separation from inbox state and model context.
The replay fold derives lifecycle mutations only from goal/change and validates JSON shape, fresh ids, revision continuity, lifecycle transitions, counters, and monotonic per-goal timestamps. Goal rounds advance only from positive sequential admitted user/message source numbers for the current active revision and cannot exceed maxGoalRounds; ordinary session turns do not affect the counter. A malformed current-format record fails replay rather than being ignored or repaired.
Incremental replay advances its cursor after each valid event and remains positioned at the first corrupt event, so later reads report the same durable fault. The durable log remains authoritative after restart.
Lifecycle and live activation
At most one goal is current. Create requires no current non-complete goal and always generates a revision-one id not used earlier in the session; a completed goal may be replaced. Every other mutation carries the expected GoalRef, and stale ids or revisions reject. Resume accepts a paused or blocked phase, or a disarmed active goal, only when the round cap has remaining capacity. The domain validates blocker reason shape but deliberately leaves reason codes and the decision to block to policy consumers.
A cache built from any seed starts disarmed, and every agent/session-start edge disarms it again. GoalService.disarm(agent) also lets a lifecycle owner remove process-local authority without a session event, revision change, or goal/changed notification. Resume, fork, and continuation-driver replacement therefore preserve the durable objective and history but never initiate work on their own. A later human prompt can be interpreted by the model, whose policy API may explicitly call resume and arm the goal.
Service boundary
The service accepts only the exact live Agent object registered under its id. A committed mutation emits the scoped goal/changed event with contained listener failures. Policy consumers use this service plus the public Agent interface and agent/* events; the goal domain does not import or modify dsh-agent-loop.
Testing
Unit coverage pins creation defaults, exact-live-agent checks, compare-and-set rejection, every lifecycle transition, blocker reason validation and retention, cap enforcement on resume, clear/replacement, seeded replay and SessionStore.fork() inheritance, session-start and lifecycle-owner disarming, active-goal rearming, durable event folding, inbox independence, stable corrupt-event replay, service/listener disposal, listener containment, backward-clock clamping, strict record decoding, lifecycle continuity, and sequential round attribution. A keyless Loader/stdio process test mounts the service and a lifecycle consumer through test-only cordis.yml, then reads the persisted JSONL externally to verify the goal record and absence of an unrequested goal round. The package source is held to the repository's per-file 100% coverage gate.
Alternatives considered
- Store goals in a separate database or session header — rejected because the session log already supplies ordering, persistence, fork prefixes, and reconstructability; a second store introduces atomicity and lineage questions.
- Couple each durable mutation to queued model context — rejected by the later goal-owned durable event decision: goal tools and scheduled continuation prompts expose state when needed, while domain persistence remains independent from queue outcomes.
- Persist activation and restart automatically — rejected because opening or resuming a session must wait for human input; durable phase records status, not fresh authority to spend resources.
- Count all session turns as goal rounds — rejected because one session can contain human clarification, inspection, and unrelated work; only goal-attributed continuation turns consume this budget.
- Add goal state or a generic loop abstraction to
dsh-agent-loop— rejected because state and continuation policy can compose through existing plugins,Agentverbs, and events without privileging the shipped loop implementation.
Consequences
- Goal history survives persistence, resume, compaction of unrelated nodes, and session fork as ordinary session data.
- Resume and fork expose the same durable phase while remaining operationally inert until an explicit resume mutation arms activation.
- Full snapshots simplify inspection, strict replay, and last-wins projection without adding mutation-only messages to model history.
- Revision and lifecycle validation reject tampered, partially written, or producer-inconsistent goal records early.
- Round caps bound continuation count only; policy consumers map round, token, currency, time, and provider limits to blocked reasons when they stop work.
Known limitations and deferred work
- This domain records state but does not schedule goal rounds, cancel active turns, or classify abnormal stops.
- The actor that records
completeorblockedis authoritative; an independent evaluator or completion certificate is deferred to a policy consumer. - There is one current goal per session; parallel objective graphs and cross-session goal storage are absent.
- Plugins share one trusted process boundary. Direct session writers can counterfeit goal records; strict replay detects inconsistency and fails goal access at the offending record, but does not isolate plugins or repair the log.
GOAL_CHANGE_VERSIONhas no pre-release compatibility promise or migration path.
中文
问题
长时间运行的目标会跨越单个提示词、轮次或模型请求。若把该目标视为内存中的循环变量,进程重启时就会丢失;若只存放在 UI 状态中,又无法重建模型行为。若把会话中的每个轮次都视为目标进度,与自动工作无关的人类消息也会消耗预算。
持久化的生命周期与继续执行的权限是两个不同事实。会话在重启或 fork 后可以保留活跃目标,但用户打开会话时静默启动工作并不符合直觉。该领域需要可回放的状态,却不能持久化自动执行权限;它还必须作为公共 agent(智能体)与会话服务上的插件存在,而不是具体循环中的特例。
决策
位于 packages/goal/goal/ 的 @deepseek-ai/dsh-goal 通过 ctx.goals 管理一个当前的同会话目标。目标包含带品牌的 id、目标描述、持久化阶段、比较并交换修订号和 maxGoalRounds。defaultMaxGoalRounds 是经过校验的部署配置,默认值为 256;create() 在变更前于内部将其解析为完整值,而不会把解析过程暴露为额外的服务动词。
持久阶段包括 active、paused、blocked 和 complete。阻塞快照包含由策略定义的全小写 kebab-case 代码和规范化自由文本消息,因此用量限制、Goal Round 上限、执行失败和等待人工输入可以共享一个生命周期状态而不丢失原因。独立的实时激活态为 armed 或 disarmed。创建与显式恢复会激活目标;暂停、完成、阻塞和清除都会解除激活。编辑保留激活态及阻塞原因;恢复和完成会清除该原因。持久快照绝不包含激活态。
持久记录与回放
每次变更都会追加带版本的 goal/change 会话事件,其中包含完整快照;清除时则包含带修订号的墓碑。会话日志是唯一的持久真源,因此持久化和 fork 会继承目标记录,而无需另设数据库或头字段。目标自有持久事件决策负责将目标持久事件与收件箱状态和模型上下文分离。
回放折叠只从 goal/change 派生生命周期变更,并校验 JSON 形状、新 id、修订连续性、生命周期转换、计数器以及单个目标内单调递增的时间戳。只有当前活跃修订上带正数且连续编号、已接纳的 user/message 来源才会推进 Goal Round,且不能超过 maxGoalRounds;普通会话轮次不会影响该计数器。当前格式的畸形记录会使回放失败,而不会被忽略或修复。
增量回放会在每个有效事件后推进游标,并停留在首个损坏事件处,因此后续读取会报告同一个持久故障。重启后仍以持久日志为准。
生命周期与实时激活态
最多只有一个当前目标。创建要求不存在未完成的当前目标,并始终生成该会话此前未使用过、修订号为一的 id;已完成目标可以被替换。其他每次变更都携带预期的 GoalRef,陈旧的 id 或修订号会被拒绝。仅当 Goal Round 上限仍有余量时,暂停或阻塞阶段以及已解除激活的活跃目标才能恢复。领域层校验阻塞原因的形状,但会把原因代码和是否阻塞的决策留给策略消费方。
从任何种子构建的缓存都以未激活状态开始,每次 agent/session-start 边沿也会再次解除激活。GoalService.disarm(agent) 还允许生命周期所有者移除进程内权限,而不写入会话事件、不改变修订号,也不发出 goal/changed 通知。因此,会话恢复、fork 和继续执行驱动器替换都会保留持久化目标与历史,但绝不会自行启动工作。后续人类提示词可由模型解释,其策略 API 可以显式调用恢复操作并激活目标。
服务边界
服务只接受在对应 id 下注册的同一个实时 Agent 对象。变更提交后,它会发出带作用域的 goal/changed 事件,并隔离监听器失败。策略消费方通过本服务、公共 Agent 接口和 agent/* 事件工作;目标领域既不导入也不修改 dsh-agent-loop。
测试
单元测试固定创建默认值、精确实时 agent 校验、比较并交换拒绝、所有生命周期转换、阻塞原因校验与保留、恢复时的 Goal Round 上限约束、清除与替换、种子回放和 SessionStore.fork() 继承、会话启动与生命周期所有者解除激活、活跃目标重新激活、持久事件折叠、收件箱独立性、损坏事件的稳定回放、服务与监听器销毁、监听器隔离、挂钟后退钳制、严格记录解码、生命周期连续性,以及连续 Goal Round 归属。无密钥 Loader/stdio 进程测试通过测试专用 cordis.yml 挂载服务与生命周期消费方,再从外部读取持久 JSONL,以验证目标记录以及不存在未经请求的 Goal Round。包源码受仓库逐文件 100% 覆盖率门禁约束。
考虑过的替代方案
- 把目标存入独立数据库或会话头——不予采纳,因为会话日志已经提供顺序、持久化、fork 前缀与可重建性;第二份存储会引入原子性和谱系问题。
- 把每次持久变更与排队的模型上下文绑定。 后续的 目标自有持久事件决策 不采用这一方案:目标工具与已调度的继续执行提示词会在需要时暴露状态,而领域持久化不依赖队列结果。
- 持久化激活态并自动重启——不予采纳,因为打开或恢复会话时必须等待人类输入;持久阶段记录状态,而不是再次消耗资源的授权。
- 把所有会话轮次都计为 Goal Round——不予采纳,因为同一会话可以包含人类澄清、检查和无关工作;只有归属于目标的继续执行轮次才消耗该预算。
- 向
dsh-agent-loop添加目标状态或通用循环抽象——不予采纳,因为状态与继续执行策略可以通过现有插件、Agent动词和事件组合,而无需赋予默认循环实现特权。
后果
- 目标历史作为普通会话数据,在持久化、恢复、无关节点压缩(compaction)和会话 fork 后继续保留。
- 恢复与 fork 会暴露同一持久阶段,但在显式恢复变更激活目标前不会执行任何操作。
- 完整快照便于检查、严格回放与 last-wins 投影,且不会向模型历史添加只用于变更的消息。
- 修订号与生命周期校验会尽早拒绝遭篡改、部分写入或生产者不一致的目标记录。
- Goal Round 上限只约束继续执行次数;当 Goal Round、token、费用、时间或提供方限制停止工作时,策略消费方会把它们映射为不同的阻塞原因。
已知限制与暂缓事项
- 本领域记录状态,但不调度 Goal Round、不取消活跃轮次,也不分类异常停止。
- 记录
complete或blocked的参与者具有最终权威;独立评估器或完成证书延期到策略消费方中实现。 - 每个会话只有一个当前目标;不存在并行目标图和跨会话目标存储。
- 插件共享同一个受信任的进程边界。直接写入会话的插件可以伪造目标记录;严格回放会检测不一致并在违规记录处使目标访问失败,但不会隔离插件或修复日志。
GOAL_CHANGE_VERSION在首次发布前不承诺兼容性,也不提供迁移路径。