← back to Norma
IG redo: openclaw deletion driver for originals (probe-first, canary, paced --go, verify-each)
f92f6e2e94c7497d9dea24c6e85f29a087f0ca1f · 2026-08-14 10:51:23 -0700 · Steve
Files touched
A agents/instagram-agent/delete-originals.js
Diff
commit f92f6e2e94c7497d9dea24c6e85f29a087f0ca1f
Author: Steve <steve@designerwallcoverings.com>
Date: Fri Aug 14 10:51:23 2026 -0700
IG redo: openclaw deletion driver for originals (probe-first, canary, paced --go, verify-each)
---
agents/instagram-agent/delete-originals.js | 127 +++++++++++++++++++++++++++++
1 file changed, 127 insertions(+)
diff --git a/agents/instagram-agent/delete-originals.js b/agents/instagram-agent/delete-originals.js
new file mode 100644
index 0000000..403daff
--- /dev/null
+++ b/agents/instagram-agent/delete-originals.js
@@ -0,0 +1,127 @@
+#!/usr/bin/env node
+/**
+ * delete-originals.js — remove the last-5-days ORIGINAL IG posts (the ones with the bad
+ * caption) via openclaw REAL Chrome, after the corrected versions were reposted. TK-10574.
+ *
+ * The Instagram Graph API cannot delete a published post, so deletion must be driven through
+ * the logged-in web UI (··· → Delete → confirm). openclaw drives a real, human-logged-in
+ * Chrome, so this only works AFTER Steve has logged into Meta/Instagram in openclaw and is
+ * on (or can switch to) the owning account.
+ *
+ * Source of truth = data/redo-results-<date>.jsonl (old_permalink + handle + new_permalink),
+ * written by redo-last5.js --go. We only ever delete an OLD post that has a confirmed NEW one.
+ *
+ * node delete-originals.js # PROBE (default, NON-destructive): for each post,
+ * # navigate + snapshot, report whether a Delete
+ * # affordance is present (= logged in as owner).
+ * node delete-originals.js --only <handle> # restrict to one account
+ * node delete-originals.js --canary # delete EXACTLY ONE probe-able post, verify, stop
+ * node delete-originals.js --go # delete all, paced, verify each (destructive)
+ *
+ * SAFETY: default is probe-only. --canary/--go actually click Delete. Pacing is deliberately
+ * long (PACE_MS) because rapid fleet-wide browser deletes are Meta's #1 automation-ban trigger.
+ * Every deletion is verified (the permalink must go unavailable) and logged; fail-loud on any
+ * post where the Delete affordance is missing (wrong account) — it is SKIPPED, never guessed.
+ */
+const fs = require('fs');
+const path = require('path');
+const { execSync } = require('child_process');
+
+const DATE = '20260814';
+const SRC = path.join(__dirname, 'data', `redo-results-${DATE}.jsonl`);
+const OUT = path.join(__dirname, 'data', `delete-results-${DATE}.jsonl`);
+const PACE_MS = 60000; // 60s between deletions — stay well under automation-detection thresholds
+
+const args = process.argv.slice(2);
+const CANARY = args.includes('--canary');
+const GO = args.includes('--go');
+const DESTRUCTIVE = CANARY || GO;
+const only = (() => { const i = args.indexOf('--only'); return i >= 0 ? args[i + 1] : null; })();
+
+if (!fs.existsSync(SRC)) {
+ console.error(`No repost mapping at ${SRC}. Run redo-last5.js --go first.`);
+ process.exit(1);
+}
+const done = new Set(fs.existsSync(OUT)
+ ? fs.readFileSync(OUT, 'utf8').trim().split('\n').filter(Boolean).map((l) => JSON.parse(l).old_permalink)
+ : []);
+const rows = fs.readFileSync(SRC, 'utf8').trim().split('\n').filter(Boolean)
+ .map((l) => JSON.parse(l))
+ .filter((r) => r.old_permalink && (only ? r.handle === only : true))
+ .filter((r) => !done.has(r.old_permalink)); // idempotent/resumable
+
+const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
+function oc(cmd) { return execSync(`openclaw browser ${cmd}`, { encoding: 'utf8', timeout: 60000, stdio: ['ignore', 'pipe', 'pipe'] }); }
+let tab = null;
+function open(url) { const o = oc(`open ${JSON.stringify(url)} --timeout 30000`); tab = (o.match(/id:\s*([A-F0-9]+)/i) || [])[1] || tab; return tab; }
+function navigate(url) { if (!tab) return open(url); oc(`navigate ${JSON.stringify(url)} --target-id ${tab}`); }
+function snapshot() { try { return oc(`snapshot --format ai --limit 800 ${tab ? `--target-id ${tab}` : ''}`); } catch { return ''; } }
+// Find the ref id of the first snapshot line whose accessible text matches `re`.
+function findRef(snap, re) {
+ for (const line of String(snap).split('\n')) {
+ if (re.test(line)) { const m = line.match(/\[ref=([A-Za-z0-9_]+)\]/); if (m) return m[1]; }
+ }
+ return null;
+}
+function click(ref) { oc(`click ${ref} --target-id ${tab}`); }
+
+// Is this post owned by the currently-logged-in account? (Delete affordance reachable.)
+async function isDeletable() {
+ const more = findRef(snapshot(), /More options|More$/i);
+ if (!more) return { deletable: false, reason: 'no ··· menu (not logged in?)' };
+ click(more); await sleep(1200);
+ const del = findRef(snapshot(), /^.*\bDelete\b.*$/);
+ // close the menu without acting (Escape) when only probing
+ try { oc(`press Escape --target-id ${tab}`); } catch { /* ignore */ }
+ return del ? { deletable: true } : { deletable: false, reason: 'no Delete item (not owner of this account)' };
+}
+
+async function deleteOne(r) {
+ navigate(r.old_permalink); await sleep(2500);
+ const more = findRef(snapshot(), /More options|More$/i);
+ if (!more) throw new Error('no ··· menu (wrong account / not logged in)');
+ click(more); await sleep(1200);
+ const del = findRef(snapshot(), /\bDelete\b/);
+ if (!del) throw new Error('no Delete item (not owner)');
+ click(del); await sleep(1200);
+ // IG's confirm is an in-DOM modal with a "Delete" button (not a native dialog).
+ const confirm = findRef(snapshot(), /\bDelete\b/);
+ if (!confirm) throw new Error('no confirm Delete button');
+ click(confirm); await sleep(3000);
+ // Verify: the permalink should now be unavailable.
+ navigate(r.old_permalink); await sleep(2500);
+ const gone = /isn't available|Sorry, this page|Page Not Found/i.test(snapshot());
+ return gone;
+}
+
+(async () => {
+ console.log(`${DESTRUCTIVE ? (CANARY ? 'CANARY DELETE (1)' : 'LIVE DELETE') : 'PROBE (non-destructive)'} — ${rows.length} candidate original(s)${only ? ` @${only}` : ''}\n`);
+ if (!rows.length) { console.log('Nothing to do (all already deleted or none mapped).'); return; }
+ // Preflight: confirm openclaw browser is up + logged into instagram.
+ try { open('https://www.instagram.com/'); await sleep(2500);
+ if (/Log in|Log In|Phone number, username/i.test(snapshot()) && !/Home|Search|Profile/i.test(snapshot())) {
+ console.error('⚠ openclaw Chrome is NOT logged into Instagram. Log in first, then re-run.'); process.exit(2);
+ }
+ } catch (e) { console.error(`⚠ openclaw browser not reachable: ${e.message}. Start it + log in, then re-run.`); process.exit(2); }
+
+ let ok = 0, skip = 0, i = 0;
+ for (const r of rows) {
+ i++;
+ const tag = `[${i}/${rows.length}] @${r.handle} ${r.old_permalink}`;
+ if (!DESTRUCTIVE) {
+ navigate(r.old_permalink); await sleep(2000);
+ const p = await isDeletable();
+ console.log(`${p.deletable ? '🟢 deletable' : '⚪ skip'} ${tag}${p.reason ? ` — ${p.reason}` : ''}`);
+ continue;
+ }
+ try {
+ const gone = await deleteOne(r);
+ fs.appendFileSync(OUT, JSON.stringify({ ...r, deleted: gone, ts: new Date().toISOString() }) + '\n');
+ console.log(`${gone ? '✓ deleted' : '⚠ clicked but not verified gone'} ${tag}`);
+ ok++;
+ } catch (e) { console.log(`✗ SKIP ${tag} — ${e.message}`); skip++; }
+ if (CANARY) { console.log('\nCanary done — verify manually, then run --go for the rest.'); break; }
+ if (i < rows.length) await sleep(PACE_MS);
+ }
+ if (DESTRUCTIVE) console.log(`\nDeleted ${ok}, skipped ${skip}. Log -> ${OUT}`);
+})().catch((e) => { console.error('FAILED:', e.message); process.exit(1); });
← 70e6d07 IG redo: make driver idempotent/resumable (skip already-repo
·
back to Norma
·
auto-data-snapshot: 2026-08-14T10:52:23 (1 data files) — age 0e01034 →