[object Object]

← back to Slack Idea Board

Idea-board: auto-retry throttled launches with live countdown (429 self-heals)

502e68120cfd7e09bbd7cb8c5cc2d4f179451004 · 2026-08-06 16:53:31 -0700 · Steve Abrams

Files touched

Diff

commit 502e68120cfd7e09bbd7cb8c5cc2d4f179451004
Author: Steve Abrams <steve@designerwallcoverings.com>
Date:   Thu Aug 6 16:53:31 2026 -0700

    Idea-board: auto-retry throttled launches with live countdown (429 self-heals)
---
 public/index.html | 26 ++++++++++++++++++++++----
 1 file changed, 22 insertions(+), 4 deletions(-)

diff --git a/public/index.html b/public/index.html
index edaa6b7..89ee279 100644
--- a/public/index.html
+++ b/public/index.html
@@ -134,17 +134,35 @@ async function copyAction(btn){
 // survives re-render (the per-button `disabled` flag does not — render() replaces the node); (3) the
 // POST carries confirm:true, which the server now requires before it will spawn anything.
 const INFLIGHT = new Set();
+const sleep = ms => new Promise(r=>setTimeout(r,ms));
+async function postBuild(id, step){
+  const r=await fetch('/api/build',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({id,step,confirm:true})});
+  return r.json();
+}
 async function launchAction(id, step, label, btn){
   const key=(id||'')+'|'+step;
   if(INFLIGHT.has(key)) return; // a launch for this id+step is already running
   if(!confirm(`Launch a REAL Claude Code session in a new iTerm2 tab?\n\n${label||step}`)) return;
   INFLIGHT.add(key);
-  if(btn){ btn.classList.add('copied'); var prev=btn.textContent; btn.textContent='launching…'; btn.disabled=true; }
+  var prev = btn ? btn.textContent : '';
+  if(btn){ btn.classList.add('copied'); btn.textContent='launching…'; btn.disabled=true; }
+  const setLbl = t => { if(btn && btn.isConnected) btn.textContent=t; };
   try{
-    const r=await fetch('/api/build',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({id,step,confirm:true})});
-    const j=await r.json();
+    // Self-heal the server's 429 circuit-breaker: a throttled POST consumes no slot, and the
+    // 30s rolling window frees a slot as old spawns age out — so we just re-POST the SAME launch
+    // on a fixed cadence (with a visible countdown) until it clears or we hit the hard deadline.
+    const MAX_WAIT_MS = 40000, RETRY_MS = 4000;   // one full window + buffer
+    const deadline = Date.now() + MAX_WAIT_MS;
+    let j;
+    while(true){
+      j = await postBuild(id, step);
+      if(!j.throttled) break;                        // launched / failed / needs-confirm → done
+      if(Date.now() + RETRY_MS > deadline) break;    // give up → falls through to the throttle toast
+      for(let s=Math.round(RETRY_MS/1000); s>0; s--){ setLbl(`throttled · retry ${s}s`); await sleep(1000); }
+    }
     if(j.launched){ toast(`🚀 <b>${esc(label||step)}</b> launched in a new iTerm2 tab<br><code>${esc((j.prompt||'').slice(0,160))}…</code>`); }
-    else if(j.throttled){ toast('⏳ Throttled — '+esc(j.reason||'too many launches, try again shortly')); }
+    else if(j.throttled){ toast('⏳ Still throttled after retries — '+esc(j.reason||'try again shortly')); }
+    else if(j.needsConfirm){ toast('Launch blocked — server requires confirm'); }
     else { toast('Launch failed: '+esc(j.error||'unknown')); }
   }catch(err){ toast('Launch failed: '+esc(String(err.message||err))); }
   finally{ INFLIGHT.delete(key); if(btn && btn.isConnected){ btn.classList.remove('copied'); btn.textContent=prev; btn.disabled=false; } }

← 5e09083 Loosen idea-board build throttle: 20/30s cap, 2s cooldown  ·  back to Slack Idea Board  ·  Idea-board: launch in a NEW iTerm2 window (not a tab); lift 0638b1f →