Releases: BlockRunAI/Franklin
v3.8.35 — prompt refinement for media generation
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:
- Scene — location, time, environment
- Subject — preserved exactly from your input
- Details — lighting, camera/lens, composition (concrete facts, not praise)
- Use Case — editorial photo, logo, UI screen, storyboard, etc.
- 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
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 insrc/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) andFRANKLIN_MODEL_STREAM_IDLE_TIMEOUT_MS(stream-idle phase). ParentAbortSignalnow 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 path —
src/telemetry/store.tsresolves both sides throughfs.realpathSyncbefore comparing. Fixes macOS/varvs/private/varsymlink drops that silently lost turns. - Live e2e skip —
test/e2e.mjsdetects gateway-unreachable andskips instead of flunking the suite. - Session picker polish —
resolvePickerSelection()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
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.33Type 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
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 onModelChange → ui.updateModel → setCurrentModel 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.32v3.8.31 — LLM-routed media generation (image + video)
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:
- Pick the right model for the style + budget (free classifier reads the live catalog + your prompt).
- 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] - Only spend USDC after you pick one. Cancel = zero charge.
What changed architecturally
-
src/gateway-models.ts: pullsGET /api/v1/models?format=jsononce, caches 5 min.estimateCostUsd(model, ctx)dispatches onbilling_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 tollama-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 passedmodelexplicitly (power-user override) or setFRANKLIN_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
modelexplicitly 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.31v3.8.30 — Panel one-click chain switcher
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
franklinagent 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
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_WEAKregex arrays (correction patterns in zh + en + casual-speech disambiguator)lastAssistantHasClaim()— checked whether the previous assistant turn had a claim worth pushing back againstdetectPushback()— the orchestrator combining the two- Total: ~70 lines
src/agent/planner.ts:
AGENTIC_KEYWORDSregex (implement|refactor|build|fix|debug|…)MULTI_STEP_PATTERNregex (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
shouldPlanmarked 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.28v3.8.27 — Unified turn analyzer: one classifier call replaces rule engines
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→ SIMPLEshould 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=trueprove 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.27v3.8.26 — Router classifies on original user input
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.26v3.8.25 — Evaluator sees prefetch, grounding retry pins model
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
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.25Retry the CRCL question — the