From 897bac5b8cd3efaa224318e486958bc8fd1019e6 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Thu, 7 May 2026 12:31:18 +0100 Subject: [PATCH] fix(sessions): skip durable fsync for session store --- CHANGELOG.md | 1 + src/config/sessions/sessions.test.ts | 25 +++++++++++++++++++++++++ src/config/sessions/store.ts | 2 +- 3 files changed, 27 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6c4b199265c..f3bcb02935c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ Docs: https://docs.openclaw.ai - Discord/voice: keep TTS playback running when another user starts speaking, ignore new capture during playback to avoid feedback loops, and downgrade expected receive-stream aborts to verbose diagnostics. - Telegram: treat successful same-chat `message` tool outbound sends during an inbound telegram turn as delivered when deciding whether to emit the rewritten silent reply fallback (#78685). Thanks @neeravmakwana. - Gateway/tasks: reconcile stale CLI run-context tasks whose live run context disappeared even when a child session row remains, and apply the default bounded reload deferral timeout to channel hot reloads so stale task records cannot block Discord/Slack/Telegram reloads forever. +- Gateway/sessions: keep session-store index writes atomic while skipping durable fsync inside the writer lock, reducing cron and channel-turn starvation on slow filesystems. Fixes #73655. Thanks @mmartoccia. - Discord/voice: make `openclaw channels capabilities --channel discord --target channel:` and `channels status --probe` audit voice-channel permissions, including auto-join targets, so missing Connect/Speak/Read Message History permissions show up before `/vc join`. - Channels CLI: make `openclaw channels list` channel-only — drop the `Auth providers (OAuth + API keys)` block (use `openclaw models auth list`), drop the per-provider usage/quota fetch and the `--no-usage` flag (use `openclaw status` or `openclaw models list`), add `--all` to surface bundled-unconfigured, catalog-not-installed, and catalog-installed-but-unconfigured channels, and render explicit `installed` / `configured` / `enabled` tags per row plus an `origin` + `installed` field in JSON. Fixes WeCom-class catalog channels disappearing from `--all` when installed on disk but not yet configured. (#78456) Thanks @sliverp. - CLI/cron: add computed `status` field to `cron list --json` and `cron show --json` output, mirroring the human-readable status column (disabled/running/ok/error/skipped/idle) so external tooling can determine job state without re-deriving it from raw state fields. (#78701) Thanks @aweiker. diff --git a/src/config/sessions/sessions.test.ts b/src/config/sessions/sessions.test.ts index 6525be9754d..ee0ad119620 100644 --- a/src/config/sessions/sessions.test.ts +++ b/src/config/sessions/sessions.test.ts @@ -337,6 +337,31 @@ describe("session store writer queue", () => { writeSpy.mockRestore(); }); + it("keeps session store writes atomic while skipping durable fsync inside the writer lock", async () => { + const key = "agent:main:no-fsync"; + const { storePath } = await makeTmpStore({ + [key]: { sessionId: "s-no-fsync", updatedAt: Date.now(), counter: 0 }, + }); + + const writeSpy = vi.spyOn(jsonFiles, "writeTextAtomic"); + await updateSessionStore( + storePath, + async (store) => { + const entry = store[key] as Record; + entry.counter = 1; + }, + { skipMaintenance: true }, + ); + + expect(writeSpy).toHaveBeenCalledTimes(1); + expect(writeSpy).toHaveBeenCalledWith( + storePath, + expect.any(String), + expect.objectContaining({ durable: false, mode: 0o600 }), + ); + writeSpy.mockRestore(); + }); + it("multiple consecutive errors do not permanently poison the queue", async () => { const key = "agent:main:multi-err"; const { storePath } = await makeTmpStore({ diff --git a/src/config/sessions/store.ts b/src/config/sessions/store.ts index 43bf615d83c..1d3d5ec7a14 100644 --- a/src/config/sessions/store.ts +++ b/src/config/sessions/store.ts @@ -499,7 +499,7 @@ async function writeSessionStoreAtomic(params: { store: Record; serialized: string; }): Promise { - await writeTextAtomic(params.storePath, params.serialized, { mode: 0o600 }); + await writeTextAtomic(params.storePath, params.serialized, { durable: false, mode: 0o600 }); updateSessionStoreWriteCaches({ storePath: params.storePath, store: params.store,