diff --git a/CHANGELOG.md b/CHANGELOG.md index 7fb94fce77e..daa694d4664 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -20,7 +20,7 @@ Docs: https://docs.openclaw.ai - Gateway/Restart: fix restart-loop edge cases by keeping `openclaw.mjs -> dist/entry.js` bootstrap detection explicit, reacquiring the gateway lock for in-process restart fallback paths, and tightening restart-loop regression coverage. (#23416) Thanks @jeffwnli. - Signal/Monitor: treat user-initiated abort shutdowns as clean exits when auto-started `signal-cli` is terminated, while still surfacing unexpected daemon exits as startup/runtime failures. (#23379) Thanks @frankekn. -- Channels/Dedupe: centralize plugin dedupe primitives in plugin SDK (memory + persistent), move Feishu inbound dedupe to a namespace-scoped persistent store, and reuse shared dedupe cache logic for Zalo webhook replay + Tlon processed-message tracking to reduce duplicate handling during reconnect/replay paths. +- Channels/Dedupe: centralize plugin dedupe primitives in plugin SDK (memory + persistent), move Feishu inbound dedupe to a namespace-scoped persistent store, and reuse shared dedupe cache logic for Zalo webhook replay + Tlon processed-message tracking to reduce duplicate handling during reconnect/replay paths. (#23377) Thanks @SidQin-cyber. - ACP/Gateway: wait for gateway hello before opening ACP requests, and fail fast on pre-hello connect failures to avoid startup hangs and early `gateway not connected` request races. (#23390) Thanks @janckerchen. - Security/Audit: add `openclaw security audit` detection for open group policies that expose runtime/filesystem tools without sandbox/workspace guards (`security.exposure.open_groups_with_runtime_or_fs`). - Security/Exec env: block request-scoped `HOME` and `ZDOTDIR` overrides in host exec env sanitizers (Node + macOS), preventing shell startup-file execution before allowlist-evaluated command bodies. This ships in the next npm release. Thanks @tdjackey for reporting. diff --git a/extensions/feishu/src/dedup-store.ts b/extensions/feishu/src/dedup-store.ts deleted file mode 100644 index 86ca3a6353c..00000000000 --- a/extensions/feishu/src/dedup-store.ts +++ /dev/null @@ -1,104 +0,0 @@ -import fs from "node:fs"; -import os from "node:os"; -import path from "node:path"; - -const DEFAULT_DEDUP_DIR = path.join(os.homedir(), ".openclaw", "feishu", "dedup"); -const MAX_ENTRIES_PER_FILE = 10_000; -const CLEANUP_PROBABILITY = 0.02; -const CACHE_STALE_MS = 30_000; - -type DedupData = Record; - -/** - * Filesystem-backed dedup store. Each "namespace" (typically a Feishu account - * ID) maps to a single JSON file containing `{ messageId: timestampMs }` pairs. - * - * Writes use atomic rename to avoid partial-read corruption. Probabilistic - * cleanup keeps the file size bounded without adding latency to every call. - */ -export class DedupStore { - private readonly dir: string; - private cache = new Map(); - private cacheLoadedAt = new Map(); - - constructor(dir?: string) { - this.dir = dir ?? DEFAULT_DEDUP_DIR; - } - - private filePath(namespace: string): string { - const safe = namespace.replace(/[^a-zA-Z0-9_-]/g, "_"); - return path.join(this.dir, `${safe}.json`); - } - - async load(namespace: string): Promise { - const loadedAt = this.cacheLoadedAt.get(namespace); - if (loadedAt != null && Date.now() - loadedAt > CACHE_STALE_MS) { - this.cache.delete(namespace); - this.cacheLoadedAt.delete(namespace); - } - - const cached = this.cache.get(namespace); - if (cached) return cached; - - try { - const raw = await fs.promises.readFile(this.filePath(namespace), "utf-8"); - const data: DedupData = JSON.parse(raw); - this.cache.set(namespace, data); - this.cacheLoadedAt.set(namespace, Date.now()); - return data; - } catch { - const data: DedupData = {}; - this.cache.set(namespace, data); - this.cacheLoadedAt.set(namespace, Date.now()); - return data; - } - } - - async has(namespace: string, messageId: string, ttlMs: number): Promise { - const data = await this.load(namespace); - const ts = data[messageId]; - if (ts == null) return false; - if (Date.now() - ts > ttlMs) { - // Expired — treat as absent. Skip the delete here to avoid silent - // cache/disk divergence; actual cleanup happens probabilistically - // inside record(). - return false; - } - return true; - } - - async record(namespace: string, messageId: string, ttlMs: number): Promise { - const data = await this.load(namespace); - data[messageId] = Date.now(); - - if (Math.random() < CLEANUP_PROBABILITY) { - this.evict(data, ttlMs); - } - - await this.flush(namespace, data); - } - - private evict(data: DedupData, ttlMs: number): void { - const now = Date.now(); - for (const key of Object.keys(data)) { - if (now - data[key] > ttlMs) delete data[key]; - } - - const keys = Object.keys(data); - if (keys.length > MAX_ENTRIES_PER_FILE) { - keys - .sort((a, b) => data[a] - data[b]) - .slice(0, keys.length - MAX_ENTRIES_PER_FILE) - .forEach((k) => delete data[k]); - } - } - - private async flush(namespace: string, data: DedupData): Promise { - await fs.promises.mkdir(this.dir, { recursive: true }); - const fp = this.filePath(namespace); - const tmp = `${fp}.tmp.${process.pid}`; - await fs.promises.writeFile(tmp, JSON.stringify(data), "utf-8"); - await fs.promises.rename(tmp, fp); - this.cacheLoadedAt.set(namespace, Date.now()); - } -} diff --git a/extensions/feishu/src/dedup.ts b/extensions/feishu/src/dedup.ts index 3b544883c23..6468e30f23d 100644 --- a/extensions/feishu/src/dedup.ts +++ b/extensions/feishu/src/dedup.ts @@ -14,9 +14,6 @@ function resolveStateDirFromEnv(env: NodeJS.ProcessEnv = process.env): string { if (stateOverride) { return stateOverride; } - if (env.VITEST || env.NODE_ENV === "test") { - return path.join(os.tmpdir(), "openclaw-vitest-" + process.pid); - } return path.join(os.homedir(), ".openclaw"); }