Skip to content

Latest commit

 

History

History
172 lines (142 loc) · 10.5 KB

File metadata and controls

172 lines (142 loc) · 10.5 KB

Architecture

For maintainers. Using T3 Code? See docs/user.

T3 Code is a server runtime that owns agent sessions, workspaces, and version control, plus clients (web, desktop, mobile) that talk to it over one authenticated Effect RPC WebSocket. The server is the execution boundary: every provider process, terminal, git operation, and filesystem read happens there, never in the client.

┌────────────────────────────────────────────────┐
│ Clients: apps/web, apps/desktop, apps/mobile   │
│ shared runtime: packages/client-runtime        │
│  connection supervisor, RPC session, Atom state│
└──────────────────┬─────────────────────────────┘
                   │ Effect RPC over WebSocket (/ws)
                   │ contract: packages/contracts
┌──────────────────▼─────────────────────────────┐
│ apps/server                                    │
│  orchestration engine (event-sourced)          │
│  provider driver registry (6 built-in drivers) │
│  checkpointing, VCS, terminals, filesystem     │
└──────────────────┬─────────────────────────────┘
                   │ per-driver transport
┌──────────────────▼─────────────────────────────┐
│ Agent CLIs: Codex, Claude, Cursor, Grok,       │
│ OpenCode, Antigravity                          │
└────────────────────────────────────────────────┘

The RPC boundary

The client/server contract is an Effect RPC group, not a hand-rolled push protocol. rpc.ts declares WS_METHODS and assembles WsRpcGroup; each member is either unary or a server stream (stream: true). Streaming members such as orchestration.subscribeShell, orchestration.subscribeThread, subscribeServerConfig, and terminal.attach replace what used to be a broadcast push bus: a client subscribes to what it needs and the server pushes only on that subscription.

ws.ts serves the group. websocketRpcRouteLayer mounts GET /ws, authenticates the upgrade through EnvironmentAuth.authenticateWebSocketUpgrade, then hands the socket to RpcServer.toHttpEffectWebsocket. Authorization is per method: RPC_REQUIRED_SCOPE maps each method to a scope, and authorizeEffect/authorizeStream enforce it. Holding a valid socket is not authorization to call everything on it. See environment-auth.md.

On the client, session.ts opens the socket and builds the typed client. RpcSessionFactory is the service; a session exposes client, initialConfig, ready, probe, and closed. It performs one attempt and does not retry. Retry, backoff, and offline policy belong to the connection supervisor.

Shared client runtime

packages/client-runtime holds every non-visual client concern: connection lifecycle, authentication, RPC, cached environment data, and domain state as Atom factories. Web and mobile compose it the same way (apps/web/src/connection/runtime.ts and apps/mobile/src/connection/runtime.ts mirror each other, differing only in platform-specific background-activity layers) and differ beyond that only in the platform layer they supply and the UI they build on top. React components never construct transports, retry loops, or RPC clients. See connection-runtime.md.

Orchestration is event-sourced

The server does not mutate app state directly. Clients dispatch typed commands; the engine turns them into persisted events; projections derive the read model.

OrchestrationEngine.ts serializes this. dispatch offers a CommandEnvelope onto commandQueue and awaits its result; a single worker fiber takes envelopes one at a time, so command processing is totally ordered. For each envelope processEnvelope:

  1. checks the durable command receipt, making retries idempotent;
  2. runs decideOrchestrationCommand (decider.ts) to produce events from command plus current state, pure and side-effect free;
  3. inside one SQL transaction, appends events to the event store, applies them to the in-memory read model via projector.ts, projects them into persisted tables, and writes the accepted receipt;
  4. after commit, swaps in the new read model, cleans up attachments, and publishes committed events to subscribers. Attachment cleanup failures are logged and do not reject committed commands.

Because persistence and projection share a transaction, the read model cannot durably disagree with the event log. On dispatch failure the engine rereads persisted events past the starting sequence and reconciles.

Command and event names live in orchestration.ts. Some commands are client dispatchable (thread.create, thread.turn.start, thread.approval.respond); others are internal and produced only by server-side reactors (thread.message.assistant.delta, thread.turn.diff.complete).

A turn is complete when its session leaves running status, projected by settledTurnStateForSessionStatus in projector.ts. Checkpoint work settling later does not define turn end.

Thread settlement is server-owned. Each server's own settings control PR and inactivity settlement. Those keys are user preferences, so clients write them to every shared-settings sync target (SHARED_SERVER_SETTING_KEYS in packages/client-runtime/src/state/sharedSettings.ts) and warn when another target drifts. A target must have an active connection and advertise the threadAutoSettlement capability, which signals that the server can hold every shared key. ThreadSettlementReactor checks threads at startup, when those settings change, and once per minute, including when no client is connected. It dispatches the guarded internal thread.auto-settle command, which uses the existing settlement event lifecycle. Automatic settlement excludes live background work and requires a comparable PR timestamp for immediate PR settlement. The command carries the latest activity timestamp and rejects any later event for its thread after the reactor's snapshot. Clients render the persisted settlement state and do not derive settlement from PR or inactivity state. A committed thread.settled event also lets ProviderCommandReactor stop an idle provider session.

Drainable workers

Follow-up work runs asynchronously in queue-backed workers built on DrainableWorker: ProviderRuntimeIngestion normalizes provider runtime streams into orchestration commands, ProviderCommandReactor dispatches provider calls in response to intent events, CheckpointReactor captures and reverts workspace checkpoints, and ThreadSettlementReactor evaluates server-owned automatic settlement rules.

DrainableWorker pairs a transactional queue with a transactional count of outstanding items. enqueue atomically offers and increments; processing always decrements. drain retries until the count reaches zero, so a test can await "queue empty and current item finished" instead of sleeping. Each of these four services exposes drain for exactly this.

Runtime receipts are a test-only mechanism. RuntimeReceiptBusLive in RuntimeReceiptBus.ts publishes nothing; only the test layer is PubSub-backed. Do not build production behavior on receipts.

Provider drivers

Six drivers ship built in, registered in builtInDrivers.ts as BUILT_IN_DRIVERS: Codex, Claude, Cursor, Grok, OpenCode, and Antigravity. A driver declares its kind and config schema and creates a scoped adapter; ProviderInstanceRegistry owns live instances and ProviderAdapterRegistry resolves an instance to its adapter, so ProviderService routes session and turn operations without knowing which agent is behind them. See providers.md.

Checkpointing

Each turn is bracketed by workspace checkpoints so diffs and reverts are exact. CheckpointStore captures state as hidden Git refs through the VCS driver's checkpoint operations; CheckpointDiffQuery answers turn and full-thread diff requests; CheckpointReactor coordinates baseline capture, completed-turn capture, diff projection, and reverting both the workspace and the provider conversation. The storage contract is VcsCheckpointOps in VcsDriver.ts, implemented for Git in the same directory.

Startup

serverRuntimeStartup.ts runs a fixed lifecycle: start keybindings, settings, and reactors; publish welcome; signal command readiness (logged as Accepting commands); wait for the HTTP listener via markHttpListening; publish ready; fork the heartbeat; then either print headless output or open the browser. Command readiness precedes the listener, so a socket that opens can already dispatch.

Related