| 1 | # Architecture |
| 2 | |
| 3 | T3 Code keeps execution in the environment that owns the workspace. Web, desktop, and mobile |
| 4 | clients control it over authenticated RPC. A remote client must never substitute its own filesystem, |
| 5 | provider credentials, or machine state for the environment's. The desktop app bundles a server, |
| 6 | but its renderer follows the same boundary. |
| 7 | |
| 8 | ## Ownership boundaries |
| 9 | |
| 10 | Provider processes, terminals, Git, and project files belong to the server. Shared connection and |
| 11 | domain state belongs in `packages/client-runtime`; clients supply platform services and UI. |
| 12 | Keeping that logic shared prevents reconnect and multi-environment behavior from diverging between |
| 13 | web and mobile. See [connection runtime](./connection-runtime.md) and |
| 14 | [remote environments](./remote.md). |
| 15 | |
| 16 | The [RPC contract](../../packages/contracts/src/rpc.ts) is the boundary between independently |
| 17 | versioned clients and servers. Subscriptions send the state a client needs, so a client viewing one |
| 18 | thread does not pay for every thread's history. Authentication of a socket does not authorize every |
| 19 | method on it. See [environment auth](./environment-auth.md). |
| 20 | |
| 21 | ### Pull request linking compatibility |
| 22 | |
| 23 | Web, desktop, mobile, and environments upgrade independently. Negotiate linking through the |
| 24 | environment descriptor, never through a client version or an assumed coordinated release: |
| 25 | |
| 26 | | Environment capability | Client behavior | |
| 27 | | ------------------------------------- | ----------------------------------------------------------------------------------------------------------------- | |
| 28 | | `threadPullRequests: true` | Use persisted `pullRequests[]`, multi-link commands, stack UI, and reverse thread lookup. | |
| 29 | | Only `threadPullRequestLinking: true` | Use `linkedPullRequest` and the existing `thread.meta.update` single-link operation. Do not call multi-link RPCs. | |
| 30 | | Neither flag | Hide linking actions; existing branch-discovered PR display remains available. | |
| 31 | |
| 32 | New environments continue advertising the legacy flag, accepting legacy metadata commands, and |
| 33 | emitting the derived `linkedPullRequest` field for older clients. That hostless field includes only |
| 34 | links in the thread project's own repository; cross-host and cross-repository links require the |
| 35 | multi-link protocol. New clients accept snapshots that |
| 36 | omit `pullRequests`. Retain the legacy wire fields, projection column, and replay support; this feature |
| 37 | does not schedule their removal. Missing new capabilities must also override cached multi-link data |
| 38 | after an environment downgrade. |
| 39 | |
| 40 | Provider-specific behavior belongs behind an adapter. Orchestration works with normalized commands |
| 41 | and events, so adding a provider should not require branches throughout the domain or clients. |
| 42 | See [provider constraints](./providers.md). |
| 43 | |
| 44 | ## Settings ownership |
| 45 | |
| 46 | Client preferences stay in the current client; environment defaults and project overrides stay |
| 47 | on their owning server. The web and desktop settings target is URL state, resolved against current |
| 48 | connections and project membership. An unavailable target must not fall back to another environment. |
| 49 | **All environments** is an explicit bulk edit of connected, loaded servers, not a durable global |
| 50 | default or a promise to synchronize offline or future environments. Project-group targets similarly |
| 51 | select known environment-local checkouts; the group itself does not store inherited defaults. |
| 52 | |
| 53 | ## Durable intent and side effects |
| 54 | |
| 55 | The event log is the source of truth for orchestration state. The |
| 56 | [v2 orchestrator](../../apps/server/src/orchestration-v2/Orchestrator.ts) serializes commands and |
| 57 | decides events without performing provider or filesystem work. |
| 58 | [EventSink](../../apps/server/src/orchestration-v2/EventSink.ts) commits events, persisted projections, |
| 59 | the accepted command receipt, and outbox effects in one database transaction. Subscribers receive |
| 60 | events after that commit. This keeps command retries idempotent and prevents a persisted projection |
| 61 | from getting ahead of the event log. |
| 62 | |
| 63 | The [effect worker](../../apps/server/src/orchestration-v2/EffectWorker.ts) performs side effects |
| 64 | after intent has been recorded, then feeds results back into orchestration. A command acknowledgement |
| 65 | therefore means the intent committed, not that the provider, checkpoint, or other follow-up work |
| 66 | finished. Keep external I/O out of command decisions and the database transaction. Effects tied to |
| 67 | a lost provider process cannot simply replay; recovery retires them before admitting new work. |
| 68 | |
| 69 | Persisted events must remain decodable on replay. Changing a schema affects old environments at |
| 70 | startup as well as live RPC traffic. Compatibility work must account for stored history, not just |
| 71 | what the newest client sends. |
| 72 | |
| 73 | ## Turn completion and checkpoints |
| 74 | |
| 75 | A provider turn ending and its follow-up work settling are separate milestones. Orchestration |
| 76 | records provider turn and run state independently from |
| 77 | [run finalization](../../apps/server/src/orchestration-v2/RunFinalizationService.ts). A late |
| 78 | checkpoint or diff must not extend the recorded provider duration or keep the client showing |
| 79 | provider work as active. PR discovery after completion also checks that the checkout still matches |
| 80 | the thread's non-default branch and that a newer run is not active. |
| 81 | |
| 82 | [Checkpoints](../../apps/server/src/checkpointing/CheckpointStore.ts) use hidden Git refs to |
| 83 | capture workspace state without adding commits to the user's branch. A revert must coordinate |
| 84 | workspace state with the provider conversation. A provider that cannot roll back its conversation |
| 85 | must reject that operation before changing the filesystem. |
| 86 | |
| 87 | Thread settlement is server-owned. The |
| 88 | [settlement service](../../apps/server/src/orchestration-v2/ThreadSettlementService.ts) evaluates PR |
| 89 | and inactivity settings without a connected client. Merge notifications invalidate cached PR state |
| 90 | and trigger a check. A merge outside T3, such as an agent running `gh pr merge`, sends no |
| 91 | notification, so the [PR sync reactor](../../apps/server/src/orchestration-v2/PullRequestSyncReactor.ts) |
| 92 | re-reads a thread's open links when a run that ran a merge or close command ends. The guarded |
| 93 | `thread.auto-settle` command rejects newer activity, explicit settlement overrides, and live or |
| 94 | blocked work. It records the activity timestamp for stable |
| 95 | sorting and detaches idle provider sessions. Clients render the persisted result; they do not |
| 96 | derive settlement from their own clocks or PR caches. |
| 97 | |
| 98 | ## Waiting for asynchronous work |
| 99 | |
| 100 | Tests use [drainable workers](../../packages/shared/src/DrainableWorker.ts) to wait until both the |
| 101 | queue and its current item have finished. An empty queue alone does not prove the worker is idle. |
| 102 | |
| 103 | V2 tests also drain the effect worker or await a specific persisted event or receipt. Test signals |
| 104 | are separate from the durable command receipts that make dispatch idempotent. Production behavior |
| 105 | must use persisted state and events, not test instrumentation or assumptions about elapsed time. |
| 106 | |
| 107 | The Electron shell acquires `DesktopPreReadyPlatform.layer` synchronously before asynchronous |
| 108 | services. On Linux this sets the desktop-entry identity and global-shortcut portal flags before |
| 109 | Chromium initializes its portal connection. Setting the identity later in `DesktopAppIdentity` |
| 110 | is too late: Chromium caches the first registration, including failures. The identity must match |
| 111 | the installed entry managed by `DesktopLinuxUrlHandler`. Pre-ready setup also refreshes that entry's |
| 112 | `Exec` path before portal registration: AppImage updates can remove the previous executable, which |
| 113 | makes the old entry invalid even though its filename is correct. The later URL handler avoids |
| 114 | rewriting an identical entry while the portal may be reading it. On Wayland, Electron's synchronous |
| 115 | shortcut-registration result only confirms submission; it does not confirm desktop consent or |
| 116 | an active binding. |
| 117 | |
| 118 | Native modules never load in the Electron main process on the startup path, and the two the |
| 119 | snapshot feature keeps are isolated: `@crowecawcaw/xa11y` runs only in forked Node-mode children |
| 120 | (`SnapShotAccessibilityWorker`, `RegionSnapShotWorker`) and a worker thread, and `ffi-rs` loads |
| 121 | lazily inside `WindowsForeground.ts` for a handful of Win32 calls. macOS window lookup shells out |
| 122 | to `osascript` instead of a native addon. A crash or stall in any of these must not take the app |
| 123 | down, so new native capability goes in a child with a deadline, not an `import` in main. |
| 124 | |
| 125 | See the [glossary](./glossary.md) for shared terms and the |
| 126 | [development runbook](../operations/development.md) for setup and checks. |