[object Object]

← back to Marketing Command Center

meta-token-health: retry transient Graph blips + only prescribe token-paste on true expiry (fix 8/20 false alarm)

8718269fecc1a91ac291d22974579158c92b2449 · 2026-08-20 09:21:02 -0700 · Steve Abrams

Files touched

Diff

commit 8718269fecc1a91ac291d22974579158c92b2449
Author: Steve Abrams <steve@designerwallcoverings.com>
Date:   Thu Aug 20 09:21:02 2026 -0700

    meta-token-health: retry transient Graph blips + only prescribe token-paste on true expiry (fix 8/20 false alarm)
---
 scripts/meta-token-health.js | 61 ++++++++++++++++++++++++++++++++++++--------
 1 file changed, 51 insertions(+), 10 deletions(-)

diff --git a/scripts/meta-token-health.js b/scripts/meta-token-health.js
index 041093a..3c8ed66 100644
--- a/scripts/meta-token-health.js
+++ b/scripts/meta-token-health.js
@@ -36,21 +36,62 @@ async function alert(subject, body) {
   }
 }
 
+// A genuine, act-on-it expiry (token is really dead — pasting a new one is the fix).
+// Meta expiry surfaces as subcode 463/467/460 or an unambiguous message; a bare
+// code 190 with a "cannot parse"/"malformed" message is a TRANSIENT Graph blip,
+// NOT an expiry (the 2026-08-20 false alarm: same static token failed at 08:05,
+// validated at 09:19, .env untouched). Don't prescribe a token-paste for those.
+const isTrueExpiry = (err) => {
+  const sub = err.error_subcode;
+  if (sub === 463 || sub === 467 || sub === 460) return true;
+  return /expired|session has expired|been invalidated|revoked|changed your password|re-?authenticate/i.test(err.message || '');
+};
+// Retry-worthy: transient blips (parse/malformed/rate/5xx, or code 190 that is NOT a true expiry).
+const isTransient = (err, httpStatus) => {
+  const m = err.message || '';
+  if (/cannot parse|malformed|temporarily|try again|unexpected error|please reduce|rate limit|an error occurred/i.test(m)) return true;
+  if ([1, 2, 4, 17, 341, 368].includes(err.code)) return true;   // Meta transient/rate classes
+  if (httpStatus >= 500) return true;
+  if (err.code === 190 && !isTrueExpiry(err)) return true;        // bare 190 w/o expiry signal → treat as blip
+  return false;
+};
+const sleep = (ms) => new Promise(r => setTimeout(r, ms));
+
 (async () => {
   const stamp = (status, detail) => { try { fs.writeFileSync(STAMP, JSON.stringify({ ok: status === 'ok', status, detail, checkedAt: new Date().toISOString() }, null, 2)); } catch {} };
   if (!TOKEN) { console.log('DEAD: no META_ACCESS_TOKEN in .env'); stamp('missing', 'no token in .env'); await alert('NOT SET', 'No META_ACCESS_TOKEN in MCC .env — Vendor IG panel cannot fetch posts.'); process.exit(0); }
 
-  let r, j;
-  try { r = await fetch(`${GRAPH}/me?fields=id,name&access_token=${encodeURIComponent(TOKEN)}`); j = await r.json(); }
-  catch (e) { console.log('NET ERR', e.message); stamp('neterr', e.message); process.exit(0); }
+  // Check /me with up to 3 attempts. Break early on success or a TRUE expiry
+  // (retrying a genuinely dead token is pointless); keep retrying transient blips
+  // so a single Graph hiccup no longer pages Steve with a wrong "paste a token".
+  let j = null, httpStatus = 0, lastErr = null;
+  for (let attempt = 1; attempt <= 3; attempt++) {
+    try {
+      const r = await fetch(`${GRAPH}/me?fields=id,name&access_token=${encodeURIComponent(TOKEN)}`);
+      httpStatus = r.status; j = await r.json();
+    } catch (e) { lastErr = { message: e.message, network: true }; j = null; }
 
-  if (j.error) {
-    const m = j.error.message || '';
-    const expired = j.error.code === 190 || /expired|session|validating access token/i.test(m);
-    console.log('DEAD:', m);
-    stamp('expired', m);
-    await alert(expired ? 'EXPIRED / INVALID' : 'ERROR',
-      `${m}\n\nThe Vendor IG panel (last-10 posts) is dark until you paste a fresh long-lived META_ACCESS_TOKEN (scopes: instagram_basic, pages_read_engagement, business_management) → route via the secrets skill into MCC .env, then click Refresh on the panel.`);
+    if (j && !j.error) { lastErr = null; break; }              // success
+    lastErr = j?.error || lastErr || { message: 'unknown error' };
+    if (j?.error && isTrueExpiry(j.error)) break;              // truly dead — stop retrying
+    if (attempt < 3) { console.log(`retry ${attempt}: ${lastErr.message}`); await sleep(1500); }
+  }
+
+  if (lastErr) {
+    const m = lastErr.message || '';
+    if (!lastErr.network && lastErr.code !== undefined && isTrueExpiry(lastErr)) {
+      // Genuine expiry — the token really is dead; pasting a fresh one is correct.
+      console.log('DEAD (expired):', m); stamp('expired', m);
+      await alert('EXPIRED / INVALID',
+        `${m}\n\nThe Vendor IG panel (last-10 posts) is dark until you paste a fresh long-lived META_ACCESS_TOKEN (scopes: instagram_basic, pages_read_engagement, business_management) → route via the secrets skill into MCC .env, then click Refresh on the panel.`);
+    } else {
+      // Persisted across retries but NOT an expiry signal → transient Graph error
+      // or a consumer problem. Do NOT tell Steve to paste a token (the secret is
+      // likely fine). Point at the real check instead.
+      console.log('TRANSIENT/UNCLEAR:', m); stamp('transient', m);
+      await alert('TRANSIENT ERROR (token likely OK)',
+        `Graph returned a non-expiry error 3x: "${m}"\n\nThis is NOT a token expiry — do NOT paste a new token yet. Likely a Meta Graph blip or the MCC consumer (:9661) not running. Verify: re-run scripts/meta-token-health.js, and if it says LIVE the token is fine; if the panel is still dark, restart the MCC server rather than rotating the token.`);
+    }
     process.exit(0);
   }
 

← 62c9b25 auto-data-snapshot: 2026-08-20T09:06:02 (1 data files) — pub  ·  back to Marketing Command Center  ·  auto-data-snapshot: 2026-08-20T09:38:44 (2 data files) — dat 169f8b1 →