@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.d.cts CHANGED
@@ -1329,7 +1329,7 @@ declare class ChatSession {
1329
1329
  * whether an ended stream means "turn done" vs "dropped, reconnect".
1330
1330
  *
1331
1331
  * ``onStall`` aborts the per-attempt connection: if no event arrives within
1332
- * SSE_STALL_TIMEOUT_MS (backend keepalives land every 15s), the stream is a
1332
+ * SSE_STALL_TIMEOUT_MS (backend keepalives land every 15-45s), the stream is a
1333
1333
  * zombie — ``reader.read()`` will never settle — so we kill the fetch and
1334
1334
  * throw a ConnectionError, feeding the caller's reconnect-from-lastSeq loop.
1335
1335
  */
@@ -1614,6 +1614,13 @@ declare class StreamManager {
1614
1614
  * its awaits. This can.
1615
1615
  */
1616
1616
  private turnCounter;
1617
+ /**
1618
+ * True while a `resync` is between its probe and its restore. Visibility and
1619
+ * focus listeners can both fire for one return, and two overlapping resyncs
1620
+ * would each detach the other's stream mid-flight — the second call must
1621
+ * find the flag set and leave the first to converge.
1622
+ */
1623
+ private _resyncing;
1617
1624
  constructor(session: ChatSession);
1618
1625
  get state(): StreamState;
1619
1626
  get activeConversationId(): string | null;
@@ -1645,6 +1652,38 @@ declare class StreamManager {
1645
1652
  switchTo(conversationId: string, opts?: {
1646
1653
  skipHistoryReplay?: boolean;
1647
1654
  }): Promise<void>;
1655
+ /**
1656
+ * Re-attach to whatever the server says is live for the ACTIVE conversation.
1657
+ *
1658
+ * The reconnect machinery inside ``consumeEventStream`` only runs while a
1659
+ * stream is being consumed — and a page suspended in the background (locked
1660
+ * phone, app switch, hidden tab) can outlive it: timers are throttled or
1661
+ * suspended, so the stall watchdog may never fire while hidden; the
1662
+ * reconnect budget (``SSE_MAX_RECONNECTS``) can burn out in fail-fast
1663
+ * attempts; and a 401 from a rotated access token ends the loop outright as
1664
+ * non-retryable. What is left is a manager that believes a turn is streaming
1665
+ * (or has given up on one that is still running) with nothing attached — and
1666
+ * no navigation will ever fix it, because ``switchTo`` early-returns on the
1667
+ * conversation it is already on.
1668
+ *
1669
+ * So the consumer calls this when the user COMES BACK
1670
+ * (``visibilitychange → visible``, window ``focus``). One ``getActiveJob``
1671
+ * probe, then:
1672
+ *
1673
+ * - attached to exactly the job the server calls live → healthy. The stall
1674
+ * watchdog owns zombie recovery from here, now that timers run again.
1675
+ * No-op.
1676
+ * - anything else — attached to a job the server no longer calls live,
1677
+ * attached to nothing while a job runs, or idle with a live job another
1678
+ * tab/device started — → detach and re-run ``restore``, the same path a
1679
+ * conversation reopen takes, with all of its supersession and takeover
1680
+ * guards inherited.
1681
+ *
1682
+ * Skipped while a restore is already in flight (it is converging on server
1683
+ * truth by itself) and while another resync holds the flag — see
1684
+ * ``_resyncing``.
1685
+ */
1686
+ resync(): Promise<void>;
1648
1687
  /**
1649
1688
  * Create a conversation and make it active.
1650
1689
  *
package/dist/index.d.ts CHANGED
@@ -1329,7 +1329,7 @@ declare class ChatSession {
1329
1329
  * whether an ended stream means "turn done" vs "dropped, reconnect".
1330
1330
  *
1331
1331
  * ``onStall`` aborts the per-attempt connection: if no event arrives within
1332
- * SSE_STALL_TIMEOUT_MS (backend keepalives land every 15s), the stream is a
1332
+ * SSE_STALL_TIMEOUT_MS (backend keepalives land every 15-45s), the stream is a
1333
1333
  * zombie — ``reader.read()`` will never settle — so we kill the fetch and
1334
1334
  * throw a ConnectionError, feeding the caller's reconnect-from-lastSeq loop.
1335
1335
  */
@@ -1614,6 +1614,13 @@ declare class StreamManager {
1614
1614
  * its awaits. This can.
1615
1615
  */
1616
1616
  private turnCounter;
1617
+ /**
1618
+ * True while a `resync` is between its probe and its restore. Visibility and
1619
+ * focus listeners can both fire for one return, and two overlapping resyncs
1620
+ * would each detach the other's stream mid-flight — the second call must
1621
+ * find the flag set and leave the first to converge.
1622
+ */
1623
+ private _resyncing;
1617
1624
  constructor(session: ChatSession);
1618
1625
  get state(): StreamState;
1619
1626
  get activeConversationId(): string | null;
@@ -1645,6 +1652,38 @@ declare class StreamManager {
1645
1652
  switchTo(conversationId: string, opts?: {
1646
1653
  skipHistoryReplay?: boolean;
1647
1654
  }): Promise<void>;
1655
+ /**
1656
+ * Re-attach to whatever the server says is live for the ACTIVE conversation.
1657
+ *
1658
+ * The reconnect machinery inside ``consumeEventStream`` only runs while a
1659
+ * stream is being consumed — and a page suspended in the background (locked
1660
+ * phone, app switch, hidden tab) can outlive it: timers are throttled or
1661
+ * suspended, so the stall watchdog may never fire while hidden; the
1662
+ * reconnect budget (``SSE_MAX_RECONNECTS``) can burn out in fail-fast
1663
+ * attempts; and a 401 from a rotated access token ends the loop outright as
1664
+ * non-retryable. What is left is a manager that believes a turn is streaming
1665
+ * (or has given up on one that is still running) with nothing attached — and
1666
+ * no navigation will ever fix it, because ``switchTo`` early-returns on the
1667
+ * conversation it is already on.
1668
+ *
1669
+ * So the consumer calls this when the user COMES BACK
1670
+ * (``visibilitychange → visible``, window ``focus``). One ``getActiveJob``
1671
+ * probe, then:
1672
+ *
1673
+ * - attached to exactly the job the server calls live → healthy. The stall
1674
+ * watchdog owns zombie recovery from here, now that timers run again.
1675
+ * No-op.
1676
+ * - anything else — attached to a job the server no longer calls live,
1677
+ * attached to nothing while a job runs, or idle with a live job another
1678
+ * tab/device started — → detach and re-run ``restore``, the same path a
1679
+ * conversation reopen takes, with all of its supersession and takeover
1680
+ * guards inherited.
1681
+ *
1682
+ * Skipped while a restore is already in flight (it is converging on server
1683
+ * truth by itself) and while another resync holds the flag — see
1684
+ * ``_resyncing``.
1685
+ */
1686
+ resync(): Promise<void>;
1648
1687
  /**
1649
1688
  * Create a conversation and make it active.
1650
1689
  *
package/dist/index.js CHANGED
@@ -1263,7 +1263,7 @@ function translateWireEvent(wire) {
1263
1263
 
1264
1264
  // src/session.ts
1265
1265
  var SSE_MAX_RECONNECTS = 6;
1266
- var SSE_STALL_TIMEOUT_MS = 45e3;
1266
+ var SSE_STALL_TIMEOUT_MS = 135e3;
1267
1267
  var TOOL_RESULT_MAX_RETRIES = 3;
1268
1268
  var CONVERSATION_PAGE_SIZE = 50;
1269
1269
  function sseReconnectDelayMs(attempt) {
@@ -1674,7 +1674,7 @@ var ChatSession = class {
1674
1674
  * whether an ended stream means "turn done" vs "dropped, reconnect".
1675
1675
  *
1676
1676
  * ``onStall`` aborts the per-attempt connection: if no event arrives within
1677
- * SSE_STALL_TIMEOUT_MS (backend keepalives land every 15s), the stream is a
1677
+ * SSE_STALL_TIMEOUT_MS (backend keepalives land every 15-45s), the stream is a
1678
1678
  * zombie — ``reader.read()`` will never settle — so we kill the fetch and
1679
1679
  * throw a ConnectionError, feeding the caller's reconnect-from-lastSeq loop.
1680
1680
  */
@@ -2324,6 +2324,13 @@ var StreamManager = class {
2324
2324
  * its awaits. This can.
2325
2325
  */
2326
2326
  this.turnCounter = 0;
2327
+ /**
2328
+ * True while a `resync` is between its probe and its restore. Visibility and
2329
+ * focus listeners can both fire for one return, and two overlapping resyncs
2330
+ * would each detach the other's stream mid-flight — the second call must
2331
+ * find the flag set and leave the first to converge.
2332
+ */
2333
+ this._resyncing = false;
2327
2334
  this.session = session;
2328
2335
  this.attach();
2329
2336
  }
@@ -2508,6 +2515,73 @@ var StreamManager = class {
2508
2515
  }
2509
2516
  }
2510
2517
  }
2518
+ // ── Resync after the page sat in the background ───────────────
2519
+ /**
2520
+ * Re-attach to whatever the server says is live for the ACTIVE conversation.
2521
+ *
2522
+ * The reconnect machinery inside ``consumeEventStream`` only runs while a
2523
+ * stream is being consumed — and a page suspended in the background (locked
2524
+ * phone, app switch, hidden tab) can outlive it: timers are throttled or
2525
+ * suspended, so the stall watchdog may never fire while hidden; the
2526
+ * reconnect budget (``SSE_MAX_RECONNECTS``) can burn out in fail-fast
2527
+ * attempts; and a 401 from a rotated access token ends the loop outright as
2528
+ * non-retryable. What is left is a manager that believes a turn is streaming
2529
+ * (or has given up on one that is still running) with nothing attached — and
2530
+ * no navigation will ever fix it, because ``switchTo`` early-returns on the
2531
+ * conversation it is already on.
2532
+ *
2533
+ * So the consumer calls this when the user COMES BACK
2534
+ * (``visibilitychange → visible``, window ``focus``). One ``getActiveJob``
2535
+ * probe, then:
2536
+ *
2537
+ * - attached to exactly the job the server calls live → healthy. The stall
2538
+ * watchdog owns zombie recovery from here, now that timers run again.
2539
+ * No-op.
2540
+ * - anything else — attached to a job the server no longer calls live,
2541
+ * attached to nothing while a job runs, or idle with a live job another
2542
+ * tab/device started — → detach and re-run ``restore``, the same path a
2543
+ * conversation reopen takes, with all of its supersession and takeover
2544
+ * guards inherited.
2545
+ *
2546
+ * Skipped while a restore is already in flight (it is converging on server
2547
+ * truth by itself) and while another resync holds the flag — see
2548
+ * ``_resyncing``.
2549
+ */
2550
+ async resync() {
2551
+ const conversationId = this._activeConversationId;
2552
+ if (!conversationId) return;
2553
+ if (this._state === "restoring" || this._resyncing) return;
2554
+ this._resyncing = true;
2555
+ const gen = this.generation;
2556
+ const turn = this.turnCounter;
2557
+ try {
2558
+ let activeJobId = null;
2559
+ try {
2560
+ activeJobId = (await this.session.client.getActiveJob(conversationId)).jobId;
2561
+ } catch {
2562
+ return;
2563
+ }
2564
+ if (gen !== this.generation || this.turnStarted(turn)) return;
2565
+ const attached = this._state === "streaming" || this.session.isStreaming;
2566
+ if (activeJobId === null && !attached) return;
2567
+ if (activeJobId !== null && this.session.isStreaming && this.session.currentJobId === activeJobId) {
2568
+ return;
2569
+ }
2570
+ this.session.detach();
2571
+ this.session.currentJobId = null;
2572
+ this._state = "idle";
2573
+ try {
2574
+ await this.restore(conversationId, gen);
2575
+ } catch {
2576
+ }
2577
+ } finally {
2578
+ this._resyncing = false;
2579
+ const restoring = "restoring";
2580
+ if (gen === this.generation && this._state === restoring) {
2581
+ this.settleIdle();
2582
+ }
2583
+ }
2584
+ }
2511
2585
  // ── Create / rename / delete conversation ─────────────────────
2512
2586
  /**
2513
2587
  * Create a conversation and make it active.