storyboard-bridge 0.9.2 → 0.9.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/package.json +1 -1
  2. package/tryinfer_client.py +88 -12
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "storyboard-bridge",
3
- "version": "0.9.2",
3
+ "version": "0.9.3",
4
4
  "description": "Desktop bridge that powers a hosted Storyboard AI webapp with your own local Claude Code / Gemini CLI login (no API key).",
5
5
  "type": "module",
6
6
  "bin": {
@@ -123,6 +123,20 @@ def _fetch_expr(method, url, body):
123
123
  return "".join(parts)
124
124
 
125
125
 
126
+ # JS run inside the tab to force Clerk to re-mint its short-lived session token (which updates the __session
127
+ # cookie tryinfer's API reads). Works on a HIDDEN/backgrounded tab — Runtime.evaluate ignores visibility — so
128
+ # we never foreground. Returns {ok:true} on success, {ok:false,reason|error} if Clerk isn't loaded yet.
129
+ _CLERK_REFRESH_EXPR = (
130
+ "(async()=>{try{"
131
+ "const c=window.Clerk;"
132
+ "if(c&&c.session&&typeof c.session.getToken==='function'){"
133
+ "await c.session.getToken({skipCache:true});"
134
+ "return JSON.stringify({ok:true});}"
135
+ "return JSON.stringify({ok:false,reason:'clerk-not-ready'});"
136
+ "}catch(e){return JSON.stringify({ok:false,error:String((e&&e.message)||e)});}})()"
137
+ )
138
+
139
+
126
140
  # CDP errors that mean the execution context we ran fetch() in is gone — the Studio tab NAVIGATED (it changes
127
141
  # route when a generation starts/finishes) or reloaded. We recover by re-attaching to the tab's fresh context.
128
142
  _CTX_DEAD = ("navigated", "context", "closed", "-32000", "detached", "Session with given id")
@@ -189,21 +203,30 @@ class BrowserSession:
189
203
  if not tid:
190
204
  raise RuntimeError(f"No open tab whose URL contains '{self.match}'. Open tryinfer.com and log in.")
191
205
  # A HOST tab left in the background CAN get discarded/frozen/throttled by Chrome (no live renderer),
192
- # in which case attaching works but Runtime.enable hangs waiting for a renderer → CDP timeout.
193
- # We DON'T foreground on the happy path (activateTarget steals focus, which is annoying in a browser
194
- # the user is actively using). Instead attempt 0 attaches WITHOUT foregrounding, with a short
195
- # Runtime.enable timeout so a genuinely stalled tab fails fast; only THEN do later attempts fall back
196
- # to activateTarget (foreground/wake) to spin the renderer back up.
206
+ # in which case attaching works but Runtime.enable hangs waiting for a renderer → CDP timeout. We wake
207
+ # it WITHOUT foregrounding (activateTarget steals focus, which is annoying in a browser the user is
208
+ # actively using), escalating only as far as needed:
209
+ # attempt 0 — plain attach, short Runtime.enable timeout so a stalled tab fails fast.
210
+ # attempt 1 — focus-free un-freeze: Page.setWebLifecycleState('active') resumes a frozen renderer
211
+ # without bringing the tab forward.
212
+ # attempt 2 — LAST RESORT ONLY: Target.activateTarget (this is the sole path that steals focus, and
213
+ # only after a plain attach AND a focus-free un-freeze both failed — a fully discarded tab).
197
214
  last = None
198
215
  for attempt in range(3):
199
216
  self.cdp = CDP(self.browser_ws)
200
217
  try:
201
- if attempt > 0:
202
- try: self.cdp.call("Target.activateTarget", {"targetId": tid}, timeout=10) # wake a stalled tab
218
+ if attempt == 2:
219
+ try: self.cdp.call("Target.activateTarget", {"targetId": tid}, timeout=10) # foreground: last resort
203
220
  except Exception: pass
204
221
  self.session_id = self.cdp.call("Target.attachToTarget", {"targetId": tid, "flatten": True}, timeout=15)["sessionId"]
205
- # short timeout on the no-focus first pass → fail fast into the activate() fallback if the
206
- # renderer is asleep; full timeout once we've foregrounded it.
222
+ if attempt == 1:
223
+ # attempt 0 stalled → renderer frozen. Resume it without foregrounding.
224
+ try:
225
+ self.cdp.call("Page.enable", session_id=self.session_id, timeout=8)
226
+ self.cdp.call("Page.setWebLifecycleState", {"state": "active"}, session_id=self.session_id, timeout=8)
227
+ except Exception as e:
228
+ log(f" focus-free un-freeze failed ({e})")
229
+ # short timeout on the first pass → fail fast into the wake escalation if the renderer is asleep.
207
230
  self.cdp.call("Runtime.enable", session_id=self.session_id, timeout=(6 if attempt == 0 else 20))
208
231
  log(f"Routing API calls through tab: {url}")
209
232
  return
@@ -212,12 +235,13 @@ class BrowserSession:
212
235
  try: self.cdp.close()
213
236
  except Exception: pass
214
237
  if attempt < 2:
215
- log(f" CDP attach stalled (waking tab in foreground) — retrying… ({e})")
238
+ log(f" CDP attach stalled — {'un-freezing (no focus)' if attempt == 0 else 'foregrounding (last resort)'}… ({e})")
216
239
  time.sleep(2)
217
240
  raise RuntimeError(f"couldn't attach to the tryinfer tab — is it awake + logged in? ({last})")
218
241
 
219
242
  def request(self, method, url, body=None, timeout=120, retries=4):
220
243
  last = None
244
+ refreshed = False # one focus-free session re-mint per request (a genuine logout still surfaces as 401)
221
245
  for attempt in range(retries):
222
246
  try:
223
247
  res = self.cdp.call(
@@ -240,15 +264,67 @@ class BrowserSession:
240
264
  last = f"evaluate failed: {res.get('exceptionDetails')}"
241
265
  else:
242
266
  data = json.loads(val)
243
- if data.get("status") == 0:
267
+ status = data.get("status")
268
+ if status == 0:
244
269
  last = f"in-page fetch error: {data.get('error')}"
270
+ elif status in (401, 403) and not refreshed:
271
+ # Stale Clerk session: a backgrounded tab stops refreshing __session, so tryinfer 401s.
272
+ # Re-mint the cookie WITHOUT foregrounding, then retry immediately with the fresh session.
273
+ refreshed = True
274
+ last = f"auth {status} — re-minting session (no focus)"
275
+ log(f" {last}…")
276
+ try: self.refresh_session(hard=True)
277
+ except Exception as e2: last = f"session refresh failed: {e2}"
278
+ continue # skip the backoff sleep — retry now that the cookie is fresh
245
279
  else:
246
- return PageResponse(data["status"], data["body"])
280
+ return PageResponse(status, data["body"])
247
281
  if attempt < retries - 1:
248
282
  log(f" transient request error, retrying… ({last})")
249
283
  time.sleep(3)
250
284
  raise RuntimeError(last)
251
285
 
286
+ def _clerk_mint(self):
287
+ """Force Clerk to mint a fresh session token (updates __session) via evaluated JS. No foreground.
288
+ Returns True if a token was minted, False if Clerk isn't loaded / errored."""
289
+ try:
290
+ res = self.cdp.call(
291
+ "Runtime.evaluate",
292
+ {"expression": _CLERK_REFRESH_EXPR, "awaitPromise": True, "returnByValue": True},
293
+ session_id=self.session_id, timeout=20)
294
+ val = res.get("result", {}).get("value")
295
+ data = json.loads(val) if val else {}
296
+ if data.get("ok"):
297
+ log(" session re-minted via Clerk (no focus)")
298
+ return True
299
+ log(f" Clerk refresh unavailable: {data.get('reason') or data.get('error')}")
300
+ except Exception as e:
301
+ log(f" Clerk refresh errored: {e}")
302
+ return False
303
+
304
+ def refresh_session(self, hard=False):
305
+ """Re-mint the Clerk session cookie WITHOUT foregrounding the tab. A backgrounded tab stops running
306
+ Clerk's periodic token refresh, so __session goes stale and tryinfer 401s. Focus-free recovery:
307
+ 1. Page.setWebLifecycleState('active') — resume a throttled/frozen renderer (does NOT foreground).
308
+ 2. window.Clerk.session.getToken({skipCache:true}) — force a fresh token → fresh cookie.
309
+ 3. (hard only) if Clerk isn't reachable, Page.reload + re-attach so Clerk re-inits from scratch.
310
+ Returns True if a fresh token was minted."""
311
+ try:
312
+ self.cdp.call("Page.enable", session_id=self.session_id, timeout=8)
313
+ self.cdp.call("Page.setWebLifecycleState", {"state": "active"}, session_id=self.session_id, timeout=8)
314
+ except Exception:
315
+ pass
316
+ if self._clerk_mint():
317
+ return True
318
+ if hard:
319
+ log(" Clerk not ready — reloading tab to re-init the session (no focus)…")
320
+ try: self.cdp.call("Page.reload", session_id=self.session_id, timeout=15)
321
+ except Exception as e: log(f" reload failed: {e}")
322
+ time.sleep(5)
323
+ try: self._attach()
324
+ except Exception as e: log(f" re-attach after reload failed: {e}")
325
+ return self._clerk_mint()
326
+ return False
327
+
252
328
  def close(self):
253
329
  try:
254
330
  self.cdp.call("Target.detachFromTarget", {"sessionId": self.session_id}, timeout=5)