diff --git a/CHANGELOG.md b/CHANGELOG.md index b86eb2249cb..126ec8a6e27 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,7 @@ Docs: https://docs.openclaw.ai ### Fixes +- Gateway/Pairing: treat operator.admin pairing tokens as satisfying operator.write requests so legacy devices stop looping through scope-upgrade prompts introduced in 2026.2.19. (#23125, #23006) Thanks @vignesh07. - Memory/QMD: add optional `memory.qmd.mcporter` search routing so QMD `query/search/vsearch` can run through mcporter keep-alive flows (including multi-collection paths) to reduce cold starts, while keeping searches on agent-scoped QMD state for consistent recall. (#19617) Thanks @nicole-luxe and @vignesh07. - Chat/UI: strip inline reply/audio directive tags (`[[reply_to_current]]`, `[[reply_to:]]`, `[[audio_as_voice]]`) from displayed chat history, live chat event output, and session preview snippets so control tags no longer leak into user-visible surfaces. - BlueBubbles/DM history: restore DM backfill context with account-scoped rolling history, bounded backfill retries, and safer history payload limits. (#20302) Thanks @Ryan-Haines. diff --git a/src/infra/gateway-lock.test.ts b/src/infra/gateway-lock.test.ts index f4a8c999d24..195a242defc 100644 --- a/src/infra/gateway-lock.test.ts +++ b/src/infra/gateway-lock.test.ts @@ -196,6 +196,55 @@ describe("gateway lock", () => { staleSpy.mockRestore(); }); + it("keeps lock when fs.stat fails until payload is stale", async () => { + vi.useRealTimers(); + const env = await makeEnv(); + const { lockPath, configPath } = resolveLockPath(env); + const payload = createLockPayload({ configPath, startTime: 111 }); + await fs.writeFile(lockPath, JSON.stringify(payload), "utf8"); + + const procSpy = mockProcStatRead({ + onProcRead: () => { + throw new Error("EACCES"); + }, + }); + const statSpy = vi + .spyOn(fs, "stat") + .mockRejectedValue(Object.assign(new Error("EPERM"), { code: "EPERM" })); + + const pending = acquireForTest(env, { + timeoutMs: 20, + staleMs: 10_000, + platform: "linux", + }); + await expect(pending).rejects.toBeInstanceOf(GatewayLockError); + + procSpy.mockRestore(); + + const stalePayload = createLockPayload({ + configPath, + startTime: 111, + createdAt: new Date(0).toISOString(), + }); + await fs.writeFile(lockPath, JSON.stringify(stalePayload), "utf8"); + + const staleProcSpy = mockProcStatRead({ + onProcRead: () => { + throw new Error("EACCES"); + }, + }); + + const lock = await acquireForTest(env, { + staleMs: 1, + platform: "linux", + }); + expect(lock).not.toBeNull(); + + await lock?.release(); + staleProcSpy.mockRestore(); + statSpy.mockRestore(); + }); + it("returns null when multi-gateway override is enabled", async () => { const env = await makeEnv(); const lock = await acquireGatewayLock({ diff --git a/src/infra/gateway-lock.ts b/src/infra/gateway-lock.ts index ccca44c4b58..34300f9545b 100644 --- a/src/infra/gateway-lock.ts +++ b/src/infra/gateway-lock.ts @@ -231,7 +231,11 @@ export async function acquireGatewayLock( const st = await fs.stat(lockPath); stale = Date.now() - st.mtimeMs > staleMs; } catch { - stale = true; + // On Windows or locked filesystems we may be unable to stat the + // lock file even though the existing gateway is still healthy. + // Treat the lock as non-stale so we keep waiting instead of + // forcefully removing another gateway's lock. + stale = false; } } if (stale) { diff --git a/src/memory/qmd-manager.test.ts b/src/memory/qmd-manager.test.ts index b0dd592cf6c..49dfca02fa9 100644 --- a/src/memory/qmd-manager.test.ts +++ b/src/memory/qmd-manager.test.ts @@ -985,8 +985,9 @@ describe("QmdMemoryManager", () => { ); expect(mcporterCall).toBeDefined(); const spawnOpts = mcporterCall?.[2] as { env?: NodeJS.ProcessEnv } | undefined; - expect(spawnOpts?.env?.XDG_CONFIG_HOME).toContain("/agents/main/qmd/xdg-config"); - expect(spawnOpts?.env?.XDG_CACHE_HOME).toContain("/agents/main/qmd/xdg-cache"); + const normalizePath = (value?: string) => value?.replace(/\\/g, "/"); + expect(normalizePath(spawnOpts?.env?.XDG_CONFIG_HOME)).toContain("/agents/main/qmd/xdg-config"); + expect(normalizePath(spawnOpts?.env?.XDG_CACHE_HOME)).toContain("/agents/main/qmd/xdg-cache"); await manager.close(); });