[object Object]

← back to Norma Platform

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

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 Platform  ·  chore: instagram-agent v1.3.0 (session close — /spoonflower 80a6ffe →