@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.
- package/dist/error-codes.d.ts +11 -0
- package/dist/error-codes.d.ts.map +1 -1
- package/dist/error-codes.js +11 -0
- package/dist/saturation-retry.d.ts +20 -0
- package/dist/saturation-retry.d.ts.map +1 -1
- package/dist/saturation-retry.js +24 -18
- package/dist/transport.d.ts.map +1 -1
- package/dist/transport.js +7 -1
- package/dist/types.d.ts +12 -4
- package/dist/types.d.ts.map +1 -1
- package/dist/useAgentInvoke.d.ts +9 -0
- package/dist/useAgentInvoke.d.ts.map +1 -1
- package/dist/useAgentInvoke.js +53 -5
- package/dist/web-adapters.d.ts +11 -0
- package/dist/web-adapters.d.ts.map +1 -1
- package/dist/web-adapters.js +4 -0
- package/package.json +2 -2
- package/src/error-codes.ts +11 -0
- package/src/saturation-retry.ts +39 -16
- package/src/transport.ts +7 -1
- package/src/types.ts +12 -4
- package/src/useAgentInvoke.ts +59 -6
- package/src/web-adapters.ts +17 -1
package/dist/error-codes.d.ts
CHANGED
|
@@ -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"}
|
package/dist/error-codes.js
CHANGED
|
@@ -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;
|
|
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"}
|
package/dist/saturation-retry.js
CHANGED
|
@@ -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
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
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;
|
package/dist/transport.d.ts.map
CHANGED
|
@@ -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,
|
|
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, {
|
|
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
|
|
120
|
-
*
|
|
121
|
-
*
|
|
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`.
|
package/dist/types.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAEH,OAAO,KAAK,EAAE,oBAAoB,EAAE,cAAc,EAAE,SAAS,EAAE,MAAM,sBAAsB,CAAC;AAE5F,uDAAuD;AACvD,MAAM,WAAW,YAAY;IAC3B,IAAI,EAAE,MAAM,GAAG,WAAW,CAAC;IAC3B,IAAI,EAAE,MAAM,CAAC;IACb;;;;;OAKG;IACH,eAAe,CAAC,EAAE,MAAM,CAAC;CAC1B;AAED;;;;;;;;;;;GAWG;AACH,MAAM,WAAW,kBAAkB;IACjC,KAAK,EAAE,MAAM,CAAC;IACd,SAAS,EAAE,MAAM,GAAG,YAAY,CAAC;CAClC;AAED;;;;;;;GAOG;AACH,MAAM,WAAW,WAAW;IAC1B,GAAG,EAAE,MAAM,CAAC;IACZ,EAAE,EAAE,MAAM,CAAC;IACX,YAAY,EAAE,SAAS,CAAC;CACzB;AAED;;;GAGG;AACH,MAAM,WAAW,aAAa;IAC5B,IAAI,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAAC;IAC1D,IAAI,CAAC,GAAG,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CAC3D;AAED,0EAA0E;AAC1E,MAAM,MAAM,UAAU,GAAG,MAAM,MAAM,CAAC;AAEtC,kDAAkD;AAClD,MAAM,WAAW,aAAa;IAC5B,iFAAiF;IACjF,GAAG,EAAE,MAAM,CAAC;IACZ,iFAAiF;IACjF,IAAI,EAAE,OAAO,CAAC;IACd,mCAAmC;IACnC,MAAM,EAAE,WAAW,CAAC;CACrB;AAED;;;;;GAKG;AACH,MAAM,MAAM,eAAe,GAAG,CAAC,GAAG,EAAE,aAAa,KAAK,aAAa,CAAC,MAAM,CAAC,CAAC;AAE5E;;;;;;;;GAQG;AACH,MAAM,MAAM,iBAAiB,GACzB;IAAE,QAAQ,EAAE,YAAY,EAAE,CAAC;IAAC,KAAK,CAAC,EAAE,WAAW,EAAE,CAAA;CAAE,GACnD;IAAE,IAAI,EAAE,IAAI,CAAA;CAAE,CAAC;AAEnB;;;;;GAKG;AACH,MAAM,WAAW,yBAAyB;IACxC,sFAAsF;IACtF,IAAI,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,iBAAiB,CAAC,CAAC;CACpD;AAED,mEAAmE;AACnE,MAAM,WAAW,mBAAmB;IAClC,OAAO,EAAE,aAAa,CAAC;IACvB,UAAU,EAAE,UAAU,CAAC;IACvB,SAAS,EAAE,eAAe,CAAC;IAC3B,yGAAyG;IACzG,OAAO,CAAC,EAAE,yBAAyB,CAAC;CACrC;AAED,iGAAiG;AACjG,MAAM,WAAW,oBAAoB;IACnC
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"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"}
|
package/dist/useAgentInvoke.d.ts
CHANGED
|
@@ -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;
|
|
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"}
|
package/dist/useAgentInvoke.js
CHANGED
|
@@ -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 = {
|
|
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
|
-
|
|
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();
|
package/dist/web-adapters.d.ts
CHANGED
|
@@ -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":"AAAA;;;;;;;GAOG;AACH,OAAO,EAIL,KAAK,iBAAiB,EAEtB,KAAK,iBAAiB,EACtB,KAAK,eAAe,EACrB,MAAM,sBAAsB,CAAC;AAC9B,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,sBAAsB,CAAC;AACzD,OAAO,KAAK,EAAE,mBAAmB,EAAmB,aAAa,EAAE,MAAM,YAAY,CAAC;
|
|
1
|
+
{"version":3,"file":"web-adapters.d.ts","sourceRoot":"","sources":["../src/web-adapters.ts"],"names":[],"mappings":"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"}
|
package/dist/web-adapters.js
CHANGED
|
@@ -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.
|
|
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.
|
|
38
|
+
"@guuey/mcp-apps-host": "0.14.2"
|
|
39
39
|
},
|
|
40
40
|
"peerDependencies": {
|
|
41
41
|
"react": ">=18"
|
package/src/error-codes.ts
CHANGED
|
@@ -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}. */
|
package/src/saturation-retry.ts
CHANGED
|
@@ -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
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
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, {
|
|
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
|
|
129
|
-
*
|
|
130
|
-
*
|
|
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`.
|
package/src/useAgentInvoke.ts
CHANGED
|
@@ -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 = {
|
|
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
|
-
|
|
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();
|
package/src/web-adapters.ts
CHANGED
|
@@ -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 {
|
|
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
|
|