@astralform/js 7.1.0 → 7.2.0

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/dist/index.cjs CHANGED
@@ -1310,7 +1310,7 @@ function translateWireEvent(wire) {
1310
1310
 
1311
1311
  // src/session.ts
1312
1312
  var SSE_MAX_RECONNECTS = 6;
1313
- var SSE_STALL_TIMEOUT_MS = 45e3;
1313
+ var SSE_STALL_TIMEOUT_MS = 135e3;
1314
1314
  var TOOL_RESULT_MAX_RETRIES = 3;
1315
1315
  var CONVERSATION_PAGE_SIZE = 50;
1316
1316
  function sseReconnectDelayMs(attempt) {
@@ -1721,7 +1721,7 @@ var ChatSession = class {
1721
1721
  * whether an ended stream means "turn done" vs "dropped, reconnect".
1722
1722
  *
1723
1723
  * ``onStall`` aborts the per-attempt connection: if no event arrives within
1724
- * SSE_STALL_TIMEOUT_MS (backend keepalives land every 15s), the stream is a
1724
+ * SSE_STALL_TIMEOUT_MS (backend keepalives land every 15-45s), the stream is a
1725
1725
  * zombie — ``reader.read()`` will never settle — so we kill the fetch and
1726
1726
  * throw a ConnectionError, feeding the caller's reconnect-from-lastSeq loop.
1727
1727
  */
@@ -2371,6 +2371,13 @@ var StreamManager = class {
2371
2371
  * its awaits. This can.
2372
2372
  */
2373
2373
  this.turnCounter = 0;
2374
+ /**
2375
+ * True while a `resync` is between its probe and its restore. Visibility and
2376
+ * focus listeners can both fire for one return, and two overlapping resyncs
2377
+ * would each detach the other's stream mid-flight — the second call must
2378
+ * find the flag set and leave the first to converge.
2379
+ */
2380
+ this._resyncing = false;
2374
2381
  this.session = session;
2375
2382
  this.attach();
2376
2383
  }
@@ -2555,6 +2562,73 @@ var StreamManager = class {
2555
2562
  }
2556
2563
  }
2557
2564
  }
2565
+ // ── Resync after the page sat in the background ───────────────
2566
+ /**
2567
+ * Re-attach to whatever the server says is live for the ACTIVE conversation.
2568
+ *
2569
+ * The reconnect machinery inside ``consumeEventStream`` only runs while a
2570
+ * stream is being consumed — and a page suspended in the background (locked
2571
+ * phone, app switch, hidden tab) can outlive it: timers are throttled or
2572
+ * suspended, so the stall watchdog may never fire while hidden; the
2573
+ * reconnect budget (``SSE_MAX_RECONNECTS``) can burn out in fail-fast
2574
+ * attempts; and a 401 from a rotated access token ends the loop outright as
2575
+ * non-retryable. What is left is a manager that believes a turn is streaming
2576
+ * (or has given up on one that is still running) with nothing attached — and
2577
+ * no navigation will ever fix it, because ``switchTo`` early-returns on the
2578
+ * conversation it is already on.
2579
+ *
2580
+ * So the consumer calls this when the user COMES BACK
2581
+ * (``visibilitychange → visible``, window ``focus``). One ``getActiveJob``
2582
+ * probe, then:
2583
+ *
2584
+ * - attached to exactly the job the server calls live → healthy. The stall
2585
+ * watchdog owns zombie recovery from here, now that timers run again.
2586
+ * No-op.
2587
+ * - anything else — attached to a job the server no longer calls live,
2588
+ * attached to nothing while a job runs, or idle with a live job another
2589
+ * tab/device started — → detach and re-run ``restore``, the same path a
2590
+ * conversation reopen takes, with all of its supersession and takeover
2591
+ * guards inherited.
2592
+ *
2593
+ * Skipped while a restore is already in flight (it is converging on server
2594
+ * truth by itself) and while another resync holds the flag — see
2595
+ * ``_resyncing``.
2596
+ */
2597
+ async resync() {
2598
+ const conversationId = this._activeConversationId;
2599
+ if (!conversationId) return;
2600
+ if (this._state === "restoring" || this._resyncing) return;
2601
+ this._resyncing = true;
2602
+ const gen = this.generation;
2603
+ const turn = this.turnCounter;
2604
+ try {
2605
+ let activeJobId = null;
2606
+ try {
2607
+ activeJobId = (await this.session.client.getActiveJob(conversationId)).jobId;
2608
+ } catch {
2609
+ return;
2610
+ }
2611
+ if (gen !== this.generation || this.turnStarted(turn)) return;
2612
+ const attached = this._state === "streaming" || this.session.isStreaming;
2613
+ if (activeJobId === null && !attached) return;
2614
+ if (activeJobId !== null && this.session.isStreaming && this.session.currentJobId === activeJobId) {
2615
+ return;
2616
+ }
2617
+ this.session.detach();
2618
+ this.session.currentJobId = null;
2619
+ this._state = "idle";
2620
+ try {
2621
+ await this.restore(conversationId, gen);
2622
+ } catch {
2623
+ }
2624
+ } finally {
2625
+ this._resyncing = false;
2626
+ const restoring = "restoring";
2627
+ if (gen === this.generation && this._state === restoring) {
2628
+ this.settleIdle();
2629
+ }
2630
+ }
2631
+ }
2558
2632
  // ── Create / rename / delete conversation ─────────────────────
2559
2633
  /**
2560
2634
  * Create a conversation and make it active.