[object Object]

← back to George Mcp

george-mcp: retry once on a Gmail quota error before fail-closed refusal — TK-11231

4cdbff59858593542397997ff4f5d1a41ee20ec6 · 2026-09-10 08:03:41 -0700 · Steve Abrams

Red-team finding: dedupe adds a search to every create, and fail-closed turns a
quota blip into a hard block on legitimate drafting. The 60s id-join cache does
not help a burst of drafts to DIFFERENT recipients — which is exactly the
vendor-blast pattern (Command, Maya Romanoff, Newmor back to back) that produced
the original backlog. Retry once with backoff on a quota/429 error, then refuse.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VTxE4MgnygQ9EY2rPZvtcK

Files touched

Diff

commit 4cdbff59858593542397997ff4f5d1a41ee20ec6
Author: Steve Abrams <steve@designerwallcoverings.com>
Date:   Thu Sep 10 08:03:41 2026 -0700

    george-mcp: retry once on a Gmail quota error before fail-closed refusal — TK-11231
    
    Red-team finding: dedupe adds a search to every create, and fail-closed turns a
    quota blip into a hard block on legitimate drafting. The 60s id-join cache does
    not help a burst of drafts to DIFFERENT recipients — which is exactly the
    vendor-blast pattern (Command, Maya Romanoff, Newmor back to back) that produced
    the original backlog. Retry once with backoff on a quota/429 error, then refuse.
    
    Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
    Claude-Session: https://claude.ai/code/session_01VTxE4MgnygQ9EY2rPZvtcK
---
 index.js | 19 +++++++++++++++----
 1 file changed, 15 insertions(+), 4 deletions(-)

diff --git a/index.js b/index.js
index e9da4da..c2fa07d 100755
--- a/index.js
+++ b/index.js
@@ -331,6 +331,20 @@ const TOOLS = [
   },
 ];
 
+// Dedupe adds a Gmail search to every create, and Gmail's per-minute "Total Query Cost"
+// quota is bursty — a blast of drafts to different recipients can trip it. Since the check
+// now fails CLOSED, an un-retried blip would hard-block legitimate drafting. Retry once.
+async function searchDraftsTo(to, account) {
+  const q = `in:draft to:${to}`;
+  try {
+    return await george("/api/search", { query: { q, maxResults: 10, account } });
+  } catch (e) {
+    if (!/quota|rate limit|429|userRateLimit/i.test(e.message)) throw e;
+    await new Promise((r) => setTimeout(r, 2500));
+    return await george("/api/search", { query: { q, maxResults: 10, account } });
+  }
+}
+
 // ─── Draft return-contract helpers (TK-11231) ───────────────────────────────
 // A draft is NOT a delivered outcome. The deepest cause of the unsent-draft
 // backlog was that creating a draft returned a plain success, so assistants
@@ -526,10 +540,7 @@ server.setRequestHandler(CallToolRequestSchema, async (req) => {
         if (!args.acknowledge_existing && args.to) {
           let existing;
           try {
-            const dupes = await george("/api/search", {
-              query: { q: `in:draft to:${args.to}`, maxResults: 10, account },
-            });
-            existing = dupes.messages || [];
+            existing = (await searchDraftsTo(args.to, account)).messages || [];
           } catch (e) {
             // FAIL CLOSED. Swallowing this is what silently re-opens the duplicate hole:
             // during testing a Gmail per-minute quota error made the check a no-op and a

← accb909 george-mcp: drafts are no longer a terminal success — TK-112  ·  back to George Mcp  ·  (newest)