@guuey/agent-client 0.14.0 → 0.14.1
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/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 +23 -3
- 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/saturation-retry.ts +39 -16
- package/src/transport.ts +7 -1
- package/src/types.ts +12 -4
- package/src/useAgentInvoke.ts +25 -4
- package/src/web-adapters.ts +17 -1
|
@@ -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;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;;;
|
|
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;;;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,CAoehF"}
|
package/dist/useAgentInvoke.js
CHANGED
|
@@ -54,13 +54,26 @@ export function applyHistoryResult(result, currentMessages) {
|
|
|
54
54
|
return { kind: "seed", messages: result.messages };
|
|
55
55
|
}
|
|
56
56
|
/** The guuey#192 stall watchdog's resolved tuning (see {@link stallProbeDecision}). */
|
|
57
|
-
export const STALL_RECOVERY_DEFAULTS = {
|
|
57
|
+
export const STALL_RECOVERY_DEFAULTS = {
|
|
58
|
+
windowMs: 25_000,
|
|
59
|
+
probeAttempts: 4,
|
|
60
|
+
/**
|
|
61
|
+
* guuey#409: the PRE-first-byte watchdog window. The #192 clock arms only
|
|
62
|
+
* on the first chunk (so a silent cold start never trips it) — which left
|
|
63
|
+
* a turn that never receives ANY byte with no watchdog at all: the
|
|
64
|
+
* eternal-"Thinking…" face of the 2026-08-22 invoke-rail turn death.
|
|
65
|
+
* Deliberately much longer than `windowMs`: the transport's cold-start
|
|
66
|
+
* retries legitimately spend up to ~90s before the first byte.
|
|
67
|
+
*/
|
|
68
|
+
preFirstByteWindowMs: 120_000,
|
|
69
|
+
};
|
|
58
70
|
function resolveStallRecovery(option) {
|
|
59
71
|
if (option === false)
|
|
60
72
|
return null;
|
|
61
73
|
return {
|
|
62
74
|
windowMs: option?.windowMs ?? STALL_RECOVERY_DEFAULTS.windowMs,
|
|
63
75
|
probeAttempts: option?.probeAttempts ?? STALL_RECOVERY_DEFAULTS.probeAttempts,
|
|
76
|
+
preFirstByteWindowMs: option?.preFirstByteWindowMs ?? STALL_RECOVERY_DEFAULTS.preFirstByteWindowMs,
|
|
64
77
|
};
|
|
65
78
|
}
|
|
66
79
|
/**
|
|
@@ -370,14 +383,21 @@ export function useAgentInvoke(opts) {
|
|
|
370
383
|
stallTimer = null;
|
|
371
384
|
}
|
|
372
385
|
};
|
|
373
|
-
const armStallTimer = () => {
|
|
386
|
+
const armStallTimer = (windowMs) => {
|
|
374
387
|
if (!stall || turnEnded || controller.signal.aborted)
|
|
375
388
|
return;
|
|
376
389
|
clearStallTimer();
|
|
377
390
|
stallTimer = setTimeout(() => {
|
|
378
391
|
void onStallWindow();
|
|
379
|
-
}, stall.windowMs);
|
|
392
|
+
}, windowMs ?? stall.windowMs);
|
|
380
393
|
};
|
|
394
|
+
// guuey#409: arm BEFORE the first byte — a turn that never receives
|
|
395
|
+
// any chunk (rail death upstream of the pod, or an in-pod pre-spawn
|
|
396
|
+
// hang) otherwise shows "Thinking…" forever with no error item. The
|
|
397
|
+
// long window keeps silent cold starts un-tripped; expiry runs the
|
|
398
|
+
// SAME probe-then-adopt-or-STREAM_STALLED machinery as mid-stream
|
|
399
|
+
// stalls — one face fix for both mechanisms.
|
|
400
|
+
armStallTimer(stall?.preFirstByteWindowMs);
|
|
381
401
|
const endTurnWith = (apply) => {
|
|
382
402
|
turnEnded = true;
|
|
383
403
|
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.1",
|
|
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.1"
|
|
39
39
|
},
|
|
40
40
|
"peerDependencies": {
|
|
41
41
|
"react": ">=18"
|
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
|
@@ -76,15 +76,29 @@ export function applyHistoryResult(
|
|
|
76
76
|
}
|
|
77
77
|
|
|
78
78
|
/** The guuey#192 stall watchdog's resolved tuning (see {@link stallProbeDecision}). */
|
|
79
|
-
export const STALL_RECOVERY_DEFAULTS = {
|
|
79
|
+
export const STALL_RECOVERY_DEFAULTS = {
|
|
80
|
+
windowMs: 25_000,
|
|
81
|
+
probeAttempts: 4,
|
|
82
|
+
/**
|
|
83
|
+
* guuey#409: the PRE-first-byte watchdog window. The #192 clock arms only
|
|
84
|
+
* on the first chunk (so a silent cold start never trips it) — which left
|
|
85
|
+
* a turn that never receives ANY byte with no watchdog at all: the
|
|
86
|
+
* eternal-"Thinking…" face of the 2026-08-22 invoke-rail turn death.
|
|
87
|
+
* Deliberately much longer than `windowMs`: the transport's cold-start
|
|
88
|
+
* retries legitimately spend up to ~90s before the first byte.
|
|
89
|
+
*/
|
|
90
|
+
preFirstByteWindowMs: 120_000,
|
|
91
|
+
} as const;
|
|
80
92
|
|
|
81
93
|
function resolveStallRecovery(
|
|
82
94
|
option: false | StallRecoveryOptions | undefined,
|
|
83
|
-
): { windowMs: number; probeAttempts: number } | null {
|
|
95
|
+
): { windowMs: number; probeAttempts: number; preFirstByteWindowMs: number } | null {
|
|
84
96
|
if (option === false) return null;
|
|
85
97
|
return {
|
|
86
98
|
windowMs: option?.windowMs ?? STALL_RECOVERY_DEFAULTS.windowMs,
|
|
87
99
|
probeAttempts: option?.probeAttempts ?? STALL_RECOVERY_DEFAULTS.probeAttempts,
|
|
100
|
+
preFirstByteWindowMs:
|
|
101
|
+
option?.preFirstByteWindowMs ?? STALL_RECOVERY_DEFAULTS.preFirstByteWindowMs,
|
|
88
102
|
};
|
|
89
103
|
}
|
|
90
104
|
|
|
@@ -399,13 +413,20 @@ export function useAgentInvoke(opts: UseAgentInvokeOptions): UseAgentInvokeRetur
|
|
|
399
413
|
stallTimer = null;
|
|
400
414
|
}
|
|
401
415
|
};
|
|
402
|
-
const armStallTimer = (): void => {
|
|
416
|
+
const armStallTimer = (windowMs?: number): void => {
|
|
403
417
|
if (!stall || turnEnded || controller.signal.aborted) return;
|
|
404
418
|
clearStallTimer();
|
|
405
419
|
stallTimer = setTimeout(() => {
|
|
406
420
|
void onStallWindow();
|
|
407
|
-
}, stall.windowMs);
|
|
421
|
+
}, windowMs ?? stall.windowMs);
|
|
408
422
|
};
|
|
423
|
+
// guuey#409: arm BEFORE the first byte — a turn that never receives
|
|
424
|
+
// any chunk (rail death upstream of the pod, or an in-pod pre-spawn
|
|
425
|
+
// hang) otherwise shows "Thinking…" forever with no error item. The
|
|
426
|
+
// long window keeps silent cold starts un-tripped; expiry runs the
|
|
427
|
+
// SAME probe-then-adopt-or-STREAM_STALLED machinery as mid-stream
|
|
428
|
+
// stalls — one face fix for both mechanisms.
|
|
429
|
+
armStallTimer(stall?.preFirstByteWindowMs);
|
|
409
430
|
const endTurnWith = (apply: () => void): void => {
|
|
410
431
|
turnEnded = true;
|
|
411
432
|
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
|
|