fix(media): ignore EPERM during best-effort fsync

This commit is contained in:
Peter Steinberger
2026-05-04 04:26:07 +01:00
parent cf40284544
commit 484195d14e
3 changed files with 42 additions and 1 deletions

View File

@@ -49,6 +49,7 @@ Docs: https://docs.openclaw.ai
- Control UI: render Dream Diary prose through the sanitized markdown pipeline, so diary bold/italic/header markdown no longer appears as literal source text. Fixes #62413.
- Control UI: render tool results whose output arrives as text-block arrays and give expanded tool output a scrollable block, so read/exec output remains visible in WebChat. Fixes #77054.
- MCP: include serialized conversation/message payloads in the primary text content for `conversations_list` and `messages_read`, while preserving `structuredContent` for capable clients. Fixes #77024.
- Media: treat `EPERM` from the post-write media fsync step as best-effort, allowing WebChat and channel uploads to finish on Windows filesystems that reject `fsync` after a successful write. Fixes #76844.
- Diagnostics: keep webhook/message OTEL attributes and Prometheus delivery labels low-cardinality and omit raw chat/message IDs from spans, so progress-draft and message-tool modes do not leak high-cardinality messaging identifiers.
- Google Meet: stop advertising legacy `mode: "realtime"` to agents and config UIs, while keeping it as a hidden compatibility alias for `mode: "agent"`, so new joins use the STT -> OpenClaw agent -> TTS path instead of selecting the direct realtime voice fallback.
- Google Meet: add `chrome.audioBufferBytes` for generated command-pair SoX audio commands and lower the default buffer from SoX's 8192 bytes to 4096 bytes to reduce Chrome talk-back latency.

View File

@@ -341,6 +341,35 @@ describe("media store", () => {
});
},
},
{
name: "saves buffers when the best-effort fsync step reports EPERM",
run: async () => {
await withTempStore(async (store) => {
const originalOpen = fs.open.bind(fs);
vi.spyOn(fs, "open").mockImplementation(async (...args) => {
const handle = await originalOpen(...args);
const filePath = args[0];
if (
typeof filePath === "string" &&
filePath.includes(`${path.sep}fsync-eperm${path.sep}`)
) {
vi.spyOn(handle, "sync").mockRejectedValueOnce(
Object.assign(new Error("operation not permitted"), { code: "EPERM" }),
);
}
return handle;
});
const saved = await store.saveMediaBuffer(
Buffer.from("docx"),
"application/zip",
"fsync-eperm",
);
await expect(fs.readFile(saved.path, "utf8")).resolves.toBe("docx");
});
},
},
{
name: "rejects traversal media subdirs before saving buffers",
run: async () => {

View File

@@ -351,7 +351,7 @@ async function writeSavedMediaBuffer(params: {
await fs.writeFile(tempDest, params.buffer, { mode: MEDIA_FILE_MODE });
const handle = await fs.open(tempDest, "r");
try {
await handle.sync();
await syncSavedMediaHandle(handle);
} finally {
await handle.close();
}
@@ -364,6 +364,17 @@ async function writeSavedMediaBuffer(params: {
return dest;
}
async function syncSavedMediaHandle(handle: fs.FileHandle): Promise<void> {
try {
await handle.sync();
} catch (err) {
if ((err as NodeJS.ErrnoException | undefined)?.code === "EPERM") {
return;
}
throw err;
}
}
export type SaveMediaSourceErrorCode =
| "invalid-path"
| "not-found"