[object Object]

← back to George Gmail

Schedule daily info@ old-draft drain (launchd) with runaway cap + heartbeat

0fd5c29e2b339eb6c4cd3202369e39e30421d697 · 2026-08-20 11:18:43 -0700 · Steve Abrams

- Daily 04:15 launchd job com.steve.george-drain-old-drafts runs the drain
  in CONFIRM=1 to keep info@ drafts >30d trimmed automatically.
- Runaway guard: MAX_AUTO_DELETE=800 — if a run ever finds a huge backlog it
  refuses to auto-delete and emits WARN for human review (never silent mass-nuke).
- Heartbeat data/drain-old-drafts-latest.json in PASS/WARN/FAIL vocabulary for
  the fleet-health rollup + meta-watchdog.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

Files touched

Diff

commit 0fd5c29e2b339eb6c4cd3202369e39e30421d697
Author: Steve Abrams <steve@designerwallcoverings.com>
Date:   Thu Aug 20 11:18:43 2026 -0700

    Schedule daily info@ old-draft drain (launchd) with runaway cap + heartbeat
    
    - Daily 04:15 launchd job com.steve.george-drain-old-drafts runs the drain
      in CONFIRM=1 to keep info@ drafts >30d trimmed automatically.
    - Runaway guard: MAX_AUTO_DELETE=800 — if a run ever finds a huge backlog it
      refuses to auto-delete and emits WARN for human review (never silent mass-nuke).
    - Heartbeat data/drain-old-drafts-latest.json in PASS/WARN/FAIL vocabulary for
      the fleet-health rollup + meta-watchdog.
    
    Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---
 com.steve.george-drain-old-drafts.plist | 43 +++++++++++++++++++++++++++++++++
 delete-old-drafts-info.js               | 15 ++++++++++++
 2 files changed, 58 insertions(+)

diff --git a/com.steve.george-drain-old-drafts.plist b/com.steve.george-drain-old-drafts.plist
new file mode 100644
index 0000000..f417846
--- /dev/null
+++ b/com.steve.george-drain-old-drafts.plist
@@ -0,0 +1,43 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
+<plist version="1.0">
+<dict>
+  <key>Label</key>
+  <string>com.steve.george-drain-old-drafts</string>
+
+  <key>ProgramArguments</key>
+  <array>
+    <string>/opt/homebrew/bin/node</string>
+    <string>/Users/macstudio3/Projects/george-gmail/delete-old-drafts-info.js</string>
+  </array>
+
+  <key>EnvironmentVariables</key>
+  <dict>
+    <key>CONFIRM</key>
+    <string>1</string>
+    <!-- Runaway guard: if >800 drafts are >30d in one run, skip auto-delete and WARN for human review. -->
+    <key>MAX_AUTO_DELETE</key>
+    <string>800</string>
+    <key>PATH</key>
+    <string>/opt/homebrew/bin:/usr/bin:/bin:/usr/sbin:/sbin</string>
+  </dict>
+
+  <key>WorkingDirectory</key>
+  <string>/Users/macstudio3/Projects/george-gmail</string>
+
+  <!-- Daily at 04:15 (low George/Gmail traffic; after the initial cleanup the daily backlog is small). -->
+  <key>StartCalendarInterval</key>
+  <dict>
+    <key>Hour</key><integer>4</integer>
+    <key>Minute</key><integer>15</integer>
+  </dict>
+
+  <key>StandardOutPath</key>
+  <string>/Users/macstudio3/Projects/george-gmail/logs/drain-old-drafts.out.log</string>
+  <key>StandardErrorPath</key>
+  <string>/Users/macstudio3/Projects/george-gmail/logs/drain-old-drafts.err.log</string>
+
+  <key>RunAtLoad</key>
+  <false/>
+</dict>
+</plist>
diff --git a/delete-old-drafts-info.js b/delete-old-drafts-info.js
index d94fb98..d083582 100644
--- a/delete-old-drafts-info.js
+++ b/delete-old-drafts-info.js
@@ -36,6 +36,13 @@ 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);
 const MAX_BATCHES = parseInt(process.env.MAX_BATCHES || '40', 10);
+// Runaway guard for unattended runs: if the >30d set exceeds this, DO NOT
+// auto-delete — flag for a human. 0/unset = no cap (manual runs).
+const MAX_AUTO_DELETE = parseInt(process.env.MAX_AUTO_DELETE || '0', 10);
+const HB_PATH = path.join(__dirname, 'data', 'drain-old-drafts-latest.json');
+function heartbeat(obj) {
+  try { fs.writeFileSync(HB_PATH, JSON.stringify({ ts: new Date().toISOString(), skill: 'delete-old-drafts-info', account: ACC, ...obj }, null, 2)); } catch (_) {}
+}
 
 function resolveAuth() {
   if (process.env.GEORGE_AUTH && process.env.GEORGE_AUTH.includes(':')) {
@@ -114,6 +121,13 @@ async function discoverSet() {
     return;
   }
 
+  // ---- runaway guard (unattended runs) ----
+  if (MAX_AUTO_DELETE > 0 && older.size > MAX_AUTO_DELETE) {
+    console.error(`ABORT: >30d set (${older.size}) exceeds MAX_AUTO_DELETE (${MAX_AUTO_DELETE}). Not auto-deleting — run manually to review.`);
+    heartbeat({ verdict: 'WARN', status: 'WARN', deleted: 0, failed: 0, older_set: older.size, cap: MAX_AUTO_DELETE, note: 'backlog exceeds cap; auto-delete skipped, human review needed' });
+    process.exit(0);
+  }
+
   // ---- DRAIN LOOP ----
   const logFile = `/tmp/george-old-drafts-deleted-${stamp}.jsonl`;
   let totalOk = 0, totalFail = 0, batch = 0;
@@ -138,4 +152,5 @@ async function discoverSet() {
   console.log(`\n\nDONE. deleted=${totalOk}  failed=${totalFail}  remaining-in-set=${older.size}`);
   console.log(`Deletion log: ${logFile}`);
   if (older.size > 0) console.log('Some >30d drafts remain (batch cap or failures). Re-run to finish.');
+  heartbeat({ verdict: totalFail > 0 ? 'WARN' : 'PASS', status: totalFail > 0 ? 'WARN' : 'PASS', deleted: totalOk, failed: totalFail, remaining_in_set: older.size });
 })().catch((e) => { console.error('FATAL:', e.message); process.exit(1); });

← 7106f29 Add drain script: permanently delete info@ drafts older than  ·  back to George Gmail  ·  auto-data-snapshot: 2026-08-20T11:21:15 (1 data files) — dat 9fa1a8a →