Post-commit reactors and the delivery gap
Durable intent crosses a non-durable hot-stream seam into independent reactor workers, whose serialized handlers can still fork overlapping provider work and whose progress is not reconstructed after a crash.
What this chapter resolves
- Locate the exact commit-before-publish crash window and predict what a command retry does.
- Distinguish serialized reactor handlers from forked provider fibers and independent consumers.
- Trace provider results back through volatile runtime ingestion into durable internal commands.
- Evaluate activation and shutdown boundaries without inventing an outbox, cursor, or production-wide drain.
The transaction gives T3 Code durable intent. It does not give every post-commit consumer durable delivery. The engine commits events, SQL projections, and an accepted receipt, returns from that transaction, replaces its in-memory read model, and only then publishes each event to an in-process PubSub. Reactors turn those hot events into provider, checkpoint, terminal, deletion, and awareness effects.
That separation is useful: an external harness call never holds the SQLite transaction open. It also creates the defining failure window of this chapter: the database can say “accepted” while every side-effect subscriber sees nothing.
The transaction ends before delivery begins
const committedCommand = yield* sql
.withTransaction(
Effect.gen(function* () {
const committedEvents: OrchestrationEvent[] = [];
let nextCommandReadModel = commandReadModel;
for (const nextEvent of eventBases) {
const savedEvent = yield* eventStore.append(nextEvent);
nextCommandReadModel = yield* projectEvent(nextCommandReadModel, savedEvent);
yield* projectionPipeline.projectEvent(savedEvent);
committedEvents.push(savedEvent);
}
const lastSavedEvent = committedEvents.at(-1) ?? null;
if (lastSavedEvent === null) {
return yield* new OrchestrationCommandInvariantError({
commandType: envelope.command.type,
detail: "Command produced no events.",
});
}
yield* commandReceiptRepository.upsert({
commandId: envelope.command.commandId,
aggregateKind: lastSavedEvent.aggregateKind,
aggregateId: lastSavedEvent.aggregateId,
acceptedAt: lastSavedEvent.occurredAt,
resultSequence: lastSavedEvent.sequence,
status: "accepted",
error: null,
});
return {
committedEvents,
lastSequence: lastSavedEvent.sequence,
nextCommandReadModel,
} as const;
}),
)
.pipe(
Effect.catchTag("SqlError", (sqlError) =>
Effect.fail(
toPersistenceSqlError("OrchestrationEngine.processEnvelope:transaction")(sqlError),
),
),
);
commandReadModel = committedCommand.nextCommandReadModel;
for (const [index, event] of committedCommand.committedEvents.entries()) {
yield* PubSub.publish(eventPubSub, event);
if (index === 0) {
yield* Metric.update(
Metric.withAttributes(
orchestrationCommandAckDuration,
metricAttributes({
...baseMetricAttributes,
ackEventType: event.type,
}),
),
Duration.millis(Math.max(0, (yield* Clock.currentTimeMillis) - envelope.startedAtMs)),
);
}
}
return { sequence: committedCommand.lastSequence };Zoom with the controls, +/−, or Ctrl/⌘ + trackpad scroll. Enable Pan to drag, use two-finger scrolling, or use the arrow keys. 0 fits the diagram; Esc leaves Pan or expanded view.
Text equivalent
The command worker decides events and commits the event log, SQL projection tables, and accepted receipt in one SQLite transaction. After commit it swaps the in-memory command model and publishes to a hot process-local event bus. Independent reactor workers consume that bus. The provider reactor can fork an external send. Provider runtime events then enter another hot bus, runtime ingestion, and an internal command that returns to the engine. A crash between commit and either hot consumer leaves durable state without a durable pending job in the inspected implementation.
apps/server/src/orchestration/Layers/OrchestrationEngine.ts:142–259 ↗apps/server/src/orchestration/Layers/ProviderCommandReactor.ts:1377–1439 ↗apps/server/src/orchestration/Layers/ProviderCommandReactor.ts:1060–1174 ↗apps/server/src/provider/Layers/ProviderService.ts:286–295 ↗apps/server/src/orchestration/Layers/ProviderRuntimeIngestion.ts:884–1162 ↗The engine’s returned sequence is therefore an acknowledgement of command acceptance, not provider completion. A client retry using the same command id finds the accepted receipt and returns its stored sequence. That lookup does not republish the event. Receipt idempotency protects the transaction from duplicate command execution; it does not repair the post-commit side-effect bridge.
Move the crash across the delivery seam
Each position means “the process disappears immediately after this boundary.”
Use the arrow keys to move the simulated crash through the ordered phases.
Position 1 of 10: Before commit
Before commit
The SQL transaction has not committed.
- Durable
- No accepted intent is durable yet.
- Volatile
- Only the command worker and transaction-local writes exist.
- External
- No provider work has started.
- Retry
- After restart, the client can retry and the command can execute normally.
apps/server/src/orchestration/Layers/OrchestrationEngine.ts:142–259 ↗All crash positions
- Before commit
The SQL transaction has not committed.
- Durable
- No accepted intent is durable yet.
- Volatile
- Only the command worker and transaction-local writes exist.
- External
- No provider work has started.
- Retry
- After restart, the client can retry and the command can execute normally.
apps/server/src/orchestration/Layers/OrchestrationEngine.ts:142–259↗ - Transaction open
Events, projections, and the accepted receipt have been written inside one SQL transaction.
- Durable
- A process crash rolls the uncommitted transaction back as a unit.
- Volatile
- The proposed in-memory command model has not replaced the live model.
- External
- The event has not reached the hot bus.
- Retry
- No accepted receipt survives, so the retry can decide and commit again.
apps/server/src/orchestration/Layers/OrchestrationEngine.ts:142–259↗ - Commit returned
The event, SQL projections, and accepted receipt are durable; publication has not happened yet.
- Durable
- Intent and its command sequence survive restart.
- Volatile
- No durable outbox row or per-reactor cursor records pending delivery.
- External
- The provider has not seen the intent.
- Retry
- The same command id returns the stored sequence and does not republish, so that retry leaves this delivery gap unrepaired.
apps/server/src/orchestration/Layers/OrchestrationEngine.ts:142–259↗ - Hot publish
The committed event is offered to current PubSub subscribers.
- Durable
- The domain event remains durable in SQLite.
- Volatile
- Subscriber queues live only in this process; publication is not a recoverable job record.
- External
- A reactor may or may not have dequeued the event.
- Retry
- A receipt hit still returns success without replaying reactor work.
apps/server/src/orchestration/Layers/ProviderCommandReactor.ts:1377–1439↗ - Turn-start observed
For a turn-start event, the provider reactor handles the event and marks its derived turn key in a volatile dedupe cache.
- Durable
- The original intent is durable, but reactor progress is not.
- Volatile
- The dedupe mark is made before the provider effect and disappears on restart.
- External
- The adapter call has not necessarily begun.
- Retry
- Within the same process a duplicate can be suppressed even after a later failure; after restart the hot event itself is gone.
apps/server/src/orchestration/Layers/ProviderCommandReactor.ts:1060–1174↗ - Send forked
The serialized handler forks providerService.sendTurn and can return to its queue.
- Durable
- There is still no durable side-effect attempt record.
- Volatile
- The child fiber is outside the worker's outstanding counter.
- External
- Two provider sends can overlap even though handler bodies were dequeued serially.
- Retry
- Closing the scope may interrupt the child; the worker drain does not prove that the send completed.
apps/server/src/orchestration/Layers/ProviderCommandReactor.ts:1060–1174↗ - Provider accepted
The external harness may have accepted the turn before local binding/state persistence finishes.
- Durable
- The original intent survives, but acceptance by the harness is outside SQLite's transaction.
- Volatile
- Local knowledge of the provider result may still be only in a running fiber.
- External
- The turn can continue in the harness after the server loses its exact completion point.
- Retry
- Recovery is ambiguous: blindly repeating can duplicate work, while doing nothing can strand it.
apps/server/src/provider/Layers/ProviderService.ts:771–801↗ - Runtime publish
ProviderService logs a canonical runtime event and then publishes it to another hot PubSub.
- Durable
- The diagnostic log is best-effort and is not the orchestration recovery ledger.
- Volatile
- Runtime ingestion has not necessarily consumed the event.
- External
- The harness action has occurred; its canonical result is in transit inside the process.
- Retry
- A crash before ingestion loses this delivery; no durable runtime-event cursor replays it.
apps/server/src/provider/Layers/ProviderService.ts:286–295↗ - Buffer released
Runtime ingestion invalidates a buffered entry before dispatching its internal command.
- Durable
- No new orchestration result is durable until that internal command commits.
- Volatile
- Dispatch interruption or failure can lose the released batch; the removed value is not restored on this path.
- External
- The provider result already exists outside the domain model.
- Retry
- Re-delivery is not generally idempotent: internal command ids include a random UUID suffix.
apps/server/src/orchestration/Layers/ProviderRuntimeIngestion.ts:884–1162↗ - Result committed
The internal command re-enters the same engine and commits the provider result as domain events.
- Durable
- The projected result and its receipt now survive restart.
- Volatile
- Any new downstream reactor work again crosses the same commit-before-publish seam.
- External
- The durable model has caught up with the observed harness result.
- Retry
- The internal command receipt protects only that generated command id; it does not repair an earlier missed hot event.
apps/server/src/orchestration/Layers/OrchestrationEngine.ts:142–259↗
The cursor exposes two different uncertainties. Before SQL commit, rollback is clean: no accepted receipt survives. After commit but before reactor observation, intent survives while work disappears. Once an external adapter call begins, the problem changes again: the server may no longer know whether repeating the call is safer than leaving it alone.
One queue serializes one handler—not the whole system
OrchestrationReactor.start starts several consumers in one scope: provider runtime
ingestion, provider command handling, checkpoint capture/revert, thread deletion,
and agent awareness. They do not share one global queue. Each worker serializes its
own handler invocations while the consumers themselves run independently.
const processTurnStartRequested = Effect.fn("processTurnStartRequested")(function* (
event: Extract<ProviderIntentEvent, { type: "thread.turn-start-requested" }>,
) {
const key = turnStartKeyForEvent(event);
if (yield* hasHandledTurnStartRecently(key)) {
return;
}
const thread = yield* resolveThread(event.payload.threadId);
if (!thread) {
return;
}
const message = thread.messages.find((entry) => entry.id === event.payload.messageId);
if (!message || message.role !== "user") {
yield* appendProviderFailureActivity({
threadId: event.payload.threadId,
kind: "provider.turn.start.failed",
summary: "Provider turn start failed",
detail: `User message '${event.payload.messageId}' was not found for turn start request.`,
turnId: null,
createdAt: event.payload.createdAt,
});
return;
}
const isFirstUserMessageTurn =
thread.messages.filter((entry) => entry.role === "user").length === 1;
if (isFirstUserMessageTurn) {
const project = yield* resolveProject(thread.projectId);
const generationCwd =
resolveThreadWorkspaceCwd({
thread,
projects: project ? [project] : [],
}) ?? process.cwd();
const generationInput = {
messageText: message.text,
...(message.attachments !== undefined ? { attachments: message.attachments } : {}),
...(event.payload.titleSeed !== undefined ? { titleSeed: event.payload.titleSeed } : {}),
};
yield* maybeGenerateAndRenameWorktreeBranchForFirstTurn({
threadId: event.payload.threadId,
branch: thread.branch,
worktreePath: thread.worktreePath,
...generationInput,
}).pipe(Effect.forkScoped);
if (canReplaceThreadTitle(thread.title, event.payload.titleSeed)) {
yield* maybeGenerateThreadTitleForFirstTurn({
threadId: event.payload.threadId,
cwd: generationCwd,
...generationInput,
}).pipe(Effect.forkScoped);
}
}
const handleTurnStartFailure = (cause: Cause.Cause<unknown>) => {
if (Cause.hasInterruptsOnly(cause)) {
return Effect.void;
}
const detail = formatFailureDetail(cause);
return setThreadSessionErrorOnTurnStartFailure({
threadId: event.payload.threadId,
detail,
createdAt: event.payload.createdAt,
}).pipe(
Effect.flatMap(() =>
appendProviderFailureActivity({
threadId: event.payload.threadId,
kind: "provider.turn.start.failed",
summary: "Provider turn start failed",
detail,
turnId: null,
createdAt: event.payload.createdAt,
}),
),
Effect.asVoid,
);
};
const recoverTurnStartFailure = (cause: Cause.Cause<unknown>) =>
handleTurnStartFailure(cause).pipe(
Effect.catchCause((recoveryCause) =>
Effect.logWarning("provider command reactor failed to recover turn start failure", {
eventType: event.type,
threadId: event.payload.threadId,
cause: Cause.pretty(recoveryCause),
originalCause: Cause.pretty(cause),
}),
),
);
const sendTurnRequest = yield* buildSendTurnRequestForThread({
threadId: event.payload.threadId,
messageText: message.text,
...(message.attachments !== undefined ? { attachments: message.attachments } : {}),
...(event.payload.modelSelection !== undefined
? { modelSelection: event.payload.modelSelection }
: {}),
interactionMode: event.payload.interactionMode,
createdAt: event.payload.createdAt,
}).pipe(
Effect.map(Option.some),
Effect.catchCause((cause) => handleTurnStartFailure(cause).pipe(Effect.as(Option.none()))),
);
if (Option.isNone(sendTurnRequest)) {
return;
}
yield* providerService
.sendTurn(sendTurnRequest.value)
.pipe(Effect.catchCause(recoverTurnStartFailure), Effect.forkScoped);
});| Consumer | Serialized body | Work outside that body | Restart behavior |
|---|---|---|---|
| provider command | domain-event handler dequeue | sendTurn, first-turn branch naming, and title work are scoped forks | fresh hot subscription; volatile dedupe disappears |
| runtime ingestion | runtime events plus selected domain inputs; the current domain handler is a no-op | buffer timers and internal commands cross back into the engine | source audit found no durable runtime-event cursor; selected activity lookup is a narrow fallback |
| checkpoint | capture/revert trigger handling | Git refs, workspace mutation, provider rollback, then internal commands | source audit found no replay of a missed hot trigger or durable saga phase |
| thread deletion | one deletion side-effect handler at a time | provider stop and terminal cleanup touch separate systems | source audit found no startup scan for a missed deletion event |
| awareness | awareness event handling | delayed confirmation fibers and relay publication | re-emits current active state, not the missing event history |
Calling this “serialized provider execution” would be wrong. The provider reactor’s
queue orders the start of its handler bodies. At ProviderCommandReactor.ts:1171–1173,
sendTurn is forked, so two provider sends can overlap. First-turn title and branch
work are also forked. Other reactors may concurrently process the same domain or
provider event through their own subscriptions.
Provider intent uses volatile duplicate suppression
For a turn-start request, the provider reactor derives a turn key and checks an in-memory bounded cache with a 30-minute TTL. The key is recorded before the external send. Within that process, a duplicate can therefore be suppressed even if later work fails. Restart clears the cache—but it also clears the hot event, so the durable original does not automatically receive a second attempt.
Failure handling records selected durable error/session activities by dispatching internal commands. Those compensating commands are themselves best-effort from the reactor’s perspective: the handler catches, logs, and swallows non-interruption causes so that one bad event does not terminate its worker.
Within one process, this is an effectively at-most-once observation path per
derived turn key, with explicit compensations. It is not a general exactly-once
executor: the cache expires and disappears at restart, while the source event is
not replayed. The underlying provider may have accepted a call before
ProviderService persists its binding or running state, leaving an ambiguous
external/local boundary after a crash.
Runtime results return through another hot bridge
Provider adapters emit canonical runtime events. ProviderService writes its
best-effort diagnostic log and publishes the event to a second PubSub. Runtime
ingestion subscribes to that provider stream alongside selected domain events; its
current domain handler is a no-op, while provider observations translate into
internal commands and dispatch through the same engine. Only the internal command’s
successful transaction makes the result part of durable domain truth.
Runtime ingestion deliberately buffers high-rate content. Its flush path removes a buffer entry before dispatching the aggregate internal command. If dispatch is interrupted or fails, the removed batch is not restored. Handler errors are logged and swallowed. This limits memory and prevents a poisoned event from killing the loop, but it is not a durable queue.
The worker also subscribes to thread.turn-start-requested, but its selected-domain
handler currently returns Effect.void. Non-interruption handler failures are logged
and swallowed so the worker can continue; this isolates a bad input without making
that input durable or retryable.
Tests cover deduplication between selected semantic completion signals, such as an item-completed event followed by turn-completed. They do not inject the exact same provider event twice across a process crash. The distinction matters: semantic coalescing is narrower than an idempotent ingestion log.
Parked roots narrow startup races, not delivery semantics
Startup creates a dedicated reactor scope and forks the reactor composite behind a shared activation deferred. Each activation-aware root first reports that it is parked. Provider-session reconciliation runs before activation; HTTP and auxiliary roots prepare; startup sends welcome, opens activation, and only then admits queued commands.
Startup provider reconciliation compares projected active sessions with live adapter inventory. An orphaned projection is marked stopped/error and the internal dispatch is retried once. That reconciler does not replay the original turn or eagerly resume every persisted provider binding; the pinned production-code audit found no separate arbitrary-reactor replay pass. Later routed operations can adopt or resume a binding lazily when enough cursor data exists.
Scope close interrupts; production does not globally drain
DrainableWorker owns an unbounded transactional queue, one scoped worker, and an
outstanding counter. enqueue increments the counter, handler completion or failure
decrements it, and drain waits for zero. Its source describes that drain as a
deterministic synchronization utility for tests.
On scope close, the worker queue is shut down and the worker fiber is interrupted. The inspected orchestration composite exposes no step that first stops ingress and drains every worker and child fiber. ProviderService has its own substantial best-effort finalizer—persist active bindings, stop adapters, revoke credentials, mark bindings stopped, flush analytics—but that is not an atomic completion barrier for every reactor effect.
The other reactors have different partial recovery stories
Checkpoint capture creates a hidden Git ref, refreshes state, computes a diff, dispatches checkpoint metadata, inspects test receipts, and then records activity. Revert runs in the opposite world: it mutates the workspace, refreshes, rolls the provider back, deletes newer refs, and only then records durable completion. Chapter 13 examines the cross-store crash windows and the staging-state overclaim.
Thread deletion stops the provider session, closes the terminal, and deletes its history. Failures are logged. The pinned production-code audit found no durable cleanup job or startup thread scan that reconstructs a missed hot deletion event.
Agent awareness is intentionally more reconstructive: it keeps volatile dedupe and confirmation timers, then publishes a delayed snapshot of currently active threads after activation. That can restore current liveness presentation. Because both the published-state map and confirmation deadlines are process-local, this path does not replay a missed historical tombstone or preserve an in-flight timer across restart.
What the tests establish—and what they do not
| Claim | Evidence | Classification |
|---|---|---|
| a later projector failure rolls back a multi-event transaction | OrchestrationEngine.test.ts:906–1024 | tested |
| the command worker continues after an append failure | OrchestrationEngine.test.ts:781–904 | tested |
| retrying one genuine command id returns its stored accepted sequence | OrchestrationEngine.test.ts:1234–1296 | tested |
| one worker drain waits for subsequently enqueued counted work | DrainableWorker.test.ts:8–56 | tested |
| hard crash between commit and first PubSub publish is repaired | no process-crash test and no outbox/cursor implementation | not guaranteed |
| same id + same aggregate + changed payload conflicts | receipt has no type/payload fingerprint; no matching test | not guaranteed |
| shutdown drains forked provider sends | fork is outside the worker counter; no production drain test | not guaranteed |
| replaying an identical provider event is a no-op | generated internal id includes random UUID; no crash/replay test | not guaranteed |