惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

博客园 - 叶小钗
D
Docker
GbyAI
GbyAI
Y
Y Combinator Blog
Google DeepMind News
Google DeepMind News
G
Google Developers Blog
P
Proofpoint News Feed
云风的 BLOG
云风的 BLOG
雷峰网
雷峰网
H
Hackread – Cybersecurity News, Data Breaches, AI and More
Stack Overflow Blog
Stack Overflow Blog
WordPress大学
WordPress大学
小众软件
小众软件
Engineering at Meta
Engineering at Meta
酷 壳 – CoolShell
酷 壳 – CoolShell
I
InfoQ
B
Blog
H
Help Net Security
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
博客园 - 聂微东
The GitHub Blog
The GitHub Blog
A
About on SuperTechFans
B
Blog RSS Feed
Microsoft Security Blog
Microsoft Security Blog

Recent Commits to openclaw:main

test: merge chat side-result checks · openclaw/openclaw@ddd2c2a test: merge cron history checks · openclaw/openclaw@f7eb746 test: merge responsive navigation shell checks · openclaw/openclaw@c2e4b47 docs(changelog): add codex oauth fixes · openclaw/openclaw@628e6cd test: merge navigation routing cases · openclaw/openclaw@5d8cecb Tests: mock channel registry bundled fallback · openclaw/openclaw@2b08233 Secrets: avoid broad web search discovery for single plugin config · openclaw/openclaw@a464f59 test: merge config view browser checks · openclaw/openclaw@20cf511 fix(status): align oauth health with runtime · openclaw/openclaw@eed7116 feat: add macOS screen snapshots for monitor preview (#67954) thanks … · openclaw/openclaw@f377db1 fix: report shared auth scopes in hello-ok (#67810) thanks @BunsDev · openclaw/openclaw@0b6c39b Auto-reply: avoid eager bundled route fallback · openclaw/openclaw@3ea1bf4 Tests: narrow session binding contract setup · openclaw/openclaw@54e4e16 fix(macOS): enable undo/redo in webchat composer text input (#34962) · openclaw/openclaw@00951dc Tests: speed up channel setup promotion · openclaw/openclaw@82b529a Docs: refresh agent instructions · openclaw/openclaw@5775fe2 fix(auth): serialize OAuth refresh across agents to fix #26322 (#67876) · openclaw/openclaw@8e79080 test: allow ollama public surface boundary test · openclaw/openclaw@7d4f1a6 Docs: add test performance guardrails · openclaw/openclaw@89706d3 Tests: restore context-engine usage proof · openclaw/openclaw@e4c4f95 Tests: slim context engine runtime coverage · openclaw/openclaw@74c198f ci: retry failed custom checkouts · openclaw/openclaw@0ee5baf test: trim duplicate provider auth onboarding cases · openclaw/openclaw@1ffc02e matrix: fix sessions_spawn --thread subagent session spawning (#67643) · openclaw/openclaw@1ce2596 test: reduce auth choice fixture churn · openclaw/openclaw@857b9cd test: mock health status config boundaries · openclaw/openclaw@9d5ab4a test: mock onboard config io boundary · openclaw/openclaw@299694d test: mock legacy state plugin boundaries · openclaw/openclaw@2713089 test: mock channel install boundaries · openclaw/openclaw@b945248 test: mock doctor preview channel boundaries · openclaw/openclaw@b1a3ad4
fix(agents): suppress Write/Edit failed warning on respon...
MoerAI · 2026-05-27 · via Recent Commits to openclaw:main

@@ -1,4 +1,5 @@

11

import path from "node:path";

2+

import { fileURLToPath } from "node:url";

23

import type { AgentToolResult, AgentToolUpdateCallback } from "@earendil-works/pi-agent-core";

34

import { expandHomePrefix, resolveOsHomeDir } from "../infra/home-dir.js";

45

import { getToolParamsRecord } from "./pi-tools.params.js";

@@ -9,6 +10,30 @@ type EditToolRecoveryOptions = {

910

readFile: (absolutePath: string) => Promise<string>;

1011

};

111213+

type WriteToolRecoveryOptions = {

14+

root: string;

15+

readFile: (absolutePath: string) => Promise<string>;

16+

statFile?: (absolutePath: string) => Promise<WriteToolFileStat | null>;

17+

};

18+19+

type WriteToolParams = {

20+

pathParam?: string;

21+

content?: string;

22+

};

23+24+

type WriteToolFileStat = {

25+

type: "file" | "directory" | "other";

26+

size: number;

27+

mtimeMs?: number;

28+

};

29+30+

type WriteToolOriginalState = "different" | "same" | "unknown";

31+32+

type WriteToolPrecheck = {

33+

state: WriteToolOriginalState;

34+

beforeStat?: WriteToolFileStat | null;

35+

};

36+1237

type EditToolParams = {

1338

pathParam?: string;

1439

edits: EditReplacement[];

@@ -21,10 +46,28 @@ type EditReplacement = {

21462247

const EDIT_MISMATCH_MESSAGE = "Could not find the exact text in";

2348

const EDIT_MISMATCH_HINT_LIMIT = 800;

49+

const WRITE_PRECHECK_READ_LIMIT_BYTES = 1024 * 1024;

50+

const UNICODE_SPACES = /[\u00A0\u2000-\u200A\u202F\u205F\u3000]/g;

245125-

function resolveEditPath(root: string, pathParam: string): string {

52+

function normalizeMutationPathLikeUpstreamWrite(pathParam: string): string {

53+

let normalized = pathParam.replace(UNICODE_SPACES, " ");

54+

if (normalized.startsWith("@")) {

55+

normalized = normalized.slice(1);

56+

}

2657

const home = resolveOsHomeDir();

27-

const expanded = home ? expandHomePrefix(pathParam, { home }) : pathParam;

58+

const expanded = home ? expandHomePrefix(normalized, { home }) : normalized;

59+

if (expanded.startsWith("file://")) {

60+

try {

61+

return fileURLToPath(expanded);

62+

} catch {

63+

return expanded;

64+

}

65+

}

66+

return expanded;

67+

}

68+69+

function resolveFileMutationPath(root: string, pathParam: string): string {

70+

const expanded = normalizeMutationPathLikeUpstreamWrite(pathParam);

2871

return path.isAbsolute(expanded) ? path.resolve(expanded) : path.resolve(root, expanded);

2972

}

3073

@@ -57,6 +100,14 @@ function readEditReplacements(record: Record<string, unknown> | undefined): Edit

57100

});

58101

}

59102103+

function readWriteToolParams(params: unknown): WriteToolParams {

104+

const record = getToolParamsRecord(params);

105+

return {

106+

pathParam: readStringParam(record, "path", "file_path", "filePath", "filepath", "file"),

107+

content: typeof record?.content === "string" ? record.content : undefined,

108+

};

109+

}

110+60111

function readEditToolParams(params: unknown): EditToolParams {

61112

const record = getToolParamsRecord(params);

62113

return {

@@ -128,6 +179,19 @@ function buildEditSuccessResult(pathParam: string, editCount: number): AgentTool

128179

} as AgentToolResult<unknown>;

129180

}

130181182+

function buildWriteSuccessResult(pathParam: string, content: string): AgentToolResult<unknown> {

183+

return {

184+

isError: false,

185+

content: [

186+

{

187+

type: "text",

188+

text: `Successfully wrote ${content.length} bytes to ${pathParam}`,

189+

},

190+

],

191+

details: undefined,

192+

} as AgentToolResult<unknown>;

193+

}

194+131195

function shouldAddMismatchHint(error: unknown) {

132196

return error instanceof Error && error.message.includes(EDIT_MISMATCH_MESSAGE);

133197

}

@@ -142,6 +206,88 @@ function appendMismatchHint(error: Error, currentContent: string): Error {

142206

return enhanced;

143207

}

144208209+

function isWriteRecoveryCandidate(error: unknown, signal: AbortSignal | undefined): boolean {

210+

if (signal?.aborted) {

211+

return true;

212+

}

213+

if (!(error instanceof Error)) {

214+

return false;

215+

}

216+

const message = error.message.toLowerCase();

217+

return (

218+

error.name === "AbortError" ||

219+

error.name === "TimeoutError" ||

220+

message.includes("timed out") ||

221+

message.includes("timeout")

222+

);

223+

}

224+225+

function isMissingFileError(error: unknown): boolean {

226+

if (!error || typeof error !== "object") {

227+

return false;

228+

}

229+

if ("code" in error && (error as { code?: unknown }).code === "ENOENT") {

230+

return true;

231+

}

232+

return error instanceof Error && error.message.includes("No such file or directory");

233+

}

234+235+

async function readOriginalWriteState(

236+

absolutePath: string,

237+

content: string,

238+

options: WriteToolRecoveryOptions,

239+

): Promise<WriteToolPrecheck> {

240+

if (!options.statFile) {

241+

return { state: "unknown" };

242+

}

243+

const contentBytes = Buffer.byteLength(content, "utf8");

244+

let stat: WriteToolFileStat | null;

245+

try {

246+

stat = await options.statFile(absolutePath);

247+

} catch (err) {

248+

return { state: isMissingFileError(err) ? "different" : "unknown" };

249+

}

250+

if (!stat) {

251+

return { state: "different", beforeStat: stat };

252+

}

253+

if (stat.type !== "file") {

254+

return { state: "unknown", beforeStat: stat };

255+

}

256+

if (stat.size !== contentBytes) {

257+

return { state: "different", beforeStat: stat };

258+

}

259+

if (stat.size > WRITE_PRECHECK_READ_LIMIT_BYTES) {

260+

return { state: "unknown", beforeStat: stat };

261+

}

262+263+

try {

264+

const originalContent = await options.readFile(absolutePath);

265+

return { state: originalContent === content ? "same" : "different", beforeStat: stat };

266+

} catch {

267+

return { state: "unknown", beforeStat: stat };

268+

}

269+

}

270+271+

async function didWriteMetadataChange(

272+

absolutePath: string,

273+

beforeStat: WriteToolFileStat | null | undefined,

274+

options: WriteToolRecoveryOptions,

275+

): Promise<boolean> {

276+

if (!beforeStat || !options.statFile) {

277+

return false;

278+

}

279+

let afterStat: WriteToolFileStat | null;

280+

try {

281+

afterStat = await options.statFile(absolutePath);

282+

} catch {

283+

return false;

284+

}

285+

if (!afterStat || afterStat.type !== "file") {

286+

return false;

287+

}

288+

return afterStat.size !== beforeStat.size || afterStat.mtimeMs !== beforeStat.mtimeMs;

289+

}

290+145291

/**

146292

* Recover from two edit-tool failure classes without changing edit semantics:

147293

* - exact-match mismatch errors become actionable by including current file contents

@@ -161,7 +307,9 @@ export function wrapEditToolWithRecovery(

161307

) => {

162308

const { pathParam, edits } = readEditToolParams(params);

163309

const absolutePath =

164-

typeof pathParam === "string" ? resolveEditPath(options.root, pathParam) : undefined;

310+

typeof pathParam === "string"

311+

? resolveFileMutationPath(options.root, pathParam)

312+

: undefined;

165313

let originalContent: string | undefined;

166314167315

if (absolutePath && edits.length > 0) {

@@ -211,3 +359,59 @@ export function wrapEditToolWithRecovery(

211359

},

212360

};

213361

}

362+363+

/**

364+

* Recover write calls that complete the disk write but abort before returning.

365+

* Readback is the source of truth; argument-derived paths never prove success.

366+

*/

367+

export function wrapWriteToolWithRecovery(

368+

base: AnyAgentTool,

369+

options: WriteToolRecoveryOptions,

370+

): AnyAgentTool {

371+

return {

372+

...base,

373+

execute: async (

374+

toolCallId: string,

375+

params: unknown,

376+

signal: AbortSignal | undefined,

377+

onUpdate?: AgentToolUpdateCallback<unknown>,

378+

) => {

379+

const { pathParam, content } = readWriteToolParams(params);

380+

const absolutePath =

381+

typeof pathParam === "string" && typeof content === "string"

382+

? resolveFileMutationPath(options.root, pathParam)

383+

: undefined;

384+

const precheck: WriteToolPrecheck =

385+

absolutePath && typeof content === "string"

386+

? await readOriginalWriteState(absolutePath, content, options)

387+

: { state: "unknown" };

388+389+

try {

390+

return await base.execute(toolCallId, params, signal, onUpdate);

391+

} catch (err) {

392+

if (

393+

!isWriteRecoveryCandidate(err, signal) ||

394+

typeof absolutePath !== "string" ||

395+

typeof pathParam !== "string" ||

396+

typeof content !== "string"

397+

) {

398+

throw err;

399+

}

400+

let currentContent: string | undefined;

401+

try {

402+

currentContent = await options.readFile(absolutePath);

403+

} catch {

404+

// Fall through to the original abort if readback fails.

405+

}

406+

const changed =

407+

precheck.state === "different" ||

408+

(precheck.state === "unknown" &&

409+

(await didWriteMetadataChange(absolutePath, precheck.beforeStat, options)));

410+

if (currentContent === content && changed) {

411+

return buildWriteSuccessResult(pathParam, content);

412+

}

413+

throw err;

414+

}

415+

},

416+

};

417+

}