---
title: Bake-Off (fan-out)
---

# Bake-Off (fan-out)

## What it is

> **Name:** shown in the UI as **Bake-Off**. The internal/code name stays **fan-out** (setting
> `fanoutEnabled`, IPC `SESSION_FANOUT`, control-server route `/fanout`, the `fanout` feature id) —
> renaming the label never renames the identifiers.

> **Status: shipped — visible to everyone.** Bake-Off ships in the **Prompt Tools** sidebar group (its
> Bake-Off row) and via the command palette; the `fanout` feature is now `status: 'shipped'`, so the
> old `fanoutEnabled` Lab toggle no longer gates it. This page is the authoritative reference for how
> it works.

Bake-Off lets you type **one prompt once** and launch it as **many separate sessions at the same
time** — each session running in a project you choose, on a harness/model setup you choose. It's the
"run this same task across everything and compare" launcher: the same prompt against Opus vs. Sonnet
vs. Codex, or against three different repos, or any mix.

You don't build a list of individual sessions row-by-row. Instead you fill in **two sections** and
Bake-Off launches the **cross-product**:

1. **Projects / repos** — a multi-select dropdown of your real projects. Pick as many as you want.
2. **Harness & model setups** — a list of setups. Each setup is the *exact same* configuration bar
   you get on a new session (Harness · Provider · Model · Thinking level · MCP servers · Local/Cloud),
   reused as a shared module so there is only one thing to maintain, not two. Add as many setups as
   you want.

It then launches **one session per (project × setup) pair**. A live line shows the math —
"2 projects × 3 setups = 6 sessions" — and the Launch button carries the count. If a setup leaves a
field on default, the session inherits the project/global default exactly like a normal new session.

**Each session is named `Bake Off [<model>]: <title>`.** The model (or the provider, when a setup
names no explicit model) comes **first** so you can tell the variants apart in the sidebar even when
the name is truncated, and `<title>` is a **real AI-generated title** of your prompt — produced by the
same titler every other session uses, not the raw prompt text (so a prompt that opens with a URL no
longer becomes a URL "name"). The title is generated **once per launch** and shared across all the
variants (they run the same prompt); a name you type into Bake-Off replaces the `<title>` half.

**Launching is instant (fire-and-forget).** Clicking Launch closes Bake-Off *immediately* and runs
the launch in the background — you're never held on a "Launching…" spinner. A summary toast confirms
the result once it settles. If the launch **fails** — the whole batch errored, or every target was
skipped so nothing started — an error toast appears with a **Try again** link that reopens Bake-Off
(a fresh dialog) so you can adjust and relaunch. (Above 10 sessions it still asks you to confirm
before it dismisses.)

## Where to find it

### How you open it

Two ways, both opening the same dialog described above:

- The **Bake-Off** row in the **Prompt Tools** sidebar group (an action row — clicking it opens the
  dialog rather than switching projects).
- The command-palette action **"Bake-Off…"** (Quick Launch / command palette).

## How it behaves

### Presets (save a setup and reload it)

Bake-Off can **save your configuration as a named preset** so you can rebuild a comparison in one
click instead of re-picking projects and setups every time. The **Presets bar** sits at the top of
the dialog:

- **Save** — names the current setup (its projects, its harness/model setups, and — if you leave the
  **"Include prompt"** box checked — the prompt) and stores it. Saving under a name that already
  exists asks you to confirm an overwrite.
- **Load a preset…** — pick a saved preset from the dropdown to drop it back into the dialog. It
  restores your projects and setups; it fills the prompt **only if that preset saved one**, so a
  "contestants-only" preset (saved with the prompt box unchecked) never wipes a prompt you are in the
  middle of writing.
- **Delete** — removes the loaded preset after a confirmation, with an **Undo** in the toast.

Presets are stored in your app settings (the same place your other preferences live), so they persist
across restarts. A preset that points at a project you have since deleted still loads — the missing
project is simply dropped. A preset that names a model or harness that has since been retired also
loads fine; that one setup is just skipped-with-a-reason at launch, exactly like any other bad target.

### Contestants who know about each other (optional)

By default the contestants are strangers: each one gets your prompt and nothing else, and none of
them knows the others exist. That is usually what you want — the whole point of a bake-off is
comparing independent attempts.

The **"Let the contestants know about each other"** toggle, under the harness setups, changes that.
It is **off by default**, and turning it on prefixes every contestant's first prompt with a short
briefing — sent to the model, never shown in your chat, which still displays exactly what you typed.

Each contestant is told:

- that it is one of N sessions running the same task at the same time, and **which one it is**;
- **every other contestant** — its model (the same label the sidebar shows), its project, and its
  **real session id**;
- the exact call to message one: `POST /sessions/<their id>/peer-message`, which token file to
  authenticate with, and which header to send.

It never tells them what to do with that. The copy states who is there and how the channel works and
stops — no "collaborate", no "stay independent". That is a deliberate owner decision: a nudge in
either direction would quietly decide the outcome of the comparison you launched. What the
contestants do with the channel is up to them.

**The ids are real, not a lookup hint.** Bake-Off mints every contestant's session id *before* it
starts launching, so contestant 1 can be told contestant 3's id even though contestant 3 does not
exist yet. Without that, no contestant could ever know about the ones created after it.

Details worth knowing:

- **A skipped target never appears in anyone's roster.** Validation runs first, so if a project was
  deleted the survivors are told there are two contestants, not three.
- **One contestant gets nothing.** If only a single session survives, there is no "you are 1 of 1"
  preamble — the prompt is untouched.
- **A cloud contestant gets the roster but no messaging recipe.** It runs on a remote VM that cannot
  reach the control server on your machine, so it is told that plainly instead of being handed an
  address that would fail. It still appears in everyone else's roster and can still be messaged by
  them — delivery happens inside Omniscio, not over the network to the VM.
- **A contestant that failed to start refuses messages**, and the briefing says so, so a peer does
  not read a refusal as its own mistake.
- **Presets remember the toggle.** A preset saved before this existed loads with it off.

### Guardrails (why it can't hurt you)

- **Capped at 20 sessions per launch.** Over the cap, the Launch button is disabled with a hint —
  it never silently drops targets. Above 10, it asks you to confirm first ("This starts N sessions").
- **It can't freeze your machine.** Every fan-out spawn is a *background* spawn and is NOT marked
  user-initiated, so it flows through Omniscio's existing session-spawn pacer (the same throttle that
  protects the box from a burst of respawns). A fan-out is a controlled, paced burst — not an
  un-throttled storm. Session count is never the cause of a freeze; the pacer serializes the launches.
- **Bad targets are skipped, not fatal.** If a target's project is missing, isn't a real spawnable
  folder, or names a model a harness no longer offers, that one target is **skipped with a reason**
  and the rest still launch. You get a "launched X, skipped Y" summary toast.
- **A paced target is queued, not skipped.** When the box is already busy the spawn pacer may accept
  a target and hold it for a moment instead of refusing it. That target is **queued** — it is coming,
  and the summary says so — rather than reported as a failure or silently dropped.
- **MCP applies to the first turn.** Because a fan-out session's whole point is the one first turn,
  any per-setup MCP-server override is stamped on the session row *before* the first spawn (not after),
  so it takes effect immediately.

## For agents

### Under the hood (for agents)

- **UI**: `src/renderer/src/features/fanout/FanOutDialog.tsx` (two sections + cross-product launch),
  `ProjectMultiSelect.tsx` (built on the shared `AnchoredPopover`). Each setup renders the shared
  `StartConfigPicker` in **value mode** — the same component the new-session empty state renders in
  **session mode** (`src/renderer/src/features/sessions/StartConfigPicker.tsx` +
  `start-config-sink.ts`). Value mode drives a plain config value with no live session and no
  store/IPC writes.
- **Backend**: the renderer calls `IPC.SESSION_FANOUT` → `fanoutLaunch(...)`
  (`src/main/services/session/fanout-launch.ts`), which loops the targets through the normal
  `launchSessionInMain` chokepoint (one per target, `background: true`, `source: 'fanout'`). Provider,
  model, thinking, MCP, and Local/Cloud are all applied the same way a normal single launch applies
  them.
- **CLI**: `POST /fanout` on the control server (`cli-server-fanout-routes.ts`) mirrors the exact
  deny-by-default auth of `POST /project/:name/new` (header Bearer; agent-session tokens are refused
  unless `allowAgentTokenSpawn` is on). Body is the same `{ prompt, targets }` shape.
- **Plugins**: a first-party plugin can call `session.fanout` (webview or worker bridge) behind a new
  `sessions.launchAny` permission — but the gate is **builtin-only**: a marketplace or dev plugin that
  declares the permission is still refused (`dispatchPluginFanout` enforces builtin source + declared
  permission before it ever reaches `fanoutLaunch`).
- **Not on your phone**: `SESSION_FANOUT` is blocked from the mobile/web WS bridge, because it spends
  real money.
- **Peer awareness**: `peerAwareness` on `fanoutLaunchSchema`. `fanoutLaunch` splits into a
  validate+RESERVE pre-pass (each surviving target gets a `randomUUID()` before any spawn) and a
  launch pass that renders one briefing per contestant via the pure
  `buildBakeOffBriefing` in `src/shared/fanout.ts`. The reserved id rides `launchSessionInMain`'s
  main-process `opts` bag — never the renderer schema — and is forwarded by all three row-creating
  branches; a structural guard (`tests/unit/lint/launch-forwards-reserved-id.test.ts`) fails the
  build if one stops. The flag is part of `fanoutDedupKey`, so toggling it makes a genuinely
  different batch. Invariant **F12** in `fanout-launch-contract.md`.
- **Presets**: saved in the governed `fanoutPresets` settings array (race-safe per-item merge, reusing
  `SETTINGS_PATCH_ARRAY_ITEM` — no new channel). Shape + Zod in `src/shared/fanout-presets.ts`
  (deliberately LENIENT so a retired provider/model can't poison the whole array); renderer mappers +
  the Presets bar in `src/renderer/src/features/fanout/`. Loading a promptless preset never clobbers a
  typed prompt.

### Contract

Invariants (skip-with-reason, cap, background+paced, MCP-before-spawn, the `fanout` source, the gated
CLI route, the phone-block, the first-party plugin gate, the one shared config module, the
fire-and-forget launch + re-open-on-problem UX, and opt-in peer awareness with pre-reserved ids) are
locked in `fanout-launch-contract.md`. The plugin
builtin-only gate is `plugin-bridge-hardening-contract.md` `session-fanout-is-builtin-only`.

## Related

The provider pages — [Claude providers](ai-providers.md), [Gemini](gemini-provider.md), [DeepSeek](deepseek-provider.md) and the rest — explain which models a Bake-Off can put in the ring and what each costs.
