diff --git a/.agents/notes/implemented/feature/2026-07-25-subagent-policy-inheritance.i18n.yaml b/.agents/notes/implemented/feature/2026-07-25-subagent-policy-inheritance.i18n.yaml index 5a96140e0e..ab957282ba 100644 --- a/.agents/notes/implemented/feature/2026-07-25-subagent-policy-inheritance.i18n.yaml +++ b/.agents/notes/implemented/feature/2026-07-25-subagent-policy-inheritance.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write .agents/notes/implemented/feature/2026-07-25-subagent-policy-inheritance.md -2026-07-25-subagent-policy-inheritance.md: ae87e7c688a53babca7bb0afc043dc8360871240 -2026-07-25-subagent-policy-inheritance.zh.md: 4e6a11e9cef5fc773b50aa32c477c97e2686c8d0 +2026-07-25-subagent-policy-inheritance.md: aeff83795eedead9c75de6bbb74c1da1945092ca +2026-07-25-subagent-policy-inheritance.zh.md: 7a09fd972f53b46655df93ddf9625455f2077460 diff --git a/.agents/notes/implemented/feature/2026-07-25-subagent-policy-inheritance.md b/.agents/notes/implemented/feature/2026-07-25-subagent-policy-inheritance.md index ae87e7c688..aeff83795e 100644 --- a/.agents/notes/implemented/feature/2026-07-25-subagent-policy-inheritance.md +++ b/.agents/notes/implemented/feature/2026-07-25-subagent-policy-inheritance.md @@ -12,9 +12,9 @@ Sandbox and approval overrides are per-session log folds. An in-process subagent The shared in-process driver snapshots `sandboxPolicy.overrideOf(parent.session)` and `approval.overrideOf(parent.session)` before its first await. A later parent switch belongs to the parent's future; cancel-and-redelegate takes a new snapshot. Both services are optional, and only explicit session overrides are copied, never deployment defaults or one-shot grants. -Each captured value becomes a source-tagged `sandbox/mode` or `approval/policy` event in the child's constructor seed. The driver places these events after any fork prefix while keeping `SessionHeader.seedLength` at the prefix length. Existing last-event-wins folds therefore make the delegation snapshot beat stale fork history and let a later child switch beat the snapshot. A grandchild folds its parent's already-seeded log, so the rule composes without another inheritance mechanism. +Each captured value becomes a source-tagged `sandbox/mode` or `approval/policy` event appended during the child factory's unpublished setup. The session constructor has already fixed `Session.firstLiveSeq` at the fork-prefix length, so the inherited facts follow fork history, reach telemetry when the child is announced, and leave `SessionHeader.seedLength` at the prefix length. Existing last-event-wins folds therefore make the delegation snapshot beat stale fork history and let a later child switch beat the snapshot. A grandchild folds its parent's logged state, so the rule composes without another inheritance mechanism. -Constructor seeds are validated before publication and captured by persistence when the session is announced. Any materialized child log therefore stores the inherited events with its first batch; there is no second policy store, schema field, or query index. The `source: 'delegation'` marker lets approval narration distinguish inheritance from a child-side user switch. +Ordinary session appends validate the inherited events before publication, and persistence captures the complete unpublished log when the session is announced. Any materialized child log therefore stores the inherited events with its first batch; there is no second policy store, schema field, or query index. The `source: 'delegation'` marker lets approval narration distinguish inheritance from a child-side user switch. ### What a blocked child experiences @@ -22,14 +22,15 @@ A confined child gets the ordinary denial marker. No answerer currently owns an ## Alternatives considered -- **Generic `SessionHeader` policy fields** — rejected: they duplicate an event-sourced fact in metadata and require propagation through core session types, persistence backends, query indexes, collision identity, and every policy consumer. Constructor-seeded events have the required ordering and reuse the existing durable store. -- **A first-prompt listener** — rejected: it introduces listener ordering and a later timing boundary even though the creation transaction already accepts initial log events. +- **Generic `SessionHeader` policy fields** — rejected: they duplicate an event-sourced fact in metadata and require propagation through core session types, persistence backends, query indexes, collision identity, and every policy consumer. Unpublished setup events have the required ordering and reuse the existing durable store. +- **Combining new policy facts with constructor history** — rejected: `Session.firstLiveSeq` classifies the complete constructor seed as replayed history, so telemetry would skip child-only facts. Unpublished setup keeps history and new facts on their existing sides of that boundary without another session option. +- **A first-prompt listener** — rejected: it introduces listener ordering and a later timing boundary even though the creation transaction already permits log appends before publication. - **Copying deployment defaults** — rejected: defaults remain operator-owned and may change; an unswitched parent stamps nothing, so its child follows the current deployment. - **Live resolution walking `parentSession` at each call** — rejected: it breaks the "two sessions never see each other's state" isolation invariant, requires the parent session to stay loaded for the child's lifetime, and makes a mid-run parent switch retroactively change a running child. Snapshot-at-delegation is the semantic: the child keeps the policy it was handed; cancel-and-respawn picks up a tightening. - **Forcing `'never'` or routing asks to the root controller** — rejected as inheritance behavior. A forced value forecloses a future child answerer; parent routing needs parent-chain ownership and the spawning `callId`, and remains deferred in [the approval-seam Agent Note](2026-07-06-approval-seam.md). ## Consequences -- Spawn, fork, and nested in-process children retain a parent's explicit sandbox and approval overrides. The focused suite proves real filesystem denial, stale-fork precedence, delegation-time capture, default omission, and context disposal. +- Spawn, fork, and nested in-process children retain a parent's explicit sandbox and approval overrides. The focused suite proves real filesystem denial, stale-fork precedence, delegation-time capture, the live-event boundary, default omission, and context disposal. - The keyless headless snapshot is the assembled regression: only the parent is `read-only`, the deployment default is `workspace-write`, and the child's persisted event plus denied disk write both fail if capture is removed. - Each delegation adds at most two log-only events. `dsh-subagent-inprocess` has optional peer types for the two policy services; compositions without either service behave unchanged. Out-of-process children retain their own deployment policy, and a running child does not follow later parent switches. diff --git a/.agents/notes/implemented/feature/2026-07-25-subagent-policy-inheritance.zh.md b/.agents/notes/implemented/feature/2026-07-25-subagent-policy-inheritance.zh.md index 4e6a11e9ce..7a09fd972f 100644 --- a/.agents/notes/implemented/feature/2026-07-25-subagent-policy-inheritance.zh.md +++ b/.agents/notes/implemented/feature/2026-07-25-subagent-policy-inheritance.zh.md @@ -12,9 +12,9 @@ Status: implemented 共享的进程内驱动器在第一次 await 之前对 `sandboxPolicy.overrideOf(parent.session)` 和 `approval.overrideOf(parent.session)` 获取快照。父级后续的切换属于父级的未来;取消后重新委派会取得新快照。这两个服务均为可选,仅复制显式会话覆盖项,绝不复制部署默认值或一次性授权。 -每个捕获值都会成为子 agent 构造种子中的一条带来源标记的 `sandbox/mode` 或 `approval/policy` 事件。驱动器把这些事件放在任意 fork 前缀之后,同时让 `SessionHeader.seedLength` 保持为此前缀的长度。因此,既有的末事件胜出折叠会让委派快照压过陈旧的 fork 历史,并让子 agent 后续的切换压过该快照。孙代 agent 会折叠其父级已预置事件的日志,因此无需另一套继承机制即可组合此规则。 +每个捕获值都会成为子 agent 工厂在未发布设置阶段追加的一条带来源标记的 `sandbox/mode` 或 `approval/policy` 事件。会话构造函数已将 `Session.firstLiveSeq` 固定为 fork 前缀的长度,因此继承事实会排在 fork 历史之后,在子 agent 公布时进入遥测,同时让 `SessionHeader.seedLength` 保持为此前缀的长度。因此,既有的末事件胜出折叠会让委派快照压过陈旧的 fork 历史,并让子 agent 后续的切换压过该快照。孙代 agent 会折叠其父级已记录的状态,因此无需另一套继承机制即可组合此规则。 -构造种子在发布前经过校验,并在会话公布时由持久化层捕获。因此,任何已物化的子 agent 日志都会在首批数据中存下继承事件;不存在第二套策略存储、schema 字段或查询索引。`source: 'delegation'` 标记让审批叙述能够区分继承与子 agent 侧的用户切换。 +普通的会话追加会在发布前校验继承事件,持久化层则在会话公布时捕获完整的未发布日志。因此,任何已物化的子 agent 日志都会在首批数据中存下继承事件;不存在第二套策略存储、schema 字段或查询索引。`source: 'delegation'` 标记让审批叙述能够区分继承与子 agent 侧的用户切换。 ### 被拦住的子 agent 会经历什么 @@ -22,14 +22,15 @@ Status: implemented ## 考虑过的替代方案 -- **通用的 `SessionHeader` 策略字段**:不予采纳。它们会在元数据中复制一项事件溯源事实,并要求贯穿核心会话类型、持久化后端、查询索引、碰撞标识与每个策略消费方进行传播。构造时预置的事件具备所需顺序,并复用现有持久化存储。 -- **首个提示词监听器**:不予采纳。尽管创建事务已经接受初始日志事件,它仍会引入监听器顺序与更晚的时序边界。 +- **通用的 `SessionHeader` 策略字段**:不予采纳。它们会在元数据中复制一项事件溯源事实,并要求贯穿核心会话类型、持久化后端、查询索引、碰撞标识与每个策略消费方进行传播。未发布设置阶段的事件具备所需顺序,并复用现有持久化存储。 +- **将新策略事实与构造历史合并**:不予采纳。`Session.firstLiveSeq` 会把完整的构造种子归类为回放历史,因此遥测会跳过仅属于子 agent 的事实。未发布设置让历史与新事实留在该边界各自原有的一侧,无需再增加会话选项。 +- **首个提示词监听器**:不予采纳。尽管创建事务已经允许在发布前追加日志,它仍会引入监听器顺序与更晚的时序边界。 - **复制部署默认值**:不予采纳。默认值仍由运维人员拥有且可能变化;未切换的父级不会盖章写入任何内容,因此其子 agent 跟随当前部署。 - **每次调用时沿 `parentSession` 实时解析**:不予采纳。这会打破「两个会话永远看不到彼此状态」的隔离不变量,要求父会话在子 agent 的整个生命周期内保持加载,还会让父级在子 agent 运行途中做的切换追溯性地改变一个正在运行的子 agent。委派时快照才是本设计的语义:子 agent 保持它被交付时的策略;取消后重新 spawn 即可拿到收紧后的策略。 - **强制使用 `'never'` 或把 ask 路由到根控制器**:不作为继承行为采纳。强制值会排除未来的子 agent 应答器;父级路由需要父链所有权与发起 spawn 的 `callId`,仍按[审批 seam Agent Note](2026-07-06-approval-seam.md) 所述延期。 ## 后果 -- spawn、fork 和嵌套的进程内子 agent 会保留父级显式的沙箱与审批覆盖项。聚焦测试套件证明真实文件系统拒绝、陈旧 fork 优先级、委派时捕获、默认值省略与上下文释放。 +- spawn、fork 和嵌套的进程内子 agent 会保留父级显式的沙箱与审批覆盖项。聚焦测试套件证明真实文件系统拒绝、陈旧 fork 优先级、委派时捕获、实时事件边界、默认值省略与上下文释放。 - 无密钥 headless 快照是组装后应用层面的回归测试:只有父级是 `read-only`,部署默认值是 `workspace-write`;若移除捕获,子 agent 的持久化事件与被拒的磁盘写入这两项检查都会失败。 - 每次委派最多增加两条仅日志事件。`dsh-subagent-inprocess` 为两个策略服务提供可选 peer 类型;未组合任一服务的组合保持原有行为。进程外子 agent 仍采用自身的部署策略,正在运行的子 agent 不跟随父级后续切换。 diff --git a/docs/cordis-catalog/services.md b/docs/cordis-catalog/services.md index 0bf299fd25..02dd9a5a76 100644 --- a/docs/cordis-catalog/services.md +++ b/docs/cordis-catalog/services.md @@ -216,7 +216,7 @@ roots(): Agent[] Types: [Agent](../core-data-structures/core.md) · [SessionId](../core-data-structures/core.md) -Source: [`packages/core/agent/src/index.ts:216`](../../packages/core/agent/src/index.ts) +Source: [`packages/core/agent/src/index.ts:215`](../../packages/core/agent/src/index.ts) ## `ctx.approval` — `ApprovalService` diff --git a/docs/core-data-structures/persistence.i18n.yaml b/docs/core-data-structures/persistence.i18n.yaml index a89ea097e6..87b7587d30 100644 --- a/docs/core-data-structures/persistence.i18n.yaml +++ b/docs/core-data-structures/persistence.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write docs/core-data-structures/persistence.md -persistence.md: 94813764bea1b87856d5c7cfc86b568df6408c68 -persistence.zh.md: fe5d497a58502356a0be56eec38edbfdbcf903c1 +persistence.md: 0280093883b9fbd4c4bbc788bce7193d59bb7182 +persistence.zh.md: d32274bbc2e96e3754a8061ba37427e34b6205f6 diff --git a/docs/core-data-structures/persistence.md b/docs/core-data-structures/persistence.md index 94813764be..0280093883 100644 --- a/docs/core-data-structures/persistence.md +++ b/docs/core-data-structures/persistence.md @@ -77,7 +77,7 @@ interface SessionHeader { ## `CreateSessionOptions` — seeding and metadata -Creating a `Session` through the store takes a `seed` (initial events for replay, fork, or creation-time facts) and `meta` (the storage-level fields the store folds into a `SessionHeader`). The store fills in `version`/`id` and defaults `createdAt`; the caller supplies the validated absolute `cwd`, the `parentSession` lineage, the `seedLength` seed boundary, the `delegationDepth`, and — only when reconstructing a persisted session — the original `createdAt` to preserve it. +Creating a `Session` through the store takes a `seed` (initial replay or fork history) and `meta` (the storage-level fields the store folds into a `SessionHeader`). The store fills in `version`/`id` and defaults `createdAt`; the caller supplies the validated absolute `cwd`, the `parentSession` lineage, the `seedLength` seed boundary, the `delegationDepth`, and — only when reconstructing a persisted session — the original `createdAt` to preserve it. ```ts type-equiv /** @@ -86,7 +86,7 @@ Creating a `Session` through the store takes a `seed` (initial events for replay * store folds into a {@link SessionHeader}. */ interface CreateSessionOptions { - /** Initial log events supplied at construction (replay, fork, or creation-time facts). */ + /** Initial replay or fork history supplied at construction. */ readonly seed?: readonly SessionEvent[] /** * Storage metadata read once before publication. `seedLength` is explicit diff --git a/docs/core-data-structures/persistence.zh.md b/docs/core-data-structures/persistence.zh.md index fe5d497a58..d32274bbc2 100644 --- a/docs/core-data-structures/persistence.zh.md +++ b/docs/core-data-structures/persistence.zh.md @@ -77,7 +77,7 @@ interface SessionHeader { ## `CreateSessionOptions`:seed 与元数据 -通过 store 创建 `Session` 时会接收 `seed`(用于回放、fork 或创建时事实的初始事件)与 `meta`(store 折叠进 `SessionHeader` 的存储层字段)。store 填充 `version`/`id` 并为 `createdAt` 提供默认值;调用方提供已校验的绝对 `cwd`、`parentSession` 谱系、`seedLength` 种子边界、`delegationDepth`,以及——仅在重建已持久化会话时——需要保留的原始 `createdAt`。 +通过 store 创建 `Session` 时会接收 `seed`(初始回放或 fork 历史)与 `meta`(store 折叠进 `SessionHeader` 的存储层字段)。store 填充 `version`/`id` 并为 `createdAt` 提供默认值;调用方提供已校验的绝对 `cwd`、`parentSession` 谱系、`seedLength` 种子边界、`delegationDepth`,以及——仅在重建已持久化会话时——需要保留的原始 `createdAt`。 ```ts type-equiv /** @@ -86,7 +86,7 @@ interface SessionHeader { * store folds into a {@link SessionHeader}. */ interface CreateSessionOptions { - /** Initial log events supplied at construction (replay, fork, or creation-time facts). */ + /** Initial replay or fork history supplied at construction. */ readonly seed?: readonly SessionEvent[] /** * Storage metadata read once before publication. `seedLength` is explicit diff --git a/packages/core/agent/src/index.ts b/packages/core/agent/src/index.ts index 49bd2c7ce1..17b115a40c 100644 --- a/packages/core/agent/src/index.ts +++ b/packages/core/agent/src/index.ts @@ -63,12 +63,11 @@ export interface CreateAgentOptions { readonly delegationDepth?: number } /** - * Initial session events. A fork starts with a balanced completed-turn - * prefix of the parent's log; creation-time log facts may follow that - * prefix. The complete seed must be contiguous from seq 0, carry only - * lossless-JSON data, and contain no open turn/step or dangling tool call. - * The factory passes it to the session's durable validator/snapshot - * boundary before publication. + * Initial replay/fork history. A fork supplies a balanced completed-turn + * prefix of the parent's log. The complete seed must be contiguous from seq + * 0, carry only lossless-JSON data, and contain no open turn/step or dangling + * tool call. The factory passes it to the session's durable + * validator/snapshot boundary before publication. */ readonly seed?: readonly SessionEvent[] /** Per-agent options (model, …). */ diff --git a/packages/core/session/src/types.ts b/packages/core/session/src/types.ts index f5027b2d84..7f3a12b54e 100644 --- a/packages/core/session/src/types.ts +++ b/packages/core/session/src/types.ts @@ -72,7 +72,7 @@ export interface SessionHeader { * store folds into a {@link SessionHeader}. */ export interface CreateSessionOptions { - /** Initial log events supplied at construction (replay, fork, or creation-time facts). */ + /** Initial replay or fork history supplied at construction. */ readonly seed?: readonly SessionEvent[] /** * Storage metadata read once before publication. `seedLength` is explicit diff --git a/packages/subagent/subagent-inprocess/README.i18n.yaml b/packages/subagent/subagent-inprocess/README.i18n.yaml index 402f3a6684..b834818199 100644 --- a/packages/subagent/subagent-inprocess/README.i18n.yaml +++ b/packages/subagent/subagent-inprocess/README.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write packages/subagent/subagent-inprocess/README.md -README.md: 02d9bd7d2dc792055a13d51402572313855ff1ff -README.zh.md: f55467b37a948ef5a0a34bd45d8a11d7a004ff29 +README.md: 980bc18de088c41dfe2f57a5ff0882a60892fc9f +README.zh.md: 1ceb628371c3ae9cee6d8afa6bc1d95ba4cda8ae diff --git a/packages/subagent/subagent-inprocess/README.md b/packages/subagent/subagent-inprocess/README.md index 02d9bd7d2d..980bc18de0 100644 --- a/packages/subagent/subagent-inprocess/README.md +++ b/packages/subagent/subagent-inprocess/README.md @@ -18,7 +18,7 @@ The driver follows this sequence: The child gets the parent's working-directory/session lineage and inherits the parent provider, model, and output-token cap unless `request.agentOptions` overrides them. It gets a fresh flat registration scope: parent ownership does not import parent tool restrictions or establish an authority subset. -When the optional sandbox-policy or approval service is composed, the driver snapshots the parent's explicit session override before child creation and seeds a source-tagged event after any fork prefix. It never copies deployment defaults or one-shot grants; later child switches still win. See the [policy-inheritance decision](../../../.agents/notes/implemented/feature/2026-07-25-subagent-policy-inheritance.md). +When the optional sandbox-policy or approval service is composed, the driver snapshots the parent's explicit session override before child creation and appends a source-tagged event during unpublished setup, after any fork history and before session publication. It never copies deployment defaults or one-shot grants; later child switches still win. See the [policy-inheritance decision](../../../.agents/notes/implemented/feature/2026-07-25-subagent-policy-inheritance.md). ## Cancellation and ownership diff --git a/packages/subagent/subagent-inprocess/README.zh.md b/packages/subagent/subagent-inprocess/README.zh.md index f55467b37a..1ceb628371 100644 --- a/packages/subagent/subagent-inprocess/README.zh.md +++ b/packages/subagent/subagent-inprocess/README.zh.md @@ -18,7 +18,7 @@ 子 agent 会获得父 agent 的工作目录/会话谱系;除非 `request.agentOptions` 覆盖,否则还会继承父 agent 的提供方、模型和输出 token 上限。它获得全新的扁平注册作用域:父级所有权不会导入父 agent 的工具限制,也不会建立权限子集。 -当组合中挂载了可选的沙箱策略或审批服务时,驱动器会在创建子 agent 前对父级的显式会话覆盖项获取快照,并在任意 fork 前缀之后预置一条带来源标记的事件。它绝不复制部署默认值或一次性授权;子 agent 后续的切换仍然优先。参见[策略继承决策](../../../.agents/notes/implemented/feature/2026-07-25-subagent-policy-inheritance.md)。 +当组合中挂载了可选的沙箱策略或审批服务时,驱动器会在创建子 agent 前对父级的显式会话覆盖项获取快照,并在未发布的设置阶段追加一条带来源标记的事件,使其位于所有 fork 历史之后、会话发布之前。它绝不复制部署默认值或一次性授权;子 agent 后续的切换仍然优先。参见[策略继承决策](../../../.agents/notes/implemented/feature/2026-07-25-subagent-policy-inheritance.md)。 ## 取消与所有权 diff --git a/packages/subagent/subagent-inprocess/src/index.ts b/packages/subagent/subagent-inprocess/src/index.ts index 50174037f8..c6f9b4059b 100644 --- a/packages/subagent/subagent-inprocess/src/index.ts +++ b/packages/subagent/subagent-inprocess/src/index.ts @@ -103,30 +103,19 @@ export async function startInProcessRun( } // Capture before the first await: a later parent switch belongs to the - // parent's future. Appending after the fork prefix makes the captured - // values the child's initial overrides without another storage plane. + // parent's future. const inheritedMode = parent.ctx.get('sandboxPolicy')?.overrideOf(parent.session) const inheritedPolicy = parent.ctx.get('approval')?.overrideOf(parent.session) - const seed: SessionEvent[] = [...options.seed ?? []] - if (inheritedMode !== undefined) { - seed.push({ - type: 'sandbox/mode', - seq: seed.length, - time: Date.now(), - data: { mode: inheritedMode, source: 'delegation' }, - }) - } - if (inheritedPolicy !== undefined) { - seed.push({ - type: 'approval/policy', - seq: seed.length, - time: Date.now(), - data: { policy: inheritedPolicy, source: 'delegation' }, - }) - } let structured: StructuredAttachment | undefined const setup = (childCtx: Context): void => { + const childSession = (childCtx.agent as Agent).session + if (inheritedMode !== undefined) { + childSession.append('sandbox/mode', { mode: inheritedMode, source: 'delegation' }) + } + if (inheritedPolicy !== undefined) { + childSession.append('approval/policy', { policy: inheritedPolicy, source: 'delegation' }) + } if (request.persona !== undefined) { childCtx.systemPrompt.section({ name: 'deployment:persona', order: 0, text: request.persona }) } @@ -146,7 +135,7 @@ export async function startInProcessRun( delegationDepth: childDepth, ...seedLength > 0 ? { seedLength } : {}, }, - ...(options.seed !== undefined || seed.length > 0) ? { seed } : {}, + ...options.seed === undefined ? {} : { seed: options.seed }, agentOptions, signal: request.signal, setup, diff --git a/packages/subagent/subagent-inprocess/tests/inheritance.spec.ts b/packages/subagent/subagent-inprocess/tests/inheritance.spec.ts index 07ba55ccfa..fb6ba97b32 100644 --- a/packages/subagent/subagent-inprocess/tests/inheritance.spec.ts +++ b/packages/subagent/subagent-inprocess/tests/inheritance.spec.ts @@ -1,4 +1,4 @@ -/** Policy inheritance through constructor-seeded child session events. */ +/** Policy inheritance through child session events appended before publication. */ import { afterEach, beforeEach, describe, expect, it } from 'vitest' import { mkdtemp, readFile, realpath, rm } from 'node:fs/promises' @@ -69,7 +69,7 @@ function toolResultTexts(agent: Agent): string[] { } describe('in-process policy inheritance', () => { - it('seeds parent overrides into a spawn child before its first request', async () => { + it('records parent overrides before publishing a spawn child', async () => { const script: Script = [] const { ctx, parent } = await setupWalled(script) const blocked = join(workspace, 'spawn-blocked.txt') @@ -93,7 +93,7 @@ describe('in-process policy inheritance', () => { { type: 'sandbox/mode', seq: 0, data: { mode: 'read-only', source: 'delegation' } }, { type: 'approval/policy', seq: 1, data: { policy: 'never', source: 'delegation' } }, ]) - expect(child.session.firstLiveSeq).toBe(2) + expect(child.session.firstLiveSeq).toBe(0) expect(child.session.header.seedLength).toBeUndefined() expect(ctx.sandboxPolicy.overrideOf(child.session)).toBe('read-only') expect(ctx.approval.overrideOf(child.session)).toBe('never') @@ -125,6 +125,7 @@ describe('in-process policy inheritance', () => { const child = run.localAgent as Agent expect(child.session.header.seedLength).toBe(1) + expect(child.session.firstLiveSeq).toBe(seed.length) expect(child.session.events.filter(event => event.type === 'sandbox/mode')).toMatchObject([ { seq: 0, data: { mode: 'workspace-write' } }, { seq: 1, data: { mode: 'read-only', source: 'delegation' } }, diff --git a/packages/telemetry/session-telemetry/tests/telemetry.spec.ts b/packages/telemetry/session-telemetry/tests/telemetry.spec.ts index 27ef67feef..a449a4053d 100644 --- a/packages/telemetry/session-telemetry/tests/telemetry.spec.ts +++ b/packages/telemetry/session-telemetry/tests/telemetry.spec.ts @@ -168,28 +168,25 @@ describe('TelemetryCoordinator capture', () => { }) describe('TelemetryCoordinator adoption', () => { - it('starts export at the construction boundary: seeded history never re-exports', async () => { + it('exports an unpublished suffix without re-exporting constructor history', async () => { const backend = new FakeBackend() const ctx = new Context() await ctx.plugin(SessionStore) const parent = liveSession(ctx, 'seed-parent') appendTurn(parent) - const child = ctx.sessions.create(SessionId('seeded'), { seed: [...parent.events], meta: {} }) await ctx.plugin({ name: 'fake-telemetry', inject: ['sessions'], apply: (inner: Context) => void new TelemetryCoordinator(inner, backend), }) - // The live parent (no constructor seed) replays in full; the child's - // inherited prefix already left the process under another identity (the - // parent's id here; the same id in a previous process for a resume) and - // must not be re-exported — only its live suffix ships. + const child = ctx.sessions.prepare(SessionId('seeded'), { seed: [...parent.events], meta: {} }) + child.append('turn/end', { turn: 1, reason: { kind: 'completed' } }) + ctx.sessions.enter(child) + ctx.sessions.announce(child) + const seqs = backend.ledger().map(r => [r.attributes['session.id'], r.attributes['event.seq']]) expect(seqs).toEqual(expect.arrayContaining([['seed-parent', 0], ['seed-parent', 1]])) - expect(seqs.filter(([id]) => id === 'seeded')).toEqual([]) - child.append('turn/end', { turn: 1, reason: { kind: 'completed' } }) - expect(backend.ledger().map(r => [r.attributes['session.id'], r.attributes['event.seq']])) - .toEqual(expect.arrayContaining([['seeded', 2]])) + expect(seqs.filter(([id]) => id === 'seeded')).toEqual([['seeded', 2]]) }) it('resume shape: a full-log seed exports nothing yet still rebuilds the chunk projection', async () => {