[object Object]

← back to George Gmail

george: give the 30-day draft drainer a KEEP-LIST — TK-11231

0d2c8e29f02c7e38297bb357cca1b03a8a376b85 · 2026-09-10 09:20:15 -0700 · Steve Abrams

The drainer was a blanket age filter with no exemption mechanism, so a READY,
wanted draft sat on exactly the same permanent-delete fuse as the garbage. That
is why a live vendor cost-list ask (Newmor / LBI Boyd, 1,294 rows priced at 0)
was days from being destroyed with no Trash, purely because it happened to live
on info@ instead of steve-office.

Being able to protect a wanted draft WITHOUT sending it is the missing
capability: 'send it' and 'lose it' should never have been the only two options.

data/drain-keep-list.json maps messageId -> why it is kept; those ids are never
deleted at any age. Seeded with the Newmor ask; the entry documents that it is
held deliberately and says to remove it once sent or dropped.

Verified against the real patched predicate (extracted from source, not
retyped): a keep-listed >30d draft is excluded, an unprotected >30d draft is
still deleted (drainer not weakened), and fresh drafts stay untouched. Live dry
run confirms the list loads.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

Files touched

Diff

commit 0d2c8e29f02c7e38297bb357cca1b03a8a376b85
Author: Steve Abrams <steve@designerwallcoverings.com>
Date:   Thu Sep 10 09:20:15 2026 -0700

    george: give the 30-day draft drainer a KEEP-LIST — TK-11231
    
    The drainer was a blanket age filter with no exemption mechanism, so a READY,
    wanted draft sat on exactly the same permanent-delete fuse as the garbage. That
    is why a live vendor cost-list ask (Newmor / LBI Boyd, 1,294 rows priced at 0)
    was days from being destroyed with no Trash, purely because it happened to live
    on info@ instead of steve-office.
    
    Being able to protect a wanted draft WITHOUT sending it is the missing
    capability: 'send it' and 'lose it' should never have been the only two options.
    
    data/drain-keep-list.json maps messageId -> why it is kept; those ids are never
    deleted at any age. Seeded with the Newmor ask; the entry documents that it is
    held deliberately and says to remove it once sent or dropped.
    
    Verified against the real patched predicate (extracted from source, not
    retyped): a keep-listed >30d draft is excluded, an unprotected >30d draft is
    still deleted (drainer not weakened), and fresh drafts stay untouched. Live dry
    run confirms the list loads.
    
    Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
---
 data/drain-keep-list.json |  3 +++
 delete-old-drafts-info.js | 17 +++++++++++++++--
 2 files changed, 18 insertions(+), 2 deletions(-)

diff --git a/data/drain-keep-list.json b/data/drain-keep-list.json
new file mode 100644
index 0000000..d982c73
--- /dev/null
+++ b/data/drain-keep-list.json
@@ -0,0 +1,3 @@
+{
+  "1a06d1ef99f846d1": "TK-11231 — Newmor/LBI Boyd cost-list ask (1,294 rows, 0 priced). READY and wanted; Steve is holding it, not abandoning it. Without this exemption the 30-day info@ drain would permanently destroy it (~2026-10-04) with no Trash. Remove this entry once the ask is sent or consciously dropped."
+}
diff --git a/delete-old-drafts-info.js b/delete-old-drafts-info.js
index d083582..b367882 100644
--- a/delete-old-drafts-info.js
+++ b/delete-old-drafts-info.js
@@ -32,6 +32,19 @@ const ACC = 'info';
 const QUERY = 'in:drafts older_than:30d';
 const DELETE = process.env.CONFIRM === '1';
 const SET_IN = process.env.SET || '';
+// ── KEEP-LIST (TK-11231) ─────────────────────────────────────────────────────
+// The drainer was a blanket age filter with NO exemption mechanism, so a draft
+// that is READY and WANTED sat on the same 30-day permanent-delete fuse as the
+// garbage — that is why a live vendor cost-list ask (Newmor, 1,294 rows / 0
+// priced) was days from being destroyed with no Trash. A wanted draft must be
+// protectable without sending it. Ids here are NEVER deleted, at any age.
+// File: data/drain-keep-list.json -> { "<messageId>": "why it is kept" }
+const KEEP_PATH = path.join(__dirname, 'data', 'drain-keep-list.json');
+const KEEP = (() => {
+  try { return JSON.parse(fs.readFileSync(KEEP_PATH, 'utf8')); } catch (_) { return {}; }
+})();
+const KEEP_IDS = new Set(Object.keys(KEEP));
+if (KEEP_IDS.size) console.log(`keep-list: ${KEEP_IDS.size} draft(s) exempt from deletion at any age`);
 const PAGE = parseInt(process.env.PAGE || '40', 10);
 const PAGE_SLEEP = parseInt(process.env.PAGE_SLEEP || '3000', 10);
 const DEL_SLEEP = parseInt(process.env.DEL_SLEEP || '250', 10);
@@ -113,7 +126,7 @@ async function discoverSet() {
   if (!DELETE) {
     // dry-run: show how many of the first 500 drafts are deletable right now
     const drafts = await jget(`${BASE}/api/drafts?account=${ACC}&maxResults=500`);
-    const matches = (drafts || []).filter((d) => d && d.message && older.has(d.message.id));
+    const matches = (drafts || []).filter((d) => d && d.message && older.has(d.message.id) && !KEEP_IDS.has(d.message.id));
     console.log(`\n--- DRY RUN (no deletions). CONFIRM=1 to drain. ---`);
     console.log(`total >30d drafts to remove : ${older.size}`);
     console.log(`deletable in first 500 window: ${matches.length}`);
@@ -135,7 +148,7 @@ async function discoverSet() {
   while (batch < MAX_BATCHES) {
     batch++;
     const drafts = await jget(`${BASE}/api/drafts?account=${ACC}&maxResults=500`);
-    const matches = (drafts || []).filter((d) => d && d.message && older.has(d.message.id));
+    const matches = (drafts || []).filter((d) => d && d.message && older.has(d.message.id) && !KEEP_IDS.has(d.message.id));
     if (matches.length === 0) { console.log(`\nbatch ${batch}: 0 matches — drain complete.`); break; }
     console.log(`\nbatch ${batch}: ${matches.length} matches (of ${Array.isArray(drafts) ? drafts.length : '?'} listed) — deleting...`);
     for (const t of matches) {

← efdf4ea george: draft contract + duplicate detection at the SHARED l  ·  back to George Gmail  ·  auto-data-snapshot: 2026-09-10T10:34:55 (1 data files) — dat ce00205 →