@blockrun/franklin 3.15.54 → 3.15.56
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.
|
@@ -6,10 +6,10 @@
|
|
|
6
6
|
* - Auth errors (401) get special handling (token refresh, not retry)
|
|
7
7
|
* - EPIPE/connection reset handled as network errors (retryable)
|
|
8
8
|
*/
|
|
9
|
-
export type AgentErrorCategory = 'rate_limit' | 'payment' | 'network' | 'timeout' | 'context_limit' | 'overloaded' | 'server' | 'auth' | 'schema' | 'unknown';
|
|
9
|
+
export type AgentErrorCategory = 'rate_limit' | 'payment' | 'payment_rejected' | 'network' | 'timeout' | 'context_limit' | 'overloaded' | 'server' | 'auth' | 'schema' | 'unknown';
|
|
10
10
|
export interface AgentErrorInfo {
|
|
11
11
|
category: AgentErrorCategory;
|
|
12
|
-
label: 'RateLimit' | 'Payment' | 'Network' | 'Timeout' | 'Context' | 'Overloaded' | 'Server' | 'Auth' | 'Schema' | 'Unknown';
|
|
12
|
+
label: 'RateLimit' | 'Payment' | 'PaymentRejected' | 'Network' | 'Timeout' | 'Context' | 'Overloaded' | 'Server' | 'Auth' | 'Schema' | 'Unknown';
|
|
13
13
|
isTransient: boolean;
|
|
14
14
|
/** Max retries for this error type (overrides default). undefined = use default. */
|
|
15
15
|
maxRetries?: number;
|
|
@@ -11,10 +11,31 @@ function includesAny(text, patterns) {
|
|
|
11
11
|
}
|
|
12
12
|
export function classifyAgentError(message) {
|
|
13
13
|
const err = message.toLowerCase();
|
|
14
|
+
// payment_rejected — the gateway received a SIGNED payment header and
|
|
15
|
+
// rejected it during verification (signature mismatch, replay-nonce
|
|
16
|
+
// reuse, clock skew, wrong-chain wallet). Different remedy from
|
|
17
|
+
// payment_required: re-presenting the same signature won't help.
|
|
18
|
+
// Verified 2026-05-04 in a live session: ExaSearch returned
|
|
19
|
+
// `Exa /v1/exa/search failed (402): {"error":"Payment verification failed",...}`.
|
|
20
|
+
// Classify BEFORE the generic 'payment' branch below since the body
|
|
21
|
+
// contains both 'payment' and 'verification failed'.
|
|
22
|
+
if (includesAny(err, [
|
|
23
|
+
'verification failed',
|
|
24
|
+
'payment verification',
|
|
25
|
+
'signature mismatch',
|
|
26
|
+
'invalid payment signature',
|
|
27
|
+
'invalid x-payment',
|
|
28
|
+
'nonce reuse',
|
|
29
|
+
'replay protection',
|
|
30
|
+
])) {
|
|
31
|
+
return {
|
|
32
|
+
category: 'payment_rejected', label: 'PaymentRejected', isTransient: false, maxRetries: 0,
|
|
33
|
+
suggestion: 'The gateway rejected your signed payment. Run `franklin balance` to confirm funds + chain. Common causes: clock skew (resync system clock), wrong chain selected (use `/chain` to switch), or stale nonce (the same retry will fail). Switch to a free model with `/model free` to keep working.',
|
|
34
|
+
};
|
|
35
|
+
}
|
|
14
36
|
if (includesAny(err, [
|
|
15
37
|
'insufficient',
|
|
16
38
|
'payment',
|
|
17
|
-
'verification failed',
|
|
18
39
|
'balance',
|
|
19
40
|
'402',
|
|
20
41
|
'free tier',
|
package/dist/agent/loop.js
CHANGED
|
@@ -288,6 +288,19 @@ function getBackoffDelay(attempt, maxDelayMs = 32_000) {
|
|
|
288
288
|
const jitter = base * 0.25 * (Math.random() * 2 - 1); // ±25%
|
|
289
289
|
return Math.max(500, Math.round(base + jitter));
|
|
290
290
|
}
|
|
291
|
+
/**
|
|
292
|
+
* Format the user-facing "switching model" line. Includes the resolved
|
|
293
|
+
* concrete model in parentheses when the user-facing alias (e.g.
|
|
294
|
+
* `blockrun/auto`) differs from what was actually being called (e.g.
|
|
295
|
+
* `anthropic/claude-sonnet-4.6`). Verified 2026-05-04 in a live session:
|
|
296
|
+
* a payment fail surfaced as `*blockrun/auto failed — switching to
|
|
297
|
+
* nvidia/qwen3-coder-480b*` with no hint of which concrete model
|
|
298
|
+
* actually failed, and no hint of why. The reason label closes that gap.
|
|
299
|
+
*/
|
|
300
|
+
function formatModelSwitch(alias, resolved, reason, newModel) {
|
|
301
|
+
const oldDisplay = alias === resolved ? alias : `${alias} (${resolved})`;
|
|
302
|
+
return `${oldDisplay} ${reason} — switching to ${newModel}`;
|
|
303
|
+
}
|
|
291
304
|
/**
|
|
292
305
|
* Identify models known to hallucinate tool calls (invented names, literal
|
|
293
306
|
* `[TOOLCALL]` / `<tool_call>` text in answers) — they need the explicit
|
|
@@ -1013,8 +1026,9 @@ export async function interactiveSession(config, getUserInput, onEvent, onAbortR
|
|
|
1013
1026
|
const oldModel = config.model;
|
|
1014
1027
|
config.model = nextModel;
|
|
1015
1028
|
config.onModelChange?.(nextModel, 'system');
|
|
1016
|
-
|
|
1017
|
-
|
|
1029
|
+
const switchLine = formatModelSwitch(oldModel, resolvedModel, 'returned empty', nextModel);
|
|
1030
|
+
logger.warn(`[franklin] ${switchLine}`);
|
|
1031
|
+
onEvent({ kind: 'text_delta', text: `\n*${switchLine}*\n` });
|
|
1018
1032
|
continue;
|
|
1019
1033
|
}
|
|
1020
1034
|
// No fallback available OR already tried 2 models — give up, tell the user.
|
|
@@ -1158,8 +1172,12 @@ export async function interactiveSession(config, getUserInput, onEvent, onAbortR
|
|
|
1158
1172
|
}
|
|
1159
1173
|
// ── Payment failure: auto-fallback to free models ──
|
|
1160
1174
|
// Track payment-failed models for the entire session — unlike transient errors,
|
|
1161
|
-
// 402s will keep failing until the user adds funds.
|
|
1162
|
-
|
|
1175
|
+
// 402s will keep failing until the user adds funds. Also handles
|
|
1176
|
+
// payment_rejected (signature verified-and-rejected by gateway):
|
|
1177
|
+
// same fallback path, but the suggestion text in classifier guides
|
|
1178
|
+
// the user toward clock-skew / chain-mismatch fixes rather than
|
|
1179
|
+
// "add funds."
|
|
1180
|
+
if (classified.category === 'payment' || classified.category === 'payment_rejected') {
|
|
1163
1181
|
turnFailedModels.add(config.model);
|
|
1164
1182
|
paymentFailedModels.set(config.model, Date.now());
|
|
1165
1183
|
// Bound the Map so long sessions don't leak. LRU-evict oldest by timestamp.
|
|
@@ -1177,7 +1195,11 @@ export async function interactiveSession(config, getUserInput, onEvent, onAbortR
|
|
|
1177
1195
|
const oldModel = config.model;
|
|
1178
1196
|
config.model = nextFree;
|
|
1179
1197
|
config.onModelChange?.(nextFree, 'system');
|
|
1180
|
-
|
|
1198
|
+
const reason = `failed [${classified.label}]`;
|
|
1199
|
+
onEvent({
|
|
1200
|
+
kind: 'text_delta',
|
|
1201
|
+
text: `\n*${formatModelSwitch(oldModel, resolvedModel, reason, nextFree)}*\n`,
|
|
1202
|
+
});
|
|
1181
1203
|
continue; // Retry with next model
|
|
1182
1204
|
}
|
|
1183
1205
|
}
|
|
@@ -1202,7 +1224,7 @@ export async function interactiveSession(config, getUserInput, onEvent, onAbortR
|
|
|
1202
1224
|
recoveryAttempts = 0;
|
|
1203
1225
|
onEvent({
|
|
1204
1226
|
kind: 'text_delta',
|
|
1205
|
-
text: `\n*${oldModel
|
|
1227
|
+
text: `\n*${formatModelSwitch(oldModel, resolvedModel, 'rate-limited', nextFree)}*\n`,
|
|
1206
1228
|
});
|
|
1207
1229
|
continue;
|
|
1208
1230
|
}
|
package/package.json
CHANGED