Skip to content

Latest commit

 

History

History
133 lines (108 loc) · 8.79 KB

overview.md

File metadata and controls

133 lines (108 loc) · 8.79 KB

Architecture

T3 Code keeps execution in the environment that owns the workspace. Web, desktop, and mobile clients control it over authenticated RPC. A remote client must never substitute its own filesystem, provider credentials, or machine state for the environment's. The desktop app bundles a server, but its renderer follows the same boundary.

Ownership boundaries

Provider processes, terminals, Git, and project files belong to the server. Shared connection and domain state belongs in packages/client-runtime; clients supply platform services and UI. Keeping that logic shared prevents reconnect and multi-environment behavior from diverging between web and mobile. See connection runtime and remote environments.

The RPC contract is the boundary between independently versioned clients and servers. Subscriptions send the state a client needs, so a client viewing one thread does not pay for every thread's history. Authentication of a socket does not authorize every method on it. See environment auth.

Pull request linking compatibility

Every protocol-2 environment advertises threadPullRequests. Once an environment's server config has loaded, clients use persisted pullRequests[], multi-link commands, stack UI, and reverse thread lookup. Before that, they hide linking actions.

Environments keep advertising threadPullRequestLinking, accepting legacy metadata commands, and emitting the derived linkedPullRequest field, because older clients still read them. That hostless field includes only links in the thread project's own repository. Retain the legacy wire fields, projection column, and replay support.

Provider-specific behavior belongs behind an adapter. Orchestration works with normalized commands and events, so adding a provider should not require branches throughout the domain or clients. See provider constraints.

Settings ownership

Client preferences stay in the current client; environment defaults and project overrides stay on their owning server. The web and desktop settings target is URL state, resolved against current connections and project membership. An unavailable target must not fall back to another environment. All environments is an explicit bulk edit of connected, loaded servers, not a durable global default or a promise to synchronize offline or future environments. Project-group targets similarly select known environment-local checkouts; the group itself does not store inherited defaults.

Durable intent and side effects

Orchestration state is decided as events, and projections are the source of truth once they apply them. The v2 orchestrator serializes commands and decides events without performing provider or filesystem work. EventSink commits events, persisted projections, the accepted command receipt, and outbox effects in one database transaction. Subscribers receive events after that commit. This keeps command retries idempotent and prevents a persisted projection from getting ahead of the event log.

The effect worker performs side effects after intent has been recorded, then feeds results back into orchestration. A command acknowledgement therefore means the intent committed, not that the provider, checkpoint, or other follow-up work finished. Keep external I/O out of command decisions and the database transaction. Effects tied to a lost provider process cannot simply replay; recovery retires them before admitting new work.

The event log only keeps what live readers need: about a day of history for reconnect replay and command-id retries. ProjectionMaintenance deletes older events the projections cover, so projections cannot be rebuilt from the log. It keeps each stream's newest event (high-water marks, stream versions, snapshot sequences), thread.created (the legacy importer's marker), every running run update (Codex usage history reads the first of each attempt), and prompt anchors that retained subagent rows reference. A client whose cursor is below both its thread's newest event and the recorded pruned-through sequence missed pruned events, so it resumes from a snapshot; an idle thread's cursor resumes without one. Resumes recheck that sequence after reading the replay, since a pass can prune during the read. V2 command receipts stay for idempotency: a retry older than the retained history is still deduplicated, but its replay comes back without the pruned events. Receipts of imported legacy commands are removed once their threads are imported.

Retained events must remain decodable on replay. Changing a schema affects old environments at startup as well as live RPC traffic. Compatibility work must account for stored history, not just what the newest client sends.

Turn completion and checkpoints

A provider turn ending and its follow-up work settling are separate milestones. Orchestration records provider turn and run state independently from run finalization. A late checkpoint or diff must not extend the recorded provider duration or keep the client showing provider work as active. PR discovery after completion also checks that the checkout still matches the thread's non-default branch and that a newer run is not active.

Checkpoints use hidden Git refs to capture workspace state without adding commits to the user's branch. A revert must coordinate workspace state with the provider conversation. A provider that cannot roll back its conversation must reject that operation before changing the filesystem.

Thread settlement is server-owned. The settlement service evaluates PR and inactivity settings without a connected client. Merge notifications invalidate cached PR state and trigger a check. A merge outside T3, such as an agent running gh pr merge, sends no notification, so the PR sync reactor re-reads a thread's open links when a run that ran a merge or close command ends. Each forge's definition names its CLI's commands in changeRequestCloseCommand. The guarded thread.auto-settle command rejects newer activity, explicit settlement overrides, and live or blocked work. It records the activity timestamp for stable sorting and detaches idle provider sessions. Clients render the persisted result; they do not derive settlement from their own clocks or PR caches.

Waiting for asynchronous work

Tests use drainable workers to wait until both the queue and its current item have finished. An empty queue alone does not prove the worker is idle.

V2 tests also drain the effect worker or await a specific persisted event or receipt. Test signals are separate from the durable command receipts that make dispatch idempotent. Production behavior must use persisted state and events, not test instrumentation or assumptions about elapsed time.

The Electron shell acquires DesktopPreReadyPlatform.layer synchronously before asynchronous services. On Linux this sets the desktop-entry identity and global-shortcut portal flags before Chromium initializes its portal connection. Setting the identity later in DesktopAppIdentity is too late: Chromium caches the first registration, including failures. The identity must match the installed entry managed by DesktopLinuxUrlHandler. Pre-ready setup also refreshes that entry's Exec path before portal registration: AppImage updates can remove the previous executable, which makes the old entry invalid even though its filename is correct. The later URL handler avoids rewriting an identical entry while the portal may be reading it. On Wayland, Electron's synchronous shortcut-registration result only confirms submission; it does not confirm desktop consent or an active binding.

Native modules never load in the Electron main process on the startup path, and the two the snapshot feature keeps are isolated: @crowecawcaw/xa11y runs only in forked Node-mode children (SnapShotAccessibilityWorker, RegionSnapShotWorker) and a worker thread, and ffi-rs loads lazily inside WindowsForeground.ts for a handful of Win32 calls. macOS window lookup shells out to osascript instead of a native addon. A crash or stall in any of these must not take the app down, so new native capability goes in a child with a deadline, not an import in main.

See the glossary for shared terms and the development runbook for setup and checks.