@guuey/agent-client 0.14.0 → 0.14.2

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.
@@ -87,6 +87,17 @@ export declare const CLIENT_ERROR_CODES: {
87
87
  * the reply if the backend completes later.
88
88
  */
89
89
  readonly STREAM_STALLED: "STREAM_STALLED";
90
+ /**
91
+ * A RESUMED thread's history read was refused (401 after the one
92
+ * forceRefresh retry) — the persisted threadId exists but the CURRENT
93
+ * identity cannot read it (guuey#413: the identity-drift face; an
94
+ * expired-and-unrefreshable session is the benign sibling). Surfaced
95
+ * LOUDLY instead of the silent fresh-looking boot: the transcript the
96
+ * user expects exists and cannot be shown, which is an ERROR, not an
97
+ * empty chat. Sends still work (the pod may fork a fresh thread) — the
98
+ * error names why the history is missing.
99
+ */
100
+ readonly THREAD_HISTORY_UNAVAILABLE: "THREAD_HISTORY_UNAVAILABLE";
90
101
  };
91
102
  /** One of this SDK's client-originated codes — see {@link CLIENT_ERROR_CODES}. */
92
103
  export type ClientErrorCode = (typeof CLIENT_ERROR_CODES)[keyof typeof CLIENT_ERROR_CODES];
@@ -1 +1 @@
1
- {"version":3,"file":"error-codes.d.ts","sourceRoot":"","sources":["../src/error-codes.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,eAAO,MAAM,iBAAiB;IAC5B,yDAAyD;;IAEzD,gDAAgD;;IAEhD,8DAA8D;;IAE9D;;;;;OAKG;;IAEH,6EAA6E;;IAE7E,qDAAqD;;IAErD;;;;OAIG;;IAEH;;;;;OAKG;;IAEH,oFAAoF;;IAEpF;;;;OAIG;;IAEH,qDAAqD;;IAErD,iEAAiE;;IAEjE,gCAAgC;;CAExB,CAAC;AAEX,mEAAmE;AACnE,MAAM,MAAM,cAAc,GAAG,CAAC,OAAO,iBAAiB,CAAC,CAAC,MAAM,OAAO,iBAAiB,CAAC,CAAC;AAExF;;;;;;;;;;GAUG;AACH,eAAO,MAAM,kBAAkB;IAC7B;;;;;OAKG;;CAEK,CAAC;AAEX,kFAAkF;AAClF,MAAM,MAAM,eAAe,GAAG,CAAC,OAAO,kBAAkB,CAAC,CAAC,MAAM,OAAO,kBAAkB,CAAC,CAAC"}
1
+ {"version":3,"file":"error-codes.d.ts","sourceRoot":"","sources":["../src/error-codes.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,eAAO,MAAM,iBAAiB;IAC5B,yDAAyD;;IAEzD,gDAAgD;;IAEhD,8DAA8D;;IAE9D;;;;;OAKG;;IAEH,6EAA6E;;IAE7E,qDAAqD;;IAErD;;;;OAIG;;IAEH;;;;;OAKG;;IAEH,oFAAoF;;IAEpF;;;;OAIG;;IAEH,qDAAqD;;IAErD,iEAAiE;;IAEjE,gCAAgC;;CAExB,CAAC;AAEX,mEAAmE;AACnE,MAAM,MAAM,cAAc,GAAG,CAAC,OAAO,iBAAiB,CAAC,CAAC,MAAM,OAAO,iBAAiB,CAAC,CAAC;AAExF;;;;;;;;;;GAUG;AACH,eAAO,MAAM,kBAAkB;IAC7B;;;;;OAKG;;IAEH;;;;;;;;;OASG;;CAEK,CAAC;AAEX,kFAAkF;AAClF,MAAM,MAAM,eAAe,GAAG,CAAC,OAAO,kBAAkB,CAAC,CAAC,MAAM,OAAO,kBAAkB,CAAC,CAAC"}
@@ -85,4 +85,15 @@ export const CLIENT_ERROR_CODES = {
85
85
  * the reply if the backend completes later.
86
86
  */
87
87
  STREAM_STALLED: "STREAM_STALLED",
88
+ /**
89
+ * A RESUMED thread's history read was refused (401 after the one
90
+ * forceRefresh retry) — the persisted threadId exists but the CURRENT
91
+ * identity cannot read it (guuey#413: the identity-drift face; an
92
+ * expired-and-unrefreshable session is the benign sibling). Surfaced
93
+ * LOUDLY instead of the silent fresh-looking boot: the transcript the
94
+ * user expects exists and cannot be shown, which is an ERROR, not an
95
+ * empty chat. Sends still work (the pod may fork a fresh thread) — the
96
+ * error names why the history is missing.
97
+ */
98
+ THREAD_HISTORY_UNAVAILABLE: "THREAD_HISTORY_UNAVAILABLE",
88
99
  };
@@ -19,6 +19,26 @@ export interface SaturationRetryOptions {
19
19
  * real 15s timer; production uses an abort-aware `setTimeout`.
20
20
  */
21
21
  sleep?: (ms: number, signal: AbortSignal) => Promise<void>;
22
+ /**
23
+ * Total send attempts on a saturated pod (guuey#406). Default 1 retry
24
+ * (2 attempts) — the historical behavior; capped at 5. A capacity-1 pod
25
+ * (demo fixtures, xs plans) refuses the SECOND simultaneous visitor, so
26
+ * end-user surfaces budget higher and pair it with `onSaturationWait` so
27
+ * the wait is a visible busy state, never a silent hang or a generic
28
+ * error boundary (the 2026-08-24 standalone incident).
29
+ */
30
+ attempts?: number;
31
+ /**
32
+ * Fired before each saturation wait — the surface's hook for an honest
33
+ * "the agent is helping someone else" state. Never fired for other error
34
+ * classes; the turn stays `connecting` throughout (the hook's state
35
+ * machine deliberately has no `retrying` status).
36
+ */
37
+ onSaturationWait?: (info: {
38
+ attempt: number;
39
+ totalAttempts: number;
40
+ waitMs: number;
41
+ }) => void;
22
42
  }
23
43
  /**
24
44
  * Wrap an invoke transport with ONE automatic retry on a saturated pod.
@@ -1 +1 @@
1
- {"version":3,"file":"saturation-retry.d.ts","sourceRoot":"","sources":["../src/saturation-retry.ts"],"names":[],"mappings":"AA0BA,OAAO,KAAK,EAAiB,eAAe,EAAE,MAAM,YAAY,CAAC;AAiBjE;;;;;;;;;;;GAWG;AACH,wBAAgB,sBAAsB,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI,GAAG,MAAM,GAAG,SAAS,CAMhF;AA4BD,+CAA+C;AAC/C,MAAM,WAAW,sBAAsB;IACrC;;;OAGG;IACH,KAAK,CAAC,EAAE,CAAC,EAAE,EAAE,MAAM,EAAE,MAAM,EAAE,WAAW,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;CAC5D;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAmCG;AACH,wBAAgB,mBAAmB,CACjC,SAAS,EAAE,eAAe,EAC1B,OAAO,GAAE,sBAA2B,GACnC,eAAe,CAqBjB;AAED,8CAA8C;AAC9C,MAAM,WAAW,qBAAqB;IACpC;;;;;;OAMG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB;;;OAGG;IACH,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,6EAA6E;IAC7E,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,4EAA4E;IAC5E,KAAK,CAAC,EAAE,CAAC,EAAE,EAAE,MAAM,EAAE,MAAM,EAAE,WAAW,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;CAC5D;AAkBD;;;;;;;;;;;;;;;;;;;GAmBG;AACH,wBAAgB,kBAAkB,CAChC,SAAS,EAAE,eAAe,EAC1B,OAAO,GAAE,qBAA0B,GAClC,eAAe,CA0BjB"}
1
+ {"version":3,"file":"saturation-retry.d.ts","sourceRoot":"","sources":["../src/saturation-retry.ts"],"names":[],"mappings":"AA0BA,OAAO,KAAK,EAAiB,eAAe,EAAE,MAAM,YAAY,CAAC;AAiBjE;;;;;;;;;;;GAWG;AACH,wBAAgB,sBAAsB,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI,GAAG,MAAM,GAAG,SAAS,CAMhF;AA4BD,+CAA+C;AAC/C,MAAM,WAAW,sBAAsB;IACrC;;;OAGG;IACH,KAAK,CAAC,EAAE,CAAC,EAAE,EAAE,MAAM,EAAE,MAAM,EAAE,WAAW,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IAC3D;;;;;;;OAOG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB;;;;;OAKG;IACH,gBAAgB,CAAC,EAAE,CAAC,IAAI,EAAE;QAAE,OAAO,EAAE,MAAM,CAAC;QAAC,aAAa,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAA;KAAE,KAAK,IAAI,CAAC;CAC/F;AAKD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAmCG;AACH,wBAAgB,mBAAmB,CACjC,SAAS,EAAE,eAAe,EAC1B,OAAO,GAAE,sBAA2B,GACnC,eAAe,CAyBjB;AAED,8CAA8C;AAC9C,MAAM,WAAW,qBAAqB;IACpC;;;;;;OAMG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB;;;OAGG;IACH,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,6EAA6E;IAC7E,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,4EAA4E;IAC5E,KAAK,CAAC,EAAE,CAAC,EAAE,EAAE,MAAM,EAAE,MAAM,EAAE,WAAW,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;CAC5D;AAkBD;;;;;;;;;;;;;;;;;;;GAmBG;AACH,wBAAgB,kBAAkB,CAChC,SAAS,EAAE,eAAe,EAC1B,OAAO,GAAE,qBAA0B,GAClC,eAAe,CA0BjB"}
@@ -82,6 +82,8 @@ function delay(ms, signal) {
82
82
  signal.addEventListener("abort", finish, { once: true });
83
83
  });
84
84
  }
85
+ /** Hard ceiling on {@link SaturationRetryOptions.attempts} retries. */
86
+ const MAX_SATURATION_RETRIES = 5;
85
87
  /**
86
88
  * Wrap an invoke transport with ONE automatic retry on a saturated pod.
87
89
  *
@@ -119,27 +121,31 @@ function delay(ms, signal) {
119
121
  * replaying a token that may have expired during the wait.
120
122
  */
121
123
  export function withSaturationRetry(transport, options = {}) {
124
+ const retries = Math.min(Math.max(options.attempts ?? 1, 1), MAX_SATURATION_RETRIES);
122
125
  return async function* retrying(req) {
123
- let yielded = false;
124
- try {
125
- for await (const chunk of transport(req)) {
126
- yielded = true;
127
- yield chunk;
126
+ for (let attempt = 1;; attempt += 1) {
127
+ let yielded = false;
128
+ try {
129
+ for await (const chunk of transport(req)) {
130
+ yielded = true;
131
+ yield chunk;
132
+ }
133
+ return;
134
+ }
135
+ catch (err) {
136
+ const saturated = err instanceof AgentResponseError && err.code === AGENT_ERROR_CODES.POD_SATURATED;
137
+ if (!saturated || yielded || attempt > retries)
138
+ throw err;
139
+ const waitMs = saturationDelayMs(err.retryAfterSeconds);
140
+ options.onSaturationWait?.({ attempt, totalAttempts: retries + 1, waitMs });
141
+ await (options.sleep ?? delay)(waitMs, req.signal);
142
+ // Aborted mid-wait: the user is done with this turn. Surface the
143
+ // refusal that caused the wait rather than spending a request that
144
+ // `fetch` would reject on the signal anyway.
145
+ if (req.signal.aborted)
146
+ throw err;
128
147
  }
129
- return;
130
- }
131
- catch (err) {
132
- const saturated = err instanceof AgentResponseError && err.code === AGENT_ERROR_CODES.POD_SATURATED;
133
- if (!saturated || yielded)
134
- throw err;
135
- await (options.sleep ?? delay)(saturationDelayMs(err.retryAfterSeconds), req.signal);
136
- // Aborted mid-wait: the user is done with this turn. Surface the refusal
137
- // that caused the wait rather than spending a request that `fetch` would
138
- // reject on the signal anyway.
139
- if (req.signal.aborted)
140
- throw err;
141
148
  }
142
- yield* transport(req);
143
149
  };
144
150
  }
145
151
  const COLD_START_DEFAULT_ATTEMPTS = 3;
@@ -1 +1 @@
1
- {"version":3,"file":"transport.d.ts","sourceRoot":"","sources":["../src/transport.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AACH,OAAO,KAAK,EAAE,aAAa,EAAE,eAAe,EAAE,MAAM,YAAY,CAAC;AAEjE,OAAO,EAIL,KAAK,qBAAqB,EAC1B,KAAK,sBAAsB,EAC5B,MAAM,uBAAuB,CAAC;AAE/B;;;;;;;;;;GAUG;AACH,eAAO,MAAM,YAAY,kBAAkB,CAAC;AAgB5C;;;;;;GAMG;AACH,wBAAgB,mBAAmB,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,GAAG,MAAM,GAAG,IAAI,CAEpF;AAoHD,gDAAgD;AAChD,MAAM,WAAW,2BAA4B,SAAQ,sBAAsB;IACzE;;;;;;;OAOG;IACH,cAAc,CAAC,EAAE,qBAAqB,GAAG,KAAK,CAAC;IAC/C;;;;;;;;;;;;;OAaG;IACH,SAAS,CAAC,EAAE,MAAM,MAAM,GAAG,IAAI,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAAC;CAC1D;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,oBAAoB,CAClC,GAAG,EAAE,aAAa,EAClB,WAAW,CAAC,EAAE,MAAM,GAAG,IAAI,EAC3B,WAAW,CAAC,EAAE,MAAM,GAAG,IAAI,EAC3B,OAAO,GAAE,2BAAgC,GACxC,aAAa,CAAC,MAAM,CAAC,CAcvB;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,oBAAoB,CAClC,SAAS,EAAE,eAAe,EAC1B,OAAO,EAAE,MAAM,IAAI,GAClB,eAAe,CAOjB"}
1
+ {"version":3,"file":"transport.d.ts","sourceRoot":"","sources":["../src/transport.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AACH,OAAO,KAAK,EAAE,aAAa,EAAE,eAAe,EAAE,MAAM,YAAY,CAAC;AAEjE,OAAO,EAIL,KAAK,qBAAqB,EAC1B,KAAK,sBAAsB,EAC5B,MAAM,uBAAuB,CAAC;AAE/B;;;;;;;;;;GAUG;AACH,eAAO,MAAM,YAAY,kBAAkB,CAAC;AAgB5C;;;;;;GAMG;AACH,wBAAgB,mBAAmB,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,GAAG,MAAM,GAAG,IAAI,CAEpF;AAoHD,gDAAgD;AAChD,MAAM,WAAW,2BAA4B,SAAQ,sBAAsB;IACzE;;;;;;;OAOG;IACH,cAAc,CAAC,EAAE,qBAAqB,GAAG,KAAK,CAAC;IAC/C;;;;;;;;;;;;;OAaG;IACH,SAAS,CAAC,EAAE,MAAM,MAAM,GAAG,IAAI,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAAC;CAC1D;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,oBAAoB,CAClC,GAAG,EAAE,aAAa,EAClB,WAAW,CAAC,EAAE,MAAM,GAAG,IAAI,EAC3B,WAAW,CAAC,EAAE,MAAM,GAAG,IAAI,EAC3B,OAAO,GAAE,2BAAgC,GACxC,aAAa,CAAC,MAAM,CAAC,CAoBvB;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,oBAAoB,CAClC,SAAS,EAAE,eAAe,EAC1B,OAAO,EAAE,MAAM,IAAI,GAClB,eAAe,CAOjB"}
package/dist/transport.js CHANGED
@@ -160,7 +160,13 @@ export function fetchStreamTransport(req, accessToken, guestSecret, options = {}
160
160
  const bearer = getBearer ? await getBearer() : accessToken;
161
161
  yield* streamInvokeOnce(attempt, bearer, guestSecret);
162
162
  };
163
- const saturated = withSaturationRetry(once, { sleep: options.sleep });
163
+ const saturated = withSaturationRetry(once, {
164
+ sleep: options.sleep,
165
+ ...(options.attempts !== undefined ? { attempts: options.attempts } : {}),
166
+ ...(options.onSaturationWait !== undefined
167
+ ? { onSaturationWait: options.onSaturationWait }
168
+ : {}),
169
+ });
164
170
  if (options.coldStartRetry === false)
165
171
  return saturated(req);
166
172
  return withColdStartRetry(saturated, {
package/dist/types.d.ts CHANGED
@@ -116,12 +116,20 @@ export interface AgentInvokeAdapters {
116
116
  /** Tuning for the guuey#192 stall watchdog — see {@link UseAgentInvokeOptions.stallRecovery}. */
117
117
  export interface StallRecoveryOptions {
118
118
  /**
119
- * Byte-inactivity window (ms) before a history probe fires. Armed only
120
- * AFTER the first byte of the turn pre-first-byte silence is a legitimate
121
- * cold start and never triggers. Every received chunk resets it.
122
- * Default 25000.
119
+ * Byte-inactivity window (ms) before a history probe fires. Armed with
120
+ * {@link preFirstByteWindowMs} at turn start, re-armed with THIS window by
121
+ * every received chunk. Default 25000.
123
122
  */
124
123
  windowMs?: number;
124
+ /**
125
+ * The PRE-first-byte window (ms) — guuey#409: a turn that never receives
126
+ * ANY byte (an invoke-rail death upstream of the pod, or an in-pod
127
+ * pre-spawn hang) previously showed "Thinking…" forever with no error
128
+ * item; this window bounds it. Much longer than `windowMs` on purpose —
129
+ * the transport's cold-start retries legitimately spend up to ~90s before
130
+ * the first byte. Default 120000.
131
+ */
132
+ preFirstByteWindowMs?: number;
125
133
  /**
126
134
  * Fruitless probes (history shows the turn still in flight, or no probe is
127
135
  * possible) before the turn fails with `CLIENT_ERROR_CODES.STREAM_STALLED`.
@@ -1 +1 @@
1
- {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"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"}
@@ -28,6 +28,15 @@ export declare function applyHistoryResult(result: HistoryLoadResult, currentMes
28
28
  export declare const STALL_RECOVERY_DEFAULTS: {
29
29
  readonly windowMs: 25000;
30
30
  readonly probeAttempts: 4;
31
+ /**
32
+ * guuey#409: the PRE-first-byte watchdog window. The #192 clock arms only
33
+ * on the first chunk (so a silent cold start never trips it) — which left
34
+ * a turn that never receives ANY byte with no watchdog at all: the
35
+ * eternal-"Thinking…" face of the 2026-08-22 invoke-rail turn death.
36
+ * Deliberately much longer than `windowMs`: the transport's cold-start
37
+ * retries legitimately spend up to ~90s before the first byte.
38
+ */
39
+ readonly preFirstByteWindowMs: 120000;
31
40
  };
32
41
  /**
33
42
  * Pure decision seam for the guuey#192 stall probe: does a freshly-loaded
@@ -1 +1 @@
1
- {"version":3,"file":"useAgentInvoke.d.ts","sourceRoot":"","sources":["../src/useAgentInvoke.ts"],"names":[],"mappings":"AAwBA,OAAO,EAAW,KAAK,oBAAoB,EAAuB,MAAM,sBAAsB,CAAC;AAK/F,OAAO,KAAK,EAGV,YAAY,EAEZ,iBAAiB,EAGjB,qBAAqB,EACrB,oBAAoB,EACrB,MAAM,YAAY,CAAC;AAMpB;;;;;GAKG;AACH,eAAO,MAAM,qCAAqC,EAAE,oBAEnD,CAAC;AAEF,yEAAyE;AACzE,MAAM,MAAM,kBAAkB,GAC1B;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,YAAY,EAAE,CAAA;CAAE,GAC1C;IAAE,IAAI,EAAE,MAAM,CAAA;CAAE,GAChB;IAAE,IAAI,EAAE,OAAO,CAAA;CAAE,CAAC;AAEtB;;;;;;GAMG;AACH,wBAAgB,kBAAkB,CAChC,MAAM,EAAE,iBAAiB,EACzB,eAAe,EAAE,YAAY,EAAE,GAC9B,kBAAkB,CAIpB;AAED,uFAAuF;AACvF,eAAO,MAAM,uBAAuB;;;CAAkD,CAAC;AAYvF;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,wBAAgB,kBAAkB,CAChC,OAAO,EAAE,YAAY,EAAE,EACvB,cAAc,EAAE,MAAM,GACrB,OAAO,GAAG,WAAW,CAOvB;AAED,wBAAgB,cAAc,CAAC,IAAI,EAAE,qBAAqB,GAAG,oBAAoB,CA6dhF"}
1
+ {"version":3,"file":"useAgentInvoke.d.ts","sourceRoot":"","sources":["../src/useAgentInvoke.ts"],"names":[],"mappings":"AAwBA,OAAO,EAAW,KAAK,oBAAoB,EAAuB,MAAM,sBAAsB,CAAC;AAM/F,OAAO,KAAK,EAGV,YAAY,EAEZ,iBAAiB,EAGjB,qBAAqB,EACrB,oBAAoB,EACrB,MAAM,YAAY,CAAC;AAMpB;;;;;GAKG;AACH,eAAO,MAAM,qCAAqC,EAAE,oBAEnD,CAAC;AAEF,yEAAyE;AACzE,MAAM,MAAM,kBAAkB,GAC1B;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,YAAY,EAAE,CAAA;CAAE,GAC1C;IAAE,IAAI,EAAE,MAAM,CAAA;CAAE,GAChB;IAAE,IAAI,EAAE,OAAO,CAAA;CAAE,CAAC;AAEtB;;;;;;GAMG;AACH,wBAAgB,kBAAkB,CAChC,MAAM,EAAE,iBAAiB,EACzB,eAAe,EAAE,YAAY,EAAE,GAC9B,kBAAkB,CAIpB;AAED,uFAAuF;AACvF,eAAO,MAAM,uBAAuB;;;IAGlC;;;;;;;OAOG;;CAEK,CAAC;AAcX;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,wBAAgB,kBAAkB,CAChC,OAAO,EAAE,YAAY,EAAE,EACvB,cAAc,EAAE,MAAM,GACrB,OAAO,GAAG,WAAW,CAOvB;AAED,wBAAgB,cAAc,CAAC,IAAI,EAAE,qBAAqB,GAAG,oBAAoB,CAmgBhF"}
@@ -27,6 +27,7 @@ import { invokeTurn, toInvokeUrl } from "./invoke-turn.js";
27
27
  import { AgentResponseError } from "./errors.js";
28
28
  import { withActivityObserver } from "./transport.js";
29
29
  import { CLIENT_ERROR_CODES } from "./error-codes.js";
30
+ import { HistoryUnauthorizedError } from "./history.js";
30
31
  function threadStorageKey(appId) {
31
32
  return `guuey:thread:${appId ?? "default"}`;
32
33
  }
@@ -54,13 +55,26 @@ export function applyHistoryResult(result, currentMessages) {
54
55
  return { kind: "seed", messages: result.messages };
55
56
  }
56
57
  /** The guuey#192 stall watchdog's resolved tuning (see {@link stallProbeDecision}). */
57
- export const STALL_RECOVERY_DEFAULTS = { windowMs: 25_000, probeAttempts: 4 };
58
+ export const STALL_RECOVERY_DEFAULTS = {
59
+ windowMs: 25_000,
60
+ probeAttempts: 4,
61
+ /**
62
+ * guuey#409: the PRE-first-byte watchdog window. The #192 clock arms only
63
+ * on the first chunk (so a silent cold start never trips it) — which left
64
+ * a turn that never receives ANY byte with no watchdog at all: the
65
+ * eternal-"Thinking…" face of the 2026-08-22 invoke-rail turn death.
66
+ * Deliberately much longer than `windowMs`: the transport's cold-start
67
+ * retries legitimately spend up to ~90s before the first byte.
68
+ */
69
+ preFirstByteWindowMs: 120_000,
70
+ };
58
71
  function resolveStallRecovery(option) {
59
72
  if (option === false)
60
73
  return null;
61
74
  return {
62
75
  windowMs: option?.windowMs ?? STALL_RECOVERY_DEFAULTS.windowMs,
63
76
  probeAttempts: option?.probeAttempts ?? STALL_RECOVERY_DEFAULTS.probeAttempts,
77
+ preFirstByteWindowMs: option?.preFirstByteWindowMs ?? STALL_RECOVERY_DEFAULTS.preFirstByteWindowMs,
64
78
  };
65
79
  }
66
80
  /**
@@ -215,8 +229,27 @@ export function useAgentInvoke(opts) {
215
229
  try {
216
230
  result = await history.load(tid);
217
231
  }
218
- catch {
219
- return; // best-effort: offline / transient chat continues without history
232
+ catch (err) {
233
+ // guuey#413 fail-LOUD carve-out from the best-effort rule: an
234
+ // UNAUTHORIZED read on a RESUMED threadId means a transcript the
235
+ // user expects exists and cannot be shown — silently booting a
236
+ // fresh-looking empty chat hid the identity-drift outage for
237
+ // exactly the accounts holding old threads. Auth refusal surfaces
238
+ // as an error item; every other failure (offline, transient)
239
+ // keeps the best-effort swallow — chat continues without history.
240
+ if (!cancelled && err instanceof HistoryUnauthorizedError) {
241
+ // Same guard as the `gone` arm below: clear + fresh (functional)
242
+ // + the notice (loud). Same concurrency guard — never clobber a
243
+ // threadId a mid-flight send just established.
244
+ if (threadIdRef.current === tid) {
245
+ threadIdRef.current = null;
246
+ setThreadId(null);
247
+ void adaptersRef.current.storage.save(threadStorageKey(appId), "");
248
+ }
249
+ setError("Couldn't restore your previous conversation — started a new one. (This session was not authorized to read the old thread.)");
250
+ setErrorCode(CLIENT_ERROR_CODES.THREAD_HISTORY_UNAVAILABLE);
251
+ }
252
+ return;
220
253
  }
221
254
  if (cancelled)
222
255
  return;
@@ -230,6 +263,14 @@ export function useAgentInvoke(opts) {
230
263
  threadIdRef.current = null;
231
264
  setThreadId(null);
232
265
  void adaptersRef.current.storage.save(threadStorageKey(appId), "");
266
+ // guuey#413: the hydration guard's LOUD half. `gone` covers 403 as
267
+ // well as 404 (history.ts) — an owner-mismatch refusal on a drifted
268
+ // identity flowed through THIS arm as a silent fresh-looking boot,
269
+ // which is the exact outage face this guard exists to kill. The
270
+ // fresh mint stays (functional); the notice makes it honest. Loud
271
+ // AND functional, never one without the other.
272
+ setError("Couldn't restore your previous conversation — started a new one.");
273
+ setErrorCode(CLIENT_ERROR_CODES.THREAD_HISTORY_UNAVAILABLE);
233
274
  return;
234
275
  }
235
276
  // Single decision authority: `applyHistoryResult` runs INSIDE the
@@ -370,14 +411,21 @@ export function useAgentInvoke(opts) {
370
411
  stallTimer = null;
371
412
  }
372
413
  };
373
- const armStallTimer = () => {
414
+ const armStallTimer = (windowMs) => {
374
415
  if (!stall || turnEnded || controller.signal.aborted)
375
416
  return;
376
417
  clearStallTimer();
377
418
  stallTimer = setTimeout(() => {
378
419
  void onStallWindow();
379
- }, stall.windowMs);
420
+ }, windowMs ?? stall.windowMs);
380
421
  };
422
+ // guuey#409: arm BEFORE the first byte — a turn that never receives
423
+ // any chunk (rail death upstream of the pod, or an in-pod pre-spawn
424
+ // hang) otherwise shows "Thinking…" forever with no error item. The
425
+ // long window keeps silent cold starts un-tripped; expiry runs the
426
+ // SAME probe-then-adopt-or-STREAM_STALLED machinery as mid-stream
427
+ // stalls — one face fix for both mechanisms.
428
+ armStallTimer(stall?.preFirstByteWindowMs);
381
429
  const endTurnWith = (apply) => {
382
430
  turnEnded = true;
383
431
  clearStallTimer();
@@ -9,6 +9,7 @@
9
9
  import { type McpToolCallResult, type ResolvedViewMount, type UiActionRequest } from "@guuey/mcp-apps-host";
10
10
  import type { AgHitlAnswer } from "@silverprotocol/core";
11
11
  import type { AgentInvokeAdapters, ThreadIdStore } from "./types.js";
12
+ import type { SaturationRetryOptions } from "./saturation-retry.js";
12
13
  /** Persists the threadId in `window.localStorage` (synchronously). */
13
14
  export declare const localStorageThreadStore: ThreadIdStore;
14
15
  /** Crypto-strong client-message id, with a non-crypto fallback. */
@@ -19,6 +20,16 @@ export interface CreateWebAdaptersOptions {
19
20
  * omitted, no history adapter is installed and reloads start empty.
20
21
  */
21
22
  apiBaseUrl?: string;
23
+ /**
24
+ * Total send attempts on a saturated pod (guuey#406) — forwarded to
25
+ * {@link SaturationRetryOptions.attempts}. End-user surfaces facing
26
+ * capacity-1 pods (demo fixtures, xs plans) budget higher than the
27
+ * 2-attempt default and pair it with {@link onSaturationWait} so the
28
+ * wait is a visible busy state.
29
+ */
30
+ saturationAttempts?: number;
31
+ /** Forwarded to {@link SaturationRetryOptions.onSaturationWait}. */
32
+ onSaturationWait?: SaturationRetryOptions["onSaturationWait"];
22
33
  /**
23
34
  * Resolve the caller's Cognito access token (fresh), or `null` when signed
24
35
  * out. When a token is present the chat transport AND the history read
@@ -1 +1 @@
1
- {"version":3,"file":"web-adapters.d.ts","sourceRoot":"","sources":["../src/web-adapters.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"web-adapters.d.ts","sourceRoot":"","sources":["../src/web-adapters.ts"],"names":[],"mappings":"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"}
@@ -59,6 +59,10 @@ export function createWebAdapters(opts = {}) {
59
59
  // resolves inside its generator).
60
60
  yield* fetchStreamTransport(req, null, getGuestSecret ? getGuestSecret() : null, {
61
61
  getBearer: getAccessToken,
62
+ ...(opts.saturationAttempts !== undefined ? { attempts: opts.saturationAttempts } : {}),
63
+ ...(opts.onSaturationWait !== undefined
64
+ ? { onSaturationWait: opts.onSaturationWait }
65
+ : {}),
62
66
  });
63
67
  };
64
68
  const adapters = {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@guuey/agent-client",
3
- "version": "0.14.0",
3
+ "version": "0.14.2",
4
4
  "description": "Client SDK for Guuey's agent runtime: the `useAgentInvoke` React hook + pure SSE helpers that speak the /agent/invoke streaming contract, plus the paginated thread-history read plane. Host adapters (storage / id / transport) are injected, so it runs on web (Next) and React Native alike.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -35,7 +35,7 @@
35
35
  },
36
36
  "dependencies": {
37
37
  "@silverprotocol/core": "0.5.1",
38
- "@guuey/mcp-apps-host": "0.14.0"
38
+ "@guuey/mcp-apps-host": "0.14.2"
39
39
  },
40
40
  "peerDependencies": {
41
41
  "react": ">=18"
@@ -89,6 +89,17 @@ export const CLIENT_ERROR_CODES = {
89
89
  * the reply if the backend completes later.
90
90
  */
91
91
  STREAM_STALLED: "STREAM_STALLED",
92
+ /**
93
+ * A RESUMED thread's history read was refused (401 after the one
94
+ * forceRefresh retry) — the persisted threadId exists but the CURRENT
95
+ * identity cannot read it (guuey#413: the identity-drift face; an
96
+ * expired-and-unrefreshable session is the benign sibling). Surfaced
97
+ * LOUDLY instead of the silent fresh-looking boot: the transcript the
98
+ * user expects exists and cannot be shown, which is an ERROR, not an
99
+ * empty chat. Sends still work (the pod may fork a fresh thread) — the
100
+ * error names why the history is missing.
101
+ */
102
+ THREAD_HISTORY_UNAVAILABLE: "THREAD_HISTORY_UNAVAILABLE",
92
103
  } as const;
93
104
 
94
105
  /** One of this SDK's client-originated codes — see {@link CLIENT_ERROR_CODES}. */
@@ -94,8 +94,27 @@ export interface SaturationRetryOptions {
94
94
  * real 15s timer; production uses an abort-aware `setTimeout`.
95
95
  */
96
96
  sleep?: (ms: number, signal: AbortSignal) => Promise<void>;
97
+ /**
98
+ * Total send attempts on a saturated pod (guuey#406). Default 1 retry
99
+ * (2 attempts) — the historical behavior; capped at 5. A capacity-1 pod
100
+ * (demo fixtures, xs plans) refuses the SECOND simultaneous visitor, so
101
+ * end-user surfaces budget higher and pair it with `onSaturationWait` so
102
+ * the wait is a visible busy state, never a silent hang or a generic
103
+ * error boundary (the 2026-08-24 standalone incident).
104
+ */
105
+ attempts?: number;
106
+ /**
107
+ * Fired before each saturation wait — the surface's hook for an honest
108
+ * "the agent is helping someone else" state. Never fired for other error
109
+ * classes; the turn stays `connecting` throughout (the hook's state
110
+ * machine deliberately has no `retrying` status).
111
+ */
112
+ onSaturationWait?: (info: { attempt: number; totalAttempts: number; waitMs: number }) => void;
97
113
  }
98
114
 
115
+ /** Hard ceiling on {@link SaturationRetryOptions.attempts} retries. */
116
+ const MAX_SATURATION_RETRIES = 5;
117
+
99
118
  /**
100
119
  * Wrap an invoke transport with ONE automatic retry on a saturated pod.
101
120
  *
@@ -136,25 +155,29 @@ export function withSaturationRetry(
136
155
  transport: InvokeTransport,
137
156
  options: SaturationRetryOptions = {},
138
157
  ): InvokeTransport {
158
+ const retries = Math.min(Math.max(options.attempts ?? 1, 1), MAX_SATURATION_RETRIES);
139
159
  return async function* retrying(req: InvokeRequest): AsyncGenerator<string> {
140
- let yielded = false;
141
- try {
142
- for await (const chunk of transport(req)) {
143
- yielded = true;
144
- yield chunk;
160
+ for (let attempt = 1; ; attempt += 1) {
161
+ let yielded = false;
162
+ try {
163
+ for await (const chunk of transport(req)) {
164
+ yielded = true;
165
+ yield chunk;
166
+ }
167
+ return;
168
+ } catch (err) {
169
+ const saturated =
170
+ err instanceof AgentResponseError && err.code === AGENT_ERROR_CODES.POD_SATURATED;
171
+ if (!saturated || yielded || attempt > retries) throw err;
172
+ const waitMs = saturationDelayMs(err.retryAfterSeconds);
173
+ options.onSaturationWait?.({ attempt, totalAttempts: retries + 1, waitMs });
174
+ await (options.sleep ?? delay)(waitMs, req.signal);
175
+ // Aborted mid-wait: the user is done with this turn. Surface the
176
+ // refusal that caused the wait rather than spending a request that
177
+ // `fetch` would reject on the signal anyway.
178
+ if (req.signal.aborted) throw err;
145
179
  }
146
- return;
147
- } catch (err) {
148
- const saturated =
149
- err instanceof AgentResponseError && err.code === AGENT_ERROR_CODES.POD_SATURATED;
150
- if (!saturated || yielded) throw err;
151
- await (options.sleep ?? delay)(saturationDelayMs(err.retryAfterSeconds), req.signal);
152
- // Aborted mid-wait: the user is done with this turn. Surface the refusal
153
- // that caused the wait rather than spending a request that `fetch` would
154
- // reject on the signal anyway.
155
- if (req.signal.aborted) throw err;
156
180
  }
157
- yield* transport(req);
158
181
  };
159
182
  }
160
183
 
package/src/transport.ts CHANGED
@@ -228,7 +228,13 @@ export function fetchStreamTransport(
228
228
  const bearer = getBearer ? await getBearer() : accessToken;
229
229
  yield* streamInvokeOnce(attempt, bearer, guestSecret);
230
230
  };
231
- const saturated = withSaturationRetry(once, { sleep: options.sleep });
231
+ const saturated = withSaturationRetry(once, {
232
+ sleep: options.sleep,
233
+ ...(options.attempts !== undefined ? { attempts: options.attempts } : {}),
234
+ ...(options.onSaturationWait !== undefined
235
+ ? { onSaturationWait: options.onSaturationWait }
236
+ : {}),
237
+ });
232
238
  if (options.coldStartRetry === false) return saturated(req);
233
239
  return withColdStartRetry(saturated, {
234
240
  sleep: options.sleep,
package/src/types.ts CHANGED
@@ -125,12 +125,20 @@ export interface AgentInvokeAdapters {
125
125
  /** Tuning for the guuey#192 stall watchdog — see {@link UseAgentInvokeOptions.stallRecovery}. */
126
126
  export interface StallRecoveryOptions {
127
127
  /**
128
- * Byte-inactivity window (ms) before a history probe fires. Armed only
129
- * AFTER the first byte of the turn pre-first-byte silence is a legitimate
130
- * cold start and never triggers. Every received chunk resets it.
131
- * Default 25000.
128
+ * Byte-inactivity window (ms) before a history probe fires. Armed with
129
+ * {@link preFirstByteWindowMs} at turn start, re-armed with THIS window by
130
+ * every received chunk. Default 25000.
132
131
  */
133
132
  windowMs?: number;
133
+ /**
134
+ * The PRE-first-byte window (ms) — guuey#409: a turn that never receives
135
+ * ANY byte (an invoke-rail death upstream of the pod, or an in-pod
136
+ * pre-spawn hang) previously showed "Thinking…" forever with no error
137
+ * item; this window bounds it. Much longer than `windowMs` on purpose —
138
+ * the transport's cold-start retries legitimately spend up to ~90s before
139
+ * the first byte. Default 120000.
140
+ */
141
+ preFirstByteWindowMs?: number;
134
142
  /**
135
143
  * Fruitless probes (history shows the turn still in flight, or no probe is
136
144
  * possible) before the turn fails with `CLIENT_ERROR_CODES.STREAM_STALLED`.
@@ -27,6 +27,7 @@ import { invokeTurn, toInvokeUrl } from "./invoke-turn.js";
27
27
  import { AgentResponseError } from "./errors.js";
28
28
  import { withActivityObserver } from "./transport.js";
29
29
  import { CLIENT_ERROR_CODES } from "./error-codes.js";
30
+ import { HistoryUnauthorizedError } from "./history.js";
30
31
  import type {
31
32
  AgentInvokeAdapters,
32
33
  AgentInvokeStatus,
@@ -76,15 +77,29 @@ export function applyHistoryResult(
76
77
  }
77
78
 
78
79
  /** The guuey#192 stall watchdog's resolved tuning (see {@link stallProbeDecision}). */
79
- export const STALL_RECOVERY_DEFAULTS = { windowMs: 25_000, probeAttempts: 4 } as const;
80
+ export const STALL_RECOVERY_DEFAULTS = {
81
+ windowMs: 25_000,
82
+ probeAttempts: 4,
83
+ /**
84
+ * guuey#409: the PRE-first-byte watchdog window. The #192 clock arms only
85
+ * on the first chunk (so a silent cold start never trips it) — which left
86
+ * a turn that never receives ANY byte with no watchdog at all: the
87
+ * eternal-"Thinking…" face of the 2026-08-22 invoke-rail turn death.
88
+ * Deliberately much longer than `windowMs`: the transport's cold-start
89
+ * retries legitimately spend up to ~90s before the first byte.
90
+ */
91
+ preFirstByteWindowMs: 120_000,
92
+ } as const;
80
93
 
81
94
  function resolveStallRecovery(
82
95
  option: false | StallRecoveryOptions | undefined,
83
- ): { windowMs: number; probeAttempts: number } | null {
96
+ ): { windowMs: number; probeAttempts: number; preFirstByteWindowMs: number } | null {
84
97
  if (option === false) return null;
85
98
  return {
86
99
  windowMs: option?.windowMs ?? STALL_RECOVERY_DEFAULTS.windowMs,
87
100
  probeAttempts: option?.probeAttempts ?? STALL_RECOVERY_DEFAULTS.probeAttempts,
101
+ preFirstByteWindowMs:
102
+ option?.preFirstByteWindowMs ?? STALL_RECOVERY_DEFAULTS.preFirstByteWindowMs,
88
103
  };
89
104
  }
90
105
 
@@ -242,8 +257,29 @@ export function useAgentInvoke(opts: UseAgentInvokeOptions): UseAgentInvokeRetur
242
257
  let result: HistoryLoadResult;
243
258
  try {
244
259
  result = await history.load(tid);
245
- } catch {
246
- return; // best-effort: offline / transient chat continues without history
260
+ } catch (err) {
261
+ // guuey#413 fail-LOUD carve-out from the best-effort rule: an
262
+ // UNAUTHORIZED read on a RESUMED threadId means a transcript the
263
+ // user expects exists and cannot be shown — silently booting a
264
+ // fresh-looking empty chat hid the identity-drift outage for
265
+ // exactly the accounts holding old threads. Auth refusal surfaces
266
+ // as an error item; every other failure (offline, transient)
267
+ // keeps the best-effort swallow — chat continues without history.
268
+ if (!cancelled && err instanceof HistoryUnauthorizedError) {
269
+ // Same guard as the `gone` arm below: clear + fresh (functional)
270
+ // + the notice (loud). Same concurrency guard — never clobber a
271
+ // threadId a mid-flight send just established.
272
+ if (threadIdRef.current === tid) {
273
+ threadIdRef.current = null;
274
+ setThreadId(null);
275
+ void adaptersRef.current.storage.save(threadStorageKey(appId), "");
276
+ }
277
+ setError(
278
+ "Couldn't restore your previous conversation — started a new one. (This session was not authorized to read the old thread.)",
279
+ );
280
+ setErrorCode(CLIENT_ERROR_CODES.THREAD_HISTORY_UNAVAILABLE);
281
+ }
282
+ return;
247
283
  }
248
284
  if (cancelled) return;
249
285
  if ("gone" in result) {
@@ -255,6 +291,16 @@ export function useAgentInvoke(opts: UseAgentInvokeOptions): UseAgentInvokeRetur
255
291
  threadIdRef.current = null;
256
292
  setThreadId(null);
257
293
  void adaptersRef.current.storage.save(threadStorageKey(appId), "");
294
+ // guuey#413: the hydration guard's LOUD half. `gone` covers 403 as
295
+ // well as 404 (history.ts) — an owner-mismatch refusal on a drifted
296
+ // identity flowed through THIS arm as a silent fresh-looking boot,
297
+ // which is the exact outage face this guard exists to kill. The
298
+ // fresh mint stays (functional); the notice makes it honest. Loud
299
+ // AND functional, never one without the other.
300
+ setError(
301
+ "Couldn't restore your previous conversation — started a new one.",
302
+ );
303
+ setErrorCode(CLIENT_ERROR_CODES.THREAD_HISTORY_UNAVAILABLE);
258
304
  return;
259
305
  }
260
306
  // Single decision authority: `applyHistoryResult` runs INSIDE the
@@ -399,13 +445,20 @@ export function useAgentInvoke(opts: UseAgentInvokeOptions): UseAgentInvokeRetur
399
445
  stallTimer = null;
400
446
  }
401
447
  };
402
- const armStallTimer = (): void => {
448
+ const armStallTimer = (windowMs?: number): void => {
403
449
  if (!stall || turnEnded || controller.signal.aborted) return;
404
450
  clearStallTimer();
405
451
  stallTimer = setTimeout(() => {
406
452
  void onStallWindow();
407
- }, stall.windowMs);
453
+ }, windowMs ?? stall.windowMs);
408
454
  };
455
+ // guuey#409: arm BEFORE the first byte — a turn that never receives
456
+ // any chunk (rail death upstream of the pod, or an in-pod pre-spawn
457
+ // hang) otherwise shows "Thinking…" forever with no error item. The
458
+ // long window keeps silent cold starts un-tripped; expiry runs the
459
+ // SAME probe-then-adopt-or-STREAM_STALLED machinery as mid-stream
460
+ // stalls — one face fix for both mechanisms.
461
+ armStallTimer(stall?.preFirstByteWindowMs);
409
462
  const endTurnWith = (apply: () => void): void => {
410
463
  turnEnded = true;
411
464
  clearStallTimer();
@@ -18,7 +18,9 @@ import {
18
18
  import type { AgHitlAnswer } from "@silverprotocol/core";
19
19
  import type { AgentInvokeAdapters, InvokeTransport, ThreadIdStore } from "./types.js";
20
20
  import { fetchThreadHistory, HistoryUnauthorizedError } from "./history.js";
21
- import { fetchStreamTransport, sendableGuestSecret, GUEST_HEADER } from "./transport.js";
21
+ import {
22
+ fetchStreamTransport, sendableGuestSecret, GUEST_HEADER } from "./transport.js";
23
+ import type { SaturationRetryOptions } from "./saturation-retry.js";
22
24
  import { toInvokeUrl } from "./invoke-turn.js";
23
25
 
24
26
  /** Persists the threadId in `window.localStorage` (synchronously). */
@@ -55,6 +57,16 @@ export interface CreateWebAdaptersOptions {
55
57
  * omitted, no history adapter is installed and reloads start empty.
56
58
  */
57
59
  apiBaseUrl?: string;
60
+ /**
61
+ * Total send attempts on a saturated pod (guuey#406) — forwarded to
62
+ * {@link SaturationRetryOptions.attempts}. End-user surfaces facing
63
+ * capacity-1 pods (demo fixtures, xs plans) budget higher than the
64
+ * 2-attempt default and pair it with {@link onSaturationWait} so the
65
+ * wait is a visible busy state.
66
+ */
67
+ saturationAttempts?: number;
68
+ /** Forwarded to {@link SaturationRetryOptions.onSaturationWait}. */
69
+ onSaturationWait?: SaturationRetryOptions["onSaturationWait"];
58
70
  /**
59
71
  * Resolve the caller's Cognito access token (fresh), or `null` when signed
60
72
  * out. When a token is present the chat transport AND the history read
@@ -139,6 +151,10 @@ export function createWebAdapters(
139
151
  // resolves inside its generator).
140
152
  yield* fetchStreamTransport(req, null, getGuestSecret ? getGuestSecret() : null, {
141
153
  getBearer: getAccessToken,
154
+ ...(opts.saturationAttempts !== undefined ? { attempts: opts.saturationAttempts } : {}),
155
+ ...(opts.onSaturationWait !== undefined
156
+ ? { onSaturationWait: opts.onSaturationWait }
157
+ : {}),
142
158
  });
143
159
  };
144
160