@blockrun/franklin 3.15.85 → 3.15.87

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.
@@ -311,6 +311,7 @@ export async function startCommand(options) {
311
311
  }
312
312
  // Resolve resume target, if requested.
313
313
  let resumeSessionId;
314
+ let resumeTranscript;
314
315
  if (options.resume || options.continue) {
315
316
  const { pickSession } = await import('../ui/session-picker.js');
316
317
  const { loadSessionMeta, loadSessionHistory } = await import('../session/storage.js');
@@ -338,10 +339,11 @@ export async function startCommand(options) {
338
339
  }
339
340
  if (resumeSessionId) {
340
341
  const meta = loadSessionMeta(resumeSessionId);
341
- const msgs = loadSessionHistory(resumeSessionId).length;
342
+ const history = loadSessionHistory(resumeSessionId);
342
343
  const when = meta ? new Date(meta.updatedAt).toLocaleString() : 'unknown';
343
344
  console.log(chalk.green(` Resuming session ${resumeSessionId.slice(0, 24)}…`));
344
- console.log(chalk.dim(` ${msgs} messages · last active ${when}\n`));
345
+ console.log(chalk.dim(` ${history.length} messages · last active ${when}\n`));
346
+ resumeTranscript = buildResumeTranscript(history);
345
347
  }
346
348
  }
347
349
  // Agent config
@@ -376,7 +378,7 @@ export async function startCommand(options) {
376
378
  if (process.stdin.isTTY) {
377
379
  await runWithInkUI(agentConfig, model, workDir, version, walletInfo, (cb) => {
378
380
  onBalanceFetched = cb;
379
- }, fetchBalance, importedKickoffPrompt);
381
+ }, fetchBalance, importedKickoffPrompt, resumeTranscript);
380
382
  }
381
383
  else {
382
384
  await runWithBasicUI(agentConfig, model, workDir, importedKickoffPrompt);
@@ -409,8 +411,40 @@ async function runOneShot(agentConfig, prompt) {
409
411
  });
410
412
  return exitCode;
411
413
  }
414
+ function buildResumeTranscript(history) {
415
+ const entries = history
416
+ .map((msg) => {
417
+ const text = extractVisibleText(msg).replace(/\s+/g, ' ').trim();
418
+ if (!text)
419
+ return null;
420
+ return { role: msg.role, text: text.length > 180 ? `${text.slice(0, 177)}...` : text };
421
+ })
422
+ .filter((entry) => entry !== null);
423
+ if (entries.length === 0)
424
+ return [];
425
+ const started = entries.slice(0, 4);
426
+ const recentStart = entries.length > 10 ? -6 : 4;
427
+ const recent = entries.slice(recentStart);
428
+ return entries.length > 10
429
+ ? [...started, { role: 'assistant', text: '...' }, ...recent]
430
+ : [...started, ...recent];
431
+ }
432
+ function extractVisibleText(msg) {
433
+ if (typeof msg.content === 'string')
434
+ return msg.content;
435
+ if (!Array.isArray(msg.content))
436
+ return '';
437
+ return msg.content
438
+ .map((part) => {
439
+ if ('type' in part && part.type === 'text')
440
+ return part.text;
441
+ return '';
442
+ })
443
+ .filter(Boolean)
444
+ .join('\n');
445
+ }
412
446
  // ─── Ink UI (interactive terminal) ─────────────────────────────────────────
413
- async function runWithInkUI(agentConfig, model, workDir, version, walletInfo, onBalanceReady, fetchBalance, initialInput) {
447
+ async function runWithInkUI(agentConfig, model, workDir, version, walletInfo, onBalanceReady, fetchBalance, initialInput, initialTranscript) {
414
448
  const startSnapshot = snapshotStats();
415
449
  const ui = launchInkUI({
416
450
  model,
@@ -418,6 +452,7 @@ async function runWithInkUI(agentConfig, model, workDir, version, walletInfo, on
418
452
  version,
419
453
  walletAddress: walletInfo?.address,
420
454
  walletBalance: walletInfo?.balance,
455
+ initialTranscript,
421
456
  chain: walletInfo?.chain,
422
457
  onModelChange: (newModel, reason) => {
423
458
  agentConfig.model = newModel;
@@ -474,25 +509,12 @@ async function runWithInkUI(agentConfig, model, workDir, version, walletInfo, on
474
509
  recordLatestSessionIfEnabled(process.cwd(), agentConfig.chain);
475
510
  }
476
511
  catch { /* telemetry is best-effort */ }
477
- // Extract learnings from the session (async, 10s timeout, never blocks exit)
478
- if (sessionHistory && sessionHistory.length >= 4) {
479
- try {
480
- const { extractLearnings } = await import('../learnings/extractor.js');
481
- const { extractBrainEntities } = await import('../brain/extract.js');
482
- const { ModelClient } = await import('../agent/llm.js');
483
- const client = new ModelClient({ apiUrl: agentConfig.apiUrl, chain: agentConfig.chain });
484
- const sid = `session-${new Date().toISOString()}`;
485
- await Promise.race([
486
- Promise.all([
487
- extractLearnings(sessionHistory, sid, client),
488
- extractBrainEntities(sessionHistory, sid, client),
489
- ]),
490
- new Promise(resolve => setTimeout(resolve, 15_000)),
491
- ]);
492
- }
493
- catch { /* extraction is best-effort */ }
512
+ // Optional post-session learning extraction. Disabled by default because any
513
+ // network-backed background promise can keep Node alive after the UI exits.
514
+ if (process.env.FRANKLIN_EXTRACT_ON_EXIT === '1') {
515
+ runExitBackgroundTasks(sessionHistory, agentConfig).catch(() => { });
494
516
  }
495
- await disconnectMcpServers();
517
+ disconnectMcpServers().catch(() => { });
496
518
  // Session summary — delta vs. snapshot at session start
497
519
  try {
498
520
  const delta = statsDelta(startSnapshot);
@@ -524,6 +546,19 @@ async function runWithInkUI(agentConfig, model, workDir, version, walletInfo, on
524
546
  }
525
547
  console.log(chalk.dim('\nGoodbye.\n'));
526
548
  }
549
+ async function runExitBackgroundTasks(sessionHistory, agentConfig) {
550
+ if (!sessionHistory || sessionHistory.length < 4)
551
+ return;
552
+ const { extractLearnings } = await import('../learnings/extractor.js');
553
+ const { extractBrainEntities } = await import('../brain/extract.js');
554
+ const { ModelClient } = await import('../agent/llm.js');
555
+ const client = new ModelClient({ apiUrl: agentConfig.apiUrl, chain: agentConfig.chain });
556
+ const sid = `session-${new Date().toISOString()}`;
557
+ await Promise.all([
558
+ extractLearnings(sessionHistory, sid, client),
559
+ extractBrainEntities(sessionHistory, sid, client),
560
+ ]);
561
+ }
527
562
  // ─── Basic readline UI (piped input) ───────────────────────────────────────
528
563
  async function runWithBasicUI(agentConfig, model, workDir, initialInput) {
529
564
  const { TerminalUI } = await import('../ui/terminal.js');
package/dist/ui/app.d.ts CHANGED
@@ -20,6 +20,10 @@ export declare function launchInkUI(opts: {
20
20
  version: string;
21
21
  walletAddress?: string;
22
22
  walletBalance?: string;
23
+ initialTranscript?: Array<{
24
+ role: 'user' | 'assistant';
25
+ text: string;
26
+ }>;
23
27
  chain?: string;
24
28
  showPicker?: boolean;
25
29
  onModelChange?: (model: string, reason?: 'user' | 'system') => void;
package/dist/ui/app.js CHANGED
@@ -345,7 +345,7 @@ function formatAgentErrorForDisplay(error) {
345
345
  out.push(`- Tip: ${tip}`);
346
346
  return out.join('\n');
347
347
  }
348
- function RunCodeApp({ initialModel, workDir, walletAddress, walletBalance, chain, startWithPicker, onSubmit, onModelChange, onAbort, onExit, }) {
348
+ function RunCodeApp({ initialModel, workDir, walletAddress, walletBalance, chain, initialTranscript, startWithPicker, onSubmit, onModelChange, onAbort, onExit, }) {
349
349
  const { exit } = useApp();
350
350
  // Track terminal rows so we can cap the dynamic-region height. Ink wipes the
351
351
  // terminal scrollback (via ansiEscapes.clearTerminal → \x1b[3J) whenever the
@@ -362,7 +362,14 @@ function RunCodeApp({ initialModel, workDir, walletAddress, walletBalance, chain
362
362
  // Last completed tool — shown in dynamic area so it can be expanded/collapsed with Tab
363
363
  const [expandableTool, setExpandableTool] = useState(null);
364
364
  // Full responses committed to Static immediately — goes into terminal scrollback
365
- const [committedResponses, setCommittedResponses] = useState([]);
365
+ const [committedResponses, setCommittedResponses] = useState(() => (initialTranscript ?? []).map((entry, idx) => ({
366
+ key: `${entry.role === 'user' ? 'user' : 'resume'}-${idx}`,
367
+ text: entry.role === 'user'
368
+ ? chalk.hex('#FFD700').bold('❯ ') + chalk.hex('#FFD700').bold(entry.text)
369
+ : entry.text,
370
+ tokens: { input: 0, output: 0, calls: 0 },
371
+ cost: 0,
372
+ })));
366
373
  // Short preview of latest response shown in dynamic area (last ~5 lines, cleared on next turn)
367
374
  const [responsePreview, setResponsePreview] = useState('');
368
375
  const [currentModel, setCurrentModel] = useState(initialModel || PICKER_MODELS_FLAT[0].id);
@@ -1138,7 +1145,7 @@ export function launchInkUI(opts) {
1138
1145
  restoreTerminalAutoWrap?.();
1139
1146
  instance?.unmount();
1140
1147
  };
1141
- instance = render(_jsx(RunCodeApp, { initialModel: opts.model, workDir: opts.workDir, walletAddress: opts.walletAddress || 'not set — run: franklin setup', walletBalance: opts.walletBalance || 'unknown', chain: opts.chain || 'base', startWithPicker: opts.showPicker, onSubmit: (value) => {
1148
+ instance = render(_jsx(RunCodeApp, { initialModel: opts.model, workDir: opts.workDir, walletAddress: opts.walletAddress || 'not set — run: franklin setup', walletBalance: opts.walletBalance || 'unknown', initialTranscript: opts.initialTranscript, chain: opts.chain || 'base', startWithPicker: opts.showPicker, onSubmit: (value) => {
1142
1149
  if (resolveInput) {
1143
1150
  resolveInput(value);
1144
1151
  resolveInput = null;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@blockrun/franklin",
3
- "version": "3.15.85",
3
+ "version": "3.15.87",
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": {