pub enum Command {
Show 61 variants
Register {
kind: ClientKind,
pid: u32,
version: String,
},
Ping {
nonce: u64,
},
Goodbye,
ReportWindowOpened {
label: String,
kind: WindowKind,
parent_label: Option<String>,
},
ReportWindowClosed {
label: String,
},
ReportPoolWindowAdded {
label: String,
saga_id: Option<u64>,
},
ReportPoolWindowRemoved {
label: String,
},
ReportHostCounts {
windows: u32,
pool: u32,
},
ReportHostPoolCount {
count: u32,
},
ReportBackendWindowIdRegistered {
label: String,
window_id: String,
},
ReportBackendWindowIdUnregistered {
label: String,
},
ReportHwndOpened {
hwnd: u64,
class_name: String,
title: String,
label_hint: Option<String>,
},
ReportHwndDestroyed {
hwnd: u64,
},
ReportHwndVisibilityChanged {
hwnd: u64,
visible: bool,
},
ReportHwndForegroundChanged {
hwnd: u64,
},
ReportHwndIconicChanged {
hwnd: u64,
iconic: bool,
},
ReportHwndPositionChanged {
hwnd: u64,
rect: Rect,
},
ReportMonitorTopologyChanged {
rects: Vec<Rect>,
},
GetSnapshot,
GetSrvSnapshot,
CreateWorkspace {
name: String,
},
DeleteWorkspace {
workspace_id: String,
force: bool,
},
CreateTab {
workspace_id: String,
name: String,
},
DeleteTab {
workspace_id: String,
tab_id: String,
force: bool,
},
SetActiveTab {
workspace_id: String,
tab_id: String,
},
ReorderTab {
workspace_id: String,
tab_id: String,
new_index: u32,
},
CreateWindow {
window_id: String,
workspace_id: String,
},
CloseWindowInternal {
window_id: String,
},
SwitchWorkspace {
window_id: String,
workspace_id: String,
},
ReorderTabsBulk {
workspace_id: String,
tab_ids: Vec<String>,
},
RenameWorkspace {
workspace_id: String,
name: String,
},
RenameTab {
tab_id: String,
name: String,
},
UpdateWorkspaceMeta {
workspace_id: String,
meta_patch: Value,
},
UpdateTabMeta {
tab_id: String,
meta_patch: Value,
},
UpdateBlockMeta {
block_id: String,
meta_patch: Value,
},
UpdateWindowMeta {
window_id: String,
meta_patch: Value,
},
MoveTab {
tab_id: String,
src_workspace_id: String,
dst_workspace_id: String,
dst_index: u32,
},
MoveBlock {
block_id: String,
src_tab_id: String,
dst_tab_id: String,
dst_index: u32,
},
CreateBlock {
tab_id: String,
meta: Value,
},
DeleteBlock {
tab_id: String,
block_id: String,
},
SetFocusedNode {
tab_id: String,
node_id: String,
},
SetMagnifiedNode {
tab_id: String,
node_id: String,
},
LayoutInsertNode {
tab_id: String,
node: LayoutNode,
parent_id: Option<String>,
index: Option<usize>,
focus_after: bool,
magnify_after: bool,
correlation_id: String,
},
LayoutInsertNodeAtIndex {
tab_id: String,
node: LayoutNode,
index_arr: Vec<usize>,
focus_after: bool,
magnify_after: bool,
correlation_id: String,
},
LayoutDeleteNode {
tab_id: String,
node_id: String,
correlation_id: String,
},
LayoutMoveNode {
tab_id: String,
node_id: String,
new_parent_id: String,
index: usize,
correlation_id: String,
},
LayoutSwapNodes {
tab_id: String,
node1_id: String,
node2_id: String,
correlation_id: String,
},
LayoutResizeNodes {
tab_id: String,
ops: Vec<ResizeOp>,
correlation_id: String,
},
LayoutReplaceNode {
tab_id: String,
target_id: String,
new_node: LayoutNode,
focus_after: bool,
correlation_id: String,
},
LayoutSplitHorizontal {
tab_id: String,
target_id: String,
new_node: LayoutNode,
position: SplitPosition,
focus_after: bool,
correlation_id: String,
},
LayoutSplitVertical {
tab_id: String,
target_id: String,
new_node: LayoutNode,
position: SplitPosition,
focus_after: bool,
correlation_id: String,
},
LayoutClear {
tab_id: String,
correlation_id: String,
},
LayoutSetTree {
tab_id: String,
new_tree: Option<LayoutNode>,
correlation_id: String,
},
GetEvents {
since: u64,
},
ReportPoolWindowPromoted {
label: String,
},
SpawnPoolWindow {
saga_id: u64,
},
ReportPanesReaped {
label: String,
saga_id: Option<u64>,
},
ReportPoolDrainDecision {
label: String,
was_last: bool,
saga_id: Option<u64>,
},
ReapPanes {
label: String,
saga_id: u64,
},
DrainPoolIfLast {
label: String,
saga_id: u64,
},
ReportSagaActionFailed {
saga_id: u64,
reason: String,
},
}Expand description
Commands flow client → launcher.
Variants§
Register
Identifies the connection. MUST be the first command on every new connection. Server enforces.
Fields
kind: ClientKindPing
Health probe — server replies with Event::Pong carrying the
same nonce. NOT a polling heartbeat (per spec §4.3) — sent
only on demand by clients that need round-trip confirmation.
Goodbye
Graceful disconnect. Server logs and closes the connection.
In B.3+ this becomes Quit { reason } with shutdown semantics;
for B.2 it’s just a polite goodbye.
ReportWindowOpened
Phase B.4: host reports that a real window has been created
(CEF on_after_created fired). Launcher records it in its
read-only mirror and broadcasts Event::WindowOpened to other
subscribers. Pool windows do NOT report via this command —
they get their own ReportPool* commands in a follow-up so
the mirror can distinguish user-visible windows from pool
inventory.
Fields
kind: WindowKindReportWindowClosed
Phase B.4: host reports a window is closing (on_before_close).
Launcher removes from mirror, broadcasts Event::WindowClosed.
Idempotent: a missing label is logged but not an error (covers
the close-before-launcher-saw-the-open race).
ReportPoolWindowAdded
Phase B.4 follow-up — host reports a pre-warmed pool window
being added (spawn_pool_window). Pool windows live in a
SEPARATE map from the user-visible window mirror; the host
transitions them out of the pool with ReportPoolWindowRemoved
ReportWindowOpenedon promote, or justReportPoolWindowRemovedon pre-promote destroy.
Fields
saga_id: Option<u64>Phase CPD-1 (cross-process dispatch) — saga correlation
echo. Some(N) when the host is replying to a saga-issued
Command::SpawnPoolWindow { saga_id: N }; None for
organic refills (e.g. host’s implicit spawn_pool_window
inside promote_pool_window). The launcher reducer
passes the value through to Event::PoolWindowAdded so
per-saga correlation (CPD-4) can match the response to
the originating saga.
#[serde(default)] for forward-compat with hosts running
pre-CPD-1 builds — they emit no saga_id field, which
deserializes as None (organic). Removed once CPD-1+CPD-3
have soaked one release cycle.
ReportPoolWindowRemoved
Phase B.4 follow-up — host reports a pool window leaving the pool (promote, destroy, or app exit).
ReportHostCounts
Phase B.4 follow-up — drift detection (full snapshot). Host
sends its own post-mutation counts after each window-level
transition; the launcher reducer compares both dimensions to
its mirror counts and emits Event::DriftDetected per
disagreeing dimension. Sent in a separate command (rather
than embedded in each Report*) so the existing wire shapes
stay unchanged.
Known limitation (B.4 observe-only): emissions originate
from multiple execution contexts (CEF UI thread for
on_after_created/on_before_close, IPC handler thread
for promote_pool_window). Cross-thread interleaving in
the outbound channel can produce a snapshot whose counts
were taken at a moment that doesn’t match the channel
order seen by the reducer, occasionally emitting a
transient false DriftDetected. Acceptable for B.4
(drift is diagnostic — false positives are ephemeral and
self-correct on the next stable state). B.5 will tighten
with a transition-ID protocol once the launcher is
authoritative. (codex P2 PR #578 round-4 — accepted as
known limitation.)
Fields
ReportHostPoolCount
Phase B.4 follow-up — pool-dimension-only drift check. Used
by spawn_pool_window (pool transitions) where snapshotting
the windows dimension would produce transient false drift:
pool refill is triggered DURING on_before_close BEFORE the
matching ReportWindowClosed lands, so the host’s window
count is mid-flight relative to the launcher mirror. Pool
count IS stable at that moment (the new pool label was just
added), so checking pool alone preserves the “check every
transition” guarantee for the dimension that actually
changed. (codex P2 PR #578 round-3.)
ReportBackendWindowIdRegistered
Phase B.5 (window_id_map step a) — host reports the
frontend’s register_backend_window call: a window’s label
→ backend window ID (a srv-side UUID the frontend resolves
via WOS.makeORef). The launcher mirrors it for the same
reasons it mirrors instance_registry: host’s authoritative
copy will be retired through the a→b→c→d→e ratchet.
ReportBackendWindowIdUnregistered
Phase B.5 (window_id_map step a) — host reports a window
closing, so the launcher should drop the label→window_id
mapping. Sent from the same close path that emits
ReportWindowClosed.
ReportHwndOpened
Phase B.9.1 (WRR — Window Reality Reconciliation) — host
reports a Win32 top-level window was created. Hooked off
EVENT_OBJECT_CREATE (objid=OBJID_WINDOW) via
SetWinEventHook(WINEVENT_OUTOFCONTEXT). The host filters
CEF subprocess HWNDs (renderer, GPU, plugin) at the hook
callback so the launcher only sees plausible app windows.
label_hint is the host’s best guess from
pending_window_creations if it can disambiguate; None
when the create event arrives before the host has linked
the HWND back to a label (caught up later by the reducer’s
pending_hwnds).
ReportHwndDestroyed
Phase B.9.1 — host reports a Win32 top-level window was
destroyed. Hooked off EVENT_OBJECT_DESTROY.
ReportHwndVisibilityChanged
Phase B.9.1 — EVENT_OBJECT_SHOW / EVENT_OBJECT_HIDE.
ReportHwndForegroundChanged
Phase B.9.1 — EVENT_SYSTEM_FOREGROUND. Tells the launcher
the user actually saw this window (not just opened it).
Used to distinguish “opened but never foregrounded” (drift)
from “opened and shown”.
ReportHwndIconicChanged
Phase B.9.1 — EVENT_SYSTEM_MINIMIZESTART /
EVENT_SYSTEM_MINIMIZEEND.
ReportHwndPositionChanged
Phase B.9.1 — WM_WINDOWPOSCHANGED from the host’s wndproc
wrapper. The host coalesces bursts (50ms debounce per HWND)
before sending so the wire stays light during topology
changes. Reducer compares rect against state.monitors to
classify off-monitor drift.
ReportMonitorTopologyChanged
Phase B.9.1 — WM_DISPLAYCHANGE. Replaces the launcher’s
state.monitors wholesale; reducer re-evaluates every
known window’s last_rect against the new topology and
emits OffMonitor drift for any that newly fall off.
GetSnapshot
Phase D.1 — request a Event::Snapshot reply containing the
reducer’s current canonical state. Used by --diag wrr for
state-now visibility, by the frontend reducer for mid-session
resync after disconnect, and by future Tool clients that need
to bootstrap without observing every prior event.
GetSrvSnapshot
Phase E.1b — srv-side equivalent of GetSnapshot. Routed to
the srv pipe; reducer replies with Event::SrvSnapshot.
Separate from GetSnapshot (launcher) per spec §4.3 — each
reducer is canonical for its domain and replies on its own
pipe.
Reply contents grow with each phase: E.1b had lifecycle +
version only; E.2 added workspaces; E.2b+ will add tabs /
blocks / layouts. See Event::SrvSnapshot for the current
shape.
CreateWorkspace
Phase E.2 — create a new workspace. The reducer assigns the
oid (UUID) and emits Event::WorkspaceCreated. In E.2 the
reducer is a session-only projection (no persist subscriber);
E.2c adds the persist subscriber + migrates HTTP/WS RPC
to flow through the reducer.
DeleteWorkspace
Phase E.2 — delete a workspace. Reducer removes from canonical
state and emits Event::WorkspaceDeleted. Cascade-to-tabs +
SQLite write happen via wcore today (RPC path); migrating to
reducer-driven persistence is E.2c.
Fields
force: boolStep 5 PR 2 — provenance marker for the delete_workspace
saga. true means the dispatch is being driven by the
saga coordinator (per-tab DeleteTab dispatches already
happened in saga steps; this final cascade is just the
workspace row + window mappings). false for legacy /
internal compensation paths (e.g. tear_off_tab /
tear_off_block rolling back a freshly-created empty
workspace) which keep the existing cascade behaviour.
The reducer’s handle_delete_workspace cascades regardless
of force — the reducer is a pure mutator and the cascade
must always execute to keep state consistent. The flag is
recorded in the saga log purely for provenance, mirroring
the saga-as-narrator pattern documented in
docs/retro/phase-fg-roadmap-2026-05-01.md.
Defaults to false via #[serde(default)] so all existing
producers (RPC, internal compensation) keep working.
CreateTab
Phase E.2b — create a tab inside an existing workspace. The
reducer assigns the tab_id (UUID), appends to the workspace’s
ordered tab list, and emits Event::TabCreated. Validates the
parent workspace exists; returns Event::Error if not.
Session-only projection (no persist subscriber yet).
DeleteTab
Phase E.2b — delete a tab from a workspace. Reducer removes the
tab from canonical state, removes its id from the workspace’s
ordered tab list, and emits Event::TabDeleted. If the deleted
tab was the active tab, also emits Event::ActiveTabChanged
pointing at the new active (next-or-prev tab, or empty if the
workspace has no tabs left).
Fields
force: bool(codex P2 PR #633 round 4 + codex P1 round 2.) Bypass the
reducer’s atomic last-tab guard. false for user-facing
flows (close button, keyboard shortcut) — reducer rejects
last-tab deletes to keep workspaces non-empty. true for
internal compensation paths (CreateTab rollback,
PromoteBlockToTab compensation) where rolling back a
just-created tab requires deleting the only tab.
Defaults to false via #[serde(default)] for backwards
compatibility — pre-existing producers that don’t set the
field get the safe (guarded) behavior.
SetActiveTab
Phase E.2b — set a workspace’s active tab. No-op if already active. Errors if the workspace doesn’t exist or the tab isn’t in that workspace’s tab list.
ReorderTab
Phase E.2c.3b — reorder a tab within its workspace’s
tab_ids. new_index is clamped to the list length; no-op
if the tab is already at that position. Errors if the
workspace doesn’t exist or the tab isn’t in its tab list.
CreateWindow
Phase E.5 — record a new window in the srv reducer’s
state.windows map. Caller pre-assigns window_id (sagas
use a fresh UUID; RPC migration in PR 4 will likewise mint
the id at the RPC boundary). Validates the parent workspace
exists; errors otherwise. Used by CreateWindow + TearOff
sagas to track window↔workspace association.
CloseWindowInternal
Phase E.5 — remove a window’s workspace mapping from the reducer. Called by CloseWindow sagas after the host’s CEF window-close completes. Idempotent silent no-op on missing.
SwitchWorkspace
Phase E.5 — switch which workspace a window points at. Errors if the window or destination workspace is unknown.
ReorderTabsBulk
Phase E.5.3 — replace a workspace’s tab_ids with the given
list. The new list must be a permutation of the current set
(same elements, possibly different order); reducer errors
otherwise. Used by the drag-reorder UI’s bulk-reorder path
(replaces wcore-direct UpdateTabIds).
RenameWorkspace
Phase E.5.3 — rename a workspace. Errors if the workspace doesn’t exist; no-op if the name is identical.
RenameTab
Phase E.5.3 — rename a tab. Errors if the tab doesn’t exist; no-op if identical.
UpdateWorkspaceMeta
Phase E.5.3 — apply a meta-patch to a workspace. The reducer
validates the entity exists and emits Event::WorkspaceMetaUpdated
with the patch payload; the persist subscriber performs the
actual merge against wstore. Reducer state does NOT track meta
in E.5.3 — pass-through preserves the reducer’s small footprint
without losing the migration property (every mutation goes
through the reducer’s broadcast bus).
UpdateTabMeta
Phase E.5.3 — apply a meta-patch to a tab. Same pass-through
shape as UpdateWorkspaceMeta.
UpdateBlockMeta
Phase E.5.3 — apply a meta-patch to a block. Same pass-through
shape as UpdateWorkspaceMeta.
UpdateWindowMeta
Phase E.5.x — apply a meta-patch to a window’s meta map.
Same pass-through shape as UpdateWorkspaceMeta. Migrated
through the reducer per issue #855 so Event::WindowMetaUpdated
lands on srv_events_tx and the WaveObjUpdate broadcast bridge
picks it up — replaces the wcore-direct fallback that bypassed
reducer + bridge entirely.
MoveTab
Phase E.5.5 — move a tab from one workspace to another. Reducer:
- Removes
tab_idfromsrc_workspace_id.tab_ids. - Updates
tab.workspace_id = dst_workspace_id. - Inserts
tab_idatdst_indexindst_workspace_id.tab_ids, clamping to the dst list length. - If
tab_idwas the source workspace’sactive_tab_id, the source’s active reverts to its first remaining tab (orNoneif the source becomes empty). Errors if any of: source workspace, dest workspace, or tab is missing; iftab.workspace_id != src_workspace_id; or if the tab is the source workspace’s last tab AND no caller has arranged a fallback (callers like tear-off should reject the move at the saga layer if removing the tab would empty the source — preserving the “workspaces have at least one tab” invariant most UI paths assume). Used by the TearOffTab, MoveTabToWorkspace, and RestoreTornOffTab sagas.
MoveBlock
Phase E.5.5 — move a block from one tab to another (or to a different position in the same tab). Reducer:
- Removes
block_idfromsrc_tab_id.block_ids. - Updates
block.tab_id = dst_tab_id. - Inserts
block_idatdst_indexindst_tab_id.block_ids, clamping to the dst list length. Errors if source tab, dest tab, or block is missing, or ifblock.tab_id != src_tab_id. Used by TearOffBlock and the MoveBlockToTab saga.
CreateBlock
Phase E.3 — create a block inside an existing tab. Reducer
validates parent tab exists, assigns the block_id (UUID),
appends to the tab’s block_ids, emits Event::BlockCreated.
Session-only projection (no persist subscriber yet).
Fields
meta: ValuePhase E.2c.4 — block metadata (view, layout hints, etc.)
passed through to the persisted Block row. The reducer
itself doesn’t track meta — it forwards it untouched into
Event::BlockCreated so the persist subscriber writes
the Block with the correct meta map. #[serde(default)]
for forward-compat with old log entries that pre-date the
meta field.
DeleteBlock
Phase E.3 — delete a block from a tab. Idempotent silent no-op on missing tab or missing block.
SetFocusedNode
Phase E.4 (Option A) — set a tab’s focusednodeid. Errors if
the tab is unknown to the reducer; no-op short-circuit when the
value is already current. Empty node_id clears the field.
Routes through the reducer so the persist subscriber writes the
new value into LayoutState.focusednodeid for the tab. The
rest of LayoutState (rootnode/leaforder/pendingbackendactions)
keeps its existing wcore-direct path until Option B lands.
SetMagnifiedNode
Phase E.4 (Option A) — set a tab’s magnifiednodeid. Same
shape as SetFocusedNode; empty node_id clears (toggle-off).
LayoutInsertNode
Insert a new node into the tree. If parent_id is None, the
heuristic findNextInsertLocation is used (first available slot);
index positions within the parent’s children (None = append).
Fields
node: LayoutNodeLayoutInsertNodeAtIndex
Insert at an exact index path through the tree (e.g. [0, 2]).
Fields
node: LayoutNodeLayoutDeleteNode
Remove a node by id; collapse empty parents.
LayoutMoveNode
Reparent a node to a new parent at the given child index.
LayoutSwapNodes
Swap two sibling (or cross-parent) nodes. Sizes travel with nodes.
LayoutResizeNodes
Apply N resize operations atomically. Rejected entirely if any
size is out of range (reducer validates; early-return on first
invalid op, matching the frontend’s existing semantic).
LayoutReplaceNode
Replace a node with a new one, preserving the target’s flex size.
Fields
new_node: LayoutNodeLayoutSplitHorizontal
Horizontal split: inserts new_node before/after target_id
in a Row parent (or wraps them in a new Row group if parent is not Row).
LayoutSplitVertical
Vertical split: inserts new_node before/after target_id
in a Column parent (or wraps them in a new Column group).
LayoutClear
Wipe the entire tree (sets rootnode = None, clears focus/magnify).
LayoutSetTree
Bulk-replace the tree. Used during Phase 7a writer migration and for tear-off where the whole subtree changes atomically.
GetEvents
Phase D.3 — request an Event::EventList reply containing the
events the launcher has emitted with version > since. Used
by subscribers that hold a snapshot at version V and want to
catch up to the live stream by replaying missed events.
Typical resync flow:
Register→Registered { version: V0 }GetSnapshot→Snapshot { version: V1 }(V1 > V0) — apply the snapshotGetEvents { since: V1 }→EventList { events, version: V2 }— apply replay events to catch up to V2- live broadcast events flow with version > V2
Replay is best-effort: the launcher’s in-memory ring is
bounded; if since is older than the oldest retained event,
the reply still contains all retained events but the
subscriber may have missed some. Caller should treat the
result as “everything I have for you” and re-fetch a snapshot
if state inconsistency is detected.
Fields
ReportPoolWindowPromoted
Phase F.5 — host explicitly reports that a pool window was
promoted to a user-visible top-level window (the
promote_pool_window flow in agentmux-cef). Sent BETWEEN the
ReportPoolWindowRemoved + ReportWindowOpened pair so the
launcher reducer has unambiguous evidence the transition was a
promote (vs a destroy followed by an unrelated open) and can
emit Event::PoolWindowPromoted. The pool-respawn saga
consumes the event to bracket the implicit refill in
SagaStarted/SagaCompleted.
Host-only — same gate as ReportPoolWindowRemoved.
SpawnPoolWindow
Phase F.5 — launcher-side saga coordinator asks the host to spawn a fresh pool window (refill after promote).
Status: live. Cross-process dispatch (CPD-1 through CPD-5)
shipped; the saga coordinator’s apply_action for
PipeTarget::Host writes this command through host_pipe
(agentmux-launcher/src/host_pipe/) and waits on the
Event::PoolWindowAdded { saga_id: Some(N) } echo. Host’s
implicit spawn_pool_window call inside promote_pool_window
remains the organic refill path for non-saga-driven
promotions (with saga_id: None).
saga_id: every host-bound command carries the originating
saga’s id so the host can echo it on the corresponding
Report* reply. 0 is reserved as “no saga” and treated as a
non-saga dispatch. #[serde(default)] retained for
forward-compat with any pre-CPD-1 deserializers (no real-world
consumer today; portable runtime is bundled per release).
ReportPanesReaped
Phase F.6 — host reports that all browser-pane HWNDs belonging
to a closing top-level window have been reaped. Emitted from
client.rs::on_before_close after the subwindow cascade and
pane lifecycle drain finish for the closing window.
Distinct from ReportWindowClosed — that event marks the
CEF browser leaving the host’s browsers map; this one marks
the host’s pane bookkeeping (lifecycle entries, pane HWND map)
for that label being fully drained. Today both happen in the
same on_before_close body so the events arrive back-to-back,
but the saga distinguishes them so future fine-grained
reapers (e.g. async pane teardown for embedded browsers) can
land without rewriting the saga.
Host-only — same gate as ReportWindowClosed.
Fields
saga_id: Option<u64>Phase CPD-1 — saga correlation echo. Some(N) when the
host is replying to a saga-issued
Command::ReapPanes { saga_id: N }; None for organic
reports (e.g. host’s existing implicit pane drain inside
on_before_close that wasn’t saga-driven).
#[serde(default)] for forward-compat with pre-CPD-1
hosts.
ReportPoolDrainDecision
Phase F.6 — host reports the result of the post-close
drain-pool-if-last decision. was_last == true when the
closing window was the last user-visible window and the host
just kicked off the warm-pool drain (Stage 1 of the two-stage
close cascade in client.rs::on_before_close); false when
other user-visible windows remain and the pool stays warm.
The launcher’s window-cleanup-cascade saga uses this to close out its bracket regardless of which branch fires (both are terminal for the saga).
Host-only.
Fields
ReapPanes
Phase F.6 — launcher-side saga coordinator asks the host to reap all browser-pane HWNDs for a window that just closed.
Status: live. Wired through host_pipe post-CPD-3. The
saga issues this with target = PipeTarget::Host; the
coordinator’s apply_action writes it through the host pipe
and waits for the Event::PanesReaped { saga_id: Some(N) }
echo. Host’s organic pane drain inside on_before_close still
emits the same Report with saga_id: None for non-saga-driven
closes.
saga_id: mandatory on the wire (host echoes back on
ReportPanesReaped). #[serde(default)] retained for
forward-compat (see SpawnPoolWindow for rationale).
DrainPoolIfLast
Phase F.6 — launcher-side saga coordinator asks the host to drain the warm pool if the just-closed window was the last user-visible window (i.e. trigger Stage 1 of the close cascade).
Status: live (same shipping path as ReapPanes). Wired
through host_pipe post-CPD-3; saga waits on
Event::PoolDrained { saga_id: Some(N) } /
Event::PoolNotLast { saga_id: Some(N) } echo. Host’s
on_before_close still runs the equivalent inline check
organically (with saga_id: None).
saga_id: mandatory on the wire. #[serde(default)] retained
for forward-compat.
ReportSagaActionFailed
Phase CPD-1 — host-emitted report that a saga-issued action
failed (e.g. window not found, IPC error). Carries the
originating saga_id and a human-readable reason. The
launcher reducer translates this into Event::SagaActionFailed
so the saga coordinator can terminate the matching saga as
SagaFailed.
Schema-only in CPD-1: hosts don’t yet read commands from the pipe (CPD-2 wires that), so no producer for this command exists yet. The shape is added now so launcher reducer arms
- saga coordinator wiring can soak before CPD-3 makes the dispatch live.