@blockrun/franklin 3.15.61 → 3.15.63

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.
@@ -614,13 +614,27 @@ function readRuntimeWallet() {
614
614
  return {};
615
615
  const blockrunDir = path.join(home, '.blockrun');
616
616
  const out = {};
617
+ // Chain selection: prefer the canonical `payment-chain` (matches
618
+ // src/config.ts:CHAIN_FILE which the rest of the codebase writes).
619
+ // Fall back to the legacy `.chain` for installs that haven't migrated.
620
+ // Verified 2026-05-05: .chain on this machine read "base" (last
621
+ // updated 2026-03-14), payment-chain read "base" (last updated
622
+ // 2026-05-04) — same value here, but the two paths can diverge any
623
+ // time the user's panel UI or `franklin <chain>` writes the new one
624
+ // while the old file stays frozen. Reading both, preferring the new,
625
+ // closes the gap silently.
617
626
  try {
618
- const chainFile = path.join(blockrunDir, '.chain');
619
- if (fs.existsSync(chainFile)) {
620
- const chain = fs.readFileSync(chainFile, 'utf-8').trim();
621
- if (chain)
622
- out.chain = chain;
627
+ const newChainFile = path.join(blockrunDir, 'payment-chain');
628
+ const legacyChainFile = path.join(blockrunDir, '.chain');
629
+ let chain = '';
630
+ if (fs.existsSync(newChainFile)) {
631
+ chain = fs.readFileSync(newChainFile, 'utf-8').trim();
623
632
  }
633
+ if (!chain && fs.existsSync(legacyChainFile)) {
634
+ chain = fs.readFileSync(legacyChainFile, 'utf-8').trim();
635
+ }
636
+ if (chain)
637
+ out.chain = chain;
624
638
  }
625
639
  catch { /* ignore */ }
626
640
  // Base address: derive via @blockrun/llm (handles the private key in .session)
@@ -230,6 +230,12 @@ function buildExecute(deps) {
230
230
  // minutes; text-to-image keeps the original 60s.
231
231
  const timeoutMs = referenceImage ? 180_000 : 60_000;
232
232
  const timeout = setTimeout(() => controller.abort(), timeoutMs);
233
+ // Wall-clock start of the paid call, including 402 retry + (optional)
234
+ // 202 polling. Used by recordUsage below so franklin-stats.json
235
+ // populates avgLatencyMs for image models. Mirrors the agent-loop fix
236
+ // in 3.15.61 — same pattern, same reason: insights couldn't surface
237
+ // "Seedance is slower than grok" while every media call recorded 0.
238
+ const callStartedAt = Date.now();
233
239
  try {
234
240
  // First request — will get 402
235
241
  let response = await fetch(endpoint, {
@@ -362,11 +368,12 @@ function buildExecute(deps) {
362
368
  // insights panel under-reported total spend and never surfaced
363
369
  // image-generation models in its "top models" list. Fire-and-forget —
364
370
  // stats write must not fail a user-visible generation.
371
+ const latencyMs = Date.now() - callStartedAt;
365
372
  void (async () => {
366
373
  try {
367
374
  const m = await findModel(imageModel);
368
375
  const estCost = m ? estimateCostUsd(m, { quantity: 1 }) : 0;
369
- recordUsage(imageModel, 0, 0, estCost, 0);
376
+ recordUsage(imageModel, 0, 0, estCost, latencyMs);
370
377
  }
371
378
  catch { /* ignore stats errors */ }
372
379
  })();
@@ -325,7 +325,9 @@ export const modalCreateCapability = {
325
325
  body.cpu = cpuCoerced;
326
326
  if (typeof memoryCoerced === 'number')
327
327
  body.memory = memoryCoerced;
328
+ const callStartedAt = Date.now();
328
329
  const res = await postWithPayment(modalEndpoint('create'), body, 'Franklin Modal sandbox create', ctx.abortSignal, 90_000);
330
+ const latencyMs = Date.now() - callStartedAt;
329
331
  if (!res.ok) {
330
332
  const err = res.body.error ? String(res.body.error) : res.raw.slice(0, 300);
331
333
  // Surface the per-field validation issues — usually the
@@ -356,7 +358,7 @@ export const modalCreateCapability = {
356
358
  });
357
359
  // Stats — surface Modal usage in `franklin insights` like other paid tools.
358
360
  try {
359
- recordUsage(`modal/${tier}`, 0, 0, price, 0);
361
+ recordUsage(`modal/${tier}`, 0, 0, price, latencyMs);
360
362
  }
361
363
  catch { /* ignore */ }
362
364
  return {
@@ -448,7 +450,9 @@ export const modalExecCapability = {
448
450
  };
449
451
  if (coercedTimeout !== undefined)
450
452
  body.timeout = coercedTimeout;
453
+ const callStartedAt = Date.now();
451
454
  const res = await postWithPayment(modalEndpoint('exec'), body, 'Franklin Modal sandbox exec', ctx.abortSignal, Math.max(30_000, ((coercedTimeout ?? 300) + 30) * 1000));
455
+ const latencyMs = Date.now() - callStartedAt;
452
456
  if (!res.ok) {
453
457
  // 400 here usually means the agent built the wrong shape (bad
454
458
  // sandbox_id, malformed command). Dump the full raw body so the
@@ -479,7 +483,7 @@ export const modalExecCapability = {
479
483
  const hasAnyOutput = stdout.length > 0 || stderr.length > 0;
480
484
  const exitCode = rawExit !== null ? rawExit : (hasAnyOutput ? 0 : -1);
481
485
  try {
482
- recordUsage('modal/exec', 0, 0, EXEC_PRICE_USD, 0);
486
+ recordUsage('modal/exec', 0, 0, EXEC_PRICE_USD, latencyMs);
483
487
  }
484
488
  catch { /* ignore */ }
485
489
  const summary = `exit ${exitCode}` + (rawExit === null ? ' (inferred — no exit_code field in response)' : '');
@@ -527,13 +531,15 @@ export const modalStatusCapability = {
527
531
  }
528
532
  catch { /* ignore */ }
529
533
  try {
534
+ const callStartedAt = Date.now();
530
535
  const res = await postWithPayment(modalEndpoint('status'), { sandbox_id }, 'Franklin Modal sandbox status', ctx.abortSignal, 30_000);
536
+ const latencyMs = Date.now() - callStartedAt;
531
537
  if (!res.ok) {
532
538
  const err = res.body.error ? String(res.body.error) : res.raw.slice(0, 300);
533
539
  return { output: `ModalStatus failed (${res.status}): ${err}`, isError: true };
534
540
  }
535
541
  try {
536
- recordUsage('modal/status', 0, 0, STATUS_PRICE_USD, 0);
542
+ recordUsage('modal/status', 0, 0, STATUS_PRICE_USD, latencyMs);
537
543
  }
538
544
  catch { /* ignore */ }
539
545
  const status = res.body.status || 'unknown';
@@ -573,7 +579,9 @@ export const modalTerminateCapability = {
573
579
  }
574
580
  catch { /* ignore */ }
575
581
  try {
582
+ const callStartedAt = Date.now();
576
583
  const res = await postWithPayment(modalEndpoint('terminate'), { sandbox_id }, 'Franklin Modal sandbox terminate', ctx.abortSignal, 30_000);
584
+ const latencyMs = Date.now() - callStartedAt;
577
585
  // Always remove from tracker — even on failure, retrying is wasteful.
578
586
  sessionSandboxTracker.remove(sandbox_id);
579
587
  if (!res.ok) {
@@ -585,7 +593,7 @@ export const modalTerminateCapability = {
585
593
  };
586
594
  }
587
595
  try {
588
- recordUsage('modal/terminate', 0, 0, TERMINATE_PRICE_USD, 0);
596
+ recordUsage('modal/terminate', 0, 0, TERMINATE_PRICE_USD, latencyMs);
589
597
  }
590
598
  catch { /* ignore */ }
591
599
  return { output: `Sandbox \`${sandbox_id}\` terminated.` };
@@ -167,6 +167,12 @@ function buildExecute(deps) {
167
167
  'Content-Type': 'application/json',
168
168
  'User-Agent': `franklin/${VERSION}`,
169
169
  };
170
+ // Wall-clock start of the paid call (submit + poll + download). Fed
171
+ // to recordUsage below so franklin-stats.json populates avgLatencyMs
172
+ // for video models. Same fix as 3.15.61 (agent loop) — five
173
+ // recordUsage callsites in this codebase, three of them were
174
+ // hardcoding 0.
175
+ const callStartedAt = Date.now();
170
176
  const onAbort = (ctrl) => () => ctrl.abort();
171
177
  // Phase 1: submit the job. First POST triggers a 402; we sign and retry.
172
178
  // The signed paymentHeaders must be reused on every GET poll — the server
@@ -300,11 +306,12 @@ function buildExecute(deps) {
300
306
  // Prefer the live gateway price when the model is in the catalog;
301
307
  // fall back to the legacy $0.05/s estimate otherwise. Fire-and-
302
308
  // forget — stats write must not fail a user-visible generation.
309
+ const latencyMs = Date.now() - callStartedAt;
303
310
  void (async () => {
304
311
  try {
305
312
  const m = await findModel(videoModel);
306
313
  const estCost = m ? estimateCostUsd(m, { duration_seconds: dur }) : estimateVideoCostUsd(dur);
307
- recordUsage(videoModel, 0, 0, estCost, 0);
314
+ recordUsage(videoModel, 0, 0, estCost, latencyMs);
308
315
  }
309
316
  catch { /* ignore stats errors */ }
310
317
  })();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@blockrun/franklin",
3
- "version": "3.15.61",
3
+ "version": "3.15.63",
4
4
  "description": "Franklin — The AI agent with a wallet. Spends USDC autonomously to get real work done. Pay per action, no subscriptions.",
5
5
  "type": "module",
6
6
  "exports": {