Part III · Transactional domain core and post-commit deliveryCommands and invariants
Chapter 9source checked

Commands, invariants, and the boundary of atomicity

Client commands are authenticated and normalized before a serialized, Effectful decider plans events; an existing-thread turn commits atomically, while first-turn bootstrap is a WebSocket saga with narrower compensation.

What this chapter resolves
  • Distinguish client wire commands, dispatchable commands, and trusted internal commands.
  • Locate authorization, normalization, decision, and SQL transaction boundaries.
  • Derive the exact two-to-four-event batch for a turn on an existing thread.
  • Contrast that atomic batch with the WebSocket-only bootstrap saga and its compensation limits.

A command is a request to change orchestration state. It is not yet a fact, and it is not a direct provider call. The server first turns an accepted client shape into a dispatchable command, decides that command against its current domain model, and only then opens the transaction that makes the resulting facts durable.

That order is the key to this part of the book:

authorize → normalize → queue → deduplicate → decide → transact → publish → react

For an accepted receipt miss, crossing the first six boundaries through a successful transaction establishes durable intent. A receipt hit refers to intent committed earlier, while a rejected command establishes no accepted intent. Provider execution begins later.

“Command” names three different shapes

The contracts deliberately separate what a client may send from what the engine may receive.

packages/contracts/src/orchestration.ts:913–1057 ↗verbatim · typescript · 318896f2
const DispatchableClientOrchestrationCommand = Schema.Union([
  ProjectCreateCommand,
  ProjectMetaUpdateCommand,
  ProjectDeleteCommand,
  ThreadCreateCommand,
  ThreadDeleteCommand,
  ThreadArchiveCommand,
  ThreadUnarchiveCommand,
  ThreadSettleCommand,
  ThreadUnsettleCommand,
  ThreadSnoozeCommand,
  ThreadUnsnoozeCommand,
  ThreadPinCommand,
  ThreadUnpinCommand,
  ThreadPinReorderCommand,
  ThreadMetaUpdateCommand,
  ThreadRuntimeModeSetCommand,
  ThreadInteractionModeSetCommand,
  ThreadTurnStartCommand,
  ThreadTurnInterruptCommand,
  ThreadApprovalRespondCommand,
  ThreadUserInputRespondCommand,
  ThreadCheckpointRevertCommand,
  ThreadSessionStopCommand,
]);
export type DispatchableClientOrchestrationCommand =
  typeof DispatchableClientOrchestrationCommand.Type;
 
export const ClientOrchestrationCommand = Schema.Union([
  ProjectCreateCommand,
  ProjectMetaUpdateCommand,
  ProjectDeleteCommand,
  ThreadCreateCommand,
  ThreadDeleteCommand,
  ThreadArchiveCommand,
  ThreadUnarchiveCommand,
  ThreadSettleCommand,
  ThreadUnsettleCommand,
  ThreadSnoozeCommand,
  ThreadUnsnoozeCommand,
  ThreadPinCommand,
  ThreadUnpinCommand,
  ThreadPinReorderCommand,
  ThreadMetaUpdateCommand,
  ThreadRuntimeModeSetCommand,
  ThreadInteractionModeSetCommand,
  ClientThreadTurnStartCommand,
  ThreadTurnInterruptCommand,
  ThreadApprovalRespondCommand,
  ThreadUserInputRespondCommand,
  ThreadCheckpointRevertCommand,
  ThreadSessionStopCommand,
]);
export type ClientOrchestrationCommand = typeof ClientOrchestrationCommand.Type;
 
const ThreadSessionSetCommand = Schema.Struct({
  type: Schema.Literal("thread.session.set"),
  commandId: CommandId,
  threadId: ThreadId,
  session: OrchestrationSession,
  createdAt: IsoDateTime,
});
 
const ThreadMessageAssistantDeltaCommand = Schema.Struct({
  type: Schema.Literal("thread.message.assistant.delta"),
  commandId: CommandId,
  threadId: ThreadId,
  messageId: MessageId,
  delta: Schema.String,
  turnId: Schema.optional(TurnId),
  createdAt: IsoDateTime,
});
 
const ThreadMessageAssistantCompleteCommand = Schema.Struct({
  type: Schema.Literal("thread.message.assistant.complete"),
  commandId: CommandId,
  threadId: ThreadId,
  messageId: MessageId,
  turnId: Schema.optional(TurnId),
  createdAt: IsoDateTime,
});
 
const ThreadProposedPlanUpsertCommand = Schema.Struct({
  type: Schema.Literal("thread.proposed-plan.upsert"),
  commandId: CommandId,
  threadId: ThreadId,
  proposedPlan: OrchestrationProposedPlan,
  createdAt: IsoDateTime,
});
 
const ThreadTurnDiffCompleteCommand = Schema.Struct({
  type: Schema.Literal("thread.turn.diff.complete"),
  commandId: CommandId,
  threadId: ThreadId,
  turnId: TurnId,
  completedAt: IsoDateTime,
  checkpointRef: CheckpointRef,
  status: OrchestrationCheckpointStatus,
  files: Schema.Array(OrchestrationCheckpointFile),
  assistantMessageId: Schema.optional(MessageId),
  checkpointTurnCount: NonNegativeInt,
  createdAt: IsoDateTime,
});
 
const ThreadActivityAppendCommand = Schema.Struct({
  type: Schema.Literal("thread.activity.append"),
  commandId: CommandId,
  threadId: ThreadId,
  activity: OrchestrationThreadActivity,
  createdAt: IsoDateTime,
});
 
const ThreadRevertCompleteCommand = Schema.Struct({
  type: Schema.Literal("thread.revert.complete"),
  commandId: CommandId,
  threadId: ThreadId,
  turnCount: NonNegativeInt,
  createdAt: IsoDateTime,
});
 
const ThreadTitleRegenerationCompleteCommand = Schema.Struct({
  type: Schema.Literal("thread.title.regeneration.complete"),
  commandId: CommandId,
  threadId: ThreadId,
  requestId: CommandId,
  title: Schema.optional(TrimmedNonEmptyString),
});
 
const InternalOrchestrationCommand = Schema.Union([
  ThreadSessionSetCommand,
  ThreadMessageAssistantDeltaCommand,
  ThreadMessageAssistantCompleteCommand,
  ThreadProposedPlanUpsertCommand,
  ThreadTurnDiffCompleteCommand,
  ThreadActivityAppendCommand,
  ThreadRevertCompleteCommand,
  ThreadTitleRegenerationCompleteCommand,
]);
export type InternalOrchestrationCommand = typeof InternalOrchestrationCommand.Type;
 
export const OrchestrationCommand = Schema.Union([
  DispatchableClientOrchestrationCommand,
  InternalOrchestrationCommand,
]);
export type OrchestrationCommand = typeof OrchestrationCommand.Type;
Read this as: The client-facing union can carry the bootstrap form of turn start. After normalization, the dispatchable client union uses the server-ready turn shape. The engine's full union adds commands reserved for trusted server and provider paths.
Client, dispatchable, and internal orchestration command boundaries
ShapeWho can construct itWhat changes before the next boundary
ClientOrchestrationCommandauthenticated HTTP or WebSocket clienttimestamps, workspace paths, and attachment data still need server normalization; turn start may include bootstrap instructions
DispatchableClientOrchestrationCommandserver after client normalizationraw attachment data has become persisted attachment metadata; the shape is ready for the domain engine
InternalOrchestrationCommandtrusted server/provider reactorsassistant deltas, sessions, plan updates, activities, diff completion, and similar runtime results re-enter the same domain engine

“Client” and “internal” are admission boundaries, not a complete actor history. Events later infer an actor class from command-id prefixes and provider metadata. Do not interpret the internal union as “commands that are not serialized” or “commands that bypass invariants”: both dispatchable client commands and trusted internal commands enter the engine’s command union.

The full command boundary

Figure 9.1 · Decision is before the SQL transactionsolid arrows are synchronous; dashed arrows cross trust or delivery boundaries
T3 Code command boundary from client admission to post-commit reactorsDiagram 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.

T3 Code command boundary from client admission to post-commit reactors
Text equivalent

An authenticated client command passes schema and scope checks, then normalization. Trusted internal commands enter through a separate boundary. Both reach one serialized command queue. The engine first checks a command receipt. A receipt hit returns its stored outcome. On a miss, an Effectful decider reads the in-memory command model and plans one or more events. Only after decision does one SQL transaction append those events, update synchronous projections, and save an accepted receipt. After commit, the engine replaces its in-memory model and publishes each event to an in-memory PubSub. The last sequence is returned only after that publication loop. Reactors consume the hot events asynchronously.

Figure 9.1. A client command is authorized and normalized before it joins trusted internal commands at the serialized engine queue. Receipt lookup and domain decision happen before the transaction. The transaction commits events, synchronous projections, and the accepted receipt together; in-memory folding and hot publication happen afterward.

The queue is an important concurrency boundary. The implementation creates one unbounded command queue and drains it with one worker. Every command therefore sees the command read model produced by the commands before it. This is how an invariant check closes a race: two clients may submit concurrently, but they do not decide concurrently against the same stale model.

Normalization can perform irreversible work early

Normalization does more than validate syntax. It replaces client timestamps with the server receipt time, canonicalizes project workspace roots, validates image data, creates attachment identifiers, creates directories, and writes attachment bytes.

apps/server/src/orchestration/Normalizer.ts:18–179 ↗verbatim · typescript · a6b4f3d0
export const canonicalizeClientCommandTimestamps = (
  command: ClientOrchestrationCommand,
  receivedAt: IsoDateTime,
): ClientOrchestrationCommand => {
  const canonicalCommand =
    "createdAt" in command
      ? {
          ...command,
          createdAt: receivedAt,
        }
      : command;
 
  if (canonicalCommand.type !== "thread.turn.start" || !canonicalCommand.bootstrap?.createThread) {
    return canonicalCommand;
  }
 
  return {
    ...canonicalCommand,
    bootstrap: {
      ...canonicalCommand.bootstrap,
      createThread: {
        ...canonicalCommand.bootstrap.createThread,
        createdAt: receivedAt,
      },
    },
  };
};
 
export const normalizeDispatchCommand = (command: ClientOrchestrationCommand) =>
  Effect.gen(function* () {
    const receivedAt = DateTime.formatIso(yield* DateTime.now);
    const canonicalCommand = canonicalizeClientCommandTimestamps(command, receivedAt);
    const fileSystem = yield* FileSystem.FileSystem;
    const path = yield* Path.Path;
    const serverConfig = yield* ServerConfig;
    const workspacePaths = yield* WorkspacePaths.WorkspacePaths;
 
    const normalizeProjectWorkspaceRoot = (workspaceRoot: string) =>
      workspacePaths.normalizeWorkspaceRoot(workspaceRoot).pipe(
        Effect.mapError(
          (cause) =>
            new OrchestrationDispatchCommandError({
              message: cause.message,
            }),
        ),
      );
 
    const normalizeProjectWorkspaceRootForCreate = (
      workspaceRoot: string,
      createIfMissing: boolean | undefined,
    ) =>
      workspacePaths
        .normalizeWorkspaceRoot(workspaceRoot, {
          createIfMissing: createIfMissing === true,
        })
        .pipe(
          Effect.mapError(
            (cause) =>
              new OrchestrationDispatchCommandError({
                message: cause.message,
              }),
          ),
        );
 
    if (canonicalCommand.type === "project.create") {
      return {
        ...canonicalCommand,
        workspaceRoot: yield* normalizeProjectWorkspaceRootForCreate(
          canonicalCommand.workspaceRoot,
          canonicalCommand.createWorkspaceRootIfMissing,
        ),
        createWorkspaceRootIfMissing: canonicalCommand.createWorkspaceRootIfMissing === true,
      } satisfies OrchestrationCommand;
    }
 
    if (
      canonicalCommand.type === "project.meta.update" &&
      canonicalCommand.workspaceRoot !== undefined
    ) {
      return {
        ...canonicalCommand,
        workspaceRoot: yield* normalizeProjectWorkspaceRoot(canonicalCommand.workspaceRoot),
      } satisfies OrchestrationCommand;
    }
 
    if (canonicalCommand.type !== "thread.turn.start") {
      return canonicalCommand as OrchestrationCommand;
    }
 
    const normalizedAttachments = yield* Effect.forEach(
      canonicalCommand.message.attachments,
      (attachment) =>
        Effect.gen(function* () {
          const parsed = parseBase64DataUrl(attachment.dataUrl);
          if (!parsed || !parsed.mimeType.startsWith("image/")) {
            return yield* new OrchestrationDispatchCommandError({
              message: `Invalid image attachment payload for '${attachment.name}'.`,
            });
          }
 
          const bytes = Buffer.from(parsed.base64, "base64");
          if (bytes.byteLength === 0 || bytes.byteLength > PROVIDER_SEND_TURN_MAX_IMAGE_BYTES) {
            return yield* new OrchestrationDispatchCommandError({
              message: `Image attachment '${attachment.name}' is empty or too large.`,
            });
          }
 
          const attachmentId = createAttachmentId(canonicalCommand.threadId);
          if (!attachmentId) {
            return yield* new OrchestrationDispatchCommandError({
              message: "Failed to create a safe attachment id.",
            });
          }
 
          const persistedAttachment = {
            type: "image" as const,
            id: attachmentId,
            name: attachment.name,
            mimeType: parsed.mimeType.toLowerCase(),
            sizeBytes: bytes.byteLength,
          };
 
          const attachmentPath = resolveAttachmentPath({
            attachmentsDir: serverConfig.attachmentsDir,
            attachment: persistedAttachment,
          });
          if (!attachmentPath) {
            return yield* new OrchestrationDispatchCommandError({
              message: `Failed to resolve persisted path for '${attachment.name}'.`,
            });
          }
 
          yield* fileSystem.makeDirectory(path.dirname(attachmentPath), { recursive: true }).pipe(
            Effect.mapError(
              () =>
                new OrchestrationDispatchCommandError({
                  message: `Failed to create attachment directory for '${attachment.name}'.`,
                }),
            ),
          );
          yield* fileSystem.writeFile(attachmentPath, bytes).pipe(
            Effect.mapError(
              () =>
                new OrchestrationDispatchCommandError({
                  message: `Failed to persist attachment '${attachment.name}'.`,
                }),
            ),
          );
 
          return persistedAttachment;
        }),
      { concurrency: 1 },
    );
 
    return {
      ...canonicalCommand,
      message: {
        ...canonicalCommand.message,
        attachments: normalizedAttachments,
      },
    } satisfies OrchestrationCommand;
  });
Read this as: Attachment directories and bytes are written while constructing the normalized command. This function runs before the orchestration engine opens the event/projection/receipt transaction.

That placement has two consequences:

  1. An invariant rejection or later SQL failure does not automatically roll back the staged attachment bytes.
  2. Every client retry normalizes before receipt lookup. With no accepted receipt it can allocate new attachment identifiers before trying the transaction again; even an accepted same-id retry can stage new files before the engine returns the old receipt result.

The decider is isolated, but not a deterministic pure function

The decider receives a normalized command and the current command read model. It does not query SQLite, write files, or call a provider. It does, however, run in Effect: some cases read the clock, and every planned event receives a random UUID.

Within a turn start, the decider enforces four concrete rules or transitions:

Invariant checks and lifecycle transitions for thread turn start
QuestionDecisionWhy it belongs here
Does the target thread exist?missing thread rejects the commandthe serialized model is authoritative at decision time
Was a proposed plan named?the referenced plan must exist on its source threada structurally valid id is not proof that the plan is present
Does that plan cross projects?source and target thread must belong to the same projectthe decider can inspect related aggregates in one model
Was the target settled or snoozed?emit lifecycle-reset events before message and turn intentvalid activity evolves state; it is not always a rejection

An existing-thread turn is one two-to-four-event decision

apps/server/src/orchestration/decider.ts:926–1036 ↗verbatim · typescript · 0d0fe115
    case "thread.turn.start": {
      const targetThread = yield* requireThread({
        readModel,
        command,
        threadId: command.threadId,
      });
      const sourceProposedPlan = command.sourceProposedPlan;
      const sourceThread = sourceProposedPlan
        ? yield* requireThread({
            readModel,
            command,
            threadId: sourceProposedPlan.threadId,
          })
        : null;
      const sourcePlan =
        sourceProposedPlan && sourceThread
          ? sourceThread.proposedPlans.find((entry) => entry.id === sourceProposedPlan.planId)
          : null;
      if (sourceProposedPlan && !sourcePlan) {
        return yield* new OrchestrationCommandInvariantError({
          commandType: command.type,
          detail: `Proposed plan '${sourceProposedPlan.planId}' does not exist on thread '${sourceProposedPlan.threadId}'.`,
        });
      }
      if (sourceThread && sourceThread.projectId !== targetThread.projectId) {
        return yield* new OrchestrationCommandInvariantError({
          commandType: command.type,
          detail: `Proposed plan '${sourceProposedPlan?.planId}' belongs to thread '${sourceThread.id}' in a different project.`,
        });
      }
      const userMessageEvent: Omit<OrchestrationEvent, "sequence"> = {
        ...(yield* withEventBase({
          aggregateKind: "thread",
          aggregateId: command.threadId,
          occurredAt: command.createdAt,
          commandId: command.commandId,
        })),
        type: "thread.message-sent",
        payload: {
          threadId: command.threadId,
          messageId: command.message.messageId,
          role: "user",
          text: command.message.text,
          attachments: command.message.attachments,
          turnId: null,
          streaming: false,
          createdAt: command.createdAt,
          updatedAt: command.createdAt,
        },
      };
      const turnStartRequestedEvent: Omit<OrchestrationEvent, "sequence"> = {
        ...(yield* withEventBase({
          aggregateKind: "thread",
          aggregateId: command.threadId,
          occurredAt: command.createdAt,
          commandId: command.commandId,
        })),
        causationEventId: userMessageEvent.eventId,
        type: "thread.turn-start-requested",
        payload: {
          threadId: command.threadId,
          messageId: command.message.messageId,
          ...(command.modelSelection !== undefined
            ? { modelSelection: command.modelSelection }
            : {}),
          ...(command.titleSeed !== undefined ? { titleSeed: command.titleSeed } : {}),
          runtimeMode: targetThread.runtimeMode,
          interactionMode: targetThread.interactionMode,
          ...(sourceProposedPlan !== undefined ? { sourceProposedPlan } : {}),
          createdAt: command.createdAt,
        },
      };
      // Real activity resets ANY override: it wakes an explicitly settled
      // thread, and it clears a keep-active pin back to neutral so the
      // thread can auto-settle again after this burst of work goes stale.
      // A snooze clears the same way — sending a message to a snoozed
      // thread is the user re-engaging, so the return ticket is spent.
      const lifecycleResetEvents: Array<Omit<OrchestrationEvent, "sequence">> = [];
      if (targetThread.settledOverride !== null) {
        lifecycleResetEvents.push({
          ...(yield* withEventBase({
            aggregateKind: "thread",
            aggregateId: command.threadId,
            occurredAt: command.createdAt,
            commandId: command.commandId,
          })),
          type: "thread.unsettled",
          payload: {
            threadId: command.threadId,
            reason: "activity",
            updatedAt: command.createdAt,
          },
        });
      }
      if (targetThread.snoozedUntil != null) {
        lifecycleResetEvents.push({
          ...(yield* withEventBase({
            aggregateKind: "thread",
            aggregateId: command.threadId,
            occurredAt: command.createdAt,
            commandId: command.commandId,
          })),
          type: "thread.unsnoozed",
          payload: {
            threadId: command.threadId,
            reason: "activity",
            updatedAt: command.createdAt,
          },
        });
      }
      return [...lifecycleResetEvents, userMessageEvent, turnStartRequestedEvent];
Read this as: The mandatory suffix is message-sent then turn-start-requested. A settled override adds thread.unsettled; an active snooze adds thread.unsnoozed. The result is therefore two, three, or four events in a fixed order.

For a normal turn on an existing thread, the planned batch is:

  1. optional thread.unsettled;
  2. optional thread.unsnoozed;
  3. mandatory thread.message-sent;
  4. mandatory thread.turn-start-requested, causally linked to the message event.

The engine opens one SQL transaction only after that complete batch exists. It appends each event, folds it into a temporary command model, applies all synchronous SQLite projectors, and finally upserts one accepted receipt whose result is the last event’s global sequence. A failure in any one of those SQL operations rolls back the whole batch.

First-turn bootstrap is a WebSocket saga

The first turn can ask the server to create a thread, prepare a Git worktree, run a setup script, and then start the turn. Those operations cannot share the existing-turn transaction: Git and process launch are external effects, and several domain subcommands each commit independently.

Figure 9.2 · Bootstrap crosses four commit classesTx labels are independent command receipts, not savepoints
WebSocket first-turn bootstrap saga and compensating deleteDiagram 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.

WebSocket first-turn bootstrap saga and compensating delete
Text equivalent

A WebSocket client sends a turn-start command with bootstrap data. After normalization, the WebSocket handler may dispatch thread.create as SQL transaction A with a server-generated command id. It can then fetch and create a Git worktree outside SQL. It dispatches thread.meta.update as SQL transaction B, refreshes Git status, and optionally launches a setup script outside SQL. Setup launch failure is recorded best effort and the saga continues. The handler strips bootstrap data and dispatches the final thread.turn.start as SQL transaction C using the outer command id. If a later non-interruption failure occurs after this saga created the thread, it dispatches thread.delete as compensation transaction D. The compensation tombstones domain state but contains no Git-worktree removal.

Figure 9.2. Only WebSocket dispatch interprets the bootstrap payload as a saga. Thread creation, metadata update, and final turn start are separately receipted domain commands around Git and setup effects. A later non-interruption failure can dispatch thread.delete, but that compensation does not undo the worktree.

The saga’s real boundaries are easy to miss:

  • thread.create, thread.meta.update, and compensation use fresh server-generated command ids; the final turn retains the client’s outer id.
  • Setup launch failure is converted into best-effort activity plus a warning, then the final turn is still attempted.
  • Cleanup runs only when this request successfully created the thread and the later failure is not interruption-only.
  • Successful cleanup reports a deleted disposition, but it adds a thread tombstone; it does not erase the committed history.

Explore the failure boundary

The same word—“failed”—can describe a request rejected before decision, SQL rolled back after decision, a hard crash after commit, or a bootstrap failure after Git state exists. Move the boundary and compare what survives.

Interactive boundary explorer

Where can a command fail?

Switch paths and move the failure position. Watch which state is durable, which state is external, and what an identical retry can actually recover.

One normalized command is decided, then its two-to-four-event batch, projections, and accepted receipt share one SQL transaction.

Existing-thread path, position 1 of 6: Before normalization. No durable intent.

Position 1 of 6

Before normalization

No durable intent

The client command has crossed schema and authorization checks, but normalization has not produced the dispatchable command.

Durable state
No orchestration event or command receipt exists.
External state
No attachment staging or provider work has begun.
Client sees
The request can fail without a durable orchestration result.
Identical retry
The same command id enters the normal path again because there is no receipt to find.
Provider implication
No provider intent exists.
Static boundary ledger

Turn on an existing thread

One normalized command is decided, then its two-to-four-event batch, projections, and accepted receipt share one SQL transaction.

PositionDurable / external stateRetry and provider meaning
1. Before normalizationNo orchestration event or command receipt exists. No attachment staging or provider work has begun.The same command id enters the normal path again because there is no receipt to find. No provider intent exists.
2. After normalization and stagingThe event store and receipt table are still unchanged. Filesystem staging can already exist outside the later SQL transaction.The command is normalized and staged again; receipt deduplication has not started yet. No provider intent exists.
3. Invariant rejectionNo event is appended. A rejected receipt is attempted afterward on a best-effort path. Any earlier normalization-time staging is outside that rejection bookkeeping.After client normalization runs again, a saved rejected receipt keeps the same id rejected without re-evaluation; if that receipt was not saved, the command can be decided again. No provider intent is published.
4. SQL transaction failureEvents, transactional projections, and the accepted receipt roll back together. Pre-transaction staged files are not part of the SQL rollback.With no accepted receipt, the same id is normalized and decided again; clock and UUID-derived values may differ. No committed event reaches the hot event bus.
5. Hard crash after commitThe complete event batch, projections, and accepted receipt are durable. The hot reactor saw none of this batch in the failed process.After any client normalization work, a same-id retry returns the stored last sequence and does not republish the missing hot events. Durable turn intent can exist without the provider reactor receiving its trigger.
6. Sequence acknowledgedTwo to four events, their synchronous projections, and one accepted receipt are committed. Publication wakes asynchronous reactors; provider execution is outside this transaction.The same id and aggregate return that stored sequence without deciding or appending again. The acknowledgement proves durable intent, not that a harness accepted, ran, or completed the turn.

Full first-turn WebSocket bootstrap

The full WebSocket path creates a thread, prepares a worktree, launches setup, and starts the turn through separate durability boundaries.

PositionDurable / external stateRetry and provider meaning
1. After normalization, before the sagaNo bootstrap step has committed. Attachment staging may already exist, but no worktree or setup process has been requested.The same outer id has no saga-level receipt that can replay the whole result. No provider intent exists.
2. Thread creation committedThe thread event and that subcommand's receipt are committed independently of the final turn. No worktree is guaranteed yet.Repeating the outer request tries the creation path again; it does not retrieve one receipt for the whole saga. The thread exists, but no turn-start request has been committed.
3. Worktree prepared and metadata committedThread metadata can point at the newly prepared worktree in a separate event and receipt. A branch and worktree now exist outside the event-store transaction.An identical outer retry can collide with already-created thread, branch, or worktree state. No final turn intent is guaranteed yet.
4. Setup launch failsFailure activity is best effort; the earlier thread and metadata commits remain. No setup process started, but the prepared worktree remains.The saga continues to the final turn; this is not a rollback boundary. The eventual provider turn may run against a worktree whose setup did not launch.
5. Final turn fails; compensateSuccessful compensation tombstones the thread in another independent commit; prior history is not erased. The Git worktree and branch are not removed by this compensation.Retrying the outer request is a new saga attempt, not a replay of an atomic transaction. No successful final turn acknowledgement exists.
6. Final turn acknowledgedEarlier saga commits plus the final two-to-four-event turn transaction are durable, each with its own receipt boundary. Git/setup effects remain outside SQL and provider work begins asynchronously from hot events.The outer bootstrap is not end-to-end idempotent even though each dispatched subcommand has receipt semantics. The acknowledgement proves durable turn intent only.

The durable unit is deliberately small: an accepted engine command and its event batch. Everything before it can leave staging residue; everything after it can miss hot delivery; and the bootstrap path composes several such units with external effects. Chapter 10 now examines the receipt and publication windows inside that unit.

T3
Source-locked editionRead against fa219001d · 23 Aug 2026
Book search

Find a concept, module, or source path

Type two or more characters.