← back to Dw Marketing Reels
TK-10395: delayed re-queue — re-attempt a Meta-2207076-failed reel on later slots (bounded 4x, double-post-safe, give-up alert at cap)
15c6386bc2fe4b5709fa2070b9c5878dbb3ff17d · 2026-08-11 08:20:12 -0700 · cre-agent
Files touched
A scripts/retry-publish.mjsA scripts/retry-run.sh
Diff
commit 15c6386bc2fe4b5709fa2070b9c5878dbb3ff17d
Author: cre-agent <steve@designerwallcoverings.com>
Date: Tue Aug 11 08:20:12 2026 -0700
TK-10395: delayed re-queue — re-attempt a Meta-2207076-failed reel on later slots (bounded 4x, double-post-safe, give-up alert at cap)
---
scripts/retry-publish.mjs | 114 ++++++++++++++++++++++++++++++++++++++++++++++
scripts/retry-run.sh | 9 ++++
2 files changed, 123 insertions(+)
diff --git a/scripts/retry-publish.mjs b/scripts/retry-publish.mjs
new file mode 100644
index 0000000..48d4393
--- /dev/null
+++ b/scripts/retry-publish.mjs
@@ -0,0 +1,114 @@
+#!/usr/bin/env node
+/**
+ * retry-publish.mjs — TK-10395 delayed re-queue.
+ *
+ * The nightly 07:10 run can fail on Meta-side error 2207076 (media PROCESSING flakiness,
+ * NOT credentials — token verified healthy 2026-08-11). A few fast in-run retries don't
+ * reliably beat an intermittent Meta processing failure; the robust fix is to re-attempt the
+ * SAME reel a few hours LATER, when Meta's processing has recovered.
+ *
+ * This runs on a delayed schedule (a few slots after 07:10). Each run:
+ * - newest reel already posted/simulated -> nothing to do (exit 0)
+ * - not armed -> nothing to do (exit 0)
+ * - armed + not-landed + under the cap -> re-run publish-social.mjs (which re-attempts an
+ * 'error' reel — the dup-guard only skips a genuine
+ * success, so this is double-post-safe), bump counter
+ * - armed + not-landed + AT the cap -> fire ONE "gave up after N delayed retries" alert
+ *
+ * Reuses: publish-social.mjs (re-attempt + Norma's own 3x in-run retry), the dup-guard
+ * (double-post-safe), and the same CNCP + George alert path as cron-run.sh.
+ *
+ * The delayed-retry counter lives at reels[0].publish.instagram_retry (a SIBLING key that
+ * publish-social.mjs never overwrites — it only writes .instagram / .tiktok).
+ *
+ * Env: SOCIAL_LIVE_ARMED (arm gate, same as publisher), REELS_RETRY_MAX (default 4),
+ * REELS_ALERT_TO, CNCP_URL. $0 — no paid APIs.
+ */
+import { readFileSync, writeFileSync } from 'node:fs';
+import { join, dirname } from 'node:path';
+import { fileURLToPath } from 'node:url';
+import { spawnSync } from 'node:child_process';
+
+const ROOT = join(dirname(fileURLToPath(import.meta.url)), '..');
+const MAN = join(ROOT, 'data', 'reels.json');
+const ARMED = process.env.SOCIAL_LIVE_ARMED === '1';
+const MAX = Number(process.env.REELS_RETRY_MAX || 4);
+const now = () => new Date().toISOString();
+const log = (m) => console.log(`[retry-publish] ${m}`);
+
+const LANDED = new Set(['posted', 'already-posted', 'simulated', 'held-claims-review']);
+
+function load() { return JSON.parse(readFileSync(MAN, 'utf8')); }
+function list(r) { return Array.isArray(r) ? r : (r.reels || r.items || []); }
+
+function fireAlert(msg) {
+ // Same channel as cron-run.sh: CNCP parking-lot card + George email to steve-office.
+ const cncp = process.env.CNCP_URL || 'http://localhost:3333';
+ const to = process.env.REELS_ALERT_TO || 'steve@designerwallcoverings.com';
+ const sh = `
+ curl -sS --max-time 10 "${cncp}/api/parking-lot" -H 'Content-Type: application/json' \
+ -d "$(node -e 'console.log(JSON.stringify({url:"http://marketing.designerwallcoverings.com/",note:process.argv[1]}))' "${msg.replace(/"/g, '\\"')}")" \
+ >/dev/null 2>&1 && echo " CNCP card posted" || echo " CNCP post failed (non-fatal)";
+ if [ -f "$HOME/.claude/skills/_shared/george-send.sh" ]; then
+ . "$HOME/.claude/skills/_shared/george-send.sh";
+ george_send steve-office "${to}" "⚠ DW nightly reel — gave up after delayed retries ($(date +%Y-%m-%d))" "<p>${msg.replace(/"/g, '\\"')}</p>" >/dev/null 2>&1 \
+ && echo " George alert sent" || echo " George send failed (non-fatal)";
+ fi`;
+ spawnSync('bash', ['-c', sh], { stdio: 'inherit' });
+}
+
+let reels = load();
+let arr = list(reels);
+if (!arr.length) { log('no reels — nothing to retry'); process.exit(0); }
+let reel = arr[0];
+const ig = (reel.publish && reel.publish.instagram) || {};
+
+if (LANDED.has(ig.status)) { log(`newest reel already ${ig.status} — nothing to retry`); process.exit(0); }
+if (ig.status === 'posted-unverified') { log('newest reel posted-unverified (outcome unknown) — leaving for manual verify, not re-firing'); process.exit(0); }
+if (!ARMED) { log(`not armed — nothing to retry (status=${ig.status || 'none'})`); process.exit(0); }
+
+// Not landed + armed. Manage the delayed-retry counter.
+reel.publish = reel.publish || {};
+const rq = reel.publish.instagram_retry || { delayed_attempts: 0, gave_up: false, history: [] };
+
+if (rq.delayed_attempts >= MAX) {
+ if (!rq.gave_up) {
+ rq.gave_up = true; rq.gave_up_at = now();
+ reel.publish.instagram_retry = rq;
+ writeFileSync(MAN, JSON.stringify(reels, null, 2));
+ const msg = `TK-10395: ${reel.file} still not posted after ${MAX} delayed re-queue attempts (Meta 2207076 persisted all day). Manual attention needed — creds are healthy, this is Meta-side processing. Last: ${ig.status}${ig.note ? ' — ' + ig.note : ''}.`;
+ log(`cap reached (${MAX}) — firing give-up alert`);
+ fireAlert(msg);
+ } else {
+ log(`cap reached (${MAX}) and already alerted — silent`);
+ }
+ process.exit(0);
+}
+
+// Under the cap → re-attempt via the real publisher (double-post-safe: dup-guard skips only a
+// genuine success; an 'error'/failed reel is re-attempted, and Norma runs its own 3x in-run retry).
+rq.delayed_attempts += 1;
+rq.last_attempt_at = now();
+reel.publish.instagram_retry = rq;
+writeFileSync(MAN, JSON.stringify(reels, null, 2));
+log(`delayed re-attempt ${rq.delayed_attempts}/${MAX} for ${reel.file} (prev status=${ig.status})`);
+
+const res = spawnSync('node', ['scripts/publish-social.mjs'], {
+ cwd: ROOT, stdio: 'inherit',
+ env: { ...process.env, SOCIAL_AUTOPOST: '1' },
+});
+
+// Re-read outcome.
+reels = load(); arr = list(reels); reel = arr[0];
+const ig2 = (reel.publish && reel.publish.instagram) || {};
+// Preserve the counter (publish-social rewrote .instagram but not our sibling key).
+reel.publish.instagram_retry = { ...rq };
+if (LANDED.has(ig2.status)) {
+ reel.publish.instagram_retry.recovered_at = now();
+ writeFileSync(MAN, JSON.stringify(reels, null, 2));
+ log(`✅ recovered on delayed attempt ${rq.delayed_attempts}/${MAX} — status=${ig2.status}`);
+ process.exit(0);
+}
+writeFileSync(MAN, JSON.stringify(reels, null, 2));
+log(`still not landed after delayed attempt ${rq.delayed_attempts}/${MAX} — status=${ig2.status}. Will re-try next slot (or give up at cap).`);
+process.exit(0); // silent between attempts; the give-up alert fires only at the cap
diff --git a/scripts/retry-run.sh b/scripts/retry-run.sh
new file mode 100755
index 0000000..96742f8
--- /dev/null
+++ b/scripts/retry-run.sh
@@ -0,0 +1,9 @@
+#!/bin/zsh
+# TK-10395 delayed re-queue runner. Runs on a few slots AFTER the 07:10 nightly (see
+# com.steve.dw-reels-retry.plist). Re-attempts an armed reel that Meta failed to process
+# (2207076) earlier in the day, when Meta's processing has recovered. Bounded + double-post-safe;
+# see scripts/retry-publish.mjs. $0 — no paid APIs.
+export PATH="/opt/homebrew/bin:/usr/local/bin:$PATH"
+cd "$(dirname "$0")/.."
+[ -f .env ] && set -a && source .env && set +a
+node scripts/retry-publish.mjs
← 176622f auto-data-snapshot: 2026-08-11T07:35:48 (2 data files) — dat
·
back to Dw Marketing Reels
·
chore: refactor (env-var alert routing), v0.8.8 (session clo 5be46a4 →