Ctrl K
ComunidadGuías…

rules/state-machines.md

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
# State Machines

Use an explicit state machine when a workflow has async races, queued work, or
events that may arrive after an operation has been superseded.

Background and before/after examples of why this pattern exists:
[docs/why-state-machines.md](../docs/why-state-machines.md).

## Required structure

- Keep domain types in `state.ts`, the pure total function in `transition.ts`,
  side-effect execution in `controller.ts` or a main-process registry, command
  adapters in `commands.ts`, and renderer bindings in a hook/provider.
- A renderer machine hook may be mounted more than once for the same actor
  (for example, both a connector and its branch manager use `useGithubOps`).
  Make timer-driven or other dispatching effects explicitly opt-in so sibling
  consumers do not emit duplicate remote events.
- `state.ts` and `transition.ts` stay pure. They must not depend on React,
  Electron, Jotai, TanStack Query, zod, timers, `Date`, or randomness.
- When that purity requires a hand-written state type to duplicate an IPC zod
  schema, add a mutual-assignability assertion beside the schema so either
  definition drifting fails type-checking.
- A strict Zod object with a `message` field still accepts an `Error` instance
  through property lookup. Wire schemas that exclude `Error` objects must
  reject them before parsing the plain error-info object, and test that case.
- When a keyed wire transport decodes its key and event independently, keep
  events entity-relative. If an event must repeat entity identity, the
  per-definition admission boundary must validate it (and cancellation refs)
  against the decoded key; separate schemas cannot enforce that relationship.
- Cover the full state × event matrix with exhaustive switches and `never`
  checks. Deliberate no-ops must use shared `ignore(state, reason)` so they are
  distinguishable from omissions and observable in telemetry.

## Enforced distributed-machine boundaries

- App-run and image generation are framework-covered surfaces. Their
  definitions must be created with
  `defineFrameworkCoveredRemoteMachine`; the constructor requires either a
  native `RuntimeRemoteIntentContract` or the narrow protocol-v1 combination
  of a declarative `RemoteIntentContract` plus `RemoteOperationContract`.
  `createProductionRemoteMachineManifest` accepts only that capability or an
  exact `defineLegacyRemoteMachineCompatibility` capability from the
  production legacy-definition inventory.
- Prefer behavioral tests for distributed-machine guarantees over repository-wide
  inventories of file paths, symbol names, or dispatch/enqueue counts. A stream
  controller enqueue is not a distributed-machine boundary.
- New remote intents declare authorization, key/intent relationship, refusal
  mapping, retry/idempotency, completion, observed revision, acceptance/input
  disposition, and wire/snapshot budgets through
  `defineRuntimeRemoteIntentContract` or `defineRemoteIntentContract`.
  `defineMachineConformance` separately requires applicable tiers and explicit
  exclusions, so purely local machines do not acquire irrelevant remote
  capabilities.
- Shared primitive scenarios run in
  `testing/framework_mechanism_conformance.test.ts`; resource failures use
  `assertNoOwnedResources` and identify the resource, owner, machine, key, and
  generation. Do not call this cross-pilot runtime conformance unless one
  reusable driver instantiates both domain façades and reads their inspectors.
  Foundation and pilot review findings are exact-mapped in the corresponding
  `*_finding_catalog.ts` files.

## Invariants

- Controllers migrated to `TransactionalDispatcher` use one event transaction:
  enqueue FIFO; run the pure transition exactly once; validate; reserve the
  command batch and any explicit post-commit outcome batch without running
  domain code; cancel exiting state-owned leases; commit the snapshot (the
  linearization point); update the authoritative projection; publish reserved
  correlated outcomes, marking the entire correlated batch terminal before
  invoking any settlement listener; notify snapshot subscribers and transition
  observers; then hand the reserved commands to the injected domain scheduler.
  Publishing authoritative outcomes before teardown-capable observers prevents
  disposal from winning after the operation's state has already committed.
  Re-entrant
  callbacks may mutate transition-owned arrays, so both batches must be
  shallow-copied before callbacks. Re-entrant sends append to the FIFO and run
  after the current transaction. Ignored events skip commit, projection,
  subscribers, outcomes, and commands, but notify observers at the equivalent
  point in FIFO order.
- `ActorHost` refs' `send()` only enqueues, so `getSnapshot()` right after it
  can still show the pre-event phase. A caller's check-then-send guard (e.g.
  `phase === "streaming"` before `CANCEL`) does not exclude a concurrent caller;
  side state recorded per sender (like `beginChatCancellationCause`) must expect
  several, and the first queued event is the one the transition acts on.
- The dispatcher isolates and reports projection, subscriber, observer,
  scheduler, and command failures. Adapters convert expected command failures
  to typed domain events; unexpected throws/rejections may be mapped by the
  domain and never create a universal failure event. Scheduler injection owns
  concurrency policy.
- When a linearization boundary requires a synchronous return value, reject
  thenables whose runtime type is either `object` or `function`; callable
  functions can also define a `.then` property and be assimilated by `await`.
- A callback typed to return `void` can still be implemented with an async
  function. Validate authoritative outcome publishers as synchronous and
  report rejected thenable results instead of relying on `try`/`catch`.
- Recheck dispatcher admission after publishing post-commit outcomes and
  notifying lifecycle callbacks. If reentry disposed the owner, do not hand a
  reserved command batch to the scheduler after teardown.
- A pre-commit lease-cancellation failure is also isolated and reported, but
  does not veto commit. Unlike pure transition and validation failures, an
  effectful cleanup hook may have partially completed; rejecting at that point
  would drop the event against a partially modified old state. Cancellation is
  required resource hygiene, while operation/state-instance token checks are
  the correctness backstop that rejects any stale callback which still fires.
- Recheck dispatcher admission after an effectful pre-commit hook. The hook can
  synchronously re-enter owner disposal; if it does, do not commit, notify,
  schedule commands, or report the current ticket as applied.
- Use `TimerLeaseScope` for migrated watchdogs. Carry the lease's operation or
  state-instance token in its event, cancel it in the dispatcher's pre-commit
  lease hook before exiting state, explicitly replace it on self-re-entry, and
  dispose it with its controller.
- Never return a value-equal state with a new reference. One-shot effects are
  commands, not identity signals.
- Snapshots are immutable and reference-stable. Notify subscribers only when
  the snapshot reference changes; use `SnapshotStore` from
  `src/state_machines/` instead of hand-rolling listener plumbing.
- Commands are data and execute in a controller/adapter. A machine that derives
  effects directly from registry transitions must document that deviation and
  its reason in the module header.
- For cross-machine dispatch followed by acknowledgement, carry a stable
  idempotency key through every queue, IPC, and persistence boundary. Make the
  receiving boundary durably deduplicate acceptance, and acknowledge only
  after that acceptance; a renderer-local enqueue is not durable acceptance.
  Cross-window cancellation cutoffs must likewise be main-owned and versioned;
  have every submission source, including main-owned follow-ups and internal
  redispatch, echo the authoritative version it observed so delayed pre-cancel
  work cannot masquerade as an explicit post-cancel resume. If a renderer has
  not bootstrapped its actor snapshot, observe or reserve that version in main
  when submission begins; never fill it from a later bootstrap snapshot, which
  may already include an intervening Stop. When admission or
  persistence completes asynchronously, gate follow-up dispatch on the current
  main-owned cutoff as well as the completion payload; a stale unpaused result
  must not override a newer Stop latch. Establish every in-memory guard used by
  later transitions in the policy-starting transition itself (for example,
  set both the Stop reason/version and `queuePaused` before asynchronously
  persisting the pause); metadata alone does not close the command-yield race.
  Bounded dedup caches may evict settled history, never unresolved receipts;
  reject excess in-flight work through a separate bounded admission limit.
  Scope renderer retries to the stable window-session identity, not an
  ephemeral `webContents.id`, so reconnect cannot bypass deduplication. Start
  the retention window when the receipt settles, not when slow authorization
  begins, or a lost receipt can be immediately evicted after acceptance. Evict
  transient pre-admission transport-lifetime rejections after settlement, with
  an identity check against the cached entry, so reconnect can retry work that
  was never admitted while concurrent in-flight duplicates still coalesce.
  Compute the complete retry fingerprint before fresh actor admission: a
  matching retained receipt must replay after its subscription is released,
  while a fresh dispatch still requires an admitted subscription.
- Make remote subscribe/bootstrap idempotent per window, machine, and key.
  Resync and reconnect retries must refresh the bootstrap without incrementing
  ownership; projection/encoding failure rolls back only ownership acquired by
  that call. Coalesce concurrent retries of the same logical attach behind one
  pending quota reservation and admission result. Track pending attach identity
  so unsubscribe, window destruction, actor disposal, or transport disposal
  invalidates authorization still in flight; after every await, reject a
  cancelled or closed lifetime before admission. Renderer bootstrap applies both
  codecs and refuses disposed actor lifetimes, or failed retries and delayed
  responses can retain stale state.
- After asynchronous remote authorization, revalidate both the sender's window
  session and the actor instance/revision used for the decision. Re-authorize
  changed state only a bounded number of times (then reject it), and keep
  admission synchronous. Remote dispatch may address only an existing actor
  created by subscription and must lock admission to that actor instance.
- Remote key codecs need an explicit encoder as well as a decoder. Use the
  canonical encoded value in every wire envelope; never emit the decoded domain
  key, which may be transformed or non-serializable. A native intent contract's
  codec, encoder, and key identity function must also own renderer store
  identity plus inbound snapshot/disposal routing; mixing in the legacy key
  contract can silently drop native updates. Bound every untrusted
  envelope before its codec runs, including subscribe/unsubscribe addresses,
  and bound snapshot envelopes before delivery. Use a
  structured-clone-compatible byte measurement; JSON sizing is not
  wire-compatible with values such as `bigint`. When an existing domain payload
  legitimately exceeds the shared default, declare a bounded per-machine
  ceiling and enforce aggregate projected state below its snapshot ceiling.
- When increasing a shared error-message bound, audit every producer, state,
  and snapshot codec that consumes it, including transitions that prepend
  context. Bound the final stored value at each tighter contract so one verbose
  failure cannot commit state that remote snapshot validation refuses to send.
- When a remote machine uses an object key, canonicalize it through the same
  interner used by main-process producers only after subscription authorization
  succeeds, then pass that canonical key to `ActorHost`. Its actor map is
  identity-keyed, but interning inside an untrusted wire decoder lets rejected
  entity IDs grow a process-lifetime cache. Post-authorization canonicalization
  must preserve the reserved encoded wire address; otherwise reject it or
  atomically re-key pending quota, subscription, disposal, and reference
  bookkeeping before admitting the actor.
- Remote authorization hooks use `DyadErrorKind.Auth` for expected access
  denial. Convert only that explicit classification to an unauthorized receipt;
  propagate unexpected hook failures so telemetry can distinguish dependency
  failures and bugs from ordinary refusal. A named domain-revision policy needs
  both an explicit renderer-observed domain token and a main-side resolver;
  never compare it with, or silently substitute, the actor snapshot revision.
- Capture receipt metadata synchronously when that dispatch ticket settles.
  Reading mutable actor metadata after awaiting the ticket can observe a
  re-entrant follow-up transaction instead of the acknowledged event.
- A completion-aware operation registered before actor dispatch must roll back
  its admission when that dispatch later fails, is disposed, is ignored, or
  lacks settled metadata. None of those paths will publish a correlated
  post-commit outcome, so leaving the admission unresolved leaks registry
  capacity.
- Machine-generated queued work must not be editable or removable (including
  through bulk-clear paths) unless removal explicitly settles or rejects the
  owning machine request; otherwise reload can resurrect abandoned work.
- Settle memory-owned requests on every destructive entity path, including
  parent-row cascade deletion, bulk deletion, and full reset—not only direct
  deletion of the child entity the request references.
- Before parent deletion snapshots child entities for settlement, fence new
  child creation and serialize the snapshot with each child's final insertion.
  Otherwise a late child can evade cleanup and disappear through the cascade.
- Register an in-memory request's disposal rejector before its first
  asynchronous admission await, and remove it in `finally`. Registering only
  before the terminal subscription leaves an admission-to-subscription race
  where owner disposal can strand the request forever.
- Model user-initiated owner rejection as a typed non-error facade outcome.
  Rejecting the transport promise routes successful cancellation through
  generic failure toasts/retry logic and can incorrectly acknowledge dispatch.
- If queue removal awaits owner settlement, first validate the optimistic
  revision and atomically claim/remove the invocation-time items so a rejected
  mutation has no external effect and the queue driver cannot start them.
  Restore failed owners, preserve items enqueued during the await, and surface
  settlement errors without aborting the whole clear.
- When a callback's direct caller owns rollback or restoration, settlement
  failure must reject that callback itself. Rejecting only a separate outer
  promise hides the failure from the component responsible for compensation.
- When changing a lifecycle facade from projecting failures in state to
  rejecting its returned promise, audit every event-handler and automatic
  fire-and-forget caller. Attach an explicit rejection consumer there while
  preserving rejection for callers that await the operation for sequencing.
- Do not persist machine-generated queue entries when their authority or
  acceptance callbacks are memory-only. Let the live authoritative registry
  rehydrate and re-enqueue them; a full restart must not restore orphan shells.
- `observeTransition` runs before a controller commits its next snapshot. If
  an observer callback can re-enter the machine (for example, by submitting a
  follow-up turn), defer that callback until the committed state is visible.
- In custom controllers that start an async command batch before committing,
  the batch executes synchronously through its first `await`. Defer any
  command callback that promises post-commit delivery (or reserve the batch
  until after commit), and test the snapshot observed inside the callback—not
  only the snapshot after the event returns.
- When a manager needs machine-specific observer behavior, compose it with the
  production trace observer (including ignored events) instead of replacing
  trace coverage.
- Command runners convert expected failures into events. A runner throw is a
  programming error: log it and keep the service usable; never wedge a queue or
  silently rewrite state.
- When a generation token suppresses superseded async probe results, apply the
  same token check to rejection handling. A stale failure must not emit a
  toast, settle newer state, or trigger recovery for the replacement probe.
- Do not treat an ambient reconciliation probe as proof that a long-running
  workflow finished. Preserve the in-progress phase until an explicit
  completion event, then enter a checking phase whose probe result decides the
  terminal or retry state.
- When a resume event can come from a global watcher as well as explicit UI
  senders, validate the captured payload in the transition. Caller-only guards
  can be bypassed after navigation or another asynchronous detour.
- Every waiter settlement path (success, decline, timeout, abort, and sweep)
  emits a correlated resolved event to every observer.
- On saga resume or retry, preserve explicit user choices from the snapshot and
  re-resolve implicit derived values through one shared resolver. Model SUBMIT
  (a new payload) and RETRY (the retained payload) as distinct events.
- A machine-owned watchdog timer needs an explicit cancel command on every
  transition that leaves the watched state, plus disposal cleanup.
- In a cancelling state, finalize on every non-stale terminal event. Reject
  staleness by identity; never infer event provenance from arrival order.
- When destructive cleanup sends a cancellation terminal through an IPC sender
  other than the actor's observer, also settle the authoritative actor with the
  same correlated cancelled terminal; a silent handler return must not be
  synthesized as successful completion.
- Compensation on abort rolls back only what the aborted operation touched.
- When a multi-step side effect can fail partway through, retain the exact
  completed/next step in the failure state. Retrying from the start can repeat
  non-idempotent external work or deterministically fail on an existing-resource
  guard even when the owning entity is correctly reused.
- Controllers are disposable and their owner must call `dispose()` on provider
  unmount or entity deletion. Renderer controller collections belong to a
  provider-owned `KeyedControllerHost`; never keep them in module globals.
- Async keyed disposal must stop admission synchronously but keep the lifetime
  addressable until its final cleanup promise settles. Every disposal caller
  awaits that same barrier, and same-key recreation stays blocked behind it.
  If the key is still under synchronous construction, publish a keyed barrier
  that adopts the eventual actor cleanup rather than treating the missing map
  entry as already disposed. Publish the barrier before invoking any cleanup
  hook, and aggregate synchronous admission/timer cleanup failures behind it.
- Reserve a keyed lifetime before running definition factories. Factories may
  synchronously re-enter host admission; reject that re-entry rather than
  constructing a second owner that can be overwritten and leaked.
- Bulk or machine-scoped disposal must publish one collection barrier before
  snapshotting members and block new member admission until it settles. Enroll
  members whose synchronous construction began before the barrier but finishes
  after publication, suppress their buffered factory-time ingress, and await
  their cleanup as part of that same barrier. Reserve every snapshotted member's
  disposal cause before stopping any member: one member's injected cancellation
  hook may otherwise re-enter and claim another member's barrier with the wrong
  cause. If the definition remains registered, reopen admission only after
  final cleanup.
- Treat bounded retention as an edge-triggered deadline. Once a snapshot
  qualifies for delayed disposal, traffic that leaves it qualifying must not
  refresh the timer; cancel the deadline only when the authoritative snapshot
  stops qualifying.
- Reject bounded terminal-retention policies that do not provide a terminal
  classifier; otherwise the configured deadline can never become eligible.
- Retention scheduling must publish a provisional ownership token before
  calling the injected clock, then attach the returned handle only if ownership
  survived synchronous re-entry. Callbacks verify that token before acting, and
  cancellation clears it before touching the clock; a clock may re-enter or
  throw without physically removing the callback.
- Route every actor event ingress—including command and timer callbacks—through
  the host's shared enqueue wrapper. Bypassing it may preserve FIFO ordering
  while skipping retention, tracing, or other host-owned settlement bookkeeping.
- Register a newly constructed actor before draining events buffered by its
  factories, and recheck collection/key admission for every drained event.
  Disposal re-entered by the first event must be able to stop that actor before
  any later buffered event commits.
- After actor activation, recheck host, machine, and keyed admission before
  returning the reference. Activation can synchronously re-enter disposal
  through buffered observers or an injected retention clock.
- When a host passes live scopes or snapshot/send methods into definition
  factories, make factory-time access safe and construction failure-atomic.
  Dispose every acquired task/timer resource if any later factory step throws.
- When registering a manager method as a disposal callback, wrap it in a stable
  closure or bind it if it reads `this`; passing a bare prototype method loses
  its receiver when the registry invokes it.
- Renderer providers must bind manager startup and disposal with the shared
  `useManagerLifecycle` hook; it preserves managers across React StrictMode
  effect replay while still disposing managers that are genuinely replaced.
- Managers that claim exclusive, reversible resources (such as an atom writer)
  must release them in `stop()` during synchronous effect cleanup. Keep only
  irreversible final teardown in deferred `dispose()` so a replacement can
  acquire the resource before the StrictMode-safe disposal microtask runs.
- When disposal can race an async command that registers external state after
  an `await`, clean up both immediately and again after the command settles.
- A captured non-creating sink validates late producer delivery but does not
  by itself make a destructive fence wait. Externally initiated work, including
  work that already owns a domain lock, must enroll its full promise under the
  captured actor/admission generation before it starts.
- When a cross-owner facade defers keyed delivery to a microtask, entity
  disposal must invalidate both queued and future deliveries for that key.
  Otherwise the deferred callback can recreate a controller after deletion.
- Producer callbacks that arrive after entity disposal must use a non-creating
  actor lookup. Never route late output through `ensure()`, which can recreate
  retained authority for a deleted entity.
- A keyed ownership replacement must adopt the incoming cleanup before running
  the previous cleanup. Otherwise a throwing unsubscribe/cancel can orphan the
  already-acquired replacement resource.
- If command adapters can be replaced while async setup is pending, late
  compensation must use the adapter captured when setup began. A fresh adapter
  lookup may release the wrong lifetime's resource.
- When that external state is created in the main process, renderer disposal
  cannot rely on reply-based IPC cleanup. Mint an operation ID before creation,
  send teardown cancellation one-way, and retain a main-owned cancellation
  tombstone so late creation completion performs the cleanup.
- Before keying a cross-entity registry by a generation counter, verify the
  counter's scope. If generations restart per entity, use a composite key or a
  separate invocation ID and test two entities with the same generation.
- Cross-lifetime operations use `InvocationRef` from `src/state_machines/`,
  minted by the injected `IdSource` at the authoritative start boundary and
  echoed through every available correlation boundary. Registry claims use
  `InvocationRegistry.claim(ref)`. When a source cannot echo the ref, use
  `InvocationRegistry.claimStructurally(...)` with a documented
  structural-safety note at the claim site.
- Correlation identity and durable idempotency identity are separate contracts.
  Name which property each boundary relies on even when a protocol deliberately
  uses the same value for both.
- Do not include main-injected sender metadata in a renderer-computed immutable
  payload hash. Bind and validate that metadata separately during authorization,
  or compute the authoritative hash only after main supplies it.
- When a machine becomes the sole scheduler for a queue, every legacy enqueue
  path must poke the machine or enqueue through it.
- Treat pending queue mutations as dispatch barriers before and after claiming a
  head; restore the claim if a mutation wins. Reconsider dispatch after rejected
  mutations too, so a hold released during the mutation cannot strand the queue.
- Execution holds for destructive mutations must survive partial-failure recovery,
  not just the operation promise. Rehydrate recovery holds before reconciliation
  and release them only when recovery settles or the owning app is disposed.

## Deliberate degrees of freedom

Concurrency and staleness policy are domain behavior, not kernel behavior.
Document in each machine's `state.ts` or `controller.ts` what runs serially or
in parallel, which events may be dropped as stale, and which must never be
dropped. Main-process machines should use an explicitly constructed registry
with injected timers, IDs, and broadcasts; renderer machines use the shared
keyed host.

When independent async operations should overlap but both gate progress, start
both through commands and model their completion as separate events joined by
explicit state flags or substates. A serial command queue must not accidentally
turn prior `Promise.all`-style behavior into additive latency.

New machines must inject `Clock` and `IdSource` from `src/state_machines/clock.ts`
when they schedule timers, read wall time, or mint operation identities. Use
`createFakeClock` and `createSequentialIdSource` in tests instead of fake global
timers or nondeterministic UUIDs; retrofitting existing machines is optional.

## Composition

- Machines communicate through typed facades injected in their dependency
  objects, or through explicit events. A machine must never import another
  machine's registry or controller module.
- Record the machine dependency graph in each participating module's header
  and keep it acyclic. Construct concrete facade adapters at an application
  composition root, outside both machines.
- Make a field that composed operations must carry **required** on every
  operation that has it, even when that changes the wire shape. An optional
  field has to be re-attached by hand at each composite edge (`compositeNext`,
  continuation operations, conflict origins), and the miss is silent: adding
  `provider?` to `push`/`rebase` but not `rebase-continue` made a resumed
  rebase on a GitLab app announce a push to GitHub. Required, the compiler
  finds every gap. Do not keep an optional field to preserve byte-identical
  payloads for the existing provider — and treat a test asserting the wire
  shape (`expect(op).not.toHaveProperty("provider")`) as a smell, since it
  pins the shape rather than any behaviour and fails on the fix.

## Read models and intents

- The former same-process Jotai projection compatibility layer was retired by
  `plans/cleanup-state-machines.md`. Do not reintroduce `projectToAtom`,
  `registerAtomWriter`, or another lifecycle-mirroring helper. Renderer
  consumers read the owner snapshot through domain hooks and pure selectors.
- Cross-process actors expose a named, serializable read model. Each renderer
  window owns its subscription/bootstrap adapter and treats unavailable or
  pre-bootstrap data as non-authoritative. The adapter may cache the remote
  snapshot for `useSyncExternalStore`; it must not create a second writable
  lifecycle authority in Jotai.
- Renderer actions cross the owner boundary as typed facade intents. Intent
  admission, transition commit, command completion, and durable acceptance are
  distinct outcomes; expose the narrow receipt or settlement signal the caller
  actually needs.
- Manager admission and transition application are separate facts. Before
  deleting an admission-gated side channel, characterize admitted events that
  the transition deliberately ignores (including startup, shutdown, and
  compatibility routing); preserve any observable data in an owner-scoped read
  model unless changing those transitions is explicitly in scope.
- A machine with interactive controls defines a pure
  `selectCapabilities(state)` whose named booleans express domain UI policy,
  and exposes those capabilities through its projection. Do not derive
  capability by probing the transition with a synthetic event: acceptance may
  depend on payload, and accepted idempotent work may still warrant hidden UI.
- Prefer derived selectors for values computable from the snapshot. Do not add
  generation counters or mirrored booleans beside a machine-owned identity or
  lifecycle state.
- When replacing a retained generation with an active-only identity, audit
  React effect dependencies for the new active-to-empty settlement edge.
  Start-only effects must explicitly require a new non-empty identity.
- When local form or dialog state dispatches a machine-owned mutation, preserve
  the user's input while the operation runs and after failure. Close dialogs
  and clear forms only on authoritative settlement; dispatch itself is not
  proof that the mutation succeeded.
- When a presentation entry mounts the component that owns its mutation hook,
  do not clear that entry before dispatch. Capture its identity and clear only
  that same entry after authoritative settlement; an entry emitted during the
  operation is newer state and must survive.
- A remote dispatch receipt proves transport admission, not runtime completion.
  When callers sequence work on the outcome, project a bounded,
  operation-correlated settlement acknowledgment. Superseded completions must
  settle their original waiters without advancing the current lifecycle.
  Keep the request correlation ID separate from a reused runtime invocation
  identity (for example, an idempotent ensure-running request targeting an
  existing process), and subscribe before the final settlement recheck so a
  completion cannot land between the initial read and listener registration.
  Track every in-flight request, not only the latest: a reused invocation can
  be superseded before its producer settles, but its original waiter must
  still complete. A producer sink captured for one invocation must also ignore
  or overwrite any conflicting invocation identity supplied by its payload.
- When final admission registers an operation before awaiting the actor ticket,
  roll back only that fresh entry on every failed, disposed, or metadata-less
  ticket exit; never mutate a coalesced or replayed operation. Registry release
  must use the same terminal predicate as settlement accounting, because a
  rejected entry has already left pending capacity even without a payload.
- A first-response-wins renderer handoff needs a correlated claim, not a
  boolean. Matching follow-ups may be revision-stale only when the opaque claim
  ID is validated by the host. Unrelated reconciliation must not release the
  claim, and renderer loss needs an owner signal or bounded actor-owned expiry.
  Clear claimant-local identity in `finally` when a matching settlement
  dispatch fails, because actor expiry cannot clear renderer memory. If the
  claimant creates a durable resource before host acknowledgement, delete it
  when acknowledgement fails or the claim expires.
- An unavailable/bootstrap remote snapshot is not authoritative idle state.
  Gate every actor-backed capability on a ready connection and defer recovery
  dispatches until subscription bootstrap has completed. Cleanup dispatched
  while navigating away must temporarily retain the old actor, resync stale
  revisions, and retry with the same stable operation identity; losing an exit
  intent can leave the external resource under hidden retained ownership.
- When window-local presentation controls a shared external lifecycle, track
  explicit per-window interest in main and clean up only when the last owner
  explicitly releases it. Window destruction should drop stale interest without
  triggering cleanup when the actor is designed to survive renderer reloads.
- A safe remote projection contains only domain facts needed by consumers.
  Keep window-local presentation fields out of the authoritative snapshot and
  route one-shot toast/navigation outcomes to the initiating window. Publish
  durable query scopes separately so every attached window converges. At the
  renderer boundary, explicitly recombine the local presentation snapshot with
  every remote lifecycle state, including transient command states; otherwise a
  correct domain transition can silently reset the visible pane or selection.
- Apply presentation for a remotely adjudicated selection only after an
  applied receipt, and serialize rapid selections through resync. Suppressing
  an earlier accepted presentation merely because a later stale dispatch is
  pending can leave the UI disagreeing with the external resource.
- Treat an operation ID's initiating window as a first-writer ownership claim.
  A duplicate intent from another window must not overwrite that routing entry,
  even if the duplicate transition will later be ignored.
- Main-owned presentation routes use `OperationRouteRegistry` from
  `src/window_infrastructure/main/`. Admit with the stable authoritative
  operation ID and an owner containing stable owner/machine identities, an
  optional window session, and an opaque route. Identical duplicates coalesce
  (or replay while terminal retention remains); conflicts never replace the
  first owner. The required `snapshotRoute` adapter must return an owned route
  value so caller or inspector mutation cannot rewrite stored ownership, and
  `sameRoute` must explicitly define equality for that opaque value. Both
  adapters are trusted synchronous code; the registry fails closed if either
  attempts to reenter ownership mutation, rejecting before stored ownership
  changes even when the adapter catches the inner rejection. Runtime thenable
  results from either adapter are rejected without assimilating arbitrary
  thenables outside the guard.
- `OperationRouteRegistry` pins unresolved routes behind a separately bounded
  admission limit and evicts only terminal routes, in settlement order, behind
  a declared finite retention count. Its constructor snapshots validated
  policy values and callbacks; later caller mutation cannot change accounting.
  Call `markTerminal(handle)` only at authoritative publication/settlement,
  then release with the opaque generation-bearing handle or an explicit
  owner/window/machine disposal method. Owner disposal is scoped by both
  machine and owner identity. Duplicate terminal/release calls and stale
  handles are no-ops.
- Window destruction must call the registry's read-only
  `inspectWindowRoutes()` before the domain explicitly chooses drop,
  entity-window, or focused-window fallback. Do not wire window unregister to
  `releaseWindow()`: unresolved ownership survives renderer loss until the
  authoritative operation settles or the domain explicitly disposes it.
- Use `inspect()` for route resource accounting and leak diagnostics; it
  reports operation identity, owner, machine/window metadata, state, and
  generation. Registry disposal is terminal and must leave the route count at
  zero.
- Authorization can run before revision admission. Keep any presentation
  ownership recorded there tentative and expire it unless an applied
  transition confirms the claim; rejected stale dispatches never reach actor
  observers and otherwise leak bounded routing capacity.
- Do not hide local presentation for a cleanup intent until main accepts the
  exit (or already reports a safe terminal state). Resync and retry stale
  cleanup receipts with one operation ID so a hidden pane cannot mask retained
  external ownership.
- Keep transport revisions separate from semantic presentation epochs. A
  revision may advance for bookkeeping-only transitions, while a reload token
  must advance exactly once for each user-visible remount.
- When an epoch keys a mounted resource, capture props such as an iframe `src`
  from the epoch-changing snapshot. Do not let later same-epoch state updates
  rewrite identity-defining DOM attributes and trigger an implicit reload.
- When a later event carries only an identity, consumers that need additional
  context after reload must recover it from the hydrated projection. Buffer
  identity-only events that can arrive before hydration completes instead of
  assuming the consumer observed an earlier, self-contained event.
- If retries may replace an input payload, carry operation facts established
  by earlier transitions (such as create-vs-update) explicitly in state. Do not
  re-derive UI or analytics semantics from the replacement payload.
- A remote renderer adapter must preserve the legacy terminal side-effect
  contract as well as lifecycle state: query invalidations, authoritative
  message refresh, preview-open policy, reload/capture requests, and settlement
  callbacks all belong in the completion projection.
- When seeding a retained-completion cursor from bootstrap, skip only receipts
  that have no matching local in-flight request. A matching receipt must take
  the normal completion path or its waiter and terminal side effects are lost.
- Main-owned work must report terminal settlement through a main-owned observer
  or return path. Renderer delivery is best-effort: a destroyed `WebContents`
  can make `safeSend` a no-op and must not strand the authoritative actor.
- `useSyncExternalStore` snapshots must be referentially stable between store
  changes. If an adapter overlays optimistic admission on a remote snapshot,
  cache the projected object by base snapshot and operation identity instead of
  allocating a new object from every `getSnapshot()` call.

## Persistence and hydration

- Model hydration explicitly when persisted state gates machine behavior.
  Persist through an adapter-owned, debounced command using a versioned zod
  schema; do not let components write snapshots independently.
- Persist semantic discriminators that affect post-restart transitions, not
  only coarse flags derived from them. For legacy rows, choose an explicit
  conservative fallback (for example, a paused queue without a reason hydrates
  as manually paused) and test both legacy and fully persisted recovery paths.
- When a side effect can make recovery state externally observable (for
  example, detaching Git HEAD), force and await persistence of the exact
  committed checkpoint before starting it. Observer error isolation must not
  allow the side effect to run after that checkpoint fails.
- A checkpoint immediately before a destructive step means that step may have
  started. Restart reconciliation must not infer "not started" from one
  unchanged external fact such as Git HEAD; validate every relevant identity
  and mutable surface (for example branch, HEAD, and user-visible index/tree),
  and classify failure from the last effect boundary that may have crossed.
- A durable `completed` checkpoint must follow every authoritative effect, not
  just the primary one. If a workflow mutates Git and then SQLite, persist an
  explicit post-Git/next-database step until the database mutation finishes.
- Define merge/replacement semantics for events received during hydration.
  On teardown, flush the latest accepted snapshot through a transport that is
  safe for the lifecycle boundary (for example, one-way IPC during pagehide).
- Entity deletion must fence new command admission before waiting for the
  entity lock. Recheck the fence inside the lock, stop actor admission before
  unrelated awaited cleanup, and make actor disposal flush every admitted
  command before database or filesystem deletion.
- Deletion settlement tracks the full command-runner continuation, including
  post-handler lifecycle work and the terminal event that may synchronously
  enqueue compensation. Waiting only for the low-level handler promise can
  dispose the actor before its compensating command exists.
- A persisted main-owned recovery actor must reconcile its domain facts with
  the external resource before accepting new mutations after restart. Treat
  matching origin state as closed and detached/divergent state as explicit
  recovery; never serialize command handles or renderer presentation state.

## Query keys and recorded decisions

- Machine-adjacent TanStack Query keys nest as `[domain, appId, ...]` so data
  invalidated together remains scoped per app; do not use sibling keys for
  those values.
- Recorded plan decisions are reviewable artifacts. When a review finding is
  rebutted as working as designed, the cited decision must actually cover the
  disputed behavior.

## Tests

- Exercise every reachable state against every event type and assert totality.
- Assert ignored transitions retain the exact state reference and changed
  transitions do not create value-equal snapshots.
- When state stores both history position and current entry, derive the current
  value from the matched slot. Traversal lookup must prefer an exact entry before
  canonical equivalence, and tests must cover duplicate equivalent entries.
- Use fake command runners. Tests must get isolation from constructed owners,
  never from a module-global reset helper.
- Run `runControllerConformanceSuite` for controllers built on the shared
  dispatcher/lifecycle contract; domain tests remain responsible for domain
  behavior. A command fixture may change domain state,
  but its test adapter must model that real transition and expose the
  controller's real snapshot. Disposal assertions compare with the snapshot
  captured immediately before disposal; never normalize or fabricate snapshots
  to make them pass.
- Normalize discovered file paths to `/` and sort filesystem-derived
  inventories before asserting literal repository paths. `path.relative()`
  returns `\` on Windows CI, and directory enumeration order differs by
  filesystem and case-sensitivity.
- `driveTransitionMatrix` remains available for hand-enumerated totality
  tests; new machines may instead use `exploreReachableStates` when a finite
  event generator can discover the reachable graph. Existing bespoke suites
  need not be migrated mechanically.
- If reachable states contain an unbounded correlation counter (such as a
  retry attempt), normalize only that counter in the explorer's `stateKey` so
  the graph stays finite; keep direct transition tests for exact stale-token
  acceptance and rejection.
- When adding an event to an explorer's generator, reuse a payload an existing
  event already uses if the two share a transition branch. A new distinct value
  (another URL, another id) multiplies the reachable graph combinatorially and
  trips `Reachable-state exploration exceeded maxStates` without covering
  anything new. Assert what is specific to the new event in a direct transition
  test instead.
- Use `assertCapabilityTransitionConsistency` with domain-supplied
  representative valid events for every capability. Every enabled
  capability/state pair must supply at least one valid representative so the
  assertion cannot pass vacuously. When payload affects acceptance, include
  representative invalid payloads; disabled capabilities may also pin their
  expected ignore reason.
- `boundaries.test.ts` enforces kernel purity and machine-to-machine isolation;
  add new machine directories to its inventory when they are introduced.
- Boundary allowlist tests must derive the actual production call sites and
  exact-compare them with the declared inventory. Checking only that declared
  markers still exist does not prevent an undeclared boundary crossing.
  Classify calls through the owning API (for example, Jotai stores and hooks),
  not an expected import directory; domain values may be local or re-exported.
- Keep host-only distributed-machine definitions outside shared machine
  directories (for example, under `src/ipc/services/` for a main-owned actor).
  Shared machine directories are scanned as renderer-reachable code and may
  not import IPC, Electron, or WindowRegistry internals. Add every intentional
  distributed-machine consumer to the exact inventory in
  `src/distributed_machines/boundaries.test.ts`.
- Keep remote transport test doubles behind an existing domain test-support
  facade. Renderer and hybrid harnesses may consume that facade, but must not
  widen the allowlist of production modules that import transport internals.
- When a test definition uses the native `remoteIntent` contract, inject
  authorization races through `remoteIntent.authorizeSubscribe` or
  `remoteIntent.authorizeDispatch`. Replacing the legacy `remote.authorize*`
  hooks exercises only compatibility definitions and can leave a native test
  gate waiting forever.
- Keep native remote-admission revalidation out of the protocol-v1
  compatibility adapter until a domain migrates. Moving trusted conversion or
  adding a second revision fence to legacy dispatch can change terminal
  delivery even when the wire envelope is unchanged; cover a representative
  legacy streaming integration when editing the adapter. Preserve the adapter's
  bounded re-authorization when an allow-stale event races an actor revision;
  exact captured-revision rejection belongs to the native prepared path.
- Model StrictMode subscriber replay and explicit subscription leases as
  different ownership classes. A shared subscriber lease may use a
  replacement generation, while every explicit `retain()` needs its own live
  token so releasing one owner cannot retire or leak another. A lease created
  before the owning client/provider starts must let `ready` adopt that startup
  bootstrap, and an in-flight completion retain must count as transport
  interest during connection replacement and disposal teardown.
- In `runCosim` suites, `maxSchedules` bounds visited configurations, not only
  quiescent leaves. If one orthogonal action (for example quit at every phase)
  causes a bound hit, split it into a focused exhaustive alphabet instead of
  raising the bound and slowing the primary scenario.
- Treat snapshots passed to `runCosim` callbacks as frozen immutable views.
  Do not mutate them, and do not replace the generic callback contract with a
  serialization-based clone that rejects valid domain values or loses
  prototypes.
- Key-aware debug retention must bound both entry payloads and per-key
  metadata. When aggregate eviction empties a key's ring, prune the ring so
  one-shot entity IDs cannot accumulate forever, and maintain a global
  insertion-order index instead of scanning every key on each production
  trace event.
- Capturing keyed admission before authorization must not allocate permanent
  per-key gate metadata. Use a shared open generation (or explicitly release
  the capture) so rejected, attacker-controlled keys cannot grow a
  process-lifetime map.
- When a fence snapshots work admitted before publication, settlement must
  remove that work from the current fence's drain set even though no fence
  existed when tracking began. Test this with controlled pre-fence work; a
  tracker that cleans up only its originally captured fence can strand sealing.
- A command runner that returns `void` has not provided a fence-trackable
  completion boundary, even when that particular branch is synchronous; the
  host cannot distinguish it from detached asynchronous work. Migrated
  definitions must return a completion promise from every command branch.
  Until a compatibility path adopts a completion promise or explicit tracked
  lease, destructive fencing must fail closed rather than treating the handoff
  as command completion.
- Prepared admission must revalidate after the last trusted synchronous domain
  callback, not merely after asynchronous authorization. Revision policies,
  intent conversion, and similar callbacks can reenter fencing, subscription,
  or disposal code before final host admission.
- Treat supersession settlement listeners as synchronous domain callbacks.
  Revalidate after they run and roll back any replacement registration before
  enqueue if they fence, replace, or dispose the intended actor.
- A transport disconnect does not prove that authoritative admission failed.
  Preserve delivery/admission ambiguity across retry and disposal; only a
  failure classified at a definitely-pre-delivery boundary may settle as
  `not-admitted`.
- When a correlated outcome is delivered before its committed snapshot, carry
  the committed actor metadata and retain request-owned snapshot observation
  until that revision (or disposal) is visible. A compatibility owner that
  emits no correlated outcome must still settle admitted ownership from an
  unavailable snapshot; otherwise disposal strands the renderer request.
- If terminal outcome or receipt construction throws, reject or explicitly
  fail the exact correlated registry entry and continue bulk disposal. Never
  leave callback-construction failures pinned as unresolved work.
- A fresh subscription or actor-reference acquisition must assert that keyed
  admission is open even when it retains an existing actor. Generation equality
  alone is insufficient because work prepared after fencing captures the
  current closed generation.
- Revalidate fence identity and phase after invoking a drain-admission policy.
  The policy is synchronous domain code and can reenter sealing, abort, or
  replacement before returning.
- Construction continuations enrolled for fencing must settle on every
  post-activation exit, including host or machine disposal triggered by buffered
  factory-time ingress, or a later fence can wait forever. Keep factory-buffered
  events bound to that construction admission; if fencing publishes before
  activation, do not reclassify them as cleanup allowed during draining.
- Disposal of an admission primitive is terminal. Clearing current records
  without a persistent disposed state lets retained references recreate open
  admission after host teardown.
- If a scheduler retains an execution callback and then throws or rejects, the
  retained callback must be invalidated. Marking the batch failed while still
  allowing that callback to run lets command side effects escape after sealing.
- Keep captured producer sinks revision-bound by default. A collection actor
  whose every producer event carries domain correlation may explicitly opt into
  actor-instance plus keyed-admission-generation binding, because cancellation
  and unrelated parallel jobs legitimately advance that actor before terminal
  output arrives. Such an opt-in must use domain invocation identity to reject
  stale or replacement-job output. Factory-buffered sequential emissions must
  retain the same captured identity through activation, and output after actor
  disposal must remain non-creating.
- A destructive fence is scoped to the actor key, not to a domain predicate
  within a collection. If owner deletion must preserve unrelated in-flight
  producers, partition the actor key by owner or add first-class scoped gate
  generations; filtering drain events alone cannot preserve unrelated captured
  generations through seal and release.