DSH / Atlas
2026-08-10implementedfeature

Continuable subagent policy inheritance — the durable child log owns the delegation-time snapshot

可继续 subagent 策略继承——持久化子日志拥有委派时快照

The one-shot in-process driver has seeded parent sandbox/approval overrides into its children since the [in-process policy-inheritance decision](2026-07-25-subagent-policy-inheritance.md), but the continuable path never did: `SubagentContinuationManager` materialization applied only child composition and the activation setup registry. The default bundle wires both delegation tools as `backgroundMode: continuable`, so

English

Problem

The one-shot in-process driver has seeded parent sandbox/approval overrides into its children since the in-process policy-inheritance decision, but the continuable path never did: SubagentContinuationManager materialization applied only child composition and the activation setup registry. The default bundle wires both delegation tools as backgroundMode: continuable, so in a default deployment every background child silently fell back to deployment defaults — a parent switched to danger-full-access produced children stuck at workspace-write whose every out-of-workspace operation raised an approval prompt, and a parent's unattended 'never' approval stance reverted to prompting (dsh-external/issues#334).

Decision

The capture/append pair moved from the one-shot driver into the seam's shared child-agent module (dsh-subagent/src/child-agent.ts), the declared one home for shared child composition: captureDelegatedPolicyOverrides(parent) snapshots sandboxPolicy.overrideOf(parent.session) through optional ctx.get and pins the child approval policy to 'never' (approvals-pinned decision), and appendDelegatedPolicyOverrides(childSession, overrides) appends the source: 'delegation' events. The one-shot driver and the continuation manager both call them, so the two paths cannot drift.

startContinuable captures before its first await (prepareContinuable), the same "a later parent switch belongs to the parent's future" boundary as one-shot. The snapshot travels in MaterializeInputs.create, so only fresh materialization appends the events during unpublished setup, after any fork seed. A cold resume passes no create inputs and appends nothing: the persisted child log already carries the delegation events, and replaying the log IS the state. The durable child log — not the current Activation, not the resuming parent — owns the child's effective policy, so a parent switch between residency epochs never retroactively changes a durable child.

Alternatives considered

  • An activation-setup-registry contribution (registerContinuableSetup) — rejected: a contribution receives only the child context, so it cannot capture the parent's overrides at the delegation boundary; the registry applies on cold resume as well as fresh creation, which would re-append or re-capture; and nothing ties a contribution's capture to the start call's synchronous prefix, so the pre-await capture guarantee would be lost.
  • Re-capturing the parent's overrides at cold resume — rejected: a resumed child would silently change policy with the parent's later switches, breaking the snapshot-at-delegation semantic and making effective policy depend on resume timing instead of the child's own log. A parent that wants a resumed child under new policy re-delegates.
  • Importing the one-shot driver's inline logic from the continuation manager — rejected: the Service Definition package cannot depend on its own provider package, and duplicating the capture/append pair in continuation.ts invites drift; child-agent.ts already holds every other shared composition step.
  • Seeding the events into the descriptor seed turn — rejected: the capture value is not known when the seed is assembled for every caller, and the one-shot precedent already establishes unpublished-setup appends as the ordering that places inherited facts after fork history with firstLiveSeq intact.

Consequences

  • Default-bundle background delegation (backgroundMode: continuable) now inherits a parent's explicit sandbox override and pins the child to 'never' approvals; compositions without either policy service behave unchanged.
  • dsh-subagent gains optional peer types on dsh-sandbox-policy and dsh-user-approval (the ctx.get pattern the one-shot driver used); dsh-subagent-in-process-driver drops its policy-service peers and type imports entirely and delegates to the shared helpers.
  • The continuable suite (packages/subagent/subagent/tests/continuation-inheritance.spec.ts) pins fresh-start seeding, pre-await capture, default omission, cold-resume snapshot stability, and fork-seed precedence; the ACP snapshot scenario subagent-continuable-inheritance pins the child's delegation event and read-only runtime context through the assembled app and fails when the capture is removed.
  • Out-of-process providers (acp, dsh-sdk, claude-code, codex) support no continuable children (prepareContinuable absent), and their one-shot children keep their own deployment policy (inheritsParentContext = false); cross-process policy propagation remains out of scope.

中文

问题

进程内策略继承决策以来,一次性进程内驱动器一直会把父级的沙箱/审批覆盖项注入其子级,但可继续路径从未这样做:SubagentContinuationManager 的物化只应用子级组合与 Activation(激活)设置注册表。默认组合包把两个委派工具都配置为 backgroundMode: continuable,因此在默认部署中,每个后台子 agent(智能体)都静默回退到部署默认值:切换到 danger-full-access 的父级产出的子 agent 卡在 workspace-write,每次工作区外操作都会触发审批提示;父级无人值守的 'never' 审批立场也退回为发起提示的行为(dsh-external/issues#334)。

决策

捕获/追加这对函数从一次性驱动器移入该 seam 的共享子 agent 模块(dsh-subagent/src/child-agent.ts),即声明的共享子级组合唯一归属之处:captureDelegatedPolicyOverrides(parent) 通过可选的 ctx.getsandboxPolicy.overrideOf(parent.session) 建立快照,并把子级审批策略钉定为 'never'审批钉定决策),appendDelegatedPolicyOverrides(childSession, overrides) 则追加 source: 'delegation' 事件。一次性驱动器与继续执行管理器都调用它们,因此两条路径不会出现偏差。

startContinuable 在其第一次 await(prepareContinuable)之前完成捕获,沿用与一次性路径相同的「父级后续切换属于父级的未来」边界。快照放在 MaterializeInputs.create 中传递,因此只有全新物化会在未发布的设置阶段、排在任何 fork 种子之后追加这些事件。冷恢复(cold resume)不传入 create 输入,也不追加任何内容:持久化的子日志已经携带委派事件,而回放该日志本身就是状态。子 agent 的生效策略由持久化子日志拥有,而不是当前 Activation,也不是发起恢复的父级,因此父级在驻留纪元(residency epoch)之间的切换绝不会追溯性地改变一个持久化子 agent。

考虑过的替代方案

  • 一项 Activation 设置注册表贡献registerContinuableSetup):不予采纳。贡献只接收子级上下文,因此无法在委派边界捕获父级的覆盖项;该注册表在冷恢复与全新创建时都会应用,会导致重复追加或重复捕获;而且没有任何机制把贡献的捕获绑定到 start 调用的同步前缀,await 前捕获的保证会因此丢失。
  • 在冷恢复时重新捕获父级覆盖项:不予采纳。恢复的子 agent 会随父级后续切换静默改变策略,这会破坏委派时快照的语义,并让生效策略取决于恢复时机而非子级自身的日志。希望恢复的子 agent 采用新策略的父级应重新委派。
  • 让继续执行管理器导入一次性驱动器的内联逻辑:不予采纳。Service Definition 包不能依赖自己的提供方包,而在 continuation.ts 中复制捕获/追加这对函数会招致偏差;child-agent.ts 已经承载其余每个共享组合步骤。
  • 把这些事件写入描述符种子轮次:不予采纳。种子为每个调用方组装时,捕获值尚不可知;而且一次性路径的先例已经确立:在未发布的设置阶段追加,才是把继承事实排在 fork 历史之后、同时保持 firstLiveSeq 不变的顺序。

后果

  • 默认组合包的后台委派(backgroundMode: continuable)现在会继承父级显式的沙箱覆盖项,并把子级钉定为 'never' 审批;未组合任一策略服务的组合保持原有行为。
  • dsh-subagent 新增针对 dsh-sandbox-policydsh-user-approval 的可选 peer 类型(即一次性驱动器所用的 ctx.get 模式);dsh-subagent-in-process-driver 完全移除自己的策略服务 peer 与类型导入,委托给共享辅助函数。
  • 可继续测试套件(packages/subagent/subagent/tests/continuation-inheritance.spec.ts)锁定全新启动的种子写入、await 前捕获、默认值省略、冷恢复快照稳定性与 fork 种子优先级;ACP 快照场景 subagent-continuable-inheritance 经组装后的应用锁定子级的委派事件与只读运行时上下文,移除捕获时即失败。
  • 进程外提供方(acpdsh-sdkclaude-codecodex)不支持可继续子 agent(没有 prepareContinuable),其一次性子 agent 保留自身的部署策略(inheritsParentContext = false);跨进程策略传播仍不在范围内。