@blockrun/franklin 3.8.32 → 3.8.34
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/agent/error-classifier.js +2 -2
- package/dist/agent/llm.js +94 -34
- package/dist/commands/daemon.js +0 -1
- package/dist/telemetry/store.d.ts +1 -1
- package/dist/telemetry/store.js +11 -6
- package/dist/ui/app.js +14 -12
- package/dist/ui/session-picker.js +4 -2
- package/package.json +1 -1
|
@@ -68,7 +68,7 @@ export function classifyAgentError(message) {
|
|
|
68
68
|
'deadline exceeded',
|
|
69
69
|
])) {
|
|
70
70
|
return {
|
|
71
|
-
category: 'timeout', label: 'Timeout', isTransient: true,
|
|
71
|
+
category: 'timeout', label: 'Timeout', isTransient: true, maxRetries: 1,
|
|
72
72
|
suggestion: 'Check your network connection. Use /retry to try again.',
|
|
73
73
|
};
|
|
74
74
|
}
|
|
@@ -84,7 +84,7 @@ export function classifyAgentError(message) {
|
|
|
84
84
|
'dns resolution',
|
|
85
85
|
])) {
|
|
86
86
|
return {
|
|
87
|
-
category: 'network', label: 'Network', isTransient: true,
|
|
87
|
+
category: 'network', label: 'Network', isTransient: true, maxRetries: 1,
|
|
88
88
|
suggestion: 'Check your network connection. Use /retry to try again.',
|
|
89
89
|
};
|
|
90
90
|
}
|
package/dist/agent/llm.js
CHANGED
|
@@ -5,7 +5,60 @@
|
|
|
5
5
|
*/
|
|
6
6
|
import { getOrCreateWallet, getOrCreateSolanaWallet, createPaymentPayload, createSolanaPaymentPayload, parsePaymentRequired, extractPaymentDetails, solanaKeyToBytes, SOLANA_NETWORK, } from '@blockrun/llm';
|
|
7
7
|
import { USER_AGENT } from '../config.js';
|
|
8
|
+
import { routeRequest, parseRoutingProfile } from '../router/index.js';
|
|
8
9
|
import { ThinkTagStripper } from './think-tag-stripper.js';
|
|
10
|
+
function parseTimeoutEnv(name) {
|
|
11
|
+
const raw = process.env[name];
|
|
12
|
+
const parsed = raw ? Number.parseInt(raw, 10) : NaN;
|
|
13
|
+
return Number.isFinite(parsed) && parsed >= 0 ? parsed : null;
|
|
14
|
+
}
|
|
15
|
+
function getModelRequestTimeoutMs() {
|
|
16
|
+
return (parseTimeoutEnv('FRANKLIN_MODEL_REQUEST_TIMEOUT_MS') ??
|
|
17
|
+
parseTimeoutEnv('FRANKLIN_MODEL_IDLE_TIMEOUT_MS') ??
|
|
18
|
+
8_000);
|
|
19
|
+
}
|
|
20
|
+
function getModelStreamIdleTimeoutMs() {
|
|
21
|
+
return (parseTimeoutEnv('FRANKLIN_MODEL_STREAM_IDLE_TIMEOUT_MS') ??
|
|
22
|
+
parseTimeoutEnv('FRANKLIN_MODEL_IDLE_TIMEOUT_MS') ??
|
|
23
|
+
25_000);
|
|
24
|
+
}
|
|
25
|
+
function linkAbortSignal(parent, child) {
|
|
26
|
+
if (!parent)
|
|
27
|
+
return () => { };
|
|
28
|
+
if (parent.aborted) {
|
|
29
|
+
child.abort(parent.reason);
|
|
30
|
+
return () => { };
|
|
31
|
+
}
|
|
32
|
+
const forward = () => child.abort(parent.reason);
|
|
33
|
+
parent.addEventListener('abort', forward, { once: true });
|
|
34
|
+
return () => parent.removeEventListener('abort', forward);
|
|
35
|
+
}
|
|
36
|
+
function createModelTimeoutError(stage, model, timeoutMs) {
|
|
37
|
+
return new Error(`Model ${stage} timed out after ${timeoutMs}ms on ${model}`);
|
|
38
|
+
}
|
|
39
|
+
async function withAbortableTimeout(work, controller, timeoutError, timeoutMs) {
|
|
40
|
+
if (timeoutMs <= 0)
|
|
41
|
+
return work();
|
|
42
|
+
let timer;
|
|
43
|
+
try {
|
|
44
|
+
return await Promise.race([
|
|
45
|
+
work(),
|
|
46
|
+
new Promise((_, reject) => {
|
|
47
|
+
timer = setTimeout(() => {
|
|
48
|
+
try {
|
|
49
|
+
controller.abort(timeoutError);
|
|
50
|
+
}
|
|
51
|
+
catch { /* ignore */ }
|
|
52
|
+
reject(timeoutError);
|
|
53
|
+
}, timeoutMs);
|
|
54
|
+
}),
|
|
55
|
+
]);
|
|
56
|
+
}
|
|
57
|
+
finally {
|
|
58
|
+
if (timer)
|
|
59
|
+
clearTimeout(timer);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
9
62
|
/**
|
|
10
63
|
* Extract the most human-readable message from an error body.
|
|
11
64
|
* Some gateways wrap provider errors multiple times, e.g.
|
|
@@ -188,9 +241,7 @@ export class ModelClient {
|
|
|
188
241
|
resolveVirtualModel(model) {
|
|
189
242
|
if (!model.startsWith('blockrun/'))
|
|
190
243
|
return model;
|
|
191
|
-
// Import router dynamically to avoid circular deps
|
|
192
244
|
try {
|
|
193
|
-
const { routeRequest, parseRoutingProfile } = require('../router/index.js');
|
|
194
245
|
const profile = parseRoutingProfile(model);
|
|
195
246
|
if (profile) {
|
|
196
247
|
const result = routeRequest('', profile);
|
|
@@ -288,39 +339,48 @@ export class ModelClient {
|
|
|
288
339
|
if (this.debug) {
|
|
289
340
|
console.error(`[franklin] POST ${endpoint} model=${request.model}`);
|
|
290
341
|
}
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
// Handle x402 payment
|
|
298
|
-
if (response.status === 402) {
|
|
299
|
-
if (this.debug)
|
|
300
|
-
console.error('[franklin] Payment required — signing...');
|
|
301
|
-
const paymentHeader = await this.signPayment(response);
|
|
302
|
-
if (!paymentHeader) {
|
|
303
|
-
yield { kind: 'error', payload: { message: 'Payment signing failed' } };
|
|
304
|
-
return;
|
|
305
|
-
}
|
|
306
|
-
response = await fetch(endpoint, {
|
|
342
|
+
const requestTimeoutMs = getModelRequestTimeoutMs();
|
|
343
|
+
const streamTimeoutMs = getModelStreamIdleTimeoutMs();
|
|
344
|
+
const requestController = new AbortController();
|
|
345
|
+
const unlinkAbort = linkAbortSignal(signal, requestController);
|
|
346
|
+
try {
|
|
347
|
+
let response = await withAbortableTimeout(() => fetch(endpoint, {
|
|
307
348
|
method: 'POST',
|
|
308
|
-
headers
|
|
349
|
+
headers,
|
|
309
350
|
body,
|
|
310
|
-
signal,
|
|
311
|
-
});
|
|
351
|
+
signal: requestController.signal,
|
|
352
|
+
}), requestController, createModelTimeoutError('request', request.model, requestTimeoutMs), requestTimeoutMs);
|
|
353
|
+
// Handle x402 payment
|
|
354
|
+
if (response.status === 402) {
|
|
355
|
+
if (this.debug)
|
|
356
|
+
console.error('[franklin] Payment required — signing...');
|
|
357
|
+
const paymentHeader = await this.signPayment(response);
|
|
358
|
+
if (!paymentHeader) {
|
|
359
|
+
yield { kind: 'error', payload: { message: 'Payment signing failed' } };
|
|
360
|
+
return;
|
|
361
|
+
}
|
|
362
|
+
response = await withAbortableTimeout(() => fetch(endpoint, {
|
|
363
|
+
method: 'POST',
|
|
364
|
+
headers: { ...headers, ...paymentHeader },
|
|
365
|
+
body,
|
|
366
|
+
signal: requestController.signal,
|
|
367
|
+
}), requestController, createModelTimeoutError('request', request.model, requestTimeoutMs), requestTimeoutMs);
|
|
368
|
+
}
|
|
369
|
+
if (!response.ok) {
|
|
370
|
+
const errorBody = await response.text().catch(() => 'unknown error');
|
|
371
|
+
const message = extractApiErrorMessage(errorBody);
|
|
372
|
+
yield {
|
|
373
|
+
kind: 'error',
|
|
374
|
+
payload: { status: response.status, message },
|
|
375
|
+
};
|
|
376
|
+
return;
|
|
377
|
+
}
|
|
378
|
+
// Parse SSE stream
|
|
379
|
+
yield* this.parseSSEStream(response, requestController, streamTimeoutMs, request.model);
|
|
312
380
|
}
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
const message = extractApiErrorMessage(errorBody);
|
|
316
|
-
yield {
|
|
317
|
-
kind: 'error',
|
|
318
|
-
payload: { status: response.status, message },
|
|
319
|
-
};
|
|
320
|
-
return;
|
|
381
|
+
finally {
|
|
382
|
+
unlinkAbort();
|
|
321
383
|
}
|
|
322
|
-
// Parse SSE stream
|
|
323
|
-
yield* this.parseSSEStream(response, signal);
|
|
324
384
|
}
|
|
325
385
|
/**
|
|
326
386
|
* Non-streaming completion for simple requests.
|
|
@@ -617,7 +677,7 @@ export class ModelClient {
|
|
|
617
677
|
return header;
|
|
618
678
|
}
|
|
619
679
|
// ─── SSE Parsing ───────────────────────────────────────────────────────
|
|
620
|
-
async *parseSSEStream(response,
|
|
680
|
+
async *parseSSEStream(response, controller, timeoutMs, model) {
|
|
621
681
|
const reader = response.body?.getReader();
|
|
622
682
|
if (!reader) {
|
|
623
683
|
yield { kind: 'error', payload: { message: 'No response body' } };
|
|
@@ -630,9 +690,9 @@ export class ModelClient {
|
|
|
630
690
|
const MAX_BUFFER = 1_000_000; // 1MB buffer cap
|
|
631
691
|
try {
|
|
632
692
|
while (true) {
|
|
633
|
-
if (signal
|
|
693
|
+
if (controller.signal.aborted)
|
|
634
694
|
break;
|
|
635
|
-
const { done, value } = await reader.read();
|
|
695
|
+
const { done, value } = await withAbortableTimeout(() => reader.read(), controller, createModelTimeoutError('stream', model, timeoutMs), timeoutMs);
|
|
636
696
|
if (done)
|
|
637
697
|
break;
|
|
638
698
|
buffer += decoder.decode(value, { stream: true });
|
package/dist/commands/daemon.js
CHANGED
|
@@ -34,7 +34,6 @@ function isRunning(pid) {
|
|
|
34
34
|
}
|
|
35
35
|
catch { /* fall through to ps */ }
|
|
36
36
|
try {
|
|
37
|
-
const { execSync } = require('node:child_process');
|
|
38
37
|
const cmd = execSync(`ps -p ${pid} -o command=`, { encoding: 'utf-8', stdio: ['pipe', 'pipe', 'ignore'] }).trim();
|
|
39
38
|
return /franklin|runcode|node.*dist\/index/.test(cmd);
|
|
40
39
|
}
|
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
* files — telemetry is just a stable, aggregation-friendly view of
|
|
21
21
|
* information the user already has.
|
|
22
22
|
*/
|
|
23
|
-
import type
|
|
23
|
+
import { type SessionMeta } from '../session/storage.js';
|
|
24
24
|
interface ConsentRecord {
|
|
25
25
|
enabled: boolean;
|
|
26
26
|
enabledAt?: number;
|
package/dist/telemetry/store.js
CHANGED
|
@@ -24,12 +24,21 @@ import fs from 'node:fs';
|
|
|
24
24
|
import path from 'node:path';
|
|
25
25
|
import crypto from 'node:crypto';
|
|
26
26
|
import { BLOCKRUN_DIR, VERSION } from '../config.js';
|
|
27
|
+
import { listSessions } from '../session/storage.js';
|
|
27
28
|
const CONSENT_FILE = path.join(BLOCKRUN_DIR, 'telemetry-consent.json');
|
|
28
29
|
const LOG_FILE = path.join(BLOCKRUN_DIR, 'telemetry.jsonl');
|
|
29
30
|
const INSTALL_ID_FILE = path.join(BLOCKRUN_DIR, 'telemetry-install-id.txt');
|
|
30
31
|
function ensureDir() {
|
|
31
32
|
fs.mkdirSync(BLOCKRUN_DIR, { recursive: true });
|
|
32
33
|
}
|
|
34
|
+
function canonicalDir(dir) {
|
|
35
|
+
try {
|
|
36
|
+
return fs.realpathSync(path.resolve(dir));
|
|
37
|
+
}
|
|
38
|
+
catch {
|
|
39
|
+
return path.resolve(dir);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
33
42
|
/** Enabled-state check. Default: false. */
|
|
34
43
|
export function isTelemetryEnabled() {
|
|
35
44
|
try {
|
|
@@ -120,13 +129,9 @@ export function recordSession(meta, chain) {
|
|
|
120
129
|
export function recordLatestSessionIfEnabled(workingDir, chain) {
|
|
121
130
|
if (!isTelemetryEnabled())
|
|
122
131
|
return;
|
|
123
|
-
|
|
124
|
-
// Using require() here keeps this module synchronous for tests.
|
|
125
|
-
/* eslint-disable @typescript-eslint/no-require-imports */
|
|
126
|
-
const { listSessions } = require('../session/storage.js');
|
|
127
|
-
/* eslint-enable @typescript-eslint/no-require-imports */
|
|
132
|
+
const targetDir = canonicalDir(workingDir);
|
|
128
133
|
const sessions = listSessions();
|
|
129
|
-
const match = sessions.find(s => s.workDir ===
|
|
134
|
+
const match = sessions.find(s => canonicalDir(s.workDir) === targetDir);
|
|
130
135
|
if (!match)
|
|
131
136
|
return;
|
|
132
137
|
recordSession(match, chain);
|
package/dist/ui/app.js
CHANGED
|
@@ -333,6 +333,18 @@ function RunCodeApp({ initialModel, workDir, walletAddress, walletBalance, chain
|
|
|
333
333
|
const trimmed = value.trim();
|
|
334
334
|
if (!trimmed)
|
|
335
335
|
return;
|
|
336
|
+
// Exit commands bypass the busy-queue gate — if the user wants out,
|
|
337
|
+
// they want out immediately, not after whatever turn is in flight.
|
|
338
|
+
// Covers bare 'exit' / 'quit' / 'q' and slash-prefixed /exit / /quit.
|
|
339
|
+
const lower = trimmed.toLowerCase();
|
|
340
|
+
const isExit = lower === 'exit' || lower === 'quit' || lower === 'q' ||
|
|
341
|
+
lower === '/exit' || lower === '/quit';
|
|
342
|
+
if (isExit) {
|
|
343
|
+
onAbort();
|
|
344
|
+
onExit();
|
|
345
|
+
exit();
|
|
346
|
+
return;
|
|
347
|
+
}
|
|
336
348
|
// If agent is busy, queue the message — it will be auto-submitted when the turn finishes
|
|
337
349
|
if (!ready) {
|
|
338
350
|
setQueuedInputs(prev => [...prev, trimmed]);
|
|
@@ -340,13 +352,6 @@ function RunCodeApp({ initialModel, workDir, walletAddress, walletBalance, chain
|
|
|
340
352
|
showStatus(`Queued message (${queuedInputsRef.current.length + 1} pending)`, 'warning', 1500);
|
|
341
353
|
return;
|
|
342
354
|
}
|
|
343
|
-
// Bare exit/quit (no slash needed)
|
|
344
|
-
const lower = trimmed.toLowerCase();
|
|
345
|
-
if (lower === 'exit' || lower === 'quit' || lower === 'q') {
|
|
346
|
-
onExit();
|
|
347
|
-
exit();
|
|
348
|
-
return;
|
|
349
|
-
}
|
|
350
355
|
// ── Slash commands ──
|
|
351
356
|
if (trimmed.startsWith('/')) {
|
|
352
357
|
setInput('');
|
|
@@ -355,11 +360,8 @@ function RunCodeApp({ initialModel, workDir, walletAddress, walletBalance, chain
|
|
|
355
360
|
const parts = trimmed.split(/\s+/);
|
|
356
361
|
const cmd = parts[0].toLowerCase();
|
|
357
362
|
switch (cmd) {
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
onExit();
|
|
361
|
-
exit();
|
|
362
|
-
return;
|
|
363
|
+
// /exit and /quit are handled earlier (before the busy-queue gate)
|
|
364
|
+
// so they exit the session immediately even mid-turn.
|
|
363
365
|
case '/model':
|
|
364
366
|
case '/models':
|
|
365
367
|
if (parts[1]) {
|
|
@@ -46,7 +46,9 @@ function modelShort(model) {
|
|
|
46
46
|
* Returns { ok, id } on success, or { ok, error, candidates } on failure.
|
|
47
47
|
*/
|
|
48
48
|
export function resolveSessionIdInput(input) {
|
|
49
|
-
|
|
49
|
+
return resolveSessionIdFromList(input, listSessions());
|
|
50
|
+
}
|
|
51
|
+
function resolveSessionIdFromList(input, sessions) {
|
|
50
52
|
// Exact match first
|
|
51
53
|
const exact = sessions.find((s) => s.id === input);
|
|
52
54
|
if (exact)
|
|
@@ -72,7 +74,7 @@ export function resolvePickerSelection(input, shown, sessions) {
|
|
|
72
74
|
const exact = sessions.find((s) => s.id === trimmed);
|
|
73
75
|
if (exact)
|
|
74
76
|
return { kind: 'selected', id: exact.id };
|
|
75
|
-
const resolved =
|
|
77
|
+
const resolved = resolveSessionIdFromList(trimmed, sessions);
|
|
76
78
|
if (resolved.ok) {
|
|
77
79
|
return { kind: 'selected', id: resolved.id };
|
|
78
80
|
}
|
package/package.json
CHANGED