Part III · Transactional domain core and post-commit deliveryPost-commit reactors
Chapter 12source checked

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

apps/server/src/orchestration/Layers/OrchestrationEngine.ts:197–259 ↗verbatim · typescript · 24e00da0
        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 };
Read this as: The event append, SQL projection, and accepted receipt share one transaction. The in-memory model swap and PubSub loop occur only after that transaction returns.
Figure 12.1 · Durable intent crosses a hot delivery seamthe red break is a real process-crash window
Post-commit reactor delivery seamDiagram loading

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.

Post-commit reactor delivery seam
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.

Figure 12.1. A successfully returned dispatch has committed the SQL transaction and completed its publication loop. It does not prove that every subscriber dequeued the event or that a provider accepted work. The inspected paths contain no durable outbox row joining the two halves.

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.

Crash-timing lab

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

All crash positions
  1. 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
  2. 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
  3. 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
  4. 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
  5. 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
  6. 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
  7. 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
  8. 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
  9. 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
  10. 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.

apps/server/src/orchestration/Layers/ProviderCommandReactor.ts:1060–1174 ↗verbatim · typescript · e612e2a0
  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);
  });
Read this as: The provider handler performs volatile duplicate suppression and first-turn setup, then forks sendTurn in the reactor scope. The worker may dequeue a later event while that child fiber is still active.
What each reactor serializes, forks, and can recover
ConsumerSerialized bodyWork outside that bodyRestart behavior
provider commanddomain-event handler dequeuesendTurn, first-turn branch naming, and title work are scoped forksfresh hot subscription; volatile dedupe disappears
runtime ingestionruntime events plus selected domain inputs; the current domain handler is a no-opbuffer timers and internal commands cross back into the enginesource audit found no durable runtime-event cursor; selected activity lookup is a narrow fallback
checkpointcapture/revert trigger handlingGit refs, workspace mutation, provider rollback, then internal commandssource audit found no replay of a missed hot trigger or durable saga phase
thread deletionone deletion side-effect handler at a timeprovider stop and terminal cleanup touch separate systemssource audit found no startup scan for a missed deletion event
awarenessawareness event handlingdelayed confirmation fibers and relay publicationre-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

Tested guarantees and untested failure windows for post-commit reactors
ClaimEvidenceClassification
a later projector failure rolls back a multi-event transactionOrchestrationEngine.test.ts:906–1024tested
the command worker continues after an append failureOrchestrationEngine.test.ts:781–904tested
retrying one genuine command id returns its stored accepted sequenceOrchestrationEngine.test.ts:1234–1296tested
one worker drain waits for subsequently enqueued counted workDrainableWorker.test.ts:8–56tested
hard crash between commit and first PubSub publish is repairedno process-crash test and no outbox/cursor implementationnot guaranteed
same id + same aggregate + changed payload conflictsreceipt has no type/payload fingerprint; no matching testnot guaranteed
shutdown drains forked provider sendsfork is outside the worker counter; no production drain testnot guaranteed
replaying an identical provider event is a no-opgenerated internal id includes random UUID; no crash/replay testnot guaranteed
T3
Source-locked editionRead against fa219001d · 23 Aug 2026
Book search

Find a concept, module, or source path

Type two or more characters.