← back to Norma
Harden IG delete-originals.js: retry stale refs, recover dead tabs, patient positive-signal login check
04b7ade580d8ac328a62a7bef81e3ba64c7d5e1a · 2026-08-17 09:15:34 -0700 · Steve Abrams
- clickByText() defeats the snapshot->click stale-ref race on IG's dynamic DOM
- navigate()/snapshot() recover from openclaw 'tab not found' (dead shared tab)
- preflight uses a retried positive login signal (app nav visible) instead of a
too-early negative check that gave false 'not logged in' readings
No change to deletion semantics/gates (canary-stops-1, verify-gone, skip-on-missing-Delete).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Files touched
M agents/instagram-agent/delete-originals.js
Diff
commit 04b7ade580d8ac328a62a7bef81e3ba64c7d5e1a
Author: Steve Abrams <steve@designerwallcoverings.com>
Date: Mon Aug 17 09:15:34 2026 -0700
Harden IG delete-originals.js: retry stale refs, recover dead tabs, patient positive-signal login check
- clickByText() defeats the snapshot->click stale-ref race on IG's dynamic DOM
- navigate()/snapshot() recover from openclaw 'tab not found' (dead shared tab)
- preflight uses a retried positive login signal (app nav visible) instead of a
too-early negative check that gave false 'not logged in' readings
No change to deletion semantics/gates (canary-stops-1, verify-gone, skip-on-missing-Delete).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---
agents/instagram-agent/delete-originals.js | 82 ++++++++++++++++++++++--------
1 file changed, 62 insertions(+), 20 deletions(-)
diff --git a/agents/instagram-agent/delete-originals.js b/agents/instagram-agent/delete-originals.js
index b4aa08f..88f37a6 100644
--- a/agents/instagram-agent/delete-originals.js
+++ b/agents/instagram-agent/delete-originals.js
@@ -53,10 +53,24 @@ const rows = fs.readFileSync(SRC, 'utf8').trim().split('\n').filter(Boolean)
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'] }); }
+const TAB_DEAD = /tab not found|target.*not found|No target/i;
+const STALE_REF = /not found or not visible/i;
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 ''; } }
+// Navigate; if the shared openclaw tab died mid-run ("tab not found"), transparently re-open.
+function navigate(url) {
+ if (!tab) return open(url);
+ try { oc(`navigate ${JSON.stringify(url)} --target-id ${tab}`); }
+ catch (e) { if (TAB_DEAD.test(e.message)) { tab = null; open(url); } else throw e; }
+}
+// Snapshot with a couple of retries; a dead tab is reset so the next open() re-grabs one.
+function snapshot() {
+ for (let i = 0; i < 3; i++) {
+ try { const s = oc(`snapshot --format ai --limit 800 ${tab ? `--target-id ${tab}` : ''}`); if (s && s.trim()) return s; }
+ catch (e) { if (TAB_DEAD.test(e.message)) tab = null; }
+ }
+ 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')) {
@@ -65,32 +79,51 @@ function findRef(snap, re) {
return null;
}
function click(ref) { oc(`click ${ref} --target-id ${tab}`); }
+// Snapshot → find `re` → click, retrying to defeat the snapshot→click stale-ref race on IG's
+// dynamic DOM (the ref goes stale between the two separate openclaw calls). Returns true on a
+// successful click, false only if `re` is genuinely absent after every try.
+async function clickByText(re, { tries = 4, settleMs = 900 } = {}) {
+ for (let i = 0; i < tries; i++) {
+ const ref = findRef(snapshot(), re);
+ if (!ref) { await sleep(settleMs); continue; } // not painted yet — wait & re-snap
+ try { click(ref); return true; }
+ catch (e) {
+ if (STALE_REF.test(e.message) || TAB_DEAD.test(e.message)) { await sleep(settleMs); continue; } // re-snap & retry
+ throw e;
+ }
+ }
+ return false;
+}
+// Does the current page show `re`? Retries a few times to let the DOM settle.
+async function seesText(re, { tries = 3, settleMs = 800 } = {}) {
+ for (let i = 0; i < tries; i++) { if (re.test(snapshot())) return true; await sleep(settleMs); }
+ return false;
+}
// 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.*$/);
+ const opened = await clickByText(/More options|More$/i);
+ if (!opened) return { deletable: false, reason: 'no ··· menu (not logged in / not painted)' };
+ await sleep(1500);
+ const del = await seesText(/\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);
+ navigate(r.old_permalink); await sleep(2800);
+ // open the ··· menu (retry-tolerant against the snapshot→click stale-ref race)
+ if (!await clickByText(/More options|More$/i)) throw new Error('no ··· menu (wrong account / not logged in)');
+ await sleep(1500);
+ // click the Delete menu item
+ if (!await clickByText(/\bDelete\b/)) throw new Error('no Delete item (not owner)');
+ await sleep(1500);
// 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);
+ if (!await clickByText(/\bDelete\b/)) throw new Error('no confirm Delete button');
+ await sleep(3000);
// Verify: the permalink should now be unavailable.
- navigate(r.old_permalink); await sleep(2500);
+ navigate(r.old_permalink); await sleep(2800);
const gone = /isn't available|Sorry, this page|Page Not Found/i.test(snapshot());
return gone;
}
@@ -99,10 +132,19 @@ async function deleteOne(r) {
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);
+ try {
+ open('https://www.instagram.com/'); await sleep(3500);
+ // Positive-signal login check, RETRIED — IG's nav chrome can take several seconds to paint,
+ // and a too-early snapshot was giving false "not logged in" readings. Logged in == we can see
+ // the app nav; only declare "not logged in" if the login form is still up after settling.
+ let loggedIn = false;
+ for (let i = 0; i < 4; i++) {
+ const s = snapshot();
+ if (/Home|Search|Profile|Create|Reels|New post/i.test(s)) { loggedIn = true; break; }
+ if (i >= 2 && /Phone number, username|Log in with|Forgot password/i.test(s)) break; // clearly the login page
+ await sleep(2500);
}
+ if (!loggedIn) { console.error('⚠ openclaw Chrome is NOT logged into Instagram (or IG did not paint). Log in / let it settle, 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;
← 1e26b11 IG /spoonflower: server-inject data into the page so it rend
·
back to Norma
·
chore: instagram-agent v1.3.0 (session close — /spoonflower 80a6ffe →