Skip to content

Releases: BlockRunAI/Franklin

v3.8.35 — prompt refinement for media generation

23 Apr 15:08
1bf5128

Choose a tag to compare

Your prompts are the recipe. Franklin fixes them before spending.

Users write loose prompts — "a cat picture", "cyberpunk anime character" — and pay $0.04–$0.80 per image/video for mediocre output because image models get "inventive in directions you will regret" without structure.

Franklin's existing free-LLM call that picks the right image/video model now also rewrites your prompt using the 5-slot anatomy codified by fal.ai's GPT-Image-2 guide:

  1. Scene — location, time, environment
  2. Subject — preserved exactly from your input
  3. Details — lighting, camera/lens, composition (concrete facts, not praise)
  4. Use Case — editorial photo, logo, UI screen, storyboard, etc.
  5. Constraints — aspect ratio, no watermark, preserve face, literal text in quotes

The refinement + cost proposal surfaces before any paid call lands. You accept, pick the cheaper model, pick the premium one, use your original prompt as-is, or cancel. Chinese in → Chinese out.

The interaction

```
Media generation proposal

Prompt: "a cat picture"
Refined: Eye-level photograph of an orange tabby cat on a wooden windowsill,
soft morning side-light, shallow depth of field, 50mm feel, editorial
photo use, no watermark.
(Added scene, lighting, lens, use case, constraint.)

● Recommended google/nano-banana-pro ~$0.04 Photoreal at moderate cost
○ Cheaper google/nano-banana ~$0.02 Same family, less detail
○ Premium openai/gpt-image-2 ~$0.19 Best fidelity

[ Continue with refined prompt + google/nano-banana-pro ]
[ Use cheaper (google/nano-banana) ]
[ Use premium (openai/gpt-image-2) ]
[ Use ORIGINAL prompt + google/nano-banana-pro ]
[ Cancel (no charge) ]
```

Opt-outs

  • One-shot: prefix your prompt with `///` to skip refinement for that call
  • Global: set `FRANKLIN_NO_MEDIA_PROMPT_REFINE=1`
  • Prompts that are already well-specified (≥3 of the 5 slots covered) are passed through unchanged automatically — the classifier decides

What didn't change

  • Zero added latency (same single classifier call)
  • Zero added cost (free-tier model)
  • Raw prompt stays yours — refinement is always visible, always optional
  • Layout unchanged when refinement isn't needed

Also

  • `docs/live-e2e-checklist.md` — canonical recipe for the networked validation pass (required reading before declaring a release fully verified against live gateway)
  • 162/162 local tests passing (157 → 162)

Install

```bash
npm install -g @blockrun/franklin@3.8.35
```

v3.8.34 — reliability pass

23 Apr 05:54
515adec

Choose a tag to compare

Reliability pass

Batch fix from an agent review. Addresses failures that would surface on real command paths (not covered by existing unit tests) plus one macOS-specific telemetry loss.

What changed

  • ESM runtime — removed lingering require() calls in src/agent/llm.ts, src/telemetry/store.ts, src/commands/daemon.ts. Project is "type": "module" so these threw at runtime on the real command path.
  • Model timeouts split — new FRANKLIN_MODEL_REQUEST_TIMEOUT_MS (request phase) and FRANKLIN_MODEL_STREAM_IDLE_TIMEOUT_MS (stream-idle phase). Parent AbortSignal now links through to the in-flight fetch so Esc propagates instantly. When the gateway is unreachable, live e2e now skips in 2–3s instead of hanging ~1m.
  • Retry budget — timeout + network errors declare maxRetries: 1 (was 5). Flaky links no longer burn 5× backoff with no signal.
  • Telemetry canonical pathsrc/telemetry/store.ts resolves both sides through fs.realpathSync before comparing. Fixes macOS /var vs /private/var symlink drops that silently lost turns.
  • Live e2e skiptest/e2e.mjs detects gateway-unreachable and skips instead of flunking the suite.
  • Session picker polishresolvePickerSelection() implemented; ambiguous/invalid raw picker input returns 'invalid' instead of silently picking the first visible session.

Tests

157/157 local passing. Happy-path live e2e verification on a live-gateway environment is follow-up — not a regression.

Install

```bash
npm install -g @blockrun/franklin@3.8.34
```

v3.8.33 — exit / quit now works mid-turn

23 Apr 05:44
92b804f

Choose a tag to compare

Bug

Type exit (or /exit, quit, /quit, q) while the agent is still streaming a response or waiting on a tool. Expected: Franklin exits. Actual: the word got queued as if it were a normal message, nothing happened, and you had to wait for the turn to finish before the exit handler would fire.

Root cause

handleSubmit in src/ui/app.tsx ran the busy-queue gate before the exit-command check:

if (!ready) {
  setQueuedInputs(prev => [...prev, trimmed]);  // ← 'exit' got queued here
  return;
}
// exit/quit check never reached while agent was busy
if (lower === 'exit') { onExit(); exit(); }

Fix

Hoist the exit detection above the queue gate. Also call onAbort() before exit so any in-flight turn is cancelled cleanly. Covers bare exit / quit / q and slash-prefixed /exit / /quit.

Upgrade

npm install -g @blockrun/franklin@latest
franklin --version   # 3.8.33

Type exit (no slash needed) and press Enter — Franklin exits immediately, even mid-response.

v3.8.32 — Auto mode no longer looks 'stuck' on the last-routed model

23 Apr 05:29
6e0bdb1

Choose a tag to compare

The bug

User on blockrun/auto. First turn: router correctly picks Sonnet. Status line at startup says Auto → anthropic/claude-sonnet-4.6 ✓. But the bottom input-box status bar now permanently displays claude-sonnet-4.6 — forever. Every subsequent turn the router is re-routing correctly, but the UI lies about it.

Root cause

src/ui/app.tsx's usage event handler called setCurrentModel(event.model) — overriding the user's selection state (blockrun/auto) with whatever concrete model served the most recent turn. currentModel drives the bottom status bar and the default selection in the /model picker. Conflating it with per-turn resolved model poisons both.

Fix

Remove the setCurrentModel call from the usage handler. Per-turn resolved model is already captured in turnModelRef and rendered in the per-response summary line ([COMPLEX] sonnet-4.6 · 10K in / 176 out), which is the correct place for it.

Explicit model switches (user runs /model, the loop itself falls back on empty-response / 402) still flow through onModelChangeui.updateModelsetCurrentModel normally. The agent-side was never buggy — loop.ts already resets config.model to the user's baseModel at the start of every turn. Purely client-side bookkeeping bug.

Effect on user

Status bar stays on auto while you're in auto mode. Per-turn resolved model still visible under each response. /model picker now defaults to auto (what you actually chose) instead of whatever the last resolved concrete model was.

Upgrade

npm install -g @blockrun/franklin@latest
franklin --version   # 3.8.32

v3.8.31 — LLM-routed media generation (image + video)

23 Apr 04:47
6dac09b

Choose a tag to compare

BlockRun gateway shipped 8 image models + 4 video models. Franklin only knew 2 by name. This release catches Franklin up and rebuilds the flow so the user sees a cost preview before every image or video.

What changed for users

Call ImageGen or VideoGen with just a prompt — no model param needed. Franklin will:

  1. Pick the right model for the style + budget (free classifier reads the live catalog + your prompt).
  2. Ask you to confirm via a proposal like:
    *Media generation proposal*
    
    Prompt: "赛博朋克风格的动漫猫咪"
    
      ● Recommended  zai/cogview-4            ~$0.016  CogView-4 excels at anime/stylized imagery.
      ○ Cheaper      google/nano-banana       ~$0.011  Cheaper but less stylized.
      ○ Premium      xai/grok-imagine-image-pro ~$0.074  Premium detail for complex anime scenes.
    
      (prices include the 5% gateway fee)
    
    [Continue with zai/cogview-4] [Use cheaper] [Use premium] [Cancel]
    
  3. Only spend USDC after you pick one. Cancel = zero charge.

What changed architecturally

  • src/gateway-models.ts: pulls GET /api/v1/models?format=json once, caches 5 min. estimateCostUsd(model, ctx) dispatches on billing_mode (per_image / per_second / per_track / flat / free / paid) and applies the x402 5% gateway margin. No hardcoded prices — adding a model or changing a price on BlockRun's side needs no Franklin release.

  • src/agent/media-router.ts: one LLM call to llama-4-maverick. Reads the live catalog + the user prompt, returns {recommended, cheaper?, premium?, intent}. Classifier prompt enumerates the catalog inline and gives few-shot examples — no keyword allowlist.

  • imagegen.ts + videogen.ts: walk the router → AskUser flow unless the caller passed model explicitly (power-user override) or set FRANKLIN_MEDIA_AUTO_APPROVE_ALL=1 (scripts/CI).

Verified live

Prompt Router picked Estimate Alternatives
photoreal cat on Mars nano-banana-pro $0.105 cheaper/premium
赛博朋克风格的动漫猫咪 cogview-4 $0.016 cheaper/premium
10s Tokyo drone video seedance-2.0 $3.15 cheaper only
5s running dog clip seedance-1.5-pro $0.158 premium only

~1–1.5s classifier latency. All prices margin-adjusted against the gateway's real /v1/models pricing.

Opt-outs

  • FRANKLIN_MEDIA_AUTO_APPROVE_ALL=1 — skip AskUser (CI, batch)
  • FRANKLIN_NO_MEDIA_ROUTER=1 — fall back to legacy hardcoded default
  • Pass model explicitly in the tool call — power-user path

Follow-up (v3.8.32)

  • Panel Markets tab: 'Media generation' subsection listing catalog + recent generations
  • Realized cost parsing from response body (replaces pre-call estimate in Content ledger)

Upgrade

npm install -g @blockrun/franklin@latest
franklin --version   # 3.8.31

v3.8.30 — Panel one-click chain switcher

22 Apr 06:55
5a16064

Choose a tag to compare

Before: switching payment chain (Base ↔ Solana) required dropping to CLI (franklin setup solana). The Panel's Wallet page showed a chain pill but had no way to flip.

Now: a two-button toggle on the Wallet page. Click Base or Solana — Franklin writes ~/.blockrun/payment-chain, loads or creates a wallet on the target chain, and refreshes the QR / balance / address / sidebar.

API

POST /api/chain
  Body: {"chain": "base" | "solana"}
  Response: {ok, chain, address, balance}

Loopback-only (same defence-in-depth as /api/wallet/secret). Strict enum validation.

Behaviour

  • Panel wallet display flips immediately.
  • A currently-running franklin agent reads its chain at startup and won't hot-swap — the switcher note says "restart Franklin to use this chain" so this isn't surprising.
  • Each chain's private key is stored independently:
    • ~/.blockrun/wallet.key (Base, 0x...)
    • ~/.blockrun/solana-wallet.key (Solana, base58)
      Switching never overwrites the other chain's key.

Upgrade

npm install -g @blockrun/franklin@latest
franklin --version   # 3.8.30
# Restart Franklin to pick up the new Panel HTML. Wallet tab now has
# the Base / Solana toggle above the receive card.

v3.8.28 — Delete pushback + shouldPlan keyword engines

22 Apr 05:19
c2b6fea

Choose a tag to compare

PR 2 of the rule-engines-to-LLM refactor started in v3.8.27. Two hand-tuned keyword scoring blocks are gone, replaced by direct reads from the turn analyzer's flags.

Deleted (~110 lines of rule engine)

src/agent/loop.ts:

  • PUSHBACK_STRONG / PUSHBACK_WEAK regex arrays (correction patterns in zh + en + casual-speech disambiguator)
  • lastAssistantHasClaim() — checked whether the previous assistant turn had a claim worth pushing back against
  • detectPushback() — the orchestrator combining the two
  • Total: ~70 lines

src/agent/planner.ts:

  • AGENTIC_KEYWORDS regex (implement|refactor|build|fix|debug|…)
  • MULTI_STEP_PATTERN regex (first…then / step N / and then / …)
  • Tier + length + keyword scoring branches inside shouldPlan
  • Total: ~40 lines

Replaced with

Pushback: After turn-analyzer runs, if turnAnalysis.isPushback the harness prepends the existing SYSTEM NOTE to the user message. Same wording, LLM-judged boolean instead of regex match.

shouldPlan signature simplified to (profile, ultrathink, planDisabled, analyzerSaysNeedsPlanning). Keeps env / profile / ultrathink gates (operator policy). The keyword scoring is gone — the analyzer's needsPlanning boolean drives the decision.

Why this is better than 'smaller code'

  • Old pushback regex missed casual English correction patterns and misfired on but how do I deploy?. The analyzer reads the prior assistant turn as context and decides from phrasing + intent.
  • Old shouldPlan marked any 'refactor' / 'build' / 'fix' as plannable regardless of actual complexity. The analyzer distinguishes "refactor one function" from "refactor the wallet module across all call sites."

Graceful degradation

If the analyzer fails (timeout, parse error), CONSERVATIVE_DEFAULT sets isPushback=false and needsPlanning=false — same net behavior as the old code paths when they missed. Main flow never blocks on analyzer failure.

Upgrade

npm install -g @blockrun/franklin@latest
franklin --version   # 3.8.28

v3.8.27 — Unified turn analyzer: one classifier call replaces rule engines

22 Apr 05:14
c6f892f

Choose a tag to compare

The idea

Prior versions ran two separate classifier calls per turn (router + prefetch) plus several keyword rule engines (PUSHBACK_STRONG/WEAK regex, AGENTIC_KEYWORDS + MULTI_STEP_PATTERN for shouldPlan, shouldCheckGrounding length thresholds). Every new harness decision tempted us to add one more classifier call or one more keyword list.

v3.8.27 consolidates every LLM-decidable pre-turn question into one call with a structured JSON response.

New module

src/agent/turn-analyzer.ts:

analyzeTurn(userInput, { lastAssistantText, sessionGoal, client })
   { tier, intent, needsPlanning, isPushback, asksForLiveData }

Three-anchor context (current user message + first 300 chars of previous assistant reply + first 200 chars of session-opening prompt), capped at ~1500 chars total. Enough to resolve deictic follow-ups like "那 AAPL 呢" without shipping full history. 30s cache, 2.5s timeout, safe fallback on any failure.

Defaults to nvidia/llama-4-maverick (clean JSON output under tight max_tokens). Opt out with FRANKLIN_NO_ANALYZER=1.

Latency impact

Before After
Prefetch classify ~800ms
Router classify ~800ms
Total: ~1.6s Turn analyze ~1.0s

~40% reduction in pre-turn classifier latency.

Verified on seven prompts (EN/zh mixed)

All classified correctly in ~700-1500ms:

  • hi → SIMPLE
  • should I sell CRCL → COMPLEX + ticker CRCL (news=true)
  • 那 AAPL 呢 with CRCL prior context → COMPLEX + ticker AAPL (news=false)
  • BTC 为什么跌了 → COMPLEX + crypto BTC (news=true)
  • refactor wallet module... → MEDIUM + needsPlanning=true
  • prove sqrt(2) is irrational → REASONING
  • 不对 你应该看 NVDA → COMPLEX + NVDA + isPushback=true

What this un-locks

Follow-up PRs can delete the PUSHBACK_STRONG/PUSHBACK_WEAK regex lists, AGENTIC_KEYWORDS/MULTI_STEP_PATTERN scoring in shouldPlan, and shouldCheckGrounding's length thresholds in favor of reading turnAnalysis.isPushback / .needsPlanning / .asksForLiveData directly. ~300-400 lines of hand-tuned rule-engine code can come out.

Upgrade

npm install -g @blockrun/franklin@latest
franklin --version   # 3.8.27

v3.8.26 — Router classifies on original user input

22 Apr 04:12
7a4e6bd

Choose a tag to compare

Replaces v3.8.25's 'pin the previous model on grounding retry' hack with the root-cause fix: give the LLM classifier a stable input every iteration.

Before

Iteration 1 — classifier saw the real user question, classified COMPLEX, routed to Sonnet.

Iteration 2 (grounding retry) — the loop had pushed [GROUNDING CHECK FAILED] Your previous answer... into history. The classifier picked that up as 'latest user turn', saw terse error-recovery text, classified SIMPLE, routed to gemini. Tier drifted mid-task every time a retry fired.

The v3.8.25 workaround hard-coded 'if retry → pin previous model'. That's a code rule the classifier should learn on its own.

After

The router block now passes lastUserInput (the user's original prompt for this turn) to the classifier directly. Synthetic mid-turn feedback messages — grounding retry, verification retry, pushback-SYSTEM-NOTE wrappers — no longer poison the classifier's input. Same input → same tier → stable routing across iterations.

Side effect: the analogous case for verification retries now works the same way without needing a separate special-case.

Upgrade

npm install -g @blockrun/franklin@latest
franklin --version   # 3.8.26

v3.8.25 — Evaluator sees prefetch, grounding retry pins model

22 Apr 04:10
9000a77

Choose a tag to compare

Two bugs from a 2026-04-22 live CRCL run.

Bug 1 — Evaluator was blind to prefetched data

The intent-prefetch harness (v3.8.20) injects fetched data into the user message as a [FRANKLIN HARNESS PREFETCH] block, then the model answers from that injected context. But the grounding evaluator's summarizeTurn only scanned tool_use / tool_result message pairs — so it couldn't see the injection. Every prefetched answer came back looking like 'assistant made up specific prices and news items' → verdict UNGROUNDED.

Concretely, in the live run: CRCL price was correct ($96.18 from TradingMarket), news items were correct (Drift Protocol lawsuit, Compass Point downgrade, insider selling — all from ExaAnswer). User saw a correct answer followed by ⚠️ claiming it was wrong. Hard trust killer.

Fix: New extractPrefetchBlock() walks the most recent user message looking for the [FRANKLIN HARNESS PREFETCH] marker. When found, summarizeTurn includes it under a 'Pre-fetched by Franklin harness (counts as tool evidence)' section. The evaluator prompt now states explicitly: prefetched data is treated identically to a model-initiated tool call.

Bug 2 — Grounding retry reclassified on the retry message

When the evaluator returns UNGROUNDED, the loop injects a [GROUNDING CHECK FAILED] message and re-enters. The router's classifier picked that injected message as the 'latest user turn' and classified it as SIMPLE → routed to gemini-2.5-flash, dropping mid-task onto the weakest model in the pool.

Fix: loop.ts router block now pins the previous lastRoutedModel on retries. If groundingRetryCount > 0, skip the classifier call entirely and stay on the model the router originally picked.

Also noted in log, not fixed this release

The 503 UNAVAILABLE error in the log was Google GCP-style — likely surfaced from an upstream Gemini call inside the gateway. Franklin's transient-5xx retry eventually recovered, but there's still a gap: same-model retries don't fall across provider families (Sonnet 5xx → try Opus or GPT-5.4). Follow-up PR.

Upgrade

npm install -g @blockrun/franklin@latest
franklin --version   # 3.8.25

Retry the CRCL question — the ⚠️ should be silent this time, and retries (if they still fire) stay on Sonnet instead of falling to Gemini.