Merge remote-tracking branch 'origin/master' into codex/session-title

# Conflicts:
#	docs/architecture.md
#	docs/config-catalog.md
#	docs/event-producer-consumer.md
#	docs/module-graph.md
#	examples/acp-agent/tests/snapshots/advanced-toolchain/stdout.expected.jsonl
#	examples/acp-agent/tests/snapshots/bash-spill/stdout.expected.jsonl
#	examples/acp-agent/tests/snapshots/both-mode-turn/stdout.expected.jsonl
#	examples/acp-agent/tests/snapshots/cancel-tool-calls/stdout.expected.jsonl
#	examples/acp-agent/tests/snapshots/cancel/stdout.expected.jsonl
#	examples/acp-agent/tests/snapshots/code-mode-turn/stdout.expected.jsonl
#	examples/acp-agent/tests/snapshots/code-mode-workspace-context/stdout.expected.jsonl
#	examples/acp-agent/tests/snapshots/cordis-inspect-jsdoc/stdout.expected.jsonl
#	examples/acp-agent/tests/snapshots/error-finish/stdout.expected.jsonl
#	examples/acp-agent/tests/snapshots/fs-edit/stdout.expected.jsonl
#	examples/acp-agent/tests/snapshots/fs-policy-reject/stdout.expected.jsonl
#	examples/acp-agent/tests/snapshots/fs-read-window/stdout.expected.jsonl
#	examples/acp-agent/tests/snapshots/fs-read/stdout.expected.jsonl
#	examples/acp-agent/tests/snapshots/fs-terminal-card/stdout.expected.jsonl
#	examples/acp-agent/tests/snapshots/fs-write-overwrite/stdout.expected.jsonl
#	examples/acp-agent/tests/snapshots/fs-write/stdout.expected.jsonl
#	examples/acp-agent/tests/snapshots/hook-cc-posttool-block/stdout.expected.jsonl
#	examples/acp-agent/tests/snapshots/hook-cc-posttool-context/stdout.expected.jsonl
#	examples/acp-agent/tests/snapshots/hook-cc-pretool-ask/stdout.expected.jsonl
#	examples/acp-agent/tests/snapshots/hook-cc-pretool-deny/stdout.expected.jsonl
#	examples/acp-agent/tests/snapshots/hook-cc-promptsubmit-context/stdout.expected.jsonl
#	examples/acp-agent/tests/snapshots/hook-cc-stop-continue/stdout.expected.jsonl
#	examples/acp-agent/tests/snapshots/hook-codex-posttool-block/stdout.expected.jsonl
#	examples/acp-agent/tests/snapshots/hook-codex-posttool-context/stdout.expected.jsonl
#	examples/acp-agent/tests/snapshots/hook-codex-pretool-block/stdout.expected.jsonl
#	examples/acp-agent/tests/snapshots/hook-codex-promptsubmit-context/stdout.expected.jsonl
#	examples/acp-agent/tests/snapshots/hook-codex-stop-continue/stdout.expected.jsonl
#	examples/acp-agent/tests/snapshots/model-switching/stdout.expected.jsonl
#	examples/acp-agent/tests/snapshots/multi-turn/stdout.expected.jsonl
#	examples/acp-agent/tests/snapshots/parallel-tool-calls/stdout.expected.jsonl
#	examples/acp-agent/tests/snapshots/repeat-tool-guard/stdout.expected.jsonl
#	examples/acp-agent/tests/snapshots/skill-load/stdout.expected.jsonl
#	examples/acp-agent/tests/snapshots/subagent-depth-two-rejection/stdout.expected.jsonl
#	examples/acp-agent/tests/snapshots/subagent-fork/stdout.expected.jsonl
#	examples/acp-agent/tests/snapshots/subagent-mixed/stdout.expected.jsonl
#	examples/acp-agent/tests/snapshots/subagent-multi/stdout.expected.jsonl
#	examples/acp-agent/tests/snapshots/subagent-spawn/stdout.expected.jsonl
#	examples/acp-agent/tests/snapshots/text-turn/stdout.expected.jsonl
#	examples/acp-agent/tests/snapshots/todo-plan/stdout.expected.jsonl
#	examples/acp-agent/tests/snapshots/tool-call-turn/stdout.expected.jsonl
#	examples/acp-agent/tests/snapshots/workflow-run/stdout.expected.jsonl
#	examples/acp-agent/tests/snapshots/workspace-context/stdout.expected.jsonl
#	examples/acp-agent/tests/snapshots/workspace-edit/stdout.expected.jsonl
#	examples/headless-agent/tests/headless.snapshot.ts
#	packages/examples/README.md
#	packages/examples/agent-spine-demo/README.md
#	packages/examples/agent-spine-demo/package.json
#	packages/ui/README.md
#	packages/ui/acp/README.md
This commit is contained in:
Tianyi Cui
2026-07-21 02:13:10 +08:00
273 changed files with 13033 additions and 314 deletions
@@ -152,6 +152,7 @@ async function handlePrompt(id: number | string): Promise<void> {
mode: process.env.DSH_SNAPSHOT,
override: process.env.DSH_SNAPSHOT_OVERRIDE ?? null,
childFiles: process.env.DSH_SNAPSHOT_CHILD_FILES ?? null,
spillRoot: process.env.DSH_SNAPSHOT_SPILL_ROOT ?? null,
})}`)
}
if (behavior.echoWorkspace === true) {
@@ -60,6 +60,15 @@ async function scenario(behavior: object): Promise<{ dir: string; fixtureFile: s
const boot: InputStep[] = [{ op: 'initialize' }, { op: 'newSession' }]
function environmentEcho(rawStdout: string): Record<string, unknown> {
const frames = rawStdout.trim().split('\n')
.map(line => JSON.parse(line) as { params?: { update?: { content?: { text?: unknown } } } })
const text = frames.map(frame => frame.params?.update?.content?.text)
.find(value => typeof value === 'string' && value.startsWith('env:'))
if (typeof text !== 'string') throw new Error('fake ACP agent did not echo its environment')
return JSON.parse(text.slice('env:'.length)) as Record<string, unknown>
}
describe('runScenario', () => {
it('surfaces an asynchronous child spawn failure through startup and close', async () => {
const { dir } = await scenario({})
@@ -309,6 +318,19 @@ describe('runScenario', () => {
expect(result.rawStdout).toContain(JSON.stringify(childFiles.join(delimiter)).slice(1, -1))
})
it('gives concurrent scenarios distinct equal-length spill roots', { timeout: 20_000 }, async () => {
const [first, second] = await Promise.all([scenario({ echoEnv: true }), scenario({ echoEnv: true })])
const results = await Promise.all([first, second].map(({ fixtureFile }) => runScenario(
{ steps: [...boot, { op: 'prompt', text: 'env?' }] },
{ agent: AGENT, mode: 'replay', fixtureFile },
)))
const roots = results.map(result => environmentEcho(result.rawStdout).spillRoot)
expect(roots.every(root => typeof root === 'string')).toBe(true)
expect(new Set(roots).size).toBe(2)
expect((roots[0] as string).length).toBe((roots[1] as string).length)
expect((roots[0] as string).length).toBe('/tmp/dsh-acp-snapshot-spill'.length)
})
it('seeds the workspace dir into the temp cwd before the run', { timeout: 20_000 }, async () => {
const { dir, fixtureFile } = await scenario({ echoWorkspace: true })
const workspaceDir = join(dir, 'workspace')
@@ -334,6 +356,21 @@ describe('runScenario', () => {
expect(result.rawStdout.indexOf('thinking about it')).toBeLessThan(result.rawStdout.indexOf('cancelled'))
})
it('promptAndWaitForAgentMessage keeps the app live through a matching later update', { timeout: 20_000 }, async () => {
const { fixtureFile } = await scenario({ prompt: 'respond' })
const result = await runScenario(
{
steps: [...boot, {
op: 'promptAndWaitForAgentMessage',
text: 'go',
waitForText: 'thinking about it',
}],
},
{ agent: AGENT, mode: 'replay', fixtureFile },
)
expect(result.rawStdout).toContain('thinking about it')
})
it('promptAndCancel can bracket cancellation with tool-call updates', { timeout: 20_000 }, async () => {
const { fixtureFile } = await scenario({
prompt: 'hang-until-cancel',
@@ -443,6 +480,7 @@ describe('runScenario', () => {
it.each([
[{ op: 'prompt', text: 'x' }, /prompt before newSession/],
[{ op: 'promptAndWaitForAgentMessage', text: 'x', waitForText: 'later' }, /promptAndWaitForAgentMessage before newSession/],
[{ op: 'promptExpectError', text: 'x' }, /promptExpectError before newSession/],
[{ op: 'promptAndCancel', text: 'x' }, /promptAndCancel before newSession/],
[{ op: 'cancel' }, /cancel before newSession/],
@@ -157,6 +157,21 @@ describe('normalizeSessionLog', () => {
expect(out).not.toContain('/tmp/dsh-acp-snapshot-spill')
})
it('scrubs scenario-owned snapshot spill paths', () => {
const ev = JSON.stringify({
type: 'tool/result', seq: 2, time: 5,
data: {
content: [{
type: 'text',
text: 'Full formatted result stored at: /tmp/dsh-acp-snap-012345678/session-c22bc3f1d2af/8a7b6c5d4e3f-bash.txt. Use read with offset/limit, or grep this path to search within it.',
}],
},
})
const out = normalizeSessionLog(`${header({ cwd: ctx.cwd })}\n${ev}\n`, ctx)
expect(out).toContain('{{spillLocator:bash.txt}}')
expect(out).not.toContain('/tmp/dsh-acp-snap-012345678')
})
it('scrubs the session id in the header', () => {
const out = normalizeSessionLog(`${header({ id: ctx.sessionIds[0] })}\n`, ctx)
expect(out).toContain('{{sessionId}}')