Releases: BlockRunAI/Franklin
v3.21.9 — VoiceCall: interruption_threshold + model controls
External contributor @BeneficialVast1048 shipped PR #66 closing #65 (filed by @KillerQueen-Z) — adds two optional pass-throughs to `VoiceCall` that the BlockRun gateway already accepted but Franklin wasn't surfacing.
New fields
| Field | Range / values | What it controls |
|---|---|---|
| `interruption_threshold` | integer 50–500 (ms) | How long the AI waits before talking over the recipient. Lower = polite, higher = AI dominates. One of the biggest factors in whether the call feels natural vs rude. |
| `model` | `base` / `enhanced` / `turbo` | Bland model tier — trades latency, quality, cost. |
Both optional and only forwarded when provided. Existing calls completely unchanged.
Bonus tidy
PR also extracts body-building into a reusable `buildVoiceCallBody()` helper so future optional pass-throughs land in one obvious place instead of scattering between schema + execute. Test covers the new fields and the refactor.
Gateway verification
Probed `/v1/voice/call` with both new fields, got past `.strict()` schema validation (hit the `task` length check downstream — proving unknown-field rejection didn't fire). Cross-checked against `blockrun/src/lib/bland.ts:39-40`.
Tests
406/406 pass (one new VoiceCall regression).
```
npm i -g @blockrun/franklin@3.21.9
```
v3.21.8 — strip RealFace from VideoGen (upstream pulled the surface)
Regression fix following BlockRun gateway commit `f527c3b` — "drop real-person video entirely." KYC at the upstream verification provider conflicts with BlockRun's wallet-only stance, so the gateway no longer accepts `real_face_asset_id` on `/v1/videos/generations`. Calls with it now 400.
Franklin shipped `real_face_asset_id` in v3.21.2 (matching the then-active gateway commit `b86d5e9`). v3.21.8 strips it:
- `VideoGenInput.real_face_asset_id` field removed
- `REAL_FACE_ASSET_ID_REGEX` + `REAL_FACE_MODELS` constants removed
- Client-side validation block (regex / model-gate / image_url-mutex) removed
- Body-forwarding line removed
- `input_schema.properties.real_face_asset_id` removed
Net: ~50 LOC removed. Existing video calls without `real_face_asset_id` are unaffected.
Other upstream Seedance changes — not actioned in this patch
Server-side now defaults to 720p + audio (commit `e6dc1f1`). Cost-per-call goes up unless the caller explicitly passes a lower resolution. New optional body fields (`generate_audio` / `resolution` / `seed` / `watermark` / `return_last_frame`, commit `4564119`) are accepted by the gateway but not yet surfaced through Franklin. Both deferred to a separate plan.
For Token360's RealFace path going forward: BlockRun's docs at `/docs/video/real-person-ip` walk users through enrollment + `ta_xxxx` asset id; users call Token360 directly. Not something Franklin should re-add as long as the gateway stays wallet-only.
Tests
405/405 pass.
```
npm i -g @blockrun/franklin@3.21.8
```
v3.21.7 — PredictionMarket schema realignment + agent-loop retry guard
External contributor @KillerQueen-Z shipped PR #62 — 9 distinct bugs fixed across all 10 `PredictionMarket` actions, plus a small agent-loop retry-guard fix.
What was broken
`PredictionMarket` was modeled on the public Polymarket Gamma / Kalshi API conventions, but data actually comes from Predexon's normalized v2 schema behind the BlockRun gateway. Hand-written types cast from `unknown` gave no compile-time signal, so every field/param mismatch was invisible until a live call.
| # | Action | Symptom | Fix |
|---|---|---|---|
| 1 | searchPolymarket | 422 every call | status enum is `{open, closed}`, not `active` |
| 2 | searchPolymarket / searchKalshi | metrics `n/a` | use `total_volume_usd` / `outcomes[].price` (PM), `last_price` (Kalshi) |
| 3 | crossPlatform | blank titles | venues are UPPERCASE nested objects |
| 4 | leaderboard | "no data" + n/a | rows under `entries`, stats under `metrics.*`, `sort_by` enum |
| 5 | smartActivity | 400 every call | requires ≥1 smart-wallet criterion |
| 6 | smartMoney | 400 → wrong shape | requires criterion; response is single `positioning` aggregate |
| 7 | smartMoney | 404 when chained | `condition_id` was truncated to 14 chars in search output |
| 8 | searchAll / searchKalshi | status rejected | `active` normalization missing |
| 9 | agent loop | spun to 50-call cap on Predexon 422 | external-wall guard didn't treat 400/422 as retry-useless |
Agent loop guard fix
`EXTERNAL_WALL_FAILURE_PATTERN` now matches `400` and `422` too — "the same bad payload won't recover by hammering the endpoint." Predexon 422s aren't billed, so the cost guard never kicked in either; the agent ran up to the 50-call `HARD_TOOL_CAP` before stopping. `404` stays out of the pattern (legitimate "retry with a different query" signal).
Verified live
PR author re-ran the 4 article-example workflows (compare Fed-cut odds, leaderboard read, smart-activity, smart-money) end-to-end through `franklin start -p`. Before: 422s, `n/a`s, runaway loops costing ~$0.30. After: real volumes, implied odds, and the 1.8% vs ~5% Polymarket-vs-Kalshi arbitrage read.
405/405 tests pass.
```
npm i -g @blockrun/franklin@3.21.7
```
v3.21.6 — VoiceCall: voicemail controls
External contributor @KillerQueen-Z shipped PR #61 adding two optional params to the `VoiceCall` tool so the agent can control voicemail behavior from natural language.
What
- `voicemail_action`: `hangup` | `leave_message` | `ignore`
- `voicemail_message`: the monologue spoken when `leave_message` is set (1–1000 chars)
Before this, a request like "call my client, and if it goes to voicemail leave this message..." had nowhere structured to go — it could only be stuffed into the free-text `task` prompt as fragile if-then logic.
Tool spec note: voicemail is one-way. `leave_message` speaks the message once and hangs up, no back-and-forth.
What stays the same
Both fields are optional and only forwarded when provided. Ordinary calls are completely unchanged — Bland.ai still hangs up on voicemail by default unless the caller explicitly opts in.
Gateway dependency
Required matching change on the BlockRun gateway side (blockrun#26) because the call body is validated with `.strict()`. That PR landed + deployed before this Franklin release shipped; gateway acceptance verified live via the 402 schema-response probe before merge.
Tests
405/405 pass. No regressions.
```
npm i -g @blockrun/franklin@3.21.6
```
v3.21.5 — UI: inline short pastes (≥5 line threshold)
External contributor @KillerQueen-Z shipped PR #60 fixing a real bracketed-paste UX bug.
The bug
Every paste — even a single-sentence prompt — was being replaced in the input box with a `[Pasted ~N lines]` placeholder, so the user couldn't see what they pasted. Root cause: `findPasteBlocks(...) > 0` triggered the collapse unconditionally with no line-count threshold.
The fix
New `PASTE_COLLAPSE_LINE_THRESHOLD = 5` constant. Pastes shorter than 5 lines render inline as plain text; longer pastes still collapse to a placeholder. Decode at submit time is unchanged — both branches expand any preserved placeholder back to the original content before the model sees it.
| Paste | Before | After |
|---|---|---|
| 1-line, 230-char prompt | `[Pasted ~1 line]` | inline |
| 4-line stack trace | `[Pasted ~4 lines]` | inline |
| 5-line code block | `[Pasted ~5 lines]` | `[Pasted ~5 lines]` |
| 50-line log dump | `[Pasted ~50 lines]` | `[Pasted ~50 lines]` |
Single-file change to `src/ui/app.tsx`. 405/405 tests pass.
```
npm i -g @blockrun/franklin@3.21.5
```
v3.21.4 — Phone/Voice tool perms fix + VoiceStatus internal polling
External contributor @KillerQueen-Z shipped PR #59 fixing two real bugs from a session today, and on top of that refactored `VoiceStatus` to block-and-poll internally. Bundling with a `/phone-call` skill update so the agent's mental model lines up with the new tool shape.
Bug 1: spammy "Allow?" prompts on every VoiceStatus poll
PR #58 wired 8 typed Phone/Voice tools but skipped the permissions classifier — so every `VoiceStatus` poll during an in-progress call triggered an interactive prompt. 11 prompts during a single call in the repro.
Fixed by classifying each tool by side-effect, not by price:
| Tool | Category | Why |
|---|---|---|
| `ListPhoneNumbers`, `PhoneLookup`, `PhoneFraudCheck`, `VoiceStatus` | READ_ONLY | Info queries — don't change the world. Same treatment as `ImageGen` / `ExaSearch` which also cost USDC. |
| `VoiceCall` | ASK | Dials a real human, irreversible |
| `BuyPhoneNumber`, `RenewPhoneNumber` | ASK | Holds / extends number for 30 days, costs $5 |
| `ReleasePhoneNumber` | ASK | Permanently returns number to pool |
Bug 2: signature-loop guard killed manual polling
Franklin's anti-infinite-loop guard kills turns at 5 identical inputs. `VoiceStatus` poll cadence (every 30 s while a call ran) hit that wall reliably. PR #59 refactored `VoiceStatus` to block-and-poll internally — 5 s interval, 35 min ceiling, returns when call reaches a terminal state (`completed` / `failed` / `cancelled` / `busy` / `no-answer` / `voicemail`).
Same pattern as `videogen.ts pollUntilReady` + `imagegen.ts pollImageJob`. Agent mental model collapses to "fire VoiceCall, then VoiceStatus once, get transcript when it ends."
`/phone-call` skill updated
Step 6 was "loop VoiceStatus every 30 s for up to 10 min." Now: "call VoiceStatus once and wait for completion." Without the skill update the LLM would still try to loop — unnecessary AND harmful (signature guard).
Tests
405/405 pass. No regressions.
```
npm i -g @blockrun/franklin@3.21.4
```
v3.21.3 — call cost displays correctly + Calls tab XSS hardening
Two issues found reviewing v3.21.2 against real call data.
Bug: call cost showed $0 instead of $0.54 after completion
`VoiceCall` POST writes a "queued" row with `paid_usd: 0.54` on initiation. Subsequent `VoiceStatus` polls (free) write update rows with `paid_usd: 0`. The panel's `/api/calls` endpoint used `log.summary()` / `log.byCallId()`, which returned the latest row per `call_id` — so the completed-status row's `paid_usd: 0` overwrote the initial $0.54 in the UI.
Fixed two ways (belt-and-suspenders):
- `src/phone/call-log.ts` — `summary()` and `byCallId()` now take `Math.max` of `paid_usd` across all rows for a given `call_id`. Largest charge wins regardless of row order or update timing.
- `src/tools/voice.ts` — `VoiceStatus` update writes carry `prior.paid_usd` instead of `0`. Non-aggregating readers also see the right value.
Two new test assertions pin the behavior. 405/405 tests pass (was 404).
Hardening: Calls tab XSS
Call data (transcripts especially, which flow unfiltered from Bland.ai) is now sanitized end-to-end:
- New `escapeHtml()` helper covers `& < > " '` (was previously only `& < >`)
- New `safeHttpUrl()` validates `http:` / `https:` protocol on recording URLs before injecting as `` — blocks `javascript:` URL smuggling
- All Calls-tab user-data renders now go through `escapeHtml`; recording href goes through `safeHttpUrl` + `escapeHtml`
Immediate risk is theoretical (the journal is local-only), but the panel renders content directly to the DOM and the discipline is worth having.
Also bundled
Deep-link from URL hash to the Calls tab — `localhost:3100/#calls` now auto-loads the list. Previously rendered empty until you clicked the sidebar.
```
npm i -g @blockrun/franklin@3.21.3
```
v3.21.2 — VideoGen RealFace + panel rebrand
Aligns Franklin with two recent BlockRun gateway changes.
RealFace asset support in VideoGen
Matches BlockRun b86d5e9. The gateway's `/v1/videos/generations` route now accepts an optional `real_face_asset_id` for Seedance 2.0 variants — BytePlus RealFace seeds the first frame from a real-person asset for cross-frame character consistency.
VideoGen adds the field with full client-side validation:
- Regex `^ta_[A-Za-z0-9]+$`
- Model gate: `bytedance/seedance-2.0` and `bytedance/seedance-2.0-fast` only — 1.5 Pro + non-Seedance reject with a clear error
- Mutually exclusive with `image_url` (both seed the first frame; client refuses both at once)
Client-side checks save an x402 round-trip; the gateway returns 400 on the same conditions anyway.
Users get asset IDs (`ta_`) from token360's Asset UI after H5 verification.
Panel rebrand: "Franklin" → "Franklin Agent"
Matches BlockRun `f69ffdb`. Two strings updated in `franklin panel`:
- Sidebar `
`: "Franklin" → "Franklin Agent"
- Browser tab title: "Franklin Panel" → "Franklin Agent Panel"
Terminal banner already said "Franklin Agent v3.X.X" since v3.8.17 — the visible brand is now consistent across panel + CLI.
The big watermark behind the panel content stays "FRANKLIN" — styled hero element, renaming breaks its sizing.
```
npm i -g @blockrun/franklin@3.21.2
```
v3.21.1 — fix: typed Phone + Voice tools now report cost
The bug
PR #58 (v3.20.2) shipped the typed Phone + Voice tools without wiring `recordUsage()` telemetry. Real-world repro after v3.21.0 shipped:
Fired `VoiceCall` for a $0.54 outbound call. The x402 payment settled on Base correctly (recipient picked up, transcript delivered, journal row wrote to `~/.blockrun/calls.jsonl`) — but the status bar only showed `-$0.0039` (the LLM cost). The $0.54 never landed in `franklin-stats.json`, so the per-turn spend delta lied.
The fix
Thread `{ tool, priceUsd }` through `postWithPayment` / `getNoPayment` in `src/tools/phone.ts` and `src/tools/voice.ts`. Call `recordUsage(tool, 0, 0, priceUsd, latencyMs)` on every successful response. Errors don't record (gateway doesn't charge on 4xx). Telemetry is best-effort — never blocks a paid call.
Costs now reported
| Tool | Price |
|---|---|
| `ListPhoneNumbers` | $0.001 |
| `BuyPhoneNumber` | $5.00 |
| `RenewPhoneNumber` | $5.00 |
| `ReleasePhoneNumber` | free |
| `PhoneLookup` | $0.01 |
| `PhoneFraudCheck` | $0.05 |
| `VoiceCall` | $0.54 |
| `VoiceStatus` | free |
`franklin` status bar, panel Audit tab, and `franklin stats` now all show the real x402 spend per Phone/Voice tool call. 404/404 tests pass.
```
npm i -g @blockrun/franklin@3.21.1
```
v3.21.0 — /phone-call skill + call journal + panel Calls tab
Phone-call orchestration
PR #58 (v3.20.2) shipped the thin typed `VoiceCall` / `VoiceStatus` tools. v3.21.0 layers the orchestration on top so calling a phone is one skill invocation away.
`/phone-call` skill — seven-step workflow:
- Extract recipient + task from the user's request
- List wallet-owned numbers via `ListPhoneNumbers` ($0.001); refuse if 0
- Compose the task script using a reusable template
- Confirm the full plan (to, from, cost, voice, max_duration, task summary)
- Fire `VoiceCall` ($0.54) — async, returns `call_id`
- Auto-poll `VoiceStatus` (free) every ~30s until terminal status or 10min cap
- Surface transcript, duration, recording URL, total cost
Compliance baked in. US/CA only. Daytime preference flagged in the confirmation step. TCPA prior-consent requirement for any task script that reads like outbound marketing/sales — refused without an explicit user attestation. No auto-fired follow-ups; the user reconfirms every call.
Call journal
`~/.blockrun/calls.jsonl` — append-only. `VoiceCall` writes a queued row on initiation; `VoiceStatus` appends a fresh row on every poll with updated status / transcript / recording / duration. `summary()` returns one row per `call_id` (latest wins) — the canonical "recent calls" view.
Panel "Calls" tab
`franklin panel` → new sidebar nav between Phone and Sessions. Lists recent calls with:
- Status badge: green (completed), amber (queued / in_progress), red (failed / no-answer / busy / voicemail)
- To / from numbers, duration (m:ss), cost ($0.54), timestamp, recording link
- Expandable `` block for the full transcript
Two read-only panel endpoints (loopback + same-origin guarded):
```
GET /api/calls?limit=50 — summary list
GET /api/calls/:callId — single-call detail
```
Read-only by design — placing calls from the panel is deferred to a future release (would need to render the confirmation gate the agent has).
What didn't ship
- Placing calls from the panel UI — deferred.
- Inbound call handling — Bland.ai webhooks not yet wired by BlockRun upstream.
- Voice cloning, conference calls — separate plans.
- Auto-renewal of caller-ID when ≤2 days from lease expiry — separate plan.
- Per-token chat billing & re-enable `/surf-chat` — BlockRun's call, not Franklin's.
Verification
- 404/404 tests pass — 5 new CallLog tests + isTerminalStatus + no regressions.
- Live e2e gated behind `VERIFY_CALL_E2E=1` in `scripts/verify-call.mjs` (real $0.54 — opt-in only).
```
npm i -g @blockrun/franklin@3.21.0
franklin skills # lists 8 bundled skills including phone-call
```