plans/cleanup-state-machines.md
Committed direction (supersedes the previous revision of this file, which treated the distributed runtime as an open option). The decision input that changed: multi-window support is on the product roadmap (multiple windows showing different chats/tabs, browser-style). Multi-window breaks the architecture's hidden axiom — that there is exactly one renderer, so "renderer-hosted machine" and "the authority" can be the same thing. Any shared lifecycle that must be observed and controlled from more than one window cannot be renderer-owned without becoming multi-primary. Authority for those shared-entity lifecycles therefore moves to the main process; windows become views over read models. Window-specific lifecycles such as iframe navigation remain renderer-owned even when another window shows the same underlying app.
Source plans and their roles:
plans/claude-cleanup-machines.md — verified atom inventory (116 atoms;
152 trace claims checked, 20 corrected) and per-atom migration recipes.
Remains the evidence appendix for Phase A; its file:line traces are
authoritative, while this plan owns PR scope and sequencing.plans/codex-cleanup-state-machines.md — ownership model, boundary
enforcement, target renderer APIs. Its architecture rules are adopted;
its renderer-runtime consolidation rollout is superseded (those
controllers are now moving to main, not being polished in place).plans/distrbuted-machines.md — adopted as the destination
architecture, resequenced by this plan. Its transport was designed
per-webContents (subscription ownership, webContents.destroyed
cleanup, reference counting, revisioned snapshot broadcasts) — it is a
multi-window fan-out protocol and is now built as one, with multi-window
scenarios as first-class acceptance criteria rather than a future.Follows plans/state-machines-hardening.md (landed). The hardening work
made the layer safe; this plan makes ownership truthful and multi-window
capable: one authoritative owner per lifecycle fact, hosted beside the
resource it controls, projected to every window that looks.
These decisions are architecture inputs, not defaults. They are approved for this plan and must be copied into the Phase B ADR before implementation.
TabInstanceId and belongs
to one WindowSessionId. Dragging moves that same tab instance and
preserves transferable presentation state. Explicit “Open in New Window”
or duplication creates a new tab instance that may reference the same
entity; ordinary navigation may focus an existing tab in the current
window. Independent instances do not automatically share scroll position,
selected file, iframe history, panels, dialogs, or drafts.Two implementation consequences are mandatory:
expectedRevision, or an invocation ref; main process (authority)
remote actors:
app_run · chat_stream* · github_ops · version_preview ·
image_generation · user_input
| ^
| revisioned snapshot read models | validated, authorized,
| + typed post-commit events | deduplicated dispatch
v |
+-------------- window 1 --------------+ +-------- window 2 --------+
| remote refs -> selectors -> hooks | | (same read models, |
| per-window machines: | | same dispatch API) |
| preview_iframe · screenshot · | | |
| voice_to_text · first_prompt | | |
| per-window Jotai (UI-only atoms) | | |
+--------------------------------------+ +--------------------------+
documented main-owned resource registries:
connection_flow -- narrow lifecycle projection; revisioned intent
hardening remains before multi-window dispatch
mcp_oauth -- terminal invoke + persisted server status; epoch
query invalidation remains required
* chat_stream completed its feasibility study and is moving to main in C3.
Placement table (final intent; per-machine moves still gate on their pilot/study):
| Machine | Host | Reason |
|---|---|---|
app_run | main | Owns child process, producer identity; app is global across windows |
user_input | main | Already main; owns waiters; survives window lifecycle |
connection_flow | main, documented resource registry | Already main; retain its narrow lifecycle projection and specialized resource ownership, but add revisioned/correlated intent admission before multi-window dispatch |
mcp_oauth | main, documented resource registry | Already main; retain listener/waiter internals and publish persisted-status invalidation through the global epoch channel, not internal lifecycle states |
github_ops | main | Git mutation/recovery is per-app, not per-window; mid-rebase must survive window close |
version_preview | main | Checkout/branch recovery is per-app; mid-checkout must survive window close |
image_generation | main | Jobs are per-chat, visible from any window |
chat_stream | main, C3 cutover complete | Stream lifecycle is per-chat; main owns process-lifetime intent admission, queue mutation, lifecycle, and completion receipts |
plan_handoff | main, C3 cutover complete | Process-lifetime cross-chat workflow with in-memory checkpoints and idempotent implementation-turn admission |
preview_iframe | renderer, per-window | Owns a window's DOM/iframe identity |
screenshot | renderer, per-window | Captures a window's iframe |
voice_to_text | renderer, per-window | Owns a window's media resources |
first_prompt | renderer, per-window | Presentation saga tied to a window's home view |
Per-window machines key by (window-local host, entity). Main-hosted machines key by entity; windows subscribe.
Core rules (unchanged from the hardening/cleanup lineage, now with a process boundary in the middle):
A lifecycle fact represented in a machine snapshot is not also stored in Jotai. A lifecycle fact owned by main is not also authoritative in any window.
send vs remote dispatch returning a
receipt); commit is not completion.Every remotely dispatchable event is classified in its machine definition:
| Intent class | Admission contract |
|---|---|
| Idempotent/current-agnostic | No expected revision; transition still validates payload and current state |
| State-sensitive mutation | Carries expectedRevision; stale intent is a typed rejected/ignored receipt |
| Cancellation | Carries the active invocation ref; entity key or initiating window alone is insufficient |
| Durable handoff | Carries a domain idempotency key; commit receipt is not durable receiver acceptance |
| Presentation-only | Routes through the window router after authoritative commit; never changes domain state |
Capabilities account for remote connection status. A control rendered from revision N may still lose a race before dispatch; the receipt is the authoritative answer, and dialogs/forms settle only from authoritative state.
Phase B introduces a main-owned WindowRegistry before remote machine
transport is used by production:
interface WindowRegistry {
register(webContentsId: number, windowSessionId: WindowSessionId): void;
unregister(webContentsId: number): void;
setFocused(windowSessionId: WindowSessionId): void;
setVisibleEntities(
windowSessionId: WindowSessionId,
entities: readonly VisibleEntity[],
): void;
findWindowsShowing(entity: VisibleEntity): readonly WindowSessionId[];
routePresentation(request: PresentationRouteRequest): WindowSessionId | null;
claimCapability(request: WindowCapabilityRequest): WindowCapabilityLease;
}
webContents.id identifies one ephemeral renderer lifetime.WindowSessionId identifies a restorable window session.TabInstanceId identifies one movable/transferable tab presentation.webContents; session/tab restoration uses
stable IDs.Window-owned resources use explicit capability leases. For example, a
main-hosted workflow requesting a screenshot asks the registry for a window
currently advertising {kind: "screenshot", appId, iframeEpoch}. The request
targets that lease and settles/retries if the window or iframe disappears.
Never broadcast a resource request and accept whichever window responds first.
Machine snapshots do not make IPC-backed entity caches coherent. Phase B adds a separate typed invalidation channel:
interface QueryInvalidationEvent {
epoch: number;
scope: QueryInvalidationScope;
originWindowSessionId?: WindowSessionId;
}
Requirements:
queryKeys factory; arbitrary renderer-supplied
query keys are not transported;Console output and LLM chunks stay off machine snapshots, but they are not
blindly broadcast to every renderer. Main maintains keyed appId/chatId
interest per webContents, batches per destination, and removes interest on
window destruction. Subscription bootstrap and terminal flush close the
attach/detach race. Performance gates measure messages delivered and renderer
work, not only snapshot fan-out.
Each C-wave PR completes this matrix before implementation:
| Machine | No subscribers | Window reload | Last window closes | App quit | App restart | Entity deletion |
|---|---|---|---|---|---|---|
app_run | Active process retained; idle policy bounded | Reattach to main snapshot/output | Platform convention; active work retained on macOS | Bounded child-process teardown | Ephemeral unless separately recovered | Stop/dispose actor and process |
user_input | Live waiter retained to deadline | Rehydrate read model | Continue while application remains alive | Settle/sweep by shutdown policy | Recover only durable pending records | Settle requests for deleted entity |
connection_flow | Continue to timeout | Hydrate via getStates; resume broadcasts | Continue while application remains alive | Explicitly dispose timers and provider work | No flow recovery; a completed deep link is an unsolicited return | N/A; provider hooks release flow resources |
mcp_oauth | Continue to timeout | No internal lifecycle reattach; settlement publishes MCP scopes through epoch-keyed query invalidation | Continue while application remains alive | Close listeners and settle waiters | No implicit recovery | Cancel/settle and fence stale writes before deletion or OAuth-relevant mutation |
github_ops | Active mutation retained | Reattach | Continue while application remains alive | Finish or enter explicit recovery | Reconcile repository state | Dispose actor after safe settlement |
version_preview | Active checkout/recovery retained | Reattach | Continue while application remains alive | Preserve/enter recovery contract | Reconcile branch/checkout state | Dispose after safe return/settlement |
image_generation | Active jobs retained; terminal jobs retained for 30 minutes | Reattach to the main-owned job-list read model | Continue while application remains alive | Stop admission; best-effort cancel; bounded settle | No active-job persistence or auto-run; committed media remains | Best-effort cancel, bounded settle, and prune jobs for deleted app |
chat_stream | Active stream and queue continue | Bootstrap all read models | Follow real platform shutdown boundary | Interrupt; abort; bounded unwind | Start with an empty queue | Settle owners and unwind before deletion |
plan_handoff | Active handoff continues | Reattach to the main-owned snapshot | Continue while application remains alive | Abort and compensate owned target chat | Start idle | Abort and dispose |
Renderer-local machines always die with their renderer resources, but must settle or compensate their callers. The matrix records product semantics; it does not imply every main actor is persisted.
Summary (full tables and recipes in plans/claude-cleanup-machines.md):
116 atoms — 69 UI-only (keep, per-window), 32 machine-mirror (retire),
12 cross-machine + 3 mixed (retire, worst class). Load-bearing verified
corrections that Phase A recipes already encode: release-age outranks
pnpm-migration in the warning priority; six preview-error writer sites
(four in PreviewIframe.tsx, including dyad-app-sourced cloud-sandbox
errors); subscribeStreamFinished is deferred but does not fire on
disposeKey (watchIdle facades must observe disposal); provider mount
order constrains facade injection sites.
Multi-window raises Phase A's value but changes its sequencing: per-window Jotai stores cannot represent shared machine state even in principle, so unambiguous projection retirement remains valuable. Work that chooses a new renderer-owned store for a future main-owned domain is not automatically no-regrets; the chat feasibility gate and per-wave host decisions run before those storage conversions.
As in the codex plan, with one amendment. Categories: machine-owned lifecycle state (snapshot only, hooks/selectors/facades, never mirrored); external entity data (React Query / main persistence); UI/runtime state (per-window Jotai or local React state); cross-process read models — now the normal case for every main-hosted machine, not an allowlisted exception: named read models, one adapter owning hydration/ordering, read-only public APIs, revisioned, per-window subscription; derived indexes (read-only external-store selectors, real consumers only).
user_input's renderer adapter stops being the special case and becomes
the reference implementation of the pattern. Documented main-owned resource
registries may expose a narrower consumer-driven boundary instead: their
resource internals remain private, while any renderer-visible lifecycle fact
or intent still follows the revision, correlation, hydration, and
multi-window-convergence rules above.
A tracked checklist; each item gets an owner and lands with the phase noted. Known items (from the inventory and review record):
event.sender vs broadcast: #4033 already widened consent
broadcasts to all windows — correct for multi-window reads; audit every
remaining event.sender-targeted emission. Responses are claimed by
requestId (first-applied-wins already exists in user_input). [Phase B]
In flight (#4104).connection_flow/mcp_oauth already are); the focus target
follows the recorded presentation-routing matrix. [Phase B]useManagerPagehideDisposal: one window's pagehide must dispose
only window-local machines, never main-hosted state. Split into
per-window disposal (renderer machines) and subscription release
(remote refs). [Phase B] In flight (#4104).EntityDisposalRegistry scope: app/chat deletion initiated in
window A must dispose window-local controllers in all windows
(broadcast) and the main actor once. [Phase B] In flight (#4104).getDefaultStore() escape hatches: each window has its
own Jotai store; all such call sites are bugs under multi-window. Phase
A already removes the known one (ImageGenerationToast); boundary test
forbids new ones. [Phase A]chatTabSessionStorageAtom): schema
becomes per-window using WindowSessionId + TabInstanceId, like browser
session restore. Moving a tab uses acknowledged adopt-then-remove.
[Phase C]setQueryData call sites — the "unless they have already installed
equivalent mutation data" carve-out requires knowing every one. [Phase B]
In flight (#4104).WindowRegistry;
no first-responder broadcast. [Phase B]This section is the self-contained execution index. Detailed per-atom writer,
reader, and test traces remain in plans/claude-cleanup-machines.md; this
plan, not deleted Git history, defines PR scope and status.
A1 — Boundary enforcement and selector bindings: completed in #4090.
A2 — S-tier mirrors, no new stores: done (#4091).
useStreamFinished;pendingToolConsentsAtom;These deletions do not choose a future host or transport.
A3 — Single-machine projections with stable direct owners: in flight (#4092).
SnapshotStore containing
requests + responding set while preserving hydrate revision handling;A3 proceeds now: image_generation sits third in C2's order, well behind A3, and its manager's snapshot/subscribe surface is the read-model shape C2 will later publish remotely — this is preparation for the host move, not a temporary API.
A4 — Cross-machine signal edges that do not preselect chat storage: in flight (#4093).
requestCapture facade injected via deps, and the mailbox atom is
deleted in this PR — the no-dual-consumption rule holds. The chat_stream
call site carries an allowlist-style marker tied to B1: when the window
capability router lands, it replaces the facade's _implementation
(lease-targeted routing instead of the singleton manager), not its call
sites. Facade indirection is what prevents permanent singleton binding;
keeping the atom alive until B1 would be the worse binding.All facade callback registries support multiple consumers. Events carry invocation/actor identity, not timestamps. Every facade method added in A4–A5 is tagged (in types or doc comments) with its remote intent class from the Remote intent policy — idempotent, state-sensitive, cancellation, or presentation — so the C-wave conversion to receipts/revisions is mechanical and misclassifications surface now.
A5 — Multi-producer channels with explicit non-machine owners: complete (#4098).
clearPreviewRuntimeForAppAtom shrinks only as each replacement registers
equivalent entity cleanup.Stores created here expose source interfaces that a later remote/main producer can feed; they are not described as authoritative shared lifecycle.
Design gate G1a — Streaming-status authority: DECIDED 2026-07-24.
Split out of G1 because it is invariant across every G1 outcome: under any
host placement, an authoritative per-chat StreamState snapshot exists
(renderer controller today; main actor read model if chat moves), and
streaming status is read from it. Recorded decisions:
StreamState snapshot is the sole
status authority. Reads use pure selectors (isStreamActive,
selectCanCancel, selectStreamError) over useChatStreamState(chatId)
with a ?? {type:"idle"} fallback everywhere — no controller means
idle (matches the retired atom's absent-key semantics). ChatTabs
aggregates via per-tab keyed subscriptions first; a manager-owned
read-only index is added only if measurement shows overhead.PlanHandoffDeps; intent classes:
idempotent read / subscription):
isIdle(chatId): boolean over the snapshot;watchIdle(chatId, cb): () => void — fires at most once;
check-subscribe-recheck; delivery always asynchronous (microtask),
even when already idle — uniform async timing makes the #4077
re-entry class impossible by contract, and matches future remote
read-model behavior; observes controller disposal (the verified
subscribeStreamFinished gap): fires on any transition to
not-active, disposal included.resyncChat receives getIsStreaming(chatId) through chat_stream
command deps — same authority.external-error
event — A6a's one sanctioned transition delta (isolated commit, called
out in the PR description). Last-error durability: a bounded
lastErrorByChatId map on the manager (cleared on next submit and on
chat deletion) — no controller pinning, no atom. Revisitable by full
G1 if errors move into the read model.A6a — Streaming status and error retirement: in flight (#4095).
The isStreamingByIdAtom + chatErrorByIdAtom stack from the appendix
recipe: useStreamChat first (~15 components follow), ChatTabs aggregate
selector, plan_handoff facade with disposal observation, resyncChat
injection, then delete both atoms, syncProjection, and both #4077
protective comments — this removes the only known synchronous
re-entrancy vector and must not wait on the storage study. No new stores
are built; readers convert from atom to snapshot source.
Design gate G1 — Chat-stream/main feasibility study: accepted 2026-07-27 (GO for C3). Its inputs (recorded decision 5, the remote intent policy, the appendix reader inventory) all exist. The study decided:
It must produce a serializability inventory, target state/read-model schemas, acceptance transaction, migration sequence, and deletion budget. Do not build temporary “revision-free read-model-shaped” renderer stores in anticipation.
A6b — Chat storage implementation: folded into the C3 cutover and implemented.
Messages, queue pair, and planState split. Only the study-approved design
proceeds. The appendix's proposed renderer MessagesStore, QueueStore,
and accepted-plan projection are recipes to evaluate—not preapproved
destination architecture.
A7 — Compatibility infrastructure removal: in flight (this PR).
registerAtomWriter/projectToAtom;Dependencies: A1 is complete; A2 and A3 proceed in parallel; A4 follows the relevant A2 deletion; A5 follows characterized ownership; G1a is a same-week decision unblocking A6a; G1 starts immediately in parallel and gates only A6b; A7 is last and may span Phase D.
Status legend for this section: pending / in flight (#PR) / done (#PR). Update statuses as PRs land — this file is the plan-of-record that reviews cite.
No Phase A PR polishes a shared-entity renderer controller scheduled for deletion by a host move.
B0 — ADR, recorded decisions, and deletion budgets: in flight (#4096).
image_generation app-quit policy and app-restart persistence
decision (they do not survive B0 as TBD);Decision record:
docs/adr/main-owned-state-machines.md.
Image generation uses best-effort cancellation with a bounded settlement
window on app quit; active jobs are not persisted or automatically resumed
after app restart. The accepted G1 study now supplies the completed
chat_stream lifecycle row and intent classification.
B1 — WindowRegistry, routing, cache coherence, and test harness: in flight (#4102).
WindowSessionId and TabInstanceId;webContents registration/cleanup;(kind, appId), revoked on webContents.destroyed or
iframe-epoch change, requester retries or settles per its declared
policy. Dialogs/navigation/focus stay on plain presentation routing; a
second real lease consumer must exist before the mechanism generalizes
(rule of three applies to leases too);The harness can create two trusted renderer windows, assign session IDs, reload/destroy either independently, inspect subscriptions, dispatch from either, and test adopt-then-remove tab transfer.
B2 — Definition + local ActorHost kernel: in flight (#4100).
ActorHost and local refs;B3 — Contract-driven remote transport: implemented.
Conformance includes:
webContents.destroyed removes only that window;B4 — Remote client, hydration, and React: implemented.
RemoteMachineClient per renderer window;connecting/ready/disconnected/incompatible capability state;Phase B has no intended user-visible behavior change until a production machine enters Phase C. Kernel and transport remain separate revert points. With B4 complete, Phase B is complete and C1 is unblocked.
C1 — app_run pilot.
Status: C1.1–C1.2 done; C1.3 authority cutover and trailing deletion
implemented — main
app-runtime orchestration is behind a transport-neutral service, the
production app_run definition is registered on the main ActorHost, and
renderer consumers use its revisioned remote read model. Producer lifecycle
events are bound to the invocation at output-sink creation and enter the actor
before keyed console fan-out. The separate rolling-deletion change removes the
legacy renderer controller/manager, invocation registry, lifecycle command
adapter, fallback projections, and renderer producer routing. C1 is accepted
when the authority-cutover and stacked deletion PRs land.
Main owns process lifecycle and binds producer output to the invocation at
creation. Renderer windows keep local console views and preview_iframe
machines while consuming one remote lifecycle read model.
Status:
Required deletions:
AppRunController and AppRunManager;Acceptance adds to the distributed pilot:
C2 — Resource-owner waves, one machine per PR.
Candidate order: github_ops, version_preview, image_generation.
Each requires serializability audit, safe remote projection, lifecycle-matrix
completion, deletion budget, crash/reload tests, same-entity-two-window tests,
and window-close-mid-operation tests.
Status: github_ops cutover and trailing deletion implemented — the verified lifecycle policy,
serializability audit, safe projection, conflict-resolution receipt decision,
and renderer adapter/channel deletion budget are recorded in the
B0 ADR. Main now owns
the actor and Git command execution, renderers consume its revisioned read
model, and B1 publishes branch/app/version invalidations. The required
separate trailing PR deletes the superseded renderer controller, manager,
command runner, and mutation/probe IPC channels. Acceptance is pending review
and merge of the cutover and its stacked deletion.
Status: image_generation main-host cutover and legacy deletion
implemented — one main-owned collection actor now runs concurrent provider
jobs and publishes the renderer-safe job list to every window. Immutable job
IDs make creation idempotent, cancellation requires the exact active
invocation ref, provider settlements remain host-only, and targeted
presentation events follow initiator-first routing. The renderer controller,
manager, command runner, invoke channels, and Jotai-style projection adapter
are deleted in the same change. The singleton collection remains available to
mounted windows while every terminal job receives its own 30-minute prune
deadline. Shutdown requests best-effort cancellation with bounded settlement;
app deletion first fences admission and prunes its read-model entries before
bounded cancellation. Restart begins with no active-job state while preserving
media already committed to disk.
Status: version_preview cutover implemented — main owns the per-app
checkout/recovery actor, command execution, restart reconciliation, and
domain-only recovery persistence. Renderers attach to its safe revisioned
projection while pane visibility, diff selection, toast, navigation, and
query-refresh effects remain window-local. Active checkout continues after an
initiating window closes; a second window can reattach; deletion fences new
work, returns or settles the actor, and disposes it before repository removal.
The renderer controller, manager, command adapter, and version-mutation IPC
channels in this wave's deletion budget are removed in the same review stack.
The verified lifecycle row remains the policy recorded above, and the
serializability, projection, event-admission, and deletion decisions are
recorded in the
B0 ADR.
connection_flow and mcp_oauth are already correctly main-authoritative.
First expose them through the common remote reference/read-model contract if
needed. Their listener, timer, waiter, claim, and close-barrier registries stay
intact unless ActorHost adoption demonstrably deletes code or fixes a known
deficiency. They are not mechanical migrations, and documented resource
registries are an acceptable end state.
Main-registries audit — audit complete; specialized registries retained, boundary hardening pending. The evidence inventory and binding disposition are recorded in the B0 ADR:
connection_flow: the renderer reads the per-provider lifecycle through
connection-flow:get-states on hydration and
connection-flow:state-changed thereafter. It separately consumes the
narrow connection-flow:unsolicited-return notification to refresh
provider data after a cold-start or stale deep-link return. This projection
is the concrete surface consumed by useConnectionFlow and the GitHub,
Neon, and Supabase connector components; do not publish additional registry
internals. Its intent boundary is not yet
multi-window-safe: start and acknowledge need authoritative revision
admission, and C2 must replace the historical string flowId with a typed
ConnectionFlowInvocationRef minted through the shared IdSource.
Cancellation requires the exact active ref rather than resolving an omitted
ID to whichever flow is current; acknowledgment, timers, provider callbacks,
and every echo-capable boundary carry the same ref. Deep-link returns that
cannot echo it use a documented structural InvocationRegistry claim. The
custom registry also needs explicit shutdown disposal for its watchdogs and
provider work. These are boundary/lifecycle hardening requirements, not
justification for replacing its specialized internals with ActorHost. The
current renderer-owned resources-loaded barrier is also not meaningful
across windows: after durable credential persistence, main should settle the
flow and publish correlated provider-status invalidation; every window
refreshes its own query families without acknowledging main lifecycle
completion.mcp_oauth: the renderer does not read the registry snapshot. Its Connect
mutation awaits the existing mcp:start-oauth invoke through terminal
settlement, then invalidates the persisted MCP server/tool queries; the
durable renderer-visible fact is McpServer.oauthConnected, read through
mcp:list-servers by useMcp/usePluginConnect. Intermediate
binding/callback/exchange states exist only to coordinate main-owned
listeners, waiters, claims, and close barriers. Publishing them would create
an unused read model. The mcp:start-oauth one-shot reply alone is
insufficient across renderer reload or window close: after persisted
settlement, main must publish the MCP server/tool scopes through the global
epoch-keyed QueryInvalidationEvent channel. Its reconnect/bootstrap and
gap recovery provide convergence; this is not a live-only terminal event.
Server deletion, disconnect, OAuth disable, and OAuth-relevant configuration
changes must cancel and settle matching in-memory flows and fence stale
provider writes before mutating the row.
C2 also replaces the registry's historical string flowId with a typed
McpOAuthInvocationRef, echoed through listener, waiter, timer, callback,
exchange, supersession, and settlement boundaries. That correlation
identity is distinct from the renderer message ID used for retry dedupe.ActorHost: replacing the specialized resource
maps and effect sequencing would not delete those internals or fix the
identified boundary deficiencies. The placement rows record the retained
registries; the lifecycle matrix records the required end state rather than
blessing current cleanup gaps.Renderer-triggerable intent disposition:
| Registry | Intent | Target admission |
|---|---|---|
connection_flow | start | State-sensitive; require expectedRevision; main mints the typed ConnectionFlowInvocationRef |
connection_flow | cancel | Cancellation; require the exact active ConnectionFlowInvocationRef |
connection_flow | acknowledge | State-sensitive; require expectedRevision and matching invocation ref |
connection_flow | resources-loaded | Renderer-triggered today; remove as an intent when correlated provider-status invalidation replaces the per-window barrier |
mcp_oauth | Connect via start-oauth | Current-agnostic last-request-wins; carry a renderer message ID for retry dedupe; main mints a typed McpOAuthInvocationRef and settles the displaced invocation |
All other preparation, callback, timer, exchange, failure, claim, and
close-barrier events remain host-only and correlated internally. The audit
does not authorize production code in this docs PR. The
pr_c2_main_registries implementation wave owns focused hardening and tests
for stale cancellation, post-persistence invalidation across reload, OAuth
configuration mutation/deletion, stale-write fencing, and explicit shutdown
disposal.
C3 — Chat-stream and plan-handoff execution: cutover complete
(c3-chat-main-authority); trailing deletion implemented
(c3-chat-delete-adapters) and pending review/landing.
Implement the G1 design only after the app-run transport proves remote hydration and multi-window dispatch. Preserve existing batched chunk channels; snapshots carry lifecycle, not stream bytes. This wave owns process-lifetime acceptance, editable queue semantics, notification routing, window reload, and window-close behavior as one reviewed protocol.
The cutover PR moves both actors to the main process, keeps immutable chat
intents and plan-handoff checkpoints in main-process memory, makes queue
mutations revision-conditional, routes plan presentation through
WindowRegistry, and keeps the existing keyed chunk fan-out. The stacked
trailing PR deletes the renderer controllers, legacy queue writer and
full-snapshot IPC, superseded projection atoms, and shadow lifecycle model.
Only the read-only legacy queue importer remains for one-time migration.
C4 — Multi-window product surface.
Status: C4a implemented; C4b implemented and pending review. C4a ships product-window creation, explicit app-surface duplication, stable per-window restore, per-window chat-tab session persistence with legacy migration, and live app visibility/focus routing. C4b owns chat-tab drag/transfer and chat-surface notification routing after the C3 authority cutover lands. C4b wires destination-adopt acknowledgement before source removal and recreates transferable chat, editor, preview, panel, terminal, draft, and selection presentation state in the destination window.
Window creation, explicit duplication, tab drag/transfer, per-window session restore, and focus routing. Moving a tab preserves serializable presentation state—including scroll anchor/position, selected file and cursor, preview history, open panels/modes, draft input, and relevant selections—using destination-adopt acknowledgement before source removal. DOM-only resources such as iframe, Monaco, and terminal views are recreated to equivalent visible state.
Architecture tests do not wait for this UX: B1's test harness exists first.
Correction recorded 2026-07-25: there is no live-IPC version-skew window
in production. Dyad updates via update-electron-app/Squirrel
(src/main.ts) — updates apply on restart, main and renderer always ship
from one bundle, and a mid-session renderer reload loads the running
bundle, not the staged one. Version discipline therefore applies to
persisted state only (durable records, persisted snapshots, tab
sessions — written by vN, read by vN+1 after restart); live transport
carries a cheap protocol-version assert (mismatch → log + reload; a
dev-only HMR phenomenon), not a compatibility matrix. The earlier
"supported update window" constraint on deletions is void.
Deletion is therefore rolling and immediate: each C wave's temporary adapters and legacy channels are deleted in a separate PR landing right behind the cutover (same day is fine). Corrections recorded 2026-07-25: (1) no update window — one bundle, updates apply on restart; (2) no runtime toggle exists, so retained adapters are dead code enabling no runtime rollback, and straggler callers are compile-time-detectable via typed IPC contracts; (3) no bake period either — dead-code deletion cannot regress runtime once typecheck/CI pass, and a later cutover revert simply reverts both PRs (two clean commands). The separation exists ONLY for review clarity: the high-scrutiny cutover diff stays pure, undiluted by mechanical deletions. The final Phase D shrinks to:
rules/state-machines.md, rules/electron-ipc.md,
rules/jotai-state.md, and docs/why-state-machines.md;Multi-window infrastructure must not regress the current product. Two standing rules:
invalidateQueries call.Everything from the codex plan (pure transition tests, controller/host conformance, renderer tests that never mock Jotai for machine state, integration scenarios, performance checks) plus the multi-window layer:
Review constraints from the distributed plan hold: kernel, transport, and each pilot are separate revert points; no PR mixes generic transport with domain behavior change; every remote definition gets security review (static manifest, event codecs as allowlist, per-definition authorization, commands never cross from renderer, projections exclude main-only data); high-blast-radius waves get deep multi-agent review.
app_run) follows
immediately and is independently justified by the single-window bug
record (#3969 class, reload teardown). If multi-window dies entirely,
stop after C1/C2 and retain the useful result: app-run authority beside
its main-owned process, without committing to every later migration.Phase A: as before — zero same-process machine-written atoms outside documented keeps or G1-approved transitional read models; typed identity-carrying facades; #4077 comments deleted only when the hazard is actually gone; boundary tests prevent reintroduction.
Phase B: window-routing/cache-coherence and actor/transport conformance suites green including all two-window scenarios; no intended user-visible behavior change before C1; dispatch tickets/receipts distinguish sent / committed / ignored / rejected / disposed.
Phase C, per wave: the machine has exactly one authoritative host; its renderer controller and compensating plumbing are deleted (deletion budget met); same-entity-two-windows and window-close-mid-operation tests pass; no regression in the wave's product flows.
End state: authority lives beside the resource it controls; ordinary actors share the transition/transaction kernel across both processes while narrow documented resource registries may retain specialized internals; windows are views, and ordinary shared actors require no machine-specific transport plumbing; window-owned resources use explicit routing adapters; Jotai holds only per-window UI state; renderer reload and window close are supported lifecycle events, not teardown hazards; contributors can explain sent, committed, completed, and durably accepted as distinct states; and the rules/docs describe this architecture, not the transitional one.