#13178: fix: dedup mapped hook dispatches to prevent Gmail Pub/Sub retries
gateway
stale
Cluster:
Hook and Gateway Improvements
## Summary
- Add a TTL-based dedup cache (5 min, 500 entries) to the hooks request handler
- When a mapped hook resolves with a session key that was already seen within TTL, return `200 {ok: true, duplicate: true}` instead of dispatching a new agent run
- Prevents at-least-once delivery systems like Gmail Pub/Sub from triggering duplicate agent runs for the same email notification
## Root Cause
Google Pub/Sub uses at-least-once delivery — the same notification can arrive multiple times. The hook handler in `server-http.ts` dispatched `dispatchAgentHook()` for every matching request without checking if that exact session key was already processed. The Gmail preset uses `hook:gmail:{{messages[0].id}}` as the session key, providing a natural dedup key.
## Changes
- `src/gateway/server-http.ts` — added `createDedupeCache` import and dedup check before mapped hook dispatch (+13 lines)
- `src/gateway/server-http-hooks-dedupe.test.ts` — 3 tests covering dispatch, dedup, and different IDs (new file)
## Test plan
- [x] `pnpm vitest run src/gateway/server-http-hooks-dedupe.test.ts` — 3/3 passing
- [x] `pnpm vitest run src/gateway/hooks-mapping.test.ts` — 7/7 passing (no regression)
- [x] `pnpm build` — clean
- [x] `pnpm check` — lint + format clean
🤖 Generated with [Claude Code](https://claude.com/claude-code)
<!-- greptile_comment -->
<h2>Greptile Overview</h2>
<h3>Greptile Summary</h3>
This PR adds a TTL-based in-memory dedupe cache to the gateway hooks HTTP handler so mapped hook dispatches (not `/hooks/agent`) are suppressed when the same `sessionKey` repeats within a 5 minute window. It returns `200 { ok: true, duplicate: true }` for duplicates instead of dispatching a new agent run, and includes a new Vitest file covering first-dispatch vs duplicate vs different IDs for the Gmail preset mapping.
The change is localized to `createHooksRequestHandler` in `src/gateway/server-http.ts`, leveraging a shared `createDedupeCache` utility from `src/infra/dedupe.ts`, and it interacts with the existing hook mapping system (`applyHookMappings` / Gmail preset `sessionKey`).
<h3>Confidence Score: 3/5</h3>
- This PR is moderately safe to merge, but the dedupe flow can drop events if dispatch fails after marking a key as seen.
- The core change is small and covered by tests, but the current `check()`-then-dispatch ordering means a transient dispatch failure can cause subsequent retries to be treated as duplicates (no agent run), which is a correctness issue for at-least-once delivery sources.
- src/gateway/server-http.ts
<!-- greptile_other_comments_section -->
<sub>(3/5) Reply to the agent's comments like "Can you suggest a fix for this @greptileai?" or ask follow-up questions!</sub>
<!-- /greptile_comment -->
Most Similar PRs
#21728: fix(boot): deduplicate startup notification within restart cycle
by irchelper · 2026-02-20
73.8%
#7301: fix(hooks): use resolveAgentIdFromSessionKey instead of split(":")[0]
by tsukhani · 2026-02-02
73.7%
#16915: fix: await compaction hooks with timeout to prevent cross-session d...
by maximalmargin · 2026-02-15
73.7%
#6853: fix: fire internal hooks on sessions.reset RPC (TUI/webchat /new)
by hamiltonchua · 2026-02-02
73.5%
#15611: fix(gateway): invalidate hook transform cache on config reload
by AI-Reviewer-QS · 2026-02-13
73.4%
#19015: Implement check for empty Gmail events in hooks mapping
by klauslochmann · 2026-02-17
73.3%
#22293: Hooks: add message-filter bundled hook with inbound message pre-filter
by MegaPhoenix92 · 2026-02-21
73.0%
#19565: feat: add agent lifecycle hook events (session, message, error)
by tag-assistant · 2026-02-17
72.4%
#9603: fix: initialize global hook runner on plugin registry cache hit
by kevins88288 · 2026-02-05
72.4%
#22101: fix(slack): dedupe mentions by ts fallback for app_mention
by AIflow-Labs · 2026-02-20
72.4%