DSH / Atlas
2026-09-03implementedbug-fixarchived 2026-09-30

User-owned goal pause exposes live activation

用户独占的 goal 暂停并暴露实时激活态

The host-pause fix in [Host-initiated goal pause aborts the live turn](../../archived/bug-fix/2026-09-01-host-goal-pause-aborts-turn.md) stopped the current model turn, but a later human turn could still use `update_goal resume` to lift a durable `paused` goal. The Web strip also read only the durable `goal` projection, so an active-but-disarmed goal and an armed goal rendered identically and offered the same pause a

English

Problem

The host-pause fix in Host-initiated goal pause aborts the live turn stopped the current model turn, but a later human turn could still use update_goal resume to lift a durable paused goal. The Web strip also read only the durable goal projection, so an active-but-disarmed goal and an armed goal rendered identically and offered the same pause action.

Decision

ctx.goals.get is a read-only Remote method. GoalService emits goal/activation-changed whenever its process-local activation changes, with { sessionId, goal: { id, revision, activation } } or no goal after a clear. The API Remote allowlist forwards that JSON payload to Web clients.

The GoalBar consumes a registrant-private activation hook source created by its slot inject. The source starts while the framework hook observes it, reads ctx.remote.goals.get, subscribes to goal/activation-changed, and refreshes on running-state or connection resets. Activation edges advance an epoch that invalidates in-flight reads, so a stale HTTP response cannot overwrite a newer edge; running refreshes retain the last activation until the read resolves. Active goals render Ongoing Goal only when armed; active-but-disarmed goals render Inactive Goal, expose resume instead of pause, and durable paused goals keep exposing resume. Pause authority remains in the goal domain and human /goal resume command, which can still resume every resumable phase.

The update_goal resume action rejects a durable paused goal with GOAL_TOOL_RESUME_PAUSED before calling the goal service. It still resumes an active-but-disarmed goal after session restore or fork and a blocked goal after human continuation. The model prompt and tool description state that the user owns durable paused resume.

Alternatives considered

Store activation in the durable GoalSnapshot. Rejected: activation is process-local by the goal domain contract and must not survive restore or fork.

Add activation to the persisted session projection. Rejected: projection state is checkpointed; a cached armed value would incorrectly outlive the process that armed it.

Forward the full scoped goal/changed event to clients. Rejected: its Agent payload is not JSON wire data. The dedicated activation event carries only the session id, goal ref, and activation clients need.

Let the model resume durable paused goals from natural-language turns. Rejected: a manual pause is a user control, and prompt-only restraint leaves the same turn-level undo available to the model.

Consequences

The Web can distinguish running, disarmed, and paused goals without persisting activation. A durable paused goal is resumable only through the Web control, /goal resume, or another direct goal-service caller; model update_goal resume is limited to disarmed-active and blocked goals. The API surface gains one read and one forwarded live event; durable goal change payloads and projection state versions are unchanged. Components own no Remote subscriptions; the activation source follows the established inject-hooks live-data channel.

Testing

Goal unit tests pin the activation event id and revision across create, session start, and resume. Tool tests pin rejection of a durable paused goal in a later human turn while restored disarmed-active goals still resume. API Remote tests pin JSON forwarding. Activation-source tests pin stale-read rejection and running-refresh retention. Web unit tests pin armed pause versus disarmed resume rendering. The assembled goal-bar browser scenario uses the fixture timing hook to pin both armed and active-disarmed goldens.

中文

问题

宿主发起的 goal 暂停中止当前轮次 修复了当前模型轮次不停止的问题,但之后的人类轮次仍可通过 update_goal resume 解除持久的 paused goal。Web 条带也只读取持久的 goal 投影,因此 active-but-disarmed 的 goal 与 armed 的 goal 渲染相同,并提供相同的暂停动作。

决策

ctx.goals.get 现在是一个只读 Remote 方法。GoalService 在进程本地 activation 变化时发出 goal/activation-changed,载荷为 { sessionId, goal: { id, revision, activation } },clear 后则不携带 goal。API Remote 允许列表把这份 JSON 载荷转发给 Web 客户端。

GoalBar 消费由 slot inject 创建的 registrant-private activation hook source。该 source 仅在框架 hook 观察期间启动,读取 ctx.remote.goals.get、订阅 goal/activation-changed,并在 running 状态或连接 reset 时刷新。activation 边界推进 epoch,使在途读取失效,因此较旧的 HTTP 响应不能覆盖更新的边界;running 刷新会保留最后一次 activation,直到读取完成。Active goal 仅在 armed 时渲染 Ongoing Goal;active-but-disarmed goal 渲染 Inactive Goal,暴露 resume 而不是 pause;持久 paused goal 继续暴露 resume。暂停权威仍属于 goal 领域和人类 /goal resume 命令,它们仍可恢复每个可恢复 phase。

update_goal resume 会在调用 goal 服务前用 GOAL_TOOL_RESUME_PAUSED 拒绝持久 paused goal。它仍会在会话恢复或 fork 后恢复 active-but-disarmed goal,并在人类要求继续时恢复 blocked goal。模型提示词和工具描述说明持久 paused 的恢复由用户独占。

考虑过的替代方案

把 activation 存入持久 GoalSnapshot。 否决:按 goal 领域约定,activation 是进程本地的,绝不能跨恢复或 fork 存活。

把 activation 加入持久 session projection。 否决:投影状态会写入检查点;缓存的 armed 会在武装它的进程消失后继续错误存在。

把完整的 scoped goal/changed 事件转发给客户端。 否决:其 Agent 载荷不是 JSON wire 数据。专用 activation 事件只携带客户端需要的 session id、goal ref 与 activation。

允许模型从自然语言轮次恢复持久 paused goal。 否决:人工暂停是用户控制,仅靠提示词约束仍会把同轮撤销能力留给模型。

后果

Web 无需持久化 activation 就能区分运行中、disarmed 与 paused goal。持久 paused goal 只能通过 Web 控件、/goal resume 或其他直接调用 goal 服务的调用方恢复;模型 update_goal resume 仅限 disarmed-active 与 blocked goal。API 表面新增一个读取和一个转发 live 事件;持久 goal change 载荷与投影 stateVersion 不变。组件不持有 Remote 订阅;activation source 遵循既有的 inject-hooks live-data 通道。

测试

Goal 单元测试固定 create、session start 与 resume 过程中 activation 事件的 id 与 revision。工具测试固定后续人类轮次中持久 paused goal 的拒绝,同时保留已恢复 disarmed-active goal 的恢复。API Remote 测试固定 JSON 转发。Activation-source 测试固定 stale read 拒绝与 running 刷新保留旧值。Web 单元测试固定 armed 显示 pause、disarmed 显示 resume;组装的 goal-bar 浏览器场景通过 fixture timing hook 同时固定 armed 与 active-disarmed golden。