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.
Frontend (renderer / SolidJS) per-process atoms consumer
▲ slice-based reducer migration (9 slices, in flight)
┃ CEF JS bridge ▲ events
Host (agentmux-cef) FFI + UI thread Layer 2
▲ pending_window_creations, active_drag, tear_off_hooks
▲ deliberately retained scaffolding: browsers, window_pool
┃ launcher → host pipe (in flight)
Launcher (agentmux-launcher) process & OS facts Layer 1
▲ lifecycle, processes, windows, monitors, pool, registries
▲ WRR (Window Reality Reconciliation) via Win32 hooks
┃ launcher pipe (named pipe IPC)
Srv (agentmux-srv) app domain Layer 3
▲ workspaces, tabs, blocks, layouts, agents, identity
▲ saga coordinator (Path A — chosen E.5)
┃ persist subscriber (idempotent SQLite write-back)
Persistence durability
▲ objects.db, filestore.db, sagas.db, launcher-sagas.db
▲ launcher-events.log (JSONL); in-memory ring (4096) + disk

Single-writer reducer over OS-level facts. Lives in agentmux-launcher. Reconciles AgentMux’s own model against Win32 reality via WRR (Window Reality Reconciliation) hooks. Durable JSONL event log at <data-dir>/data/launcher-events.log (one log file per version, appended to by every same-version launcher). 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.

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: