Merge remote-tracking branch 'origin/master' into codex/pr335-merge-master-20260719

# Conflicts:
#	packages/support/acp-snapshot/README.md
#	packages/support/acp-snapshot/src/harness.ts
#	packages/support/acp-snapshot/src/suite.ts
#	packages/support/acp-snapshot/tests/harness.spec.ts
#	packages/support/acp-snapshot/tests/suite.spec.ts
This commit is contained in:
Tianyi Cui
2026-07-19 11:41:10 +08:00
330 changed files with 7947 additions and 4450 deletions

View File

@@ -46,6 +46,8 @@ The tool passes `exec` (the tool-execution context) as the opaque `actor` on eve
`fs/observed` fires AFTER the read/write/edit already succeeded, via a plain `ctx.emit`. A listener is contractually a synchronous, side-effect-only recorder (`@deepseek-ai/dsh-fs-policy`'s is a `WeakMap.set`); the tool does not guard the emit, so a listener that throws would surface as the tool's `isError` result — async or fallible observation does not belong on this event.
`read` opts into concurrent scheduling because its only mutation is the synchronous version recorder. Recorder races fail closed when a later `write` or `edit` re-checks the version under its target lock; both mutation tools remain exclusive. See the [parallel tool-call RFC](../../../docs/rfc/implemented/feature/2026-07-10-parallel-tool-call-execution.md).
The package root exports only the Cordis plugin contract (`name`, `inject`, `Config`, and `apply`). Read rendering (line windowing + output formatting) lives in `src/read-render.ts` (Cordis-free, independently unit-tested); `src/read.ts`/`write.ts`/`edit.ts` are the tool executors and `src/index.ts` composes them.
## Model Experience

View File

@@ -84,6 +84,8 @@ export function applyReadTool(ctx: Context, caps: ReadToolCaps): void {
offset: { type: 'number', description: '1-based first line to return. Defaults to 1.' },
limit: { type: 'number', description: `Maximum number of lines to return. Defaults to ${caps.limit}.` },
},
// Observation races fail closed because guarded mutations re-check the version in-lock.
isConcurrencySafe: () => true,
async execute(args, exec): Promise<ContentBlock[]> {
const input = parseReadArgs(args, caps.limit)
const target = await ctx.fs.resolve(input.filePath, sessionResolveOptions(exec))

View File

@@ -3,7 +3,6 @@ import { tmpdir } from 'node:os'
import { join } from 'node:path'
import { afterEach, describe, expect, it } from 'vitest'
import type { Context } from 'cordis'
import { AgentId } from '@deepseek-ai/dsh-agent'
import { SessionId } from '@deepseek-ai/dsh-session'
import { fsHarness, waitForIdle } from './harness.ts'
@@ -35,7 +34,7 @@ describe.skipIf(!process.env.DEEPSEEK_API_KEY)('fs tools with-key smoke', () =>
ctx = await fsHarness(workdir, SYSTEM)
// agentLoop.create prepares a session with no cwd, so the provider default
// (config.cwd = workdir) is the workspace.
const agent = ctx.agentLoop.create(AgentId('fs-e2e'), { provider: 'deepseek', model: 'deepseek-v4-flash' })
const agent = ctx.agentLoop.create(SessionId('fs-e2e'), { provider: 'deepseek', model: 'deepseek-v4-flash' })
agent.send([{ type: 'text', text:
'Create a file named note.txt containing exactly the line: status: draft. '
@@ -65,7 +64,6 @@ describe.skipIf(!process.env.DEEPSEEK_API_KEY)('fs tools with-key smoke', () =>
try {
ctx = await fsHarness(configDir, SYSTEM)
const handle = await ctx.agents.create({
agentId: AgentId('fs-e2e-cwd'),
sessionId: SessionId(`fs-e2e-cwd-${Date.now()}`),
meta: { cwd: sessionDir },
agentOptions: { provider: 'deepseek', model: 'deepseek-v4-flash' },

View File

@@ -384,6 +384,33 @@ describe('signal, concurrency, and the fs/observed contract', () => {
expect(onDisk === 'ONE value here' || onDisk === 'base TWO here').toBe(true)
})
it('a stale observed version from an older read fails closed at edit CAS', async () => {
await writeFile(join(dir, 'a.txt'), 'older content\n')
const target = await ctx.fs.resolve('a.txt')
const firstInfo = await ctx.fs.stat(target)
if (!firstInfo) throw new Error('expected first stat')
expect((await callOwned('read', { file_path: 'a.txt' })).isError).toBe(false)
await writeFile(join(dir, 'a.txt'), 'newer current content\n')
const secondInfo = await ctx.fs.stat(target)
if (!secondInfo) throw new Error('expected second stat')
expect(secondInfo.version).not.toBe(firstInfo.version)
expect((await callOwned('read', { file_path: 'a.txt' })).isError).toBe(false)
// Reproduce an older concurrent read winning the observation race.
ctx.emit('fs/observed', target, firstInfo.version, { agent: { session } })
const edit = await callOwned('edit', {
file_path: 'a.txt',
old_string: 'newer',
new_string: 'edited',
})
expect(edit.isError).toBe(true)
expect(edit.error).toMatchObject({ code: 'FS_STALE_VERSION' })
expect(await readFile(join(dir, 'a.txt'), 'utf8')).toBe('newer current content\n')
})
it('a throwing fs/observed listener surfaces as isError, but the mutation already hit disk', async () => {
// fs/observed is a plain ctx.emit after the write succeeded; a throwing listener cannot
// roll the write back — it only turns the tool result into isError.

View File

@@ -108,6 +108,16 @@ describe('registration', () => {
expect(ctx.tools.schemas().map(s => s.name).sort()).toEqual(['edit', 'read', 'write'])
})
it('declares read parallel-safe while write/edit remain exclusive', async () => {
const { ctx } = await setup()
expect(ctx.tools.executionMode({ callId: CallId('read-safe'), name: 'read', arguments: { file_path: 'a.txt' } }))
.toEqual({ kind: 'parallel' })
expect(ctx.tools.executionMode({ callId: CallId('write-exclusive'), name: 'write', arguments: { file_path: 'a.txt', content: 'x' } }))
.toEqual({ kind: 'exclusive' })
expect(ctx.tools.executionMode({ callId: CallId('edit-exclusive'), name: 'edit', arguments: { file_path: 'a.txt', old_string: 'x', new_string: 'y' } }))
.toEqual({ kind: 'exclusive' })
})
it('registers prompt sections for each tool', async () => {
const { ctx } = await setup()
const prompt = renderPrompt(await ctx.systemPrompt.assemble())