Skip to content

The reducer stack

AgentMux is built on a strict reducer model. Every cross-process state mutation goes through a single layer’s reducer, emits events that other layers project, and is persisted by the layer that owns the truth. This page is the entry point into that model.

The canonical living document for current status is docs/specs/MASTER_REDUCER_STACK_STATUS_2026-05-05.md in the source repo. The rolling tracking thread is Discussion #707. Both are appended to as work ships.

Three pressures drove it:

  1. Multi-process truth — the launcher knows OS facts, the host knows CEF facts, the sidecar knows app facts. Without a reducer model, each process develops ad-hoc gossip channels and the shared mental model rots.
  2. Auditable mutations — every command leaves a trail. Bug reports become “find the last dispatch(blockId, MoveBlockToWindow) for that block_id” instead of “did anyone call setUrl() mid-load?”.
  3. Testable invariants — pure update(state, command) functions are unit-testable without a CEF context. The agent-pane-state slice has 32 invariants asserted in its test file alone; none of them require a running app.
Reducer stack: five layers from bottom (Persistence) through Srv, Launcher, Host up to Frontend, connected by labeled arrows showing data flow.

Single-writer reducer over OS-level facts. Lives in agentmux-launcher. On Windows: reconciles AgentMux’s own model against Win32 reality via WRR hooks. On Linux (v0.42.x+): driven over Unix-domain-socket IPC from the host; same reducer, same 60+ command handlers, same saga coordinator — WRR drift detection not yet implemented. Durable JSONL event log at <data-dir>/data/launcher-events.log. Status: done.

CEF-side coordination: pending window creations, active drag operations, tear-off hooks, lifecycle. Lives in agentmux-cef. Status: partial — Phase B.5 shadow mirrors shipped; the host’s own reducer migration is in flight. Instance lifecycle has landed: quit-on-last-window (#1676) routes through the count_live_user_windows gate in agentmux-cef/src/reducer/quit.rs, and the window-count consolidation (#1701) makes the status-bar count accurate after CEF Views closes via an explicit WindowClosed event plus snapshot reconciliation in the frontend. Folding the remaining duplicated lifecycle/window-count state into a single reducer authority is the ongoing SSOT-consolidation work tracked in SPEC_REDUCER_SSOT_CONSOLIDATION_2026_06_22.

Owns the app domain — every workspace, tab, block, layout, agent identity, agent definition. Lives in agentmux-srv/src/reducer.rs plus the per-domain modules (block.rs, tab.rs, layout.rs, lifecycle.rs, snapshot.rs, window.rs, workspace.rs). Saga coordinator handles cross-block lifecycle. Idempotent persist subscriber writes back to SQLite. Status: mostly done.

Per-pane state cells that project the layers above into Solid signals. Each slice is a pure update(state, command) → { state, events } plus a slot store with register / dispatch / unregister lifecycle.

SliceWhat it ownsStatus
#1 — agent-document-storePer-pane document state cellshipped
#2 — conventionsSlot lifecycle, audit trail, echo-loop guardspec done
#3 — frontend-command-busSingle outbound dispatch chokepointspec done
#4 — agent-pane-state-storeStreaming, sessionStats, currentTool, turnTokens, turnActive, stopping, pendingshipped
#5 — frontend-layout-reducerPer-tab pane treespec done
#6 — launcher-event-reducerMirrors launcher window state into the frontendshipped
#7 — tab-state-reducerActive tab, tab orderspec done
#8 — pane-tree-reducerCross-tab pane mappingdeferred
#9 — browser-pane-statePer-pane closed / loading / error / canGoBack / canGoForward / title (cells migrated); url, faviconUrl pendingpartial: 4 phases shipped (3a/b/c/e); danger-cell url migration (3d) + slot store + recordDispatch (4) pending

A user clicks a link inside a browser pane. Here’s what happens:

  1. CEF captures the click. Win32 WM_LBUTTONDOWN fires on the pane’s HWND.
  2. Host emits browser-pane-clicked over the JS bridge. The launcher (Layer 1) records nothing — this is a renderer concern.
  3. Frontend slice receives the event. The handler blurs any main-input that held stale DOM focus (so giveFocus() doesn’t bounce OS focus back), then calls refocusNode(blockId). Today this is a direct call site; Phase 4 routes it through a PaneClicked reducer command + slot dispatch so the audit ring records every transition.
  4. Saga turns the event into a side effect: layout’s focus state updates. The layout slice (#5) records the change.
  5. Sidecar persists the new layout via the persist subscriber. The next time the user opens the same workspace, the focus is restored.

If any of those four hops fails — host doesn’t emit, slice doesn’t subscribe, saga drops the event, sidecar doesn’t persist — there’s exactly one place to look. That’s the value the reducer stack delivers over the prior ad-hoc model.

Every slice routes through command-source.ts’s recordDispatch(...) helper, which appends a structured record (slice name, key, command, emitted events, source, timestamp) to an in-memory ring buffer. The diagnostics panel surfaces this; debugging a “what mutated this state?” question becomes filtering the ring for the relevant key.

Sagas are the “what should happen as a side effect of this state change” layer. They live srv-side per SPEC_PHASE_E_SAGAS_2026-04-30.md (Path A — chosen during Phase E.5). Frontend slices subscribe to terminal saga events; orchestration is upstream.

This is intentional: the frontend can’t be the source of truth for “did this CLI command actually finish?” — the answer lives in the sidecar’s saga state, and the frontend projects it.

Each layer’s persistence is independent:

StoreWriterWhat
objects.db (SQLite)sidecarblock / tab / window / workspace meta, layouts
filestore.db (SQLite)sidecarper-block content blobs (agent configs, snapshots)
sagas.db (SQLite)sidecarsaga state for crash-resume
launcher-sagas.db (SQLite)launcherlauncher-side saga state
launcher-events.log (JSONL)launcherappend-only event log; in-memory ring with disk overflow

Each can be backed up independently. None of them depend on a single point of failure.

The TypeScript slice surface is documented in the TypeScript API reference on this site (the four slice store files). For the host and sidecar reducer code, see the upstream Rust source — agentmux-launcher/src/reducer/, agentmux-cef/src/, and agentmux-srv/src/reducer/ in the agentmux repo.

For deep design docs: