Every write travels one spine
A typed WebSocket request becomes a command. A pure function decides it. One SQLite transaction persists the events, folds the read models, and records the receipt. Only then do reactors perform the side effects. This page walks that path end to end, with the code that enforces it.
Sources: packages/contracts/src/orchestration.ts:1553,
apps/server/src/orchestration/Layers/OrchestrationEngine.ts:341,
apps/server/src/trading/TradingExecutionReceipts.ts:4-8,
packages/trading-contracts/src/execution.ts:78-80.
One request's journey
Follow one command from the socket to the side effect. Every hop is a named function in the repository, and the order below is the order the code runs in.
A typed request, not a string
The client sends a member of the ClientOrchestrationCommand union through WsOrchestrationDispatchCommandRpc: the payload is the command, the success type is a single {sequence}, and the errors are a typed union. Every RPC on the socket belongs to one WsRpcGroup, so the wire has one vocabulary, schema-checked at both ends.
packages/contracts/src/rpc.ts:906-913
packages/contracts/src/rpc.ts:1219
A Deferred and an envelope
The engine's dispatch(command, {origin}) creates a Deferred, offers a CommandEnvelope (the command, the client origin, the result cell, a start timestamp) onto the queue, and awaits. The caller's eventual answer is the persisted sequence number, nothing looser.
apps/server/src/orchestration/Layers/OrchestrationEngine.ts:56-61
apps/server/src/orchestration/Layers/OrchestrationEngine.ts:350-360
One queue, one worker
The engine creates a Queue.unbounded<CommandEnvelope> for commands and a PubSub.unbounded<OrchestrationEvent> for committed events. One Effect fiber takes commands from the queue, so they commit one at a time in arrival order. The single worker enforces this ordering without a separate application lock.
apps/server/src/orchestration/Layers/OrchestrationEngine.ts:101-102
apps/server/src/orchestration/Layers/OrchestrationEngine.ts:341
Idempotency before decision
Before deciding anything, the worker reads commandReceiptRepository.getByCommandId. A receipt for an accepted command returns the original sequence, so a replay is a no-op with the same answer. A commandId that was rejected once fails forever with OrchestrationCommandPreviouslyRejectedError. The code is blunt about scope: "A receipt only proves this exact command was handled", so the same commandId aimed at a different aggregate is a conflict, not a success.
apps/server/src/orchestration/Layers/OrchestrationEngine.ts:149-177
apps/server/src/persistence/Services/OrchestrationCommandReceipts.ts:26-34
A pure function decides
decideOrchestrationCommand({command, readModel}) maps the command to its event or events, and does nothing else: no clock, no exchange, no write. Trading commands become trading.*-requested events. The risk-control case deliberately resolves no target status, because "what the mission becomes depends on what the exchange does, so the reactor decides it after the fact".
apps/server/src/orchestration/decider.ts:218
apps/server/src/orchestration/decider.ts:1475-1478
Origin is an engine concern
The engine, not the decider, stamps the dispatching client's origin onto every event the command produced. The comment states the rule: "The decider stays pure; attribution is an engine concern."
apps/server/src/orchestration/Layers/OrchestrationEngine.ts:195-203
One transaction, four writes
Inside one sql.withTransaction, for each event: eventStore.append persists it, projectEvent folds it onto the in-memory read model, projectionPipeline.projectEvent writes the durable SQLite projections, and finally the accepted receipt is upserted with resultSequence set to the last event's sequence. A command that produced no events is itself an invariant error. Because the receipt shares the transaction, an accepted receipt exists exactly when its events do.
apps/server/src/orchestration/Layers/OrchestrationEngine.ts:204-241
apps/server/src/orchestration/Layers/OrchestrationEngine.ts:218-222
Reactors answer
Committed events publish to the PubSub, and consumers do not share a queue: each access to streamDomainEvents creates a fresh subscription, so the WS push path, provider ingestion, checkpointing, and the trading reactor each receive every event independently. TradingMissionReactor filters the stream down to its eight HANDLED_EVENT_TYPES and enqueues matches into a drainable worker, where the side effects run. What happens inside that worker is the next page, Execution.
apps/server/src/orchestration/Layers/OrchestrationEngine.ts:251-252
apps/server/src/orchestration/Layers/OrchestrationEngine.ts:365-370
apps/server/src/trading/TradingMissionReactor.ts:115-124
apps/server/src/trading/TradingMissionReactor.ts:2922-2930
Requested is a question, not an answer
The decider never pretends a side effect happened. Press pause in the UI and the event on the wire is trading.mission-control-requested, a request, and its contract says so: "the request itself is not a promise that it will be accepted". The reactor then runs the transition through TradingMissionService, where the status legality table and the one-active-mission invariant are enforced, and only after that write succeeds does it dispatch the internal trading.mission.status-set command whose event the projector folds.
"That ordering is the whole point. The UI never sees a status the domain refused, and mission state still reaches clients over T3's ordered WS push path rather than a side channel." apps/server/src/trading/TradingMissionReactor.ts:10-12
Creation works the same way. The mission-create payload's contract states: "Nothing is persisted yet: TradingMissionReactor performs the write and then raises trading.mission.status-set, so the projection only ever reflects state the domain accepted." And a refusal is not an exception path: "A refused control or execution is a normal outcome, not a crash: the projection keeps the state the domain still holds."
This ordering requires every mission event to join an ordered stream. Each trading command names the thread bound to the mission's agent runtime, called the harness in code. The mission events then use the thread's existing WebSocket stream to reach the UI. Manual orders have no thread, so they use a synthetic per-account mission aggregate, manual:{accountId}, which preserves per-account ordering.
The green path is the only path to a status the UI can see.
apps/server/src/trading/TradingMissionReactor.ts:1-18
packages/contracts/src/trading.ts:1574-1578
Idempotency, layered four ways
Retries are certain: WebSocket reconnects, provider replays, watchdog ticks. Each layer below makes a specific replay harmless, at the layer where its damage would occur.
Sticky command receipts
A receipt row is {commandId, aggregateKind, aggregateId, acceptedAt, resultSequence, status, error}. Replay an accepted command and you get the original sequence back. Replay a rejected one and it fails again, forever: rejection is sticky, so a bad command cannot be retried into existence. A receipt for aggregate A replayed against aggregate B is a conflict error, because the receipt proves only that exact command on that exact aggregate was handled.
status: accepted | rejected
apps/server/src/persistence/Services/OrchestrationCommandReceipts.ts:26-34
apps/server/src/orchestration/Layers/OrchestrationEngine.ts:149-177
Deterministic cloids
The exchange correlation id is derived from SHA-256 over (missionId, executionSequence, actionType). The code takes the first 16 bytes and encodes them as a 34-character hexadecimal string with a 0x prefix. Retries of the same action reuse the same pair and therefore the same cloid. The prefix is load-bearing: an unprefixed cloid is accepted on submission and then silently stored as null, which is how one entry once ended up unjoinable to its own fill.
SHA-256(missionId, executionSequence, actionType)[0:16]
packages/trading-contracts/src/execution.ts:49-88
The inbox will not wake twice
Observed facts persist as inbox events deduplicated on (missionId, deduplicationKey) under the unique index idx_trading_event_inbox_dedupe, "so reconnects and replays cannot generate a second wake-up". An event moves pending → included_in_run → consumed, and claimPending atomically flips the set a run owns, so a run always sees exactly the events it started with.
apps/server/src/trading/TradingEventInbox.ts:5-12
apps/server/src/trading/TradingEventInbox.ts:58-67
The latch settles last
The tool that requested an execution waits on one in-process latch per (mission, executionSequence). The reactor opens the latch only after every durable update completes. Opening it earlier could make the waiter report a completed execution as still in flight. The latch remains available for 30 seconds for late waiters, but it never replaces the persisted result.
apps/server/src/trading/TradingMissionReactor.ts:2215-2227
apps/server/src/trading/TradingExecutionReceipts.ts:1-23
apps/server/src/trading/TradingExecutionReceipts.ts:64
The event envelope
The OrchestrationEvent union has 45 member types, from project.created to trading.execution-requested, and every member carries the same nine base fields. The envelope is what makes the stream auditable years later: what happened, to what, why, and who asked.
packages/contracts/src/orchestration.ts:1541-1551
packages/contracts/src/orchestration.ts:1520-1539
apps/server/src/orchestration/Layers/OrchestrationEngine.ts:75-81
apps/server/src/orchestration/Layers/OrchestrationEngine.ts:371-375
apps/server/src/orchestration/Normalizer.ts:24-28
apps/server/src/ws.ts:1454
Two classes of command, two answers
Not every operation belongs on the event spine. Use a direct RPC when the operator needs an immediate result or refusal. For example, preview returns its refusal in the RPC response, while placement uses an event-sourced command. The same reasoning appears throughout the client wiring: reviseTradingPlan is an RPC command because "the operator needs the answer, not an acknowledgement", and a plan-document activation needs its refusal (stale_hash) on screen before the operator believes the plan is active.
When the write must be durable, ordered, and replayable, it is a dispatched command. The §14.7 user controls (pause, resume, revoke, risk control) dispatch straight to the server, and that directness is the point: "no harness turn stands between the press and the action." The dispatch is acknowledged with a sequence, the outcome arrives later as an event on the feed, and the UI tracks the press as pending until the result does. These controls keep working when the provider process is unavailable. Safety invariants explains that independence.
The manual order ticket uses both classes at once: preview and manual close are RPCs, "the user needs the refusal on screen; the place itself is an event-sourced dispatch, answered through the feed." The client builders that mint commandIds for these dispatches are covered in Web & desktop.
- previewTradingOrder
- closeTradingManualPosition
- armTradingWatch / cancelTradingWatch
- reviseTradingPlan
- activateTradingPlanDocument
Question answered in the response. Nothing persisted.
- missionControl (pause, resume, revoke)
- riskControl (reduce, close, cancel entries)
- missionCreate
- trading.order.place
Acknowledged with a sequence, answered by an event the projector folds.
packages/contracts/src/trading.ts:1322-1327
packages/client-runtime/src/state/orchestration.ts:144-186
Built to change
An event store is a permanent record, so schema evolution is a compatibility discipline, not a migration you can run and forget. Three habits keep old bytes readable.
Optional, never sentinel
allocatedCapitalUsd is optional "so the two cases stay distinguishable in the event stream: a mission created without a stated capital records that it was created without one". The decider agrees: "Absent stays absent", because a default injected there would erase the distinction before the reactor ever reads it.
packages/contracts/src/trading.ts:1584-1590
apps/server/src/orchestration/decider.ts:1435-1437
Deprecated, still decodable
thread.workspace-mode-set remains in the union with the comment: "Deprecated: no emitter remains. Kept only so event stores written by older builds still decode; the projector ignores it." Old stores open; nothing new emits; the projector skips.
deletion would corrupt history
packages/contracts/src/orchestration.ts:1634-1640
Tolerance readers
Persisted JSON that predates a schema axis is read leniently: decodeMarketRef treats a bare "ETH" as the default venue, "because that is the only venue there has ever been". The venue enum itself is a one-member literal on purpose, so a second venue is "a data question rather than a rewrite".
packages/trading-contracts/src/primitives.ts:124-142
packages/trading-contracts/src/primitives.ts:75-85
What the spine buys
Everything that crosses a boundary is a typed contract, and every safety decision lives in a pure function or a deterministic service. The decider decides, the services enforce, the reactor sequences. No rule lives only in prompt prose or client state, so neither the model nor the UI can bypass a gate.
The same spine carries the rest of the system. The budget gates the reactor consults are pure equations, covered in Risk control. Local tables converge to exchange truth through reconciliation triggers that ride the same event flow, covered in Reconciliation. The schema families and their producers and consumers are catalogued in Contracts, and the wallet permitted to sign what the reactor submits is the subject of Signer & authority.
Go deeper: the spine itself
-
apps/server/src/orchestration/Layers/OrchestrationEngine.tsqueue, worker fiber, receipt gate, transaction, fan-out -
apps/server/src/orchestration/decider.tsthe pure command-to-event mapping, trading cases at 1413-1758 -
apps/server/src/orchestration/projector.tsthe in-memory fold every command is decided against -
apps/server/src/persistence/Services/OrchestrationCommandReceipts.tsthe receipt contract
Go deeper: reactors and receipts
-
apps/server/src/trading/TradingMissionReactor.tsanswers every requested event, settles last (2215-2227) -
apps/server/src/trading/TradingExecutionReceipts.tsthe latch, its 30-second retention, its philosophy -
apps/server/src/trading/TradingEventInbox.tsdeduplicated observed facts, claimed per run -
packages/trading-contracts/src/execution.tscloid determinism and the execution intent