storyboard-bridge 0.9.2 → 0.9.4
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.
- package/package.json +1 -1
- package/tryinfer_client.py +98 -18
package/package.json
CHANGED
package/tryinfer_client.py
CHANGED
|
@@ -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
|
-
#
|
|
194
|
-
#
|
|
195
|
-
# Runtime.enable timeout so a
|
|
196
|
-
#
|
|
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
|
|
202
|
-
try: self.cdp.call("Target.activateTarget", {"targetId": tid}, timeout=10) #
|
|
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
|
-
|
|
206
|
-
|
|
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
|
|
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(
|
|
@@ -226,12 +250,16 @@ class BrowserSession:
|
|
|
226
250
|
session_id=self.session_id, timeout=timeout)
|
|
227
251
|
except Exception as e:
|
|
228
252
|
last = f"CDP evaluate error: {e}"
|
|
229
|
-
# tab
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
253
|
+
# The tab stopped answering CDP. Two shapes, ONE recovery: the context died (tab navigated /
|
|
254
|
+
# closed — _CTX_DEAD) OR the renderer froze/was discarded and the evaluate TIMED OUT waiting
|
|
255
|
+
# for a response. Previously we only re-attached on _CTX_DEAD, so a mid-request freeze just
|
|
256
|
+
# hammered the same dead session until it failed. Now we re-attach on ANY evaluate error —
|
|
257
|
+
# _attach focus-free un-freezes a stalled renderer (setWebLifecycleState) — and it also swaps
|
|
258
|
+
# in a fresh CDP socket, clearing any response desynced by the timeout.
|
|
259
|
+
log(f" tab unresponsive — re-attaching (no focus)… ({e})")
|
|
260
|
+
time.sleep(1)
|
|
261
|
+
try: self._attach()
|
|
262
|
+
except Exception as e2: last = f"re-attach failed: {e2}"
|
|
235
263
|
if attempt < retries - 1:
|
|
236
264
|
time.sleep(2); continue
|
|
237
265
|
raise RuntimeError(last)
|
|
@@ -240,15 +268,67 @@ class BrowserSession:
|
|
|
240
268
|
last = f"evaluate failed: {res.get('exceptionDetails')}"
|
|
241
269
|
else:
|
|
242
270
|
data = json.loads(val)
|
|
243
|
-
|
|
271
|
+
status = data.get("status")
|
|
272
|
+
if status == 0:
|
|
244
273
|
last = f"in-page fetch error: {data.get('error')}"
|
|
274
|
+
elif status in (401, 403) and not refreshed:
|
|
275
|
+
# Stale Clerk session: a backgrounded tab stops refreshing __session, so tryinfer 401s.
|
|
276
|
+
# Re-mint the cookie WITHOUT foregrounding, then retry immediately with the fresh session.
|
|
277
|
+
refreshed = True
|
|
278
|
+
last = f"auth {status} — re-minting session (no focus)"
|
|
279
|
+
log(f" {last}…")
|
|
280
|
+
try: self.refresh_session(hard=True)
|
|
281
|
+
except Exception as e2: last = f"session refresh failed: {e2}"
|
|
282
|
+
continue # skip the backoff sleep — retry now that the cookie is fresh
|
|
245
283
|
else:
|
|
246
|
-
return PageResponse(
|
|
284
|
+
return PageResponse(status, data["body"])
|
|
247
285
|
if attempt < retries - 1:
|
|
248
286
|
log(f" transient request error, retrying… ({last})")
|
|
249
287
|
time.sleep(3)
|
|
250
288
|
raise RuntimeError(last)
|
|
251
289
|
|
|
290
|
+
def _clerk_mint(self):
|
|
291
|
+
"""Force Clerk to mint a fresh session token (updates __session) via evaluated JS. No foreground.
|
|
292
|
+
Returns True if a token was minted, False if Clerk isn't loaded / errored."""
|
|
293
|
+
try:
|
|
294
|
+
res = self.cdp.call(
|
|
295
|
+
"Runtime.evaluate",
|
|
296
|
+
{"expression": _CLERK_REFRESH_EXPR, "awaitPromise": True, "returnByValue": True},
|
|
297
|
+
session_id=self.session_id, timeout=20)
|
|
298
|
+
val = res.get("result", {}).get("value")
|
|
299
|
+
data = json.loads(val) if val else {}
|
|
300
|
+
if data.get("ok"):
|
|
301
|
+
log(" session re-minted via Clerk (no focus)")
|
|
302
|
+
return True
|
|
303
|
+
log(f" Clerk refresh unavailable: {data.get('reason') or data.get('error')}")
|
|
304
|
+
except Exception as e:
|
|
305
|
+
log(f" Clerk refresh errored: {e}")
|
|
306
|
+
return False
|
|
307
|
+
|
|
308
|
+
def refresh_session(self, hard=False):
|
|
309
|
+
"""Re-mint the Clerk session cookie WITHOUT foregrounding the tab. A backgrounded tab stops running
|
|
310
|
+
Clerk's periodic token refresh, so __session goes stale and tryinfer 401s. Focus-free recovery:
|
|
311
|
+
1. Page.setWebLifecycleState('active') — resume a throttled/frozen renderer (does NOT foreground).
|
|
312
|
+
2. window.Clerk.session.getToken({skipCache:true}) — force a fresh token → fresh cookie.
|
|
313
|
+
3. (hard only) if Clerk isn't reachable, Page.reload + re-attach so Clerk re-inits from scratch.
|
|
314
|
+
Returns True if a fresh token was minted."""
|
|
315
|
+
try:
|
|
316
|
+
self.cdp.call("Page.enable", session_id=self.session_id, timeout=8)
|
|
317
|
+
self.cdp.call("Page.setWebLifecycleState", {"state": "active"}, session_id=self.session_id, timeout=8)
|
|
318
|
+
except Exception:
|
|
319
|
+
pass
|
|
320
|
+
if self._clerk_mint():
|
|
321
|
+
return True
|
|
322
|
+
if hard:
|
|
323
|
+
log(" Clerk not ready — reloading tab to re-init the session (no focus)…")
|
|
324
|
+
try: self.cdp.call("Page.reload", session_id=self.session_id, timeout=15)
|
|
325
|
+
except Exception as e: log(f" reload failed: {e}")
|
|
326
|
+
time.sleep(5)
|
|
327
|
+
try: self._attach()
|
|
328
|
+
except Exception as e: log(f" re-attach after reload failed: {e}")
|
|
329
|
+
return self._clerk_mint()
|
|
330
|
+
return False
|
|
331
|
+
|
|
252
332
|
def close(self):
|
|
253
333
|
try:
|
|
254
334
|
self.cdp.call("Target.detachFromTarget", {"sessionId": self.session_id}, timeout=5)
|