fix(feishu): persist dedup cache across gateway restarts via warmup#31605
fix(feishu): persist dedup cache across gateway restarts via warmup#31605Takhoffman merged 2 commits intoopenclaw:mainfrom
Conversation
Greptile SummaryThis PR fixes duplicate Feishu message processing after gateway restarts by adding a Key issue found:
Confidence Score: 3/5
Last reviewed commit: b400af4 |
| async function warmup(namespace = "global", onError?: (error: unknown) => void): Promise<number> { | ||
| const filePath = options.resolveFilePath(namespace); | ||
| const now = Date.now(); | ||
| try { | ||
| const { value } = await readJsonFileWithFallback<PersistentDedupeData>(filePath, {}); | ||
| const data = sanitizeData(value); | ||
| let loaded = 0; | ||
| for (const [key, ts] of Object.entries(data)) { | ||
| if (ttlMs > 0 && now - ts >= ttlMs) { | ||
| continue; | ||
| } | ||
| const scopedKey = `${namespace}:${key}`; | ||
| memory.check(scopedKey, ts); | ||
| loaded++; | ||
| } | ||
| return loaded; | ||
| } catch (error) { | ||
| onError?.(error); | ||
| return 0; | ||
| } | ||
| } |
There was a problem hiding this comment.
onError callback is unreachable dead code
readJsonFileWithFallback catches every error internally — including non-ENOENT cases like permission denied or a corrupt file — and returns the fallback {} without re-throwing. This means the catch block in warmup can never execute, so onError is silently never called.
Looking at json-store.ts:
ENOENT→ returns fallback, no throw- Any other error → also returns fallback, no throw
As a result:
- The error logging callback wired up in
warmupDedupFromDisk(dedup.ts) will never fire. - If the dedup file is unreadable at startup (e.g. a permission issue), warmup silently returns 0 with no diagnostic, and messages that should be deduplicated will be re-processed — the exact problem this PR aims to fix.
Two options to address this:
Option A — Use the exists flag returned by readJsonFileWithFallback to distinguish "file absent" from a real read failure, and surface the latter via onError. Since readJsonFileWithFallback already swallows errors, you would need a prior existence probe (e.g. fs.access) or refactor readJsonFileWithFallback to re-throw non-ENOENT errors.
Option B — If disk errors at warmup time are intentionally silent, remove the onError parameter and all associated wiring to avoid a misleading API surface (callers reasonably expect the callback to fire on failure).
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/plugin-sdk/persistent-dedupe.ts
Line: 136-156
Comment:
**`onError` callback is unreachable dead code**
`readJsonFileWithFallback` catches every error internally — including non-ENOENT cases like permission denied or a corrupt file — and returns the fallback `{}` without re-throwing. This means the `catch` block in `warmup` can **never** execute, so `onError` is silently never called.
Looking at `json-store.ts`:
- `ENOENT` → returns fallback, no throw
- Any other error → also returns fallback, no throw
As a result:
- The error logging callback wired up in `warmupDedupFromDisk` (`dedup.ts`) will never fire.
- If the dedup file is unreadable at startup (e.g. a permission issue), warmup silently returns 0 with no diagnostic, and messages that should be deduplicated will be re-processed — the exact problem this PR aims to fix.
Two options to address this:
**Option A** — Use the `exists` flag returned by `readJsonFileWithFallback` to distinguish "file absent" from a real read failure, and surface the latter via `onError`. Since `readJsonFileWithFallback` already swallows errors, you would need a prior existence probe (e.g. `fs.access`) or refactor `readJsonFileWithFallback` to re-throw non-ENOENT errors.
**Option B** — If disk errors at warmup time are intentionally silent, remove the `onError` parameter and all associated wiring to avoid a misleading API surface (callers reasonably expect the callback to fire on failure).
How can I resolve this? If you propose a fix, please make it concise.Feishu uses at-least-once delivery, re-pushing recent messages on gateway reconnect. The persistent dedup writes to disk but never loads that state back on startup, so restarts cause duplicate processing until the persistent check re-reads the file. Add a warmup() method to PersistentDedupe that pre-reads the disk file into the in-memory cache on startup. Also record keys in memory when disk writes fail, so same-process retries are caught. Call warmupDedupFromDisk() in monitorSingleAccount before registering event handlers to pre-seed the dedup cache. Closes openclaw#31541 Made-with: Cursor
b400af4 to
bb95a66
Compare
|
PR #31605 - fix(feishu): persist dedup cache across gateway restarts via warmup (#31605) Merged after verification.
|
…penclaw#31605) thanks @Sid-Qin Verified: - pnpm install --frozen-lockfile - pnpm build - pnpm check - pnpm test:macmini (fails on unrelated baseline test: src/config/config.legacy-config-detection.rejects-routing-allowfrom.test.ts) Co-authored-by: Sid-Qin <201593046+Sid-Qin@users.noreply.github.com> Co-authored-by: Tak Hoffman <781889+Takhoffman@users.noreply.github.com>
…penclaw#31605) thanks @Sid-Qin Verified: - pnpm install --frozen-lockfile - pnpm build - pnpm check - pnpm test:macmini (fails on unrelated baseline test: src/config/config.legacy-config-detection.rejects-routing-allowfrom.test.ts) Co-authored-by: Sid-Qin <201593046+Sid-Qin@users.noreply.github.com> Co-authored-by: Tak Hoffman <781889+Takhoffman@users.noreply.github.com>
…penclaw#31605) thanks @Sid-Qin Verified: - pnpm install --frozen-lockfile - pnpm build - pnpm check - pnpm test:macmini (fails on unrelated baseline test: src/config/config.legacy-config-detection.rejects-routing-allowfrom.test.ts) Co-authored-by: Sid-Qin <201593046+Sid-Qin@users.noreply.github.com> Co-authored-by: Tak Hoffman <781889+Takhoffman@users.noreply.github.com>
Summary
warmup(namespace)method toPersistentDedupethat pre-reads the disk file into the in-memory cache.warmupDedupFromDisk()export in Feishu dedup module.monitorSingleAccountbefore registering event handlers.Change Type (select all)
Scope (select all touched areas)
Linked Issue/PR
User-visible / Behavior Changes
feishu[<accountId>]: dedup warmup loaded N entries from diskwhen entries are loaded.Security Impact (required)
NoNoNoNoNo— reads existing dedup files from~/.openclaw/feishu/dedup/Repro + Verification
Environment
Steps
openclaw gateway restart)Expected
Actual
Evidence
npx vitest run src/plugin-sdk/persistent-dedupe.test.ts # 6 tests pass (3 new: warmup loads entries, warmup empty file, warmup skips expired)Human Verification (required)
Compatibility / Migration
Yes— additive; warmup is a new call that safely reads existing filesNoNoFailure Recovery (if this breaks)
warmupDedupFromDiskcall inmonitor.account.tssrc/plugin-sdk/persistent-dedupe.ts,extensions/feishu/src/dedup.ts,extensions/feishu/src/monitor.account.tsRisks and Mitigations