feat(fs): append recovery remedy to guarded-mutation errors

write/edit failures with FS_STALE_VERSION or FS_NOT_OBSERVED now reach the
model with the correct recovery instruction appended (re-read / read, then
retry) while preserving the structured code and chaining the cause. The
edit-intent waterfall sits inside the same try, so the policy's
FS_NOT_OBSERVED refusal is remediated too. Re-recorded the fs-policy-reject
keyless snapshot and the bilingual README pairs.
This commit is contained in:
_Kerman
2026-08-03 19:49:30 +08:00
parent 012bc40f04
commit 1ee167aeac
13 changed files with 174 additions and 16 deletions

View File

@@ -11,6 +11,7 @@ import type { DiffCallView, DiffResultView, ToolResult } from '@deepseek-ai/dsh-
import type {} from '@deepseek-ai/dsh-fs'
import type {} from '@deepseek-ai/dsh-system-prompt'
import { computeHunkDiffs, diffsFromMeta } from './diff.ts'
import { remediateFsError } from './error.ts'
import { sessionResolveOptions } from './session-cwd.ts'
import type { FsSandboxSurface } from './sandbox.ts'
@@ -116,10 +117,13 @@ export function applyEditTool(ctx: Context, sandbox: FsSandboxSurface): void {
const target = await ctx.fs.resolve(input.filePath, sessionResolveOptions(exec, input.filePath, sandboxPolicy?.workspaceRoot))
// Single-slot decision: the policy plugin returns { version: vObserved } or
// throws FS_NOT_OBSERVED; the bare default is undefined (unconditional edit).
// No stat — the bare default never manufactures a version basis.
const intent = await ctx.waterfall('fs/edit-intent', target, exec, () => undefined)
// No stat — the bare default never manufactures a version basis. The intent
// slot itself can throw FS_NOT_OBSERVED for an unread target, so it sits
// inside the try: both that refusal and the provider's guarded-mutation
// failure get the model-facing remedy below.
let outcome
try {
const intent = await ctx.waterfall('fs/edit-intent', target, exec, () => undefined)
outcome = await ctx.fs.editText(
target,
{ oldString: input.oldString, newString: input.newString, replaceAll: input.replaceAll },
@@ -128,8 +132,10 @@ export function applyEditTool(ctx: Context, sandbox: FsSandboxSurface): void {
sandboxPolicy,
)
} catch (error: unknown) {
// A sandbox denial becomes the shared [sandbox: …] marker; any other error passes through.
throw sandbox.mapError(error, sandboxPolicy)
// A sandbox denial becomes the shared [sandbox: …] marker (the model
// recognizes it from bash); stale/not-observed failures gain their
// model-facing remedy; anything else passes through.
throw remediateFsError(sandbox.mapError(error, sandboxPolicy))
}
// Record the observed version (a no-op when no policy plugin listens).
ctx.emit('fs/observed', target, outcome.version, exec)