Sfoglia il codice sorgente

fix(test): make session replay portable in CI

Tianyi Cui 1 mese fa
parent
commit
d1e8f4672e
25 ha cambiato i file con 153 aggiunte e 73 eliminazioni
  1. 2 2
      .agents/notes/implemented/testing/2026-08-24-session-log-snapshot-corpus.i18n.yaml
  2. 1 0
      .agents/notes/implemented/testing/2026-08-24-session-log-snapshot-corpus.md
  3. 1 0
      .agents/notes/implemented/testing/2026-08-24-session-log-snapshot-corpus.zh.md
  4. 2 2
      apps/web/tests/approval-composer.e2e.ts
  5. 8 4
      apps/web/tests/steering.e2e.ts
  6. 1 1
      packages/extensions/tool-cordis/src/index.ts
  7. 1 0
      packages/test-support/llm-replay/src/index.ts
  8. 16 1
      packages/test-support/llm-replay/tests/llm-replay.spec.ts
  9. 1 1
      packages/test-support/session-snapshot/src/launcher.ts
  10. 1 0
      packages/test-support/session-snapshot/src/manifest.ts
  11. 3 5
      packages/test-support/session-snapshot/src/suite.ts
  12. 2 5
      packages/test-support/session-snapshot/src/workspace.ts
  13. 2 0
      packages/test-support/session-snapshot/tests/fixtures/suite/plain-turn/snapshot.yml
  14. 1 0
      packages/test-support/session-snapshot/tests/fixtures/suite/plain-turn/workspace.expected/seed.txt
  15. 1 1
      packages/test-support/session-snapshot/tests/harness.spec.ts
  16. 28 0
      packages/test-support/session-snapshot/tests/identity.spec.ts
  17. 39 0
      packages/test-support/session-snapshot/tests/manifest.spec.ts
  18. 2 2
      snapshots/acp/cancel-tool-calls/replay.override.json
  19. 4 4
      snapshots/acp/cancel-tool-calls/session.jsonl
  20. 1 1
      snapshots/acp/cancel-tool-calls/stdout.expected.jsonl
  21. 14 3
      snapshots/session/headless.snapshot.ts
  22. 1 4
      snapshots/web/approval-composer/session.jsonl
  23. 0 4
      snapshots/web/steering/mid-steer.expected.md
  24. 19 23
      snapshots/web/steering/session.jsonl
  25. 2 10
      snapshots/web/steering/settled.expected.md

+ 2 - 2
.agents/notes/implemented/testing/2026-08-24-session-log-snapshot-corpus.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/testing/2026-08-24-session-log-snapshot-corpus.md
-2026-08-24-session-log-snapshot-corpus.md: 30dad40098e5d1da08052384374425cad595a1ba
-2026-08-24-session-log-snapshot-corpus.zh.md: 49edffc2a63486030ace8e76455e9bae209ee902
+2026-08-24-session-log-snapshot-corpus.md: eb77f74e85ad97f74df48ecbf831579b6a643cbf
+2026-08-24-session-log-snapshot-corpus.zh.md: 5aff7fe8d04d8bf04f5cdfd443a7ed121dcbad25

+ 1 - 0
.agents/notes/implemented/testing/2026-08-24-session-log-snapshot-corpus.md

@@ -40,6 +40,7 @@ Workspace inputs remain scenario-local. A mutating scenario compares a complete
 - Committed session fixtures are redaction fixed points, contain no system-prompt or tool-schema bulk, and retain exactly one pin per header class.
 - Mutating scenarios verify their final workspace externally.
 - Owner-local process expectations use `*.expected.e2e.ts` and a separate built-output gate.
+- Source and built adapters install replay-only packages in isolated profile fallbacks; distinct prompt-section orders keep their request headers byte-identical.
 - Source and built launch modes, browser replay, SDK projections, packaged Python runtime cases, documentation gates, and repository hygiene pass.
 
 ## Consequences

+ 1 - 0
.agents/notes/implemented/testing/2026-08-24-session-log-snapshot-corpus.zh.md

@@ -40,6 +40,7 @@ Workspace 输入继续归各场景本地所有。变更文件的场景比较完
 - 提交的会话 fixture 是脱敏固定点,不含 system prompt 或工具 schema 正文,并为每个 header 类保留且仅保留一个 pin。
 - 变更内容的场景从外部验证最终 workspace。
 - 所属位置的进程预期使用 `*.expected.e2e.ts`,并由单独的构建产物门禁运行。
+- 源码与构建适配器在隔离的 profile fallback 中安装仅回放包;不同的提示词 section 顺序值使两种模式的请求 header 保持字节一致。
 - 源码和构建启动模式、浏览器回放、SDK 投影、打包 Python 运行时场景、文档门禁和仓库卫生检查通过。
 
 ## Consequences

+ 2 - 2
apps/web/tests/approval-composer.e2e.ts

@@ -127,8 +127,8 @@ describe('web e2e: approval takeover keeps its actions reachable', () => {
       await assertFinalWorkspaceSnapshot(SNAPSHOT_DIR, join(scaffold.workspaceCwd, 'workspace'))
       return
     }
-    // The denied attempt contains platform-specific OS text, so direct state
-    // and DOM assertions cover the answered outcome.
+    // Direct state and DOM assertions cover the answered outcome beyond the
+    // pending panel's expected output.
     expect(JSON.stringify(sessionEvents.filter(e => e.type === 'approval/decided').at(-1)))
       .toContain('allowed-once')
     const written = await readFile(join(scaffold.workspaceCwd, 'workspace', 'notes.txt'), 'utf8')

+ 8 - 4
apps/web/tests/steering.e2e.ts

@@ -26,10 +26,10 @@ const MID_EXPECTED = join(SNAPSHOT_DIR, 'mid-steer.expected.md')
 const SETTLED_EXPECTED = join(SNAPSHOT_DIR, 'settled.expected.md')
 const MODE = webSnapshotMode()
 // The question composer replaces the textarea, so fill → Queue row → Steer
-// must finish inside the first replay chunk window. At 15 ms that window is
-// shorter than Playwright's round trips; 50 ms supplies test-only headroom,
-// while larger values lengthen all three replay scenarios linearly.
-const REPLAY_PACE_MS = 50
+// starts only after request/context and must finish before the first replay
+// chunk. The compact canonical call plus 500 ms pacing gives loaded CI enough
+// time without stretching a long provider-authored chunk sequence.
+const REPLAY_PACE_MS = 500
 
 const PROMPT = 'Use the ask_user_question tool to ask me exactly one question with id "checkpoint", question "Ready to continue?", header "Checkpoint", and options labeled "Yes" and "No". After I answer, reply with one short sentence acknowledging my answer and stop.'
 const STEER = 'Interjection: include the word BANANA in your final reply.'
@@ -101,6 +101,10 @@ describe('web e2e: mid-turn steering lands durably and visibly', () => {
     const settled = scaffold.whenTurnSettled(MODE === 'record' ? 180_000 : 30_000)
     await input.fill(PROMPT)
     await input.press('Enter')
+    await expect.poll(
+      () => sessionEvents.some(event => event.type === 'request/context'),
+      { timeout: 10_000 },
+    ).toBe(true)
 
     // Enter remains the Queue gesture. The row action then atomically moves
     // this exact occurrence into the current turn's steering outbox.

+ 1 - 1
packages/extensions/tool-cordis/src/index.ts

@@ -33,7 +33,7 @@ function requireAgent(exec: ToolExecution): Agent {
 
 /** Register the Cordis tools and explicit `@pluginId` context injection. */
 export function apply(ctx: Context): void {
-  ctx.systemPrompt.section({ name: 'tool:cordis', order: 115, text: CORDIS_SYSTEM_PROMPT })
+  ctx.systemPrompt.section({ name: 'tool:cordis', order: 115.5, text: CORDIS_SYSTEM_PROMPT })
   for (const provider of hostInspectProviders(ctx)) {
     ctx.effect(() => ctx.cordisInspect.register(provider), `tool-cordis: inspect ${provider.manifest.id}`)
   }

+ 1 - 0
packages/test-support/llm-replay/src/index.ts

@@ -449,6 +449,7 @@ function inferStartedSubagents(
   for (const leaf of leaves) {
     for (const match of leaf.matchAll(/started subagent ([^\s"'<>]+)/g)) {
       const id = match[1]
+      /* v8 ignore next -- the fixed regular expression always has capture group 1. */
       if (id === undefined || liveSessionIds.includes(id)) continue
       const index = liveSessionIds.findIndex((value, candidate) => candidate > 0 && value === undefined)
       if (index < 0) return

+ 16 - 1
packages/test-support/llm-replay/tests/llm-replay.spec.ts

@@ -1246,7 +1246,7 @@ describe('installLlmReplay (per-session keying)', () => {
     const options: GenerateOptions = {
       ...live('live-parent'),
       messages: [createUserMessage({
-        content: [{ type: 'text', text: 'started subagent live-child-before-call' }],
+        content: [{ type: 'text', text: 'started subagent live-child-before-call started subagent live-child-before-call' }],
         source: { kind: 'user' },
       })],
     }
@@ -1257,6 +1257,21 @@ describe('installLlmReplay (per-session keying)', () => {
     ])
   })
 
+  it('ignores started-subagent text after every recorded session has bound', async () => {
+    const parentFile = writeSession('session.jsonl', { id: '{{session:1}}', createdAt: 1 }, [TEXT_CHUNKS])
+    const ctx = new Context()
+    await ctx.plugin(LlmRuntime)
+    installLlmReplay(ctx, { file: parentFile })
+
+    expect(await drain(ctx.llm.stream({
+      ...live('live-parent'),
+      messages: [createUserMessage({
+        content: [{ type: 'text', text: 'started subagent unrecorded-child' }],
+        source: { kind: 'user' },
+      })],
+    }))).toEqual(TEXT_CHUNKS)
+  })
+
   it('treats a call with no sessionId as the single anonymous (primary) session', async () => {
     const parentFile = writeSession('session.jsonl', { id: 'p', createdAt: 1 }, [TEXT_CHUNKS])
     const ctx = new Context()

+ 1 - 1
packages/test-support/session-snapshot/src/launcher.ts

@@ -397,7 +397,7 @@ function linkProfilePackage(source: string, cwd: string, packageName: string): v
   mkdirSync(dirname(link), { recursive: true })
   if (existsSync(link)) {
     if (realpathSync(link) !== packageDir) {
-      throw new Error(`ACP profile package ${packageName} resolves to two directories`)
+      throw new Error(`snapshot profile package ${packageName} resolves to two directories`)
     }
     return
   }

+ 1 - 0
packages/test-support/session-snapshot/src/manifest.ts

@@ -348,6 +348,7 @@ export function parseSnapshotManifest(source: string, path = 'snapshot.yml'): Sn
       ...(session === undefined ? {} : { session }),
     }
   } catch (error) {
+    /* v8 ignore next -- every parser and validator above throws Error instances. */
     throw new Error(`session-snapshot: ${path}: ${error instanceof Error ? error.message : String(error)}`)
   }
 }

+ 3 - 5
packages/test-support/session-snapshot/src/suite.ts

@@ -1338,12 +1338,10 @@ export function defineAcpSnapshotSuite(options: SnapshotSuiteOptions): void {
           expect(result.sessionLogs.length, 'this scenario must persist one log per session fixture').toBe(fixtureFiles.length)
           const harvested = result.sessionLogs.map(log => log.content)
           const fixtures = await Promise.all(fixtureFiles.map(file => readFile(join(dir, file), 'utf8')))
-          const fixtureHeaders = fixtures.map(fixture => JSON.parse(
-            fixture.split('\n').find(line => line.trim() !== '') ?? '{}',
-          ) as { id?: unknown; cwd?: unknown })
+          const fixtureContexts = fixtures.map(fixtureContext)
           const fixtureCtx: NormalizeContext = {
-            sessionIds: fixtureHeaders.flatMap(header => typeof header.id === 'string' ? [header.id] : []),
-            cwd: typeof fixtureHeaders[0]?.cwd === 'string' ? fixtureHeaders[0].cwd : '\0no-cwd\0',
+            sessionIds: fixtureContexts.flatMap(context => context.sessionIds),
+            cwd: (fixtureContexts[0] as NormalizeContext).cwd,
           }
           const actualSnapshots = normalizeSessionSnapshots(harvested, ctx)
           const expectedSnapshots = normalizeSessionSnapshots(fixtures, fixtureCtx)

+ 2 - 5
packages/test-support/session-snapshot/src/workspace.ts

@@ -78,7 +78,7 @@ export async function captureWorkspaceSnapshot(
   const visit = async (directory: string, segments: readonly string[]): Promise<WorkspaceSnapshotEntry[]> => {
     const entries = (await readdir(directory, { withFileTypes: true }))
       .filter(entry => segments.length > 0 || !ignoredRootEntries.has(entry.name))
-      .sort((left, right) => left.name < right.name ? -1 : left.name > right.name ? 1 : 0)
+      .sort((left, right) => Buffer.compare(Buffer.from(left.name), Buffer.from(right.name)))
     const captured: WorkspaceSnapshotEntry[] = []
     for (const entry of entries) {
       const childSegments = [...segments, entry.name]
@@ -93,11 +93,8 @@ export async function captureWorkspaceSnapshot(
         captured.push(content === undefined
           ? { path, kind: 'binary', base64: bytes.toString('base64') }
           : { path, kind: 'text', content })
-      } else if (entry.isSymbolicLink()) {
-        captured.push({ path, kind: 'symlink', target: await readlink(absolute) })
       } else {
-        /* v8 ignore next -- ordinary Git workspaces contain only files, directories, and links. */
-        throw new Error(`session-snapshot: unsupported workspace entry ${JSON.stringify(path)}`)
+        captured.push({ path, kind: 'symlink', target: await readlink(absolute) })
       }
     }
     return captured

+ 2 - 0
packages/test-support/session-snapshot/tests/fixtures/suite/plain-turn/snapshot.yml

@@ -1,2 +1,4 @@
 version: 1
 profile: acp
+workspace:
+  final: true

+ 1 - 0
packages/test-support/session-snapshot/tests/fixtures/suite/plain-turn/workspace.expected/seed.txt

@@ -0,0 +1 @@
+prepared at runtime

+ 1 - 1
packages/test-support/session-snapshot/tests/harness.spec.ts

@@ -242,7 +242,7 @@ describe('runScenario', () => {
       agent: { ...profileAgent, configPath: conflictPatch },
       cwd: dir,
       env: { DSH_SNAPSHOT: 'record', DSH_SNAPSHOT_FILE: fixtureFile },
-    })).toThrow('ACP profile package conflict-package resolves to two directories')
+    })).toThrow('snapshot profile package conflict-package resolves to two directories')
 
     const invalidPatch = join(dir, 'invalid.cordis.yml')
     await writeFile(invalidPatch, 'not: a-list\n')

+ 28 - 0
packages/test-support/session-snapshot/tests/identity.spec.ts

@@ -53,4 +53,32 @@ describe('session snapshot identity redaction', () => {
     expect(redacted[0]).toContain('session {{session:2}}')
     expect(redactSessionSnapshotIds(redacted)).toEqual(redacted)
   })
+
+  it('classifies semantic text plus command, RPC, and retry identity fields', () => {
+    const semanticMessage = '88888888-8888-4888-8888-888888888888'
+    const anonymousUser = '99999999-9999-4999-8999-999999999999'
+    const retryId = 'aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa'
+    const source = [
+      JSON.stringify({ type: 'not-a-session', data: { value: 'plain' } }),
+      JSON.stringify({
+        type: 'example',
+        data: {
+          commandId: 'command-7',
+          rpcId: 'rpc-9',
+          retryId,
+          requestId: 'stable-readable-id',
+          text: `Retain this as message ${semanticMessage}. Anonymous user: ${anonymousUser}`,
+        },
+      }),
+    ].join('\n')
+
+    const [redacted] = redactSessionSnapshotIds([source])
+    expect(redacted).toContain('"commandId":"{{command:1}}"')
+    expect(redacted).toContain('"rpcId":"{{rpc:1}}"')
+    expect(redacted).toContain('"retryId":"{{retry:1}}"')
+    expect(redacted).toContain('as message {{message:1}}')
+    expect(redacted).toContain('Anonymous user: {{id:1}}')
+    expect(redacted).toContain('"requestId":"stable-readable-id"')
+    expect(redacted?.endsWith('\n')).toBe(false)
+  })
 })

+ 39 - 0
packages/test-support/session-snapshot/tests/manifest.spec.ts

@@ -26,6 +26,7 @@ describe('snapshot manifest', () => {
   it('parses composition, recording, header, and exceptional replay metadata', () => {
     expect(parseSnapshotManifest([
       'version: 1',
+      'scenario: sdk-case',
       'profile: sdk',
       'composition: continuable-subagent',
       'recording: authored',
@@ -56,6 +57,7 @@ describe('snapshot manifest', () => {
       '',
     ].join('\n'))).toEqual({
       version: 1,
+      scenario: 'sdk-case',
       profile: 'sdk',
       composition: 'continuable-subagent',
       recording: 'authored',
@@ -80,6 +82,38 @@ describe('snapshot manifest', () => {
     })
   })
 
+  it('parses independently optional header and input fields', () => {
+    expect(parseSnapshotManifest([
+      'version: 1',
+      'profile: headless',
+      'header:',
+      '  class: default',
+      'input:',
+      '  task: Run once.',
+      '',
+    ].join('\n'))).toEqual({
+      version: 1,
+      profile: 'headless',
+      header: { class: 'default' },
+      input: { task: 'Run once.' },
+    })
+
+    expect(parseSnapshotManifest([
+      'version: 1',
+      'profile: sdk',
+      'input:',
+      '  attachments:',
+      '    - id: sha256:one',
+      '      mediaType: image/png',
+      '      data: AQ==',
+      '',
+    ].join('\n'))).toEqual({
+      version: 1,
+      profile: 'sdk',
+      input: { attachments: [{ id: 'sha256:one', mediaType: 'image/png', data: 'AQ==' }] },
+    })
+  })
+
   it.each([
     ['', 'manifest must be a mapping'],
     ['version: 2\nprofile: acp\n', 'manifest.version must equal 1'],
@@ -91,6 +125,7 @@ describe('snapshot manifest', () => {
     ['version: 1\nprofile: acp\nheader:\n  class: base\n  pin: false\n', 'manifest.header.pin must equal true when present'],
     ['version: 1\nprofile: acp\nheader:\n  class: base\n  childToolSchemas: [1, 1]\n', 'manifest.header.childToolSchemas must be an array of unique positive integers'],
     ['version: 1\nprofile: acp\nheader:\n  class: base\n  changes: -1\n', 'manifest.header.changes must be a non-negative integer'],
+    ['version: 1\nprofile: acp\nheader:\n  class: base\n  systemPromptSource: ../bad\n', 'manifest.header.systemPromptSource must be a lower-kebab-case name or corpus-relative path'],
     ['version: 1\nprofile: acp\nreplay:\n  override: false\n', 'manifest.replay.override must equal true'],
     ['version: 1\nprofile: acp\nplatform: windows\n', 'manifest.platform must be posix or pwsh'],
     ['version: 1\nprofile: acp\npermission: root\n', 'manifest.permission must be read-only, workspace-write, or danger-full-access'],
@@ -101,6 +136,10 @@ describe('snapshot manifest', () => {
     ['version: 1\nprofile: acp\ninput:\n  task: ""\n', 'manifest.input.task must be a non-empty string when present'],
     ['version: 1\nprofile: acp\ninput: {}\n', 'manifest.input must declare task or attachments'],
     ['version: 1\nprofile: acp\ninput:\n  attachments: []\n', 'manifest.input.attachments must be a non-empty array'],
+    ['version: 1\nprofile: acp\ninput:\n  attachments:\n    - id: raw\n      mediaType: image/png\n      data: AQ==\n', 'manifest.input.attachments[0].id must start with sha256:'],
+    ['version: 1\nprofile: acp\ninput:\n  attachments:\n    - id: sha256:one\n      mediaType: image\n      data: AQ==\n', 'manifest.input.attachments[0].mediaType must be a MIME type'],
+    ['version: 1\nprofile: acp\ninput:\n  attachments:\n    - id: sha256:one\n      mediaType: image/png\n      data: ""\n', 'manifest.input.attachments[0].data must be non-empty base64'],
+    ['version: 1\nprofile: acp\ninput:\n  attachments:\n    - id: sha256:one\n      mediaType: image/png\n      data: AQ==\n    - id: sha256:one\n      mediaType: image/png\n      data: Ag==\n', 'manifest.input.attachments must have unique ids'],
     ['version: 1\nprofile: acp\nsession: {}\n', 'manifest.session.source must be a non-empty string'],
     ['version: 1\nprofile: acp\nsession:\n  source: /tmp/session.jsonl\n', 'manifest.session.source must be a relative POSIX path'],
     ['version: 1\nprofile: acp\nsession:\n  source: ..\\session.jsonl\n', 'manifest.session.source must be a relative POSIX path'],

+ 2 - 2
snapshots/acp/cancel-tool-calls/replay.override.json

@@ -3,8 +3,8 @@
     "kind": "chunks",
     "chunks": [
       { "type": "block-start", "index": 0, "blockType": "tool-call" },
-      { "type": "tool-call-delta", "index": 0, "id": "call_wait", "name": "bash", "argumentsDelta": "{\"command\":\"node -e \\\"require('node:fs').writeFileSync('started.txt', 'started'); setInterval(() => {}, 1000)\\\"\",\"description\":\"Wait until cancellation\"}" },
-      { "type": "block-end", "index": 0, "block": { "type": "tool-call", "id": "call_wait", "name": "bash", "arguments": "{\"command\":\"node -e \\\"require('node:fs').writeFileSync('started.txt', 'started'); setInterval(() => {}, 1000)\\\"\",\"description\":\"Wait until cancellation\"}" } },
+      { "type": "tool-call-delta", "index": 0, "id": "call_wait", "name": "bash", "argumentsDelta": "{\"command\":\"node -e \\\"const fs=require('node:fs'); fs.writeFileSync('started.tmp', 'started'); fs.renameSync('started.tmp', 'started.txt'); setInterval(() => {}, 1000)\\\"\",\"description\":\"Wait until cancellation\"}" },
+      { "type": "block-end", "index": 0, "block": { "type": "tool-call", "id": "call_wait", "name": "bash", "arguments": "{\"command\":\"node -e \\\"const fs=require('node:fs'); fs.writeFileSync('started.tmp', 'started'); fs.renameSync('started.tmp', 'started.txt'); setInterval(() => {}, 1000)\\\"\",\"description\":\"Wait until cancellation\"}" } },
       { "type": "block-start", "index": 1, "blockType": "tool-call" },
       { "type": "tool-call-delta", "index": 1, "id": "call_skipped", "name": "bash", "argumentsDelta": "{\"command\":\"printf skipped > skipped.txt\",\"description\":\"Write skipped marker\"}" },
       { "type": "block-end", "index": 1, "block": { "type": "tool-call", "id": "call_skipped", "name": "bash", "arguments": "{\"command\":\"printf skipped > skipped.txt\",\"description\":\"Write skipped marker\"}" } },

+ 4 - 4
snapshots/acp/cancel-tool-calls/session.jsonl

@@ -12,15 +12,15 @@
 {"type":"request/header","data":{"header":{"config":{"provider":"deepseek-official","model":"deepseek-v4-flash"},"system":"{{system}}","tools":"{{tools}}"},"reason":"initial"}}
 {"type":"request/context","data":{"provider":"deepseek-official","model":"deepseek-v4-flash"}}
 {"type":"assistant/chunk","data":{"turn":1,"step":1,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}}}
-{"type":"assistant/chunk","data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":0,"id":"call_wait","name":"bash","argumentsDelta":"{\"command\":\"node -e \\\"require('node:fs').writeFileSync('started.txt', 'started'); setInterval(() => {}, 1000)\\\"\",\"description\":\"Wait until cancellation\"}"}}}
-{"type":"assistant/chunk","data":{"turn":1,"step":1,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"call_wait","name":"bash","arguments":"{\"command\":\"node -e \\\"require('node:fs').writeFileSync('started.txt', 'started'); setInterval(() => {}, 1000)\\\"\",\"description\":\"Wait until cancellation\"}"}}}}
+{"type":"assistant/chunk","data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":0,"id":"call_wait","name":"bash","argumentsDelta":"{\"command\":\"node -e \\\"const fs=require('node:fs'); fs.writeFileSync('started.tmp', 'started'); fs.renameSync('started.tmp', 'started.txt'); setInterval(() => {}, 1000)\\\"\",\"description\":\"Wait until cancellation\"}"}}}
+{"type":"assistant/chunk","data":{"turn":1,"step":1,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"call_wait","name":"bash","arguments":"{\"command\":\"node -e \\\"const fs=require('node:fs'); fs.writeFileSync('started.tmp', 'started'); fs.renameSync('started.tmp', 'started.txt'); setInterval(() => {}, 1000)\\\"\",\"description\":\"Wait until cancellation\"}"}}}}
 {"type":"assistant/chunk","data":{"turn":1,"step":1,"chunk":{"type":"block-start","index":1,"blockType":"tool-call"}}}
 {"type":"assistant/chunk","data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_skipped","name":"bash","argumentsDelta":"{\"command\":\"printf skipped > skipped.txt\",\"description\":\"Write skipped marker\"}"}}}
 {"type":"assistant/chunk","data":{"turn":1,"step":1,"chunk":{"type":"block-end","index":1,"block":{"type":"tool-call","id":"call_skipped","name":"bash","arguments":"{\"command\":\"printf skipped > skipped.txt\",\"description\":\"Write skipped marker\"}"}}}}
 {"type":"assistant/chunk","data":{"turn":1,"step":1,"chunk":{"type":"usage","usage":{"inputTokens":10,"outputTokens":10}}}}
 {"type":"assistant/chunk","data":{"turn":1,"step":1,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}}
-{"type":"assistant/message","data":{"turn":1,"step":1,"message":{"role":"assistant","content":[{"type":"tool-call","id":"call_wait","name":"bash","arguments":"{\"command\":\"node -e \\\"require('node:fs').writeFileSync('started.txt', 'started'); setInterval(() => {}, 1000)\\\"\",\"description\":\"Wait until cancellation\"}"},{"type":"tool-call","id":"call_skipped","name":"bash","arguments":"{\"command\":\"printf skipped > skipped.txt\",\"description\":\"Write skipped marker\"}"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{message:3}}"},"usage":{"inputTokens":10,"outputTokens":10}},"sourceEventSeqs":[12,13,14,15,16,17,18,19],"surfaceOp":"append"}
-{"type":"tool/call","data":{"turn":1,"step":1,"callId":"call_wait","name":"bash","arguments":"{\"command\":\"node -e \\\"require('node:fs').writeFileSync('started.txt', 'started'); setInterval(() => {}, 1000)\\\"\",\"description\":\"Wait until cancellation\"}"}}
+{"type":"assistant/message","data":{"turn":1,"step":1,"message":{"role":"assistant","content":[{"type":"tool-call","id":"call_wait","name":"bash","arguments":"{\"command\":\"node -e \\\"const fs=require('node:fs'); fs.writeFileSync('started.tmp', 'started'); fs.renameSync('started.tmp', 'started.txt'); setInterval(() => {}, 1000)\\\"\",\"description\":\"Wait until cancellation\"}"},{"type":"tool-call","id":"call_skipped","name":"bash","arguments":"{\"command\":\"printf skipped > skipped.txt\",\"description\":\"Write skipped marker\"}"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{message:3}}"},"usage":{"inputTokens":10,"outputTokens":10}},"sourceEventSeqs":[12,13,14,15,16,17,18,19],"surfaceOp":"append"}
+{"type":"tool/call","data":{"turn":1,"step":1,"callId":"call_wait","name":"bash","arguments":"{\"command\":\"node -e \\\"const fs=require('node:fs'); fs.writeFileSync('started.tmp', 'started'); fs.renameSync('started.tmp', 'started.txt'); setInterval(() => {}, 1000)\\\"\",\"description\":\"Wait until cancellation\"}"}}
 {"type":"tool/result","data":{"turn":1,"step":1,"message":{"source":{"kind":"tool","callId":"call_wait"},"content":[{"type":"tool-result","toolCallId":"call_wait","content":[{"type":"text","text":"Error: tool call aborted"}],"isError":true}],"role":"user","id":"{{message:4}}"},"error":{"name":"AbortError","code":"ABORTED"}},"sourceEventSeqs":[21],"surfaceOp":"append"}
 {"type":"tool/call","data":{"turn":1,"step":1,"callId":"call_skipped","name":"bash","arguments":"{\"command\":\"printf skipped > skipped.txt\",\"description\":\"Write skipped marker\"}"}}
 {"type":"tool/result","data":{"turn":1,"step":1,"message":{"source":{"kind":"tool","callId":"call_skipped"},"content":[{"type":"tool-result","toolCallId":"call_skipped","content":[{"type":"text","text":"Error: tool call aborted before dispatch"}],"isError":true}],"role":"user","id":"{{message:5}}"},"error":{"name":"AbortError","code":"ABORTED_BEFORE_DISPATCH"}},"sourceEventSeqs":[23],"surfaceOp":"append"}

+ 1 - 1
snapshots/acp/cancel-tool-calls/stdout.expected.jsonl

@@ -1,6 +1,6 @@
 {"jsonrpc":"2.0","id":1,"result":{"protocolVersion":1,"agentInfo":{"name":"deepseek-harness-acp","version":"0.0.1"},"agentCapabilities":{"mcpCapabilities":{"http":true},"promptCapabilities":{"image":false,"audio":false,"embeddedContext":false},"sessionCapabilities":{"close":{},"list":{},"resume":{}}},"authMethods":[]}}
 {"jsonrpc":"2.0","id":2,"result":{"sessionId":"{{sessionId}}","configOptions":[{"id":"model","name":"Model","category":"model","type":"select","currentValue":"[\"deepseek-official\",\"deepseek-v4-flash\"]","options":[{"group":"deepseek-official","name":"DeepSeek","options":[{"value":"[\"deepseek-official\",\"deepseek-v4-flash\"]","name":"deepseek-v4-flash"},{"value":"[\"deepseek-official\",\"deepseek-v4-pro\"]","name":"deepseek-v4-pro"}]}]}]}}
-{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"tool_call","toolCallId":"call_wait","title":"bash","kind":"other","status":"in_progress","rawInput":{"command":"node -e \"require('node:fs').writeFileSync('started.txt', 'started'); setInterval(() => {}, 1000)\"","description":"Wait until cancellation"}}}}
+{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"tool_call","toolCallId":"call_wait","title":"bash","kind":"other","status":"in_progress","rawInput":{"command":"node -e \"const fs=require('node:fs'); fs.writeFileSync('started.tmp', 'started'); fs.renameSync('started.tmp', 'started.txt'); setInterval(() => {}, 1000)\"","description":"Wait until cancellation"}}}}
 {"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"tool_call_update","toolCallId":"call_wait","status":"failed","content":[{"type":"content","content":{"type":"text","text":"Error: tool call aborted"}}]}}}
 {"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"tool_call","toolCallId":"call_skipped","title":"bash","kind":"other","status":"in_progress","rawInput":{"command":"printf skipped > skipped.txt","description":"Write skipped marker"}}}}
 {"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"tool_call_update","toolCallId":"call_skipped","status":"failed","content":[{"type":"content","content":{"type":"text","text":"Error: tool call aborted before dispatch"}}]}}}

+ 14 - 3
snapshots/session/headless.snapshot.ts

@@ -4,7 +4,7 @@ import { cp, copyFile, mkdir, readFile, readdir, rm, utimes, writeFile } from 'n
 import { existsSync } from 'node:fs'
 import { spawnSync } from 'node:child_process'
 import { homedir } from 'node:os'
-import { delimiter, dirname, join } from 'node:path'
+import { basename, delimiter, dirname, join } from 'node:path'
 import { fileURLToPath } from 'node:url'
 import { describe, expect, it } from 'vitest'
 import {
@@ -13,6 +13,7 @@ import {
   fixtureContext,
   formatSystemPromptSnapshot,
   formatToolSchemasSnapshot,
+  materializeProfilePatch,
   normalizeSessionSnapshots,
   normalizedHeaders,
   normalizedSystemPrompts,
@@ -62,7 +63,7 @@ function snapshotMode(value: string | undefined): SnapshotMode {
 }
 
 const mode = snapshotMode(process.env.DSH_SNAPSHOT)
-const RUNTIME_WORKSPACE_ENTRIES = ['.agents', '.dsh'] as const
+const RUNTIME_WORKSPACE_ENTRIES = ['.agents', '.dsh', '.snapshot-patches'] as const
 
 interface JsonObject {
   [key: string]: unknown
@@ -507,11 +508,15 @@ describe('headless recorded-session snapshots', () => {
       const fixtureFiles = sessionFixtureNames(await readdir(scenario.dir))
       const replaying = mode !== 'record'
       const compositionPatch = join(composition.dir, replaying ? 'cordis.snapshot.yml' : 'cordis.yml')
-      const patches = [
+      const patchSources = [
         join(baseComposition.dir, 'cordis.yml'),
         ...composition === baseComposition && !replaying ? [] : [compositionPatch],
         join(baseComposition.dir, 'model.cordis.yml'),
       ]
+      const patchRoot = '.snapshot-patches'
+      const patches = patchSources.map((source, index) => source.endsWith('.snapshot.yml')
+        ? join(patchRoot, `${String(index)}-${basename(source)}`)
+        : source)
 
       let actualLogs: SessionLog[] = []
       let initialWorkspace: WorkspaceSnapshotEntry[] | undefined
@@ -556,6 +561,12 @@ describe('headless recorded-session snapshots', () => {
             DSH_TELEMETRY_DISABLED: '1',
           },
           prepare: async (cwd) => {
+            await mkdir(join(cwd, patchRoot), { recursive: true })
+            patchSources.forEach((source, index) => {
+              if (source.endsWith('.snapshot.yml')) {
+                materializeProfilePatch(source, cwd, join(cwd, patchRoot), index)
+              }
+            })
             await seedWorkspace(scenario, cwd)
             initialWorkspace = await captureWorkspaceSnapshot(cwd, {
               ignoredRootEntries: RUNTIME_WORKSPACE_ENTRIES,

File diff suppressed because it is too large
+ 1 - 4
snapshots/web/approval-composer/session.jsonl


+ 0 - 4
snapshots/web/steering/mid-steer.expected.md

@@ -16,10 +16,6 @@
   - img
   - img
   - text: Context injection @deepseek-ai/dsh-system-prompt
-- button "Think The user wants me to use the ask_user_question tool to ask them a specific question with the given parameters. Let me do exactly that.":
-  - img
-  - img
-  - text: Think The user wants me to use the ask_user_question tool to ask them a specific question with the given parameters. Let me do exactly that.
 - text: Running
 - button "Ask question waiting":
   - img

+ 19 - 23
snapshots/web/steering/session.jsonl

@@ -1,40 +1,36 @@
-{"type":"session","version":0,"id":"{{session:1}}","createdAt":1787520632977,"cwd":"{{cwd}}","agentPreset":"standard"}
+{"type":"session","version":0,"id":"{{session:1}}","createdAt":1787528667010,"cwd":"{{cwd}}","agentPreset":"standard"}
 {"type":"permission/preset","data":{"preset":"workspace-write"}}
 {"type":"sandbox/mode","data":{"mode":"workspace-write"}}
 {"type":"approval/policy","data":{"policy":"ask"}}
 {"type":"agent/inbox/spliced","data":{"target":"next-turn","start":0,"inserted":[{"content":[{"type":"text","text":"Use the ask_user_question tool to ask me exactly one question with id \"checkpoint\", question \"Ready to continue?\", header \"Checkpoint\", and options labeled \"Yes\" and \"No\". After I answer, reply with one short sentence acknowledging my answer and stop."}],"source":{"kind":"user","rpcId":"{{rpc:1}}","clientTimeZone":"Asia/Shanghai"},"role":"user","id":"{{message:1}}"}]}}
 {"type":"turn/start","data":{"turn":1}}
 {"type":"agent/inbox/spliced","data":{"target":"next-turn","start":0,"removedCount":1,"inserted":[]}}
-{"type":"agent/inbox/spliced","data":{"target":"next-step","start":0,"inserted":[{"content":[{"type":"text","text":"Interjection: include the word BANANA in your final reply."}],"source":{"kind":"user","rpcId":"{{rpc:2}}","clientTimeZone":"Asia/Shanghai"},"role":"user","id":"{{message:2}}"}]}}
 {"type":"step/start","data":{"turn":1,"step":1}}
 {"type":"user/message","data":{"content":[{"type":"text","text":"Use the ask_user_question tool to ask me exactly one question with id \"checkpoint\", question \"Ready to continue?\", header \"Checkpoint\", and options labeled \"Yes\" and \"No\". After I answer, reply with one short sentence acknowledging my answer and stop."}],"source":{"kind":"user","rpcId":"{{rpc:1}}","clientTimeZone":"Asia/Shanghai"},"role":"user","id":"{{message:1}}"},"surfaceOp":"append"}
-{"type":"user/message","data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent DSH file policy: workspace-write. Any available operation enforced by the DSH file sandbox may modify files under the session workspace: \"{{cwd}}\". Some platform temporary areas may also be writable.\n\nApproval policy: ask. Operations that require approval may ask through the configured answerers; without an available answerer, the request fails closed."}],"source":{"kind":"plugin","plugin":"@deepseek-ai/dsh-system-prompt","form":"snapshot","sections":[{"name":"sandbox:policy","text":"Current DSH file policy: workspace-write. Any available operation enforced by the DSH file sandbox may modify files under the session workspace: \"{{cwd}}\". Some platform temporary areas may also be writable."},{"name":"approval:policy","text":"Approval policy: ask. Operations that require approval may ask through the configured answerers; without an available answerer, the request fails closed."}]},"role":"user","id":"{{message:3}}"},"surfaceOp":"append"}
-{"type":"session/title","data":{"title":"Use the ask_user_question tool to","messageSeqs":[8],"source":{"kind":"fallback"}}}
+{"type":"user/message","data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent DSH file policy: workspace-write. Any available operation enforced by the DSH file sandbox may modify files under the session workspace: \"{{cwd}}\". Some platform temporary areas may also be writable.\n\nApproval policy: ask. Operations that require approval may ask through the configured answerers; without an available answerer, the request fails closed."}],"source":{"kind":"plugin","plugin":"@deepseek-ai/dsh-system-prompt","form":"snapshot","sections":[{"name":"sandbox:policy","text":"Current DSH file policy: workspace-write. Any available operation enforced by the DSH file sandbox may modify files under the session workspace: \"{{cwd}}\". Some platform temporary areas may also be writable."},{"name":"approval:policy","text":"Approval policy: ask. Operations that require approval may ask through the configured answerers; without an available answerer, the request fails closed."}]},"role":"user","id":"{{message:2}}"},"surfaceOp":"append"}
+{"type":"session/title","data":{"title":"Use the ask_user_question tool to","messageSeqs":[7],"source":{"kind":"fallback"}}}
 {"type":"request/header","data":{"header":{"config":{"provider":"deepseek-official","model":"deepseek-v4-flash"},"system":"{{system}}","tools":"{{tools}}"},"reason":"initial"}}
 {"type":"request/context","data":{"provider":"deepseek-official","model":"deepseek-v4-flash","contextWindow":128000}}
-{"type":"assistant/chunk","data":{"turn":1,"step":1,"chunk":{"type":"block-start","index":0,"blockType":"reasoning"}}}
-{"type":"reasoning-chunks","data":{"turn":1,"step":1,"index":0,"dt":[52,52,52,52,52,50,50,52,52,51,51,50,52,53,50,52,52,52,52,51,52,51,50,51,52,52,51,51],"texts":["The"," user"," wants"," me"," to"," use"," the"," ask","_user","_","question"," tool"," to"," ask"," them"," a"," specific"," question"," with"," the"," given"," parameters","."," Let"," me"," do"," exactly"," that","."]}}
-{"type":"assistant/chunk","data":{"turn":1,"step":1,"chunk":{"type":"block-start","index":1,"blockType":"tool-call"}}}
-{"type":"tool-call-chunks","data":{"turn":1,"step":1,"index":1,"dt":[51,52,51,51,51,52,52,52,50,52,52,50,52,50,52,50,50,51,51,51,52,50,53,50,51,50,53,51,52,51,52,51,49,51,53,52,51,51,52,52,50,52,53,53,51,52,50],"id":"call_00_sAvjivLShvnWVk0sPQPV7661","name":"ask_user_question","args":["","{","\"","questions","\"",": ","[","{\"","id","\":"," \"","check","point","\","," \"","question","\":"," \"","Ready"," to"," continue","?\","," \"","header","\":"," \"","Check","point","\","," \"","options","\":"," [","{\"","label","\":"," \"","Yes","\"},"," {\"","label","\":"," \"","No","\"","}]","}]","}"]}}
-{"type":"assistant/chunk","data":{"turn":1,"step":1,"chunk":{"type":"block-end","index":0,"block":{"type":"reasoning","text":"The user wants me to use the ask_user_question tool to ask them a specific question with the given parameters. Let me do exactly that."}}}}
-{"type":"assistant/chunk","data":{"turn":1,"step":1,"chunk":{"type":"block-end","index":1,"block":{"type":"tool-call","id":"call_00_sAvjivLShvnWVk0sPQPV7661","name":"ask_user_question","arguments":"{\"questions\": [{\"id\": \"checkpoint\", \"question\": \"Ready to continue?\", \"header\": \"Checkpoint\", \"options\": [{\"label\": \"Yes\"}, {\"label\": \"No\"}]}]}"}}}}
-{"type":"assistant/chunk","data":{"turn":1,"step":1,"chunk":{"type":"usage","usage":{"inputTokens":151,"outputTokens":115,"cacheReadTokens":7680,"reasoningTokens":29}}}}
+{"type":"agent/inbox/spliced","data":{"target":"next-turn","start":0,"inserted":[{"content":[{"type":"text","text":"Interjection: include the word BANANA in your final reply."}],"source":{"kind":"user","rpcId":"{{rpc:2}}","clientTimeZone":"Asia/Shanghai"},"role":"user","id":"{{message:3}}"}]}}
+{"type":"agent/inbox/spliced","data":{"target":"next-turn","start":0,"removedCount":1,"inserted":[],"outcome":"canceled"}}
+{"type":"agent/inbox/spliced","data":{"target":"next-step","start":0,"inserted":[{"content":[{"type":"text","text":"Interjection: include the word BANANA in your final reply."}],"source":{"kind":"user","rpcId":"{{rpc:2}}","clientTimeZone":"Asia/Shanghai"},"role":"user","id":"{{message:3}}"}]}}
+{"type":"assistant/chunk","data":{"turn":1,"step":1,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}}}
+{"type":"assistant/chunk","data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":0,"id":"steering-question","name":"ask_user_question","argumentsDelta":"{\"questions\":[{\"id\":\"checkpoint\",\"question\":\"Ready to continue?\",\"header\":\"Checkpoint\",\"options\":[{\"label\":\"Yes\"},{\"label\":\"No\"}]}]}"}}}
+{"type":"assistant/chunk","data":{"turn":1,"step":1,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"steering-question","name":"ask_user_question","arguments":"{\"questions\":[{\"id\":\"checkpoint\",\"question\":\"Ready to continue?\",\"header\":\"Checkpoint\",\"options\":[{\"label\":\"Yes\"},{\"label\":\"No\"}]}]}"}}}}
+{"type":"assistant/chunk","data":{"turn":1,"step":1,"chunk":{"type":"usage","usage":{"inputTokens":10,"outputTokens":5}}}}
 {"type":"assistant/chunk","data":{"turn":1,"step":1,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}}
-{"type":"assistant/message","data":{"turn":1,"step":1,"message":{"role":"assistant","content":[{"type":"reasoning","text":"The user wants me to use the ask_user_question tool to ask them a specific question with the given parameters. Let me do exactly that."},{"type":"tool-call","id":"call_00_sAvjivLShvnWVk0sPQPV7661","name":"ask_user_question","arguments":"{\"questions\": [{\"id\": \"checkpoint\", \"question\": \"Ready to continue?\", \"header\": \"Checkpoint\", \"options\": [{\"label\": \"Yes\"}, {\"label\": \"No\"}]}]}"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{message:4}}"},"usage":{"inputTokens":151,"outputTokens":115,"cacheReadTokens":7680,"reasoningTokens":29}},"sourceEventSeqs":[13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31,32,33,34,35,36,37,38,39,40,41,42,43,44,45,46,47,48,49,50,51,52,53,54,55,56,57,58,59,60,61,62,63,64,65,66,67,68,69,70,71,72,73,74,75,76,77,78,79,80,81,82,83,84,85,86,87,88,89,90,91,92,93,94,95],"surfaceOp":"append"}
-{"type":"tool/call","data":{"turn":1,"step":1,"callId":"call_00_sAvjivLShvnWVk0sPQPV7661","name":"ask_user_question","arguments":"{\"questions\": [{\"id\": \"checkpoint\", \"question\": \"Ready to continue?\", \"header\": \"Checkpoint\", \"options\": [{\"label\": \"Yes\"}, {\"label\": \"No\"}]}]}"}}
-{"type":"tool/result","data":{"turn":1,"step":1,"message":{"source":{"kind":"tool","callId":"call_00_sAvjivLShvnWVk0sPQPV7661"},"content":[{"type":"tool-result","toolCallId":"call_00_sAvjivLShvnWVk0sPQPV7661","content":[{"type":"text","text":"{\"answers\":[{\"id\":\"checkpoint\",\"selected\":[\"Yes\"]}]}"}],"isError":false}],"role":"user","id":"{{message:5}}"}},"sourceEventSeqs":[97],"surfaceOp":"append"}
+{"type":"assistant/message","data":{"turn":1,"step":1,"message":{"role":"assistant","content":[{"type":"tool-call","id":"steering-question","name":"ask_user_question","arguments":"{\"questions\":[{\"id\":\"checkpoint\",\"question\":\"Ready to continue?\",\"header\":\"Checkpoint\",\"options\":[{\"label\":\"Yes\"},{\"label\":\"No\"}]}]}"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{message:4}}"},"usage":{"inputTokens":10,"outputTokens":5}},"sourceEventSeqs":[15,16,17,18,19],"surfaceOp":"append"}
+{"type":"tool/call","data":{"turn":1,"step":1,"callId":"steering-question","name":"ask_user_question","arguments":"{\"questions\":[{\"id\":\"checkpoint\",\"question\":\"Ready to continue?\",\"header\":\"Checkpoint\",\"options\":[{\"label\":\"Yes\"},{\"label\":\"No\"}]}]}"}}
+{"type":"tool/result","data":{"turn":1,"step":1,"message":{"source":{"kind":"tool","callId":"steering-question"},"content":[{"type":"tool-result","toolCallId":"steering-question","content":[{"type":"text","text":"{\"answers\":[{\"id\":\"checkpoint\",\"selected\":[\"Yes\"]}]}"}],"isError":false}],"role":"user","id":"{{message:5}}"}},"sourceEventSeqs":[21],"surfaceOp":"append"}
 {"type":"step/end","data":{"turn":1,"step":1}}
 {"type":"agent/inbox/spliced","data":{"target":"next-step","start":0,"removedCount":1,"inserted":[]}}
 {"type":"step/start","data":{"turn":1,"step":2}}
-{"type":"user/message","data":{"content":[{"type":"text","text":"Interjection: include the word BANANA in your final reply."}],"source":{"kind":"user","rpcId":"{{rpc:2}}","clientTimeZone":"Asia/Shanghai"},"role":"user","id":"{{message:2}}"},"surfaceOp":"append"}
-{"type":"assistant/chunk","data":{"turn":1,"step":2,"chunk":{"type":"block-start","index":0,"blockType":"reasoning"}}}
-{"type":"reasoning-chunks","data":{"turn":1,"step":2,"index":0,"dt":[53,53,51,51,53,51,52,52,51,52,50,53,52,51,49,51,52,52,50,52,52,52,51,53,51,51,50,52],"texts":["The"," user"," selected"," \"","Yes","\""," and"," wants"," me"," to"," include"," the"," word"," \"","B","AN","ANA","\""," in"," my"," final"," reply","."," Let"," me"," acknowledge"," their"," answer","."]}}
-{"type":"assistant/chunk","data":{"turn":1,"step":2,"chunk":{"type":"block-start","index":1,"blockType":"text"}}}
-{"type":"text-chunks","data":{"turn":1,"step":2,"index":1,"dt":[51,53,53,52,51,52,53,51,51,51],"texts":["Great",","," let","'s"," move"," forward","."," B","AN","ANA","!"]}}
-{"type":"assistant/chunk","data":{"turn":1,"step":2,"chunk":{"type":"block-end","index":0,"block":{"type":"reasoning","text":"The user selected \"Yes\" and wants me to include the word \"BANANA\" in my final reply. Let me acknowledge their answer."}}}}
-{"type":"assistant/chunk","data":{"turn":1,"step":2,"chunk":{"type":"block-end","index":1,"block":{"type":"text","text":"Great, let's move forward. BANANA!"}}}}
-{"type":"assistant/chunk","data":{"turn":1,"step":2,"chunk":{"type":"usage","usage":{"inputTokens":172,"outputTokens":41,"cacheReadTokens":7808,"reasoningTokens":29}}}}
+{"type":"user/message","data":{"content":[{"type":"text","text":"Interjection: include the word BANANA in your final reply."}],"source":{"kind":"user","rpcId":"{{rpc:2}}","clientTimeZone":"Asia/Shanghai"},"role":"user","id":"{{message:3}}"},"surfaceOp":"append"}
+{"type":"assistant/chunk","data":{"turn":1,"step":2,"chunk":{"type":"block-start","index":0,"blockType":"text"}}}
+{"type":"assistant/chunk","data":{"turn":1,"step":2,"chunk":{"type":"text-delta","index":0,"text":"Great, let's move forward. BANANA!"}}}
+{"type":"assistant/chunk","data":{"turn":1,"step":2,"chunk":{"type":"block-end","index":0,"block":{"type":"text","text":"Great, let's move forward. BANANA!"}}}}
+{"type":"assistant/chunk","data":{"turn":1,"step":2,"chunk":{"type":"usage","usage":{"inputTokens":10,"outputTokens":5}}}}
 {"type":"assistant/chunk","data":{"turn":1,"step":2,"chunk":{"type":"finish","reason":{"kind":"stop"}}}}
-{"type":"assistant/message","data":{"turn":1,"step":2,"message":{"role":"assistant","content":[{"type":"reasoning","text":"The user selected \"Yes\" and wants me to include the word \"BANANA\" in my final reply. Let me acknowledge their answer."},{"type":"text","text":"Great, let's move forward. BANANA!"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{message:6}}"},"usage":{"inputTokens":172,"outputTokens":41,"cacheReadTokens":7808,"reasoningTokens":29}},"sourceEventSeqs":[103,104,105,106,107,108,109,110,111,112,113,114,115,116,117,118,119,120,121,122,123,124,125,126,127,128,129,130,131,132,133,134,135,136,137,138,139,140,141,142,143,144,145,146,147,148],"surfaceOp":"append"}
+{"type":"assistant/message","data":{"turn":1,"step":2,"message":{"role":"assistant","content":[{"type":"text","text":"Great, let's move forward. BANANA!"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{message:6}}"},"usage":{"inputTokens":10,"outputTokens":5}},"sourceEventSeqs":[27,28,29,30,31],"surfaceOp":"append"}
 {"type":"step/end","data":{"turn":1,"step":2}}
 {"type":"turn/end","data":{"turn":1,"reason":{"kind":"completed"}}}

+ 2 - 10
snapshots/web/steering/settled.expected.md

@@ -16,10 +16,6 @@
   - img
   - img
   - text: Context injection @deepseek-ai/dsh-system-prompt
-- button "Think The user wants me to use the ask_user_question tool to ask them a specific question with the given parameters. Let me do exactly that.":
-  - img
-  - img
-  - text: Think The user wants me to use the ask_user_question tool to ask them a specific question with the given parameters. Let me do exactly that.
 - button "Ask question 1/1 answered":
   - img
   - img
@@ -27,10 +23,6 @@
 - text: "Interjection: include the word BANANA in your final reply. {{clock}}"
 - button "Copy":
   - img
-- button "Think The user selected \"Yes\" and wants me to include the word \"BANANA\" in my final reply. Let me acknowledge their answer.":
-  - img
-  - img
-  - text: Think The user selected "Yes" and wants me to include the word "BANANA" in my final reply. Let me acknowledge their answer.
 - paragraph: Great, let's move forward. BANANA!
 - button "Copy":
   - img
@@ -48,6 +40,6 @@
 - button "Select model, current DeepSeek-V4-Flash":
   - text: DeepSeek-V4-Flash
   - img
-- button "6% of context used"
+- button "0% of context used"
 - button "Send message" [disabled]
-- text: 1 turns · 2 steps LLM {{duration}} · Tool call {{duration}} TTFT avg {{duration}} · {{throughput}} tok/s Cache hit 98% Input 15.8K tok · Output 156 tok
+- text: 1 turns · 2 steps LLM {{duration}} · Tool call {{duration}} TTFT avg {{duration}} · {{throughput}} tok/s Cache hit 0% Input 20 tok · Output 10 tok

Some files were not shown because too many files changed in this diff