# Files changed · PR \#9755 · pingdotgg/t3code

[View on GitCafe](https://git.cafe/pingdotgg/t3code/pull/9755/diffs)

Repository: [pingdotgg/t3code](https://git.cafe/pingdotgg/t3code)

Visibility: public

[docs: keep internal guides focused on architecture](https://git.cafe/pingdotgg/t3code/pull/9755.md)

Version: 1

Head commit: 33a9aaeeb8f50f2cc568e1ffbcde67d0b48527ad

Base commit: f6db4206258b0ef30e8dd8949627acfd209bf338

Comparison base: f6db4206258b0ef30e8dd8949627acfd209bf338

Showing up to 20 entries from this revision. If the pull request changes, reopen the first page before continuing.

## AGENTS.md

Status: modified; +13; −2

```diff
@@ -72,7 +72,7 @@
 - **Contracts.** Anything crossing the wire is typed in `packages/contracts`. Change the schema and the server, web, mobile, and desktop all follow.
 - **Reverse states.** If you added a way in, add the way out and the way to see it. Snooze needs unsnooze. Close needs reopen. A one-way door is a bug.
 - **Connection modes.** Local, remote/relay, and tunnel behave differently. Multi-device and multi-environment cases are real.
-- **Docs.** `docs/` splits by audience. Behavior changes that a user would notice belong in `docs/user/` (shipped-product voice, no repo tooling or source paths); architecture and contributor changes in `docs/internals/`; runbooks in `docs/operations/`; new vocabulary in `docs/internals/glossary.md`.
+- **Docs.** Check whether the change makes existing guidance inaccurate. Apply the [documentation rules](#documentation) before adding anything.
 
 ## Dev servers
 
@@ -119,12 +119,23 @@
 - Upload PR evidence to GitHub. Never commit PR-only screenshots or assets such as `.github/pr-assets/`.
 - One concern per PR. If the description says "also", split it.
 - When babysitting: poll checks and comments newer than the last push, verify each bot finding against the source, fix real ones, dismiss false positives with a written reason. Stay quiet when nothing is new. Stop when the bots are green on the latest commit.
+
+## Documentation
 
+Most code changes do not need an internal documentation change. Agents can read the code.
+
+- `docs/internals/` is for architectural decisions and their reasons, constraints that span components, and implementation traps that are hard to discover from the source. Before adding a paragraph, ask what a maintainer would get wrong without it. If reading the relevant code answers the question, leave it out.
+- Do not document every feature, enumerate fields or methods, narrate control flow, maintain file catalogs, or append PR summaries. Types, tests, and code already record the implementation. The glossary defines shared vocabulary; it is not a feature index.
+- Keep a local implementation explanation in a nearby code comment. Use an internal doc when the reasoning crosses boundaries or needs context the code cannot carry well. Link to the relevant source instead of copying it.
+- When a documented decision or constraint changes, rewrite or remove the affected text. Do not append another account of the new behavior. A new internal page needs a distinct, durable reason to exist.
+- `docs/user/` helps users accomplish tasks. Give each major feature a concise section explaining what it does, how to start, and anything unintuitive. A settings path is useful; descriptions of visible buttons, icons, layouts, animations, or every UI state are not. Before adding text, ask what task or decision it helps the user with.
+- Keep user docs in the shipped product's voice, without implementation details or contributor tooling. Update the relevant feature section when how to use it changes. A UI tweak does not need a documentation entry, and a new control does not need its own page.
+- `docs/operations/` holds maintainer setup, release, and debugging procedures. Keep instructions for operating an installed T3 Code server in the user guides.
+
 ## Plans and work artifacts
 
 - Do not commit implementation plans, research notes, or agent scratch files. Keep temporary working material outside the worktree. `.plans/` is gitignored only as a safety net for legacy tooling.
 - Track active maintainer work in the GitHub issue or project item that owns it. External proposals follow `CONTRIBUTING.md` and belong in Ideas discussions.
-- Put durable architecture, constraints, and decisions in `docs/internals/`. Update those docs when the product changes so agents find current facts instead of abandoned intentions.
 - A merged PR is the implementation record. Close or update its tracking item when the work lands; do not preserve a second checklist in the repository.
 
 ## How it works
```

## CONTRIBUTING.md

Status: modified; +5; −1

```diff
@@ -2,7 +2,7 @@
 
 ## Developer Setup
 
-See the [maintainer scripts guide](docs/internals/scripts.md#first-checkout) for the initial checkout,
+See the [development runbook](docs/operations/development.md#first-checkout) for the initial checkout,
 development commands, tests, and platform-specific desktop packaging prerequisites.
 
 ## Read This First
@@ -49,6 +49,10 @@
 
 Explain exactly why the change should exist.
 
+Follow the [documentation rules](AGENTS.md#documentation). Keep internal docs for decisions and
+hard-to-discover constraints. Update user guides when how to use a feature changes; skip descriptions
+of obvious controls and cosmetic changes.
+
 Do not mix unrelated fixes together.
 
 If the PR makes anything resembling a UI change, include clear before/after images.
```

## apps/mobile/README.md

Status: modified; +6; −6

````diff
@@ -38,12 +38,8 @@
 Run that reset once after installing or changing the Uniwind dependency patch. Cached transforms
 can otherwise reference its previous pnpm package path. Ordinary Metro starts still keep the cache.
 
-Component edits use Fast Refresh. Connection-runtime edits replace the active Effect layer through
-a stable atom runtime, preserving navigation and existing atom subscribers. Replaced registries
-and managed runtimes dispose their resources; the app does not force a JavaScript reload. The Uniwind patch
-skips global style invalidation when generated styles and themes are unchanged, while real style
-changes still refresh. See [mobile development lifecycle](../../docs/internals/mobile-development.md)
-for the lifetime boundaries.
+Component edits use Fast Refresh. See [mobile development lifecycle](../../docs/internals/mobile-development.md)
+before changing runtime ownership or refresh behavior.
 
 Build and run the local iOS dev client:
 
@@ -51,6 +47,10 @@
 vp run ios:dev
 ```
 
+After changing a native dependency patch, rerun CocoaPods before rebuilding an existing iOS
+project. pnpm gives each patch hash a new package path; Pods can otherwise keep compiling the
+previous directory.
+
 If your Xcode account only has a Personal Team, use a bundle identifier you control and opt into the
 reduced-capability local build. Personal Team builds omit the widget and share extensions, push
 entitlement, and native Sign in with Apple entitlement; builds without this opt-in are unchanged.
````

## docs/README.md

Status: modified; +14; −7

```diff
@@ -22,28 +22,35 @@
 ---
 
 ## Working on T3 Code
+
+Start with the [development runbook](./operations/development.md) and
+[contribution policy](../CONTRIBUTING.md).
 
+Internal notes preserve architectural decisions, constraints, and implementation traps that the
+source alone does not explain. Most code changes do not need an internal documentation update. Follow the
+[documentation rules](../AGENTS.md#documentation) before adding one.
-Everything below is for maintainers. Setup lives in the [root README](../README.md);
-policy in [CONTRIBUTING.md](../CONTRIBUTING.md); agent rules in [AGENTS.md](../AGENTS.md).
 
 - [Architecture overview](./internals/overview.md)
-- [Workspace layout](./internals/workspace-layout.md)
 - [Glossary](./internals/glossary.md)
-- [Scripts](./internals/scripts.md)
 - [Connection runtime](./internals/connection-runtime.md)
-- [Voice input](./internals/voice-input.md)
 - [Providers](./internals/providers.md)
+- [Model classification](./internals/model-manifest.md)
 - [Remote environments](./internals/remote.md)
 - [Server updates](./internals/server-updates.md)
 - [Resource telemetry](./internals/resource-telemetry.md)
 - [Product analytics](./internals/product-analytics.md)
 - [Environment auth](./internals/environment-auth.md)
 - [T3 Connect](./internals/t3-connect.md)
+- [Assistant citations](./internals/assistant-citations.md)
+- [Mobile navigation](./internals/mobile-navigation.md)
-- [CI gates](./internals/ci.md)
-- [Engineering work artifacts](./internals/work-artifacts.md)
+- [Mobile development lifecycle](./internals/mobile-development.md)
+- [Terminal runtime](./internals/terminal-runtime.md)
+- [Voice input](./internals/voice-input.md)
 
 ### Runbooks
 
+- [Development and local builds](./operations/development.md)
+- [T3 Connect setup](./operations/connect-setup.md)
 - [Release](./operations/release.md)
 - [Observability](./operations/observability.md)
 - [Relay observability](./operations/relay-observability.md)
```

## docs/architecture/terminal-renderers.md

Status: deleted; +0; −42

````diff
@@ -1,42 +1,0 @@
-# Terminal renderers
-
-Terminal sessions remain server-owned PTYs. Clients receive the existing raw byte stream and send
-input and resize events over the existing terminal contracts; renderer choices never cross the
-wire.
-
-## Ghostty alignment
-
-Android and web use the official `libghostty-vt` C ABI for parsing, terminal state, grapheme
-boundaries, keyboard encoding, selection, and scrollback:
-
-- Android links the native shared library and converts render state into a compact JNI snapshot.
-- Web loads a separately cached WebAssembly build and reads render state into a Canvas 2D surface.
-- Both artifacts are built from the revision in
-  `native/libghostty-vt/VERSION`.
-
-The platform adapters deliberately own only platform behavior. Android owns its Kotlin Canvas and
-touch integration. Web owns browser font shaping, the hidden IME textarea, clipboard and DOM input,
-and its Canvas renderer. The web adapter also delegates application mouse encoding, word and line
-selection, and OSC 8 hyperlink metadata to the official ABI. Browser conventions remain available:
-holding Shift bypasses application mouse capture, and the platform link modifier opens hyperlinks.
-React does not participate in terminal frames.
-
-The web runtime is singleton-scoped per browser tab so split terminals share one compiled module
-and memory. Each visible terminal owns and frees its own terminal, render state, row iterator, cell
-iterator, key and mouse encoder, and input event handles. Restoring captured scrollback temporarily
-detaches the PTY callback so historical device queries cannot emit replies into the current shell.
-
-## Updating Ghostty
-
-Update and rebuild Android first, because mobile's `VERSION` file is the single source of truth for
-the upstream pin (the upstream `LICENSE` lives beside it). Then run:
-
-```sh
-pnpm --dir apps/web build:ghostty-wasm
-```
-
-Commit the regenerated web `wasm` artifacts. The build embeds the pinned revision into the binary as
-semver build metadata, and the focused web ABI test reads it back through `ghostty_build_info` and
-compares it against mobile's `VERSION` — so the web vendor directory holds only the artifacts, drift
-cannot hide, and there is no second pin to keep in sync. The same test enforces the artifact budget
-and exercises repeated create/write/free cycles with multi-codepoint graphemes.
````

## docs/internals/assistant-citations.md

Status: modified; +24; −130

````diff
@@ -1,135 +1,29 @@
 # Assistant citations
 
-> For maintainers. Using T3 Code? See [Quote an assistant response](../user/composer.md#quote-an-assistant-response).
-
-Assistant citations store a selected excerpt and its source in ordinary message text. They need
-no database migration, citation table, or sidecar draft metadata. The saved excerpt remains
-available even when the source response is missing or has changed.
-
-## Serialized form
-
-[`AssistantCitation`][contract] defines version 1. [`assistantCitations.ts`][shared] serializes it
-as a Markdown link with the literal label `Assistant quote`:
-
-```text
-[Assistant quote](t3-citation://v1/<environmentId>/<threadId>/<messageId>?text=<quote>&start=<start>&end=<end>&prefix=<prefix>&suffix=<suffix>&comment=<optional-user-comment>)
-```
-
-The formatter percent-encodes each path segment and uses `URLSearchParams` for the query. The
-environment, thread, and message IDs identify the source without a browser origin, so local,
-remote, and tunnel clients share one form. Opening the source still requires access to its
-environment.
-
-The parser accepts only version 1 with exactly three source IDs and one of each selector field;
-`comment` is optional and links without one keep their original format. It rejects credentials,
-ports, malformed encoding, and extra or duplicate query fields. IDs have a 512-character limit,
-the quote and comment each fit in 8,000 UTF-16 code units, offsets are nonnegative safe integers
-with `end > start`, prefix and suffix each fit in 32 code units, and the URI itself is capped at
-160,000 characters. Malformed or unsupported links remain ordinary prompt text everywhere:
-collectors, provider expansion, and readable fallbacks all skip them. Parsing validates the
-payload, not the existence of its source.
-
-## Selection and source matching
-
-[`assistantTextSelection.ts`][selection] captures one native DOM range inside one assistant
-response, including backward and paragraph selections. Empty range endpoints at neighboring blocks
-are narrowed to the first and last selected text nodes before validating the source, so
-triple-click selections that end at the next block still resolve. The text stream follows DOM
-order, inserts line breaks between blocks and at `<br>`, and includes displayed code and link
-text. Controls, hidden subtrees, CSS-generated content, and soft wrapping do not contribute.
-
-`text` keeps the exact selected excerpt, including line breaks and indentation. `start`, `end`,
-`prefix`, and `suffix` use the rendered-text stream after each JavaScript whitespace run collapses
-to one space, without trimming, in UTF-16 code units. Context boundaries avoid splitting
-surrogate pairs.
-
-Resolution uses the same normalization and case-sensitive matching. It checks the saved offsets
-against the quote and context, then searches for a context-matching occurrence if positions have
-moved. A unique quote can still match after its context changes. Multiple context-matching
-occurrences are ambiguous, even if one remains at the saved offsets; the resolver does not guess.
-
-## Composer and clients
-
-[`AssistantSelectionToolbar`][toolbar] offers **Cite in composer** for a selection within one
-response and disables it above the quote limit. It shares [`selectionActions.ts`][actions] with
-the terminal's selection menu: the observer opens actions only after a primary-button release from
-a gesture that started inside the surface, waits out the 500 ms repeat-click window so
-double/triple clicks can finish, anchors to the release point (or the text end for keyboard
-selections), and dismisses on outside interaction, scrolling, or `Escape`. The toolbar registers
-its own element with the observer so interacting with it cannot replace the captured quote.
-
-`ChatView` inserts the serialized link at the last composer cursor. A one-shot editor request then
-opens the comment editor beside the cited source text, carrying the live DOM range for positioning
-and an editing highlight; that range is transient and never becomes part of the citation.
-`withAssistantCitationComment` trims outer whitespace and removes the field when blank; it never
-changes the saved quote or selector. Restoring or pasting a citation does not reopen the editor.
-
-[`ComposerCitationNode`][node] is an inline Lexical `DecoratorNode` holding the quote and comment
-as one atomic chip. Its label shows the comment when present, otherwise a short quote preview; the
-node retains the full serialized source and returns it from `getTextContent()`. Comment edits
-update the citation and serialized source together as one undoable change. The token parser,
-clipboard handling, and cursor mapping treat citations as inline tokens alongside mentions and
-skills, so drafts, stashes, and pastes reconstruct chips from the prompt string alone. Sent
-messages keep the same links; `ChatMarkdown` renders valid citation URIs as read-only
-[`AssistantCitationChip`][chip]s, and the whole chip supplies its serialized value to Markdown
-copy so the displayed label is not duplicated as text.
-
-## Source navigation
-
-[`assistantCitationNavigation.ts`][navigation] routes to the source thread with
+Citations carry the selected excerpt and source identity in ordinary message text.
+Drafts, clipboard copies, stashes, and sent messages keep that representation
-`#assistant-citation=<base64url-encoded citation URI>`; the encoding keeps router hash
-normalization from decoding quote whitespace or source IDs. Each click adds a fresh activation ID
-to navigation state so the same link can reveal its source again after dismissal.
+without a separate citation store. The saved quote remains usable when its source
+disappears or changes. The [shared format](../../packages/shared/src/assistantCitations.ts) uses
+stable environment IDs without a browser origin, so moving between local, remote,
+and tunnel connections does not change a citation's identity.
 
-[`useAssistantCitationTarget`][target] loads earlier history (bounded at 20 pages or a repeated
-cursor, with a warning past that), expands a folded turn, and asks the virtual list to mount the
+Source navigation is best effort. [Text selectors](../../apps/web/src/lib/assistantTextSelection.ts)
-source row. While positioning, the timeline disables initial end-scroll and scroll maintenance and
-keeps the cited row mounted until measured; the activation ID doubles as the list's data version
-so pinning rows recalculates a stationary Legend list without discarding measurements.
+refer to rendered text after whitespace normalization, measured in UTF-16 units.
+They cannot be applied to raw Markdown. The original excerpt keeps its whitespace;
+normalization is only for locating it. Repeated text needs an unambiguous context
+match, even if one occurrence still sits at the saved offsets. Guessing could
+highlight the wrong claim.
 
-[`AssistantCitationSource`][source] resolves the DOM range, smoothly scrolls to it, and pulses
+Navigation owns the highlight's lifetime across virtual-row remounts. The source
-only that text through the CSS Highlight API: two 650 ms pulses driven by a registered opacity
-property, with the second settling into a held highlight before fading out. Navigation owns the
-highlight's start time and completion state: range repairs keep the original clock, remounting
+row must be fetched, unfolded, mounted, and measured before quote positioning
+takes over from normal timeline scrolling. Remounting must not revive a dismissed
+highlight. The [navigation hash](../../apps/web/src/lib/assistantCitationNavigation.ts)
+uses base64url because router normalization otherwise decodes whitespace and
+source IDs inside the citation URI.
-resumes only the remaining time, and an expired or dismissed highlight cannot revive. Reduced
-motion scrolls instantly and holds the highlight without pulsing. Browsers
-without the Highlight API temporarily select the range and remove only that owned selection.
-User navigation or `Escape` cancels pending positioning; missing messages or unmatched text
-produce warnings without changing the saved quote. The separate comment-editing highlight uses
-the same range-repair observer while its editor is open.
 
+[ProviderService](../../apps/server/src/provider/Layers/ProviderService.ts) expands
+citations before dispatch to any provider adapter. It sends the saved excerpt
+without looking up the source, distinguishes quoted reference material from the
-Desktop shares the web implementation. Mobile's `ThreadFeed` uses `renderAssistantCitationsAsText`
-to show sent quotes as readable Markdown blockquotes with each user comment below its quote; it
-does not create citations or navigate to sources.
-
-## Provider input and titles
-
-[`ProviderService.sendTurn`][provider] calls `expandAssistantCitationsForProvider` before routing
-to any adapter; persisted messages keep their links. Each distinct serialized link becomes an
-inline `[assistant-quote-N]` marker, and one `<assistant_citations>` block carries JSON entries
-with an `id` and the complete `citation`. The block identifies `citation.text` as quoted reference
-material, not new instructions, and `citation.comment` as the user's comment about that quote.
-JSON encoding escapes `<`, `>`, and `&` so content cannot close the wrapper. The expansion is
-shared by all providers; there is no per-adapter citation handling or source lookup at send time.
-
-The 120,000-character provider input limit applies both before and after expansion:
-[`composerSubmission.ts`][submission] checks the larger length client-side and keeps an over-limit
-draft editable, and the server revalidates the expanded input before dispatch. Expansion does not
-apply to pending user-input answers.
-
-`assistantCitationsToPlainText` supplies readable input for title seeds, stash previews, and the
-server's title and branch-name helpers, which see the quoted text and comment rather than an
-encoded URI.
-
-[contract]: ../../packages/contracts/src/assistantCitations.ts
+user's comment, and leaves persisted messages in their original form. Input
+limits apply after expansion as well as before it. A draft that fits as encoded
+links can exceed the provider limit once expanded.
-[shared]: ../../packages/shared/src/assistantCitations.ts
-[selection]: ../../apps/web/src/lib/assistantTextSelection.ts
-[toolbar]: ../../apps/web/src/components/chat/AssistantSelectionToolbar.tsx
-[actions]: ../../apps/web/src/lib/selectionActions.ts
-[node]: ../../apps/web/src/components/ComposerCitationNode.tsx
-[chip]: ../../apps/web/src/components/chat/AssistantCitationChip.tsx
-[navigation]: ../../apps/web/src/lib/assistantCitationNavigation.ts
-[target]: ../../apps/web/src/components/chat/useAssistantCitationTarget.ts
-[source]: ../../apps/web/src/components/chat/AssistantCitationSource.tsx
-[provider]: ../../apps/server/src/provider/Layers/ProviderService.ts
-[submission]: ../../apps/web/src/components/chat/composerSubmission.ts
````

## docs/internals/ci.md

Status: deleted; +0; −44

```diff
@@ -1,44 +1,0 @@
-# CI quality gates
-
-> For maintainers. Using T3 Code? See [docs/user](../user/).
-
-[`.github/workflows/ci.yml`](../../.github/workflows/ci.yml) runs these quality gates on pull requests
-and pushes to `main`:
-
-- **Check**: `vp check` (format and lint; this repo sets `typeCheck: false` in its lint options),
-  then `vpr typecheck` for the workspace type check. The same job
-  builds the desktop pipeline (`vp run build:desktop`) and verifies the preload bundle exists and
-  uses only imports that Electron's sandbox can load. The verifier parses imports, then executes the
-  trusted artifact with controlled bridge stubs to confirm that its required APIs are callable.
-- **Test**: `vp run test` across the workspace.
-- **Mobile Native Static Analysis**: `vp run lint:mobile` on macOS, wrapping
-  `scripts/mobile-native-static-check.ts`. A cheap Linux **Mobile Native Changes** job gates it:
-  the macOS runner only boots when the diff touches `apps/mobile` Swift/Kotlin sources, the
-  SwiftLint/detekt/ktlint configuration, the `Brewfile`, the check script, the root `package.json`
-  that defines `lint:mobile`, or `ci.yml`. Otherwise the job is skipped, which GitHub reports as
-  success for the required check. Renames are matched on both their old and new path. The gate fails
-  open in every other case: if the changed-file list cannot be resolved, GitHub truncates it, or the
-  gate job itself fails, the lint runs.
-- **Release Smoke**: exercises release-only workflow steps through `scripts/release-smoke.ts`, so
-  release breakage surfaces on PRs rather than at tag time.
-
-[`.github/workflows/windows-tests.yml`](../../.github/workflows/windows-tests.yml) is a manual
-Windows lane (`workflow_dispatch` only) on a Blacksmith Windows 2025 runner. The suite does not
-pass on Windows yet, so it is not a required check; it exists so the work to get there can be
-iterated against a real Windows box without one on hand. Dispatch it with `gh workflow run
-windows-tests.yml --ref <branch>`, optionally with `-f package=<dir>` to run one workspace package
-and `-f files="<paths>"` to run specific test files inside it. Once it is green, fold it into
-`ci.yml`.
-
-`.github/workflows/release.yml` builds macOS (`arm64` and `x64`), Linux (`x64`), and Windows (`x64`)
-desktop artifacts from a single `v*.*.*` tag and publishes one GitHub release. It auto-enables
-signing only when platform credentials are present. macOS passkey builds additionally require
-`APPLE_TEAM_ID` and the `MACOS_PROVISIONING_PROFILE` secret; Windows uses Azure Trusted Signing.
-Without the core signing credentials, it still releases unsigned artifacts.
-
-Preflight shares pnpm's lockfile verification results with the desktop build jobs through a small
-artifact. This avoids repeating dependency checks, especially on Windows, without transferring the
-large registry metadata cache. pnpm checks the current lockfile and policy before it reuses a result.
-If the artifact is unavailable, installation runs the checks again.
-
-See [Release Checklist](../operations/release.md) for the full release/signing setup checklist.
```

## docs/internals/connection-runtime.md

Status: modified; +49; −200

```diff
@@ -1,211 +1,60 @@
-# Connection Runtime
+# Connection runtime
 
-> For maintainers. Using T3 Code? See [docs/user](../user/).
+Web, the desktop renderer, and mobile share one connection owner per environment
+in `packages/client-runtime`. Platform code supplies storage, credentials, network
+signals, and application lifecycle events. React views consume the runtime.
+Keeping retries and session lifetime here prevents competing reconnect loops when
+several views need the same environment.
 
-The connection runtime is shared by web and mobile. It owns connectivity,
-authentication, retries, transport lifetime, cached environment data, and
-environment-scoped operations.
+## One retry owner
 
-Web and mobile mount this runtime once at the application root and compose it
-identically: `apps/web/src/connection/runtime.ts` and
-`apps/mobile/src/connection/runtime.ts` differ only in the platform layer they
-supply. There is no legacy connection owner or supported mixed mode.
+The [supervisor](../../packages/client-runtime/src/connection/supervisor.ts) owns
+retry policy; resolving an endpoint and opening an RPC session are single
+attempts. Transient failures retry with capped backoff. Offline states and
+authentication failures wait for a wakeup instead of spending attempts on
+unchanged conditions.
 
-## Composition
+Foregrounding needs different treatment depending on the connection's state.
+It wakes a retry immediately, leaves an ordinary in-flight attempt alone, and
+probes an established session before replacing it. A long mobile background
+suspension forces replacement because the OS can kill a socket without reporting
+closure. Treating every foreground event as a reconnect delays healthy attempts;
+treating every resume as harmless leaves suspended sockets stuck.
 
+The [registry](../../packages/client-runtime/src/connection/registry.ts) scopes
+connections by environment. An involuntary disconnect retains the registration
+and cached data. Explicit removal closes the scope and clears credentials,
+projections, and platform-owned state such as drafts. Cloud-account changes apply
-[`connection/layer.ts`][layer] assembles the runtime:
+to relay registrations; they must not discard directly paired environments.
 
-- `ConnectionResolver` ([resolver.ts][resolver]) resolves a catalog entry into a
-  prepared, authenticated endpoint for primary, bearer, relay, or SSH targets.
-- `ConnectionDriver` ([driver.ts][driver]) prepares through the resolver, opens
-  one RPC session, and reports `preparing`, `opening`, and `synchronizing`.
-- `RpcSessionFactory` ([rpc/session.ts][session]) performs one transport
-  attempt. It does not retry. `RpcSession` is the interface it returns,
-  exposing `client`, `initialConfig`, `ready`, `probe`, and `closed`.
-- `EnvironmentRegistry` ([registry.ts][registry]) owns the catalog and the
-  per-environment scopes.
-- `ConnectionOnboarding` and `RelayEnvironmentDiscovery` sit alongside the
-  registry. Startup calls `EnvironmentRegistry.start` and streams platform
-  registrations into `reconcilePlatform`.
+## Transport health and data freshness are separate
 
+A socket opening is insufficient evidence that the environment is usable. The
-The registry creates one environment-scoped supervisor per environment.
-`acquireSupervisor` serializes access per environment, reuses an existing
-supervisor when the catalog entry is unchanged, and closes and recreates the
+[RPC session](../../packages/client-runtime/src/rpc/session.ts) waits for the
+initial server configuration before becoming ready. Shell and thread data then
+have their own synchronization state. A failed shell subscription can coexist
+with a healthy connection; labeling that state "reconnecting" promises a
+transport retry that will never happen.
-scope when it changed. `createServiceScope` builds an `EnvironmentSupervisor`
-bound to a closeable scope and connects it; `run` and `runStream` execute caller
-effects with that supervisor provided.
 
+Cached projections remain readable offline. They must neither imply a live
+connection nor overwrite newer live data during a reconnect. Loading and
+resuming snapshots belongs to the shared state services, so every view agrees
+on which data is current.
-`EnvironmentSupervisor` owns desired state, retry scheduling, and the active
-session scope. React components do not create connections, transports, retry
-loops, or RPC clients.
 
-## Connection State
-
-The supervisor is the only retry owner.
+[Thread detail](../../packages/client-runtime/src/state/threads.ts) separates
+subscription lifetime from cache lifetime. Mounted consumers share one live
+stream, which stops when the last consumer unmounts; hidden mounted routes still
+count. A registry-local cache retains state and its replay cursor for five idle
+minutes so back navigation can resume without another snapshot download.
 
-1. A persisted or platform registration marks an environment as desired.
-2. If the device is offline, the supervisor releases the active session and
-   waits for a signal without consuming retry attempts or running a timer.
-3. When online, it asks the driver for one prepared connection and one RPC
-   session.
-4. Transient failures retry forever with exponential backoff capped at 16
-   seconds (`RETRY_DELAYS_MS`). A connection stable for 30 seconds resets
-   accumulated backoff.
-5. Authentication or configuration failures remain blocked until an external
-   wakeup changes the relevant input.
-6. An involuntary session close keeps the registration and cache, then retries.
-7. Explicit removal closes the session and deletes the registration,
-   credentials, shell cache, and thread cache.
-
-### Wakeups
-
-Wakeup handling differs by phase, in [supervisor.ts][supervisor]:
-
-- During establishment, `waitForEstablishmentInterrupt` consumes and **ignores**
-  plain application activation. Restarting an in-flight attempt because the app
-  came to the foreground would only delay it. The exception is
-  `application-active-reconnect`, which mobile emits after a meaningful
-  background suspension; it interrupts establishment and resets the retry
-  ladder, because the OS may have silently killed the socket underneath the
-  attempt.
-- Credential changes interrupt establishment only for relay targets, where a new
-  credential changes what is being established.
-- Explicit disconnect, explicit retry, and going offline interrupt establishment
-  in every case.
-- While waiting out backoff, application activation resets the retry ladder so a
-  foregrounded app reconnects immediately instead of serving the remaining
-  delay.
+Retain state and cursor together only after an update finishes. Cancellation must
+not advance the cached cursor beyond the applied data, and an old scope must not
+overwrite its successor's cache. Preserve pagination data on reuse, but clear
+canceled loading state.
-- Once connected, `monitorConnectedLease` handles plain activation by probing
-  the existing session (`lease.session.probe`, with a shorter timeout for
-  mobile's `application-active-probe`) rather than reconnecting; a healthy
-  session survives foregrounding. `application-active-reconnect` skips the probe
-  and replaces the lease outright.
 
+The [RPC boundary](../../packages/client-runtime/src/rpc/client.ts) resolves
+requests against the current session at execution time. Durable subscriptions
+follow replacement sessions. After a transport failure they wait for the
-The UI derives `available`, `offline`, `connecting`, `reconnecting`,
+supervisor; an expected domain failure may resubscribe on the same healthy
+session. Reconnection does not automatically replay mutations, whose retry and
+idempotency rules belong to the operation.
-`connected`, and `error` from supervisor state plus explicit data-sync state.
-It does not infer connection health from cached data or the existence of a
-transport object. An environment becomes `connected` after the socket opens and
-the initial config RPC succeeds, proving that the server is responsive. Shell
-and thread synchronization are independent data states. A healthy RPC transport
-with a failed shell subscription is shown as connected with a synchronization
-error, not as a reconnect that is not actually scheduled.
-
-## Data Boundary
-
-Finite requests, durable subscriptions, and commands are separate APIs:
-
-- Query atoms revalidate when the RPC generation changes.
-- Subscription atoms switch to replacement sessions.
-- Subscription failure handling in [rpc/client.ts][client] distinguishes two
-  cases. A transport failure (`isTransportFailure`: every failure is an RPC
-  client error) ends the inner subscription without resubscribing, so the outer
-  stream waits for the supervisor to supply a replacement session. A handled
-  domain failure runs `onExpectedFailure` and, when
-  `retryExpectedFailureAfter` is set, sleeps and resubscribes on the **same**
-  session. A healthy transport is never torn down for a domain failure.
-- Mutations resolve the current environment runtime at execution time.
-- Shell and thread snapshots are available while offline.
-- Sync status is explicit and independent per domain. Shell status is `empty`,
-  `cached`, `synchronizing`, or `live`, with a separate `error` field; there is
-  no `failed` status. Thread status adds `deleted`.
-- Cached shell and thread projections are never allowed to overwrite newer live
-  data during a fast reconnect.
-- Domain atom factories route effects through the environment registry and
-  resolve the current scoped service at execution time. Project and thread
-  commands are Atom factories under `src/state`
-  (`createProjectEnvironmentAtoms`, `createThreadEnvironmentAtoms`), as are the
-  shell and thread state factories (`createEnvironmentShellAtoms`,
-  `createEnvironmentThreadStateAtoms`).
-- Web and mobile own their Atom runtimes, React hooks, and feature composition.
-
-The Promise bridge exists only at the React/Atom boundary. Runtime and business
-logic remain Effect-native.
-
-## Platform Layers
-
-Web and mobile provide:
-
-- network status and network-change streams;
-- application lifecycle wakeups;
-- cloud session credentials;
-- device identity;
-- platform registrations;
-- persistent catalog, credential, shell, and thread stores;
-- HTTP, crypto, and telemetry layers.
-
-Platform layers adapt operating-system capabilities. They do not implement
-connection policy. `EnvironmentOwnedDataCleanup` is part of this contract: on
-removal the registry clears its cache and calls the platform implementation, so
-web clears composer drafts and mobile clears drafts plus the thread outbox.
-
-Mobile cloud sign-out first saves relay drafts and queued messages in the local
-composer store under the owning account. These saved copies retain attachment
-files during cleanup and remain outside the active composer and upload queue.
-Signing back into that account restores them before relay credentials activate.
-Directly paired environments keep their drafts and outbox when cloud sign-out runs.
-
-Mobile composer attachments upload over HTTP while their environment is connected,
-with at most three concurrent transfers. Drafts retain local image data or an owned
-file URI alongside the pending upload ID. Sending verifies and reuses that ID, or
-uploads the local bytes again if it expired. Disconnecting cancels active transfers
-without discarding drafts; reconnecting resumes preparation. Older servers without
-attachment-upload support continue to receive inline images.
-
-## Source Boundaries
-
-Applications must import explicit package subpaths; the package intentionally
-has no root export. The subpaths are documented in
-[packages/client-runtime/README.md](../../packages/client-runtime/README.md),
-with the `exports` map in that package's `package.json` as the authoritative
-list. Files that are not exported are implementation details.
-
-## Application Boundary
-
-The application root mounts the shared connection layer, creates its own Atom
-runtime, and selects the domain atom factories required by that platform. Web
-and mobile may expose different hooks and features without changing connection
-ownership.
-
-Application code must not construct RPC clients, retry loops, or raw
-orchestration commands. Persistence paths belong to the platform registration
-and cache stores, with explicit migration or invalidation policy.
-
-### Thread detail ownership
-
-Mounted detail consumers share one live thread subscription. The subscription
-ends when the last consumer leaves. Hidden routes that remain mounted still
-count as consumers.
-
-A separate registry-local atom retains the last thread state and replay cursor
-for five idle minutes. A warm mount renders that state and resumes the stream
-without another snapshot download. The cache holds completed state and cursor
-updates together. It retains pagination data but clears canceled loading state.
-An ownership token prevents an old scope from replacing its successor's cache.
-Closing a thread skips snapshots that were already saved. Changed snapshots and
-failed background writes still get a final save attempt.
-
-## Verification
-
-Core state-machine tests use `@effect/vitest` and deterministic service layers.
-Required coverage includes:
-
-- offline startup and online wakeup;
-- forever retry with the 16-second cap;
-- explicit retry interrupting backoff;
-- authentication wakeups;
-- involuntary close and reconnect;
-- explicit removal clearing all owned state;
-- relay token reuse and refresh;
-- progressive relay discovery;
-- shell and thread cache hydration;
-- thread stream shutdown, warm resume, and snapshot expiry;
-- durable subscriptions switching sessions;
-- command metadata and idempotent queued-command metadata.
-
-[layer]: ../../packages/client-runtime/src/connection/layer.ts
-[resolver]: ../../packages/client-runtime/src/connection/resolver.ts
-[driver]: ../../packages/client-runtime/src/connection/driver.ts
-[registry]: ../../packages/client-runtime/src/connection/registry.ts
-[supervisor]: ../../packages/client-runtime/src/connection/supervisor.ts
-[session]: ../../packages/client-runtime/src/rpc/session.ts
-[client]: ../../packages/client-runtime/src/rpc/client.ts
```

## docs/internals/environment-auth.md

Status: modified; +49; −186

````diff
@@ -1,197 +1,60 @@
-# Environment Authentication Profile
+# Environment authentication
 
-> For maintainers. Using T3 Code? See [docs/user](../user/).
+The environment issues its own sessions and enforces their capabilities. Cloud
+identity and relay credentials belong to a separate trust boundary, described in
+[T3 Connect](./t3-connect.md). A relay token is never an environment login.
 
-The environment server and the relay use separate credentials, issuers, and trust
+## Authority survives transport changes
-boundaries. They intentionally use a similar OAuth-shaped model so that permission
-checks and token exchange behavior can be audited against established concepts.
 
+Pairing delegates a set of scopes. Exchanging a bootstrap credential can narrow
+that grant but cannot widen it. Ordinary pairing does not grant access-management
+or relay-management authority. Creating another pairing link requires both
+`access:write` and every scope being delegated. The
-## Authorization Model
+[auth handlers](../../apps/server/src/auth/http.ts) enforce this at issuance;
+client labels and device metadata have no authorization role.
 
-Environment authorization is capability-based. A session carries zero or more
-OAuth-style scope strings:
+The access read model contains pairing metadata, never recoverable pairing
+secrets. Only the creation response returns the raw credential. Otherwise read
+access to the connections list would become a way to acquire another client's
+authority.
 
-| Scope                   | Permission                                                               |
-| ----------------------- | ------------------------------------------------------------------------ |
-| `orchestration:read`    | Read snapshots, status, events, configuration, and filesystem/VCS state. |
-| `orchestration:operate` | Dispatch user operations and mutate environment-side workspace state.    |
-| `terminal:operate`      | Create, attach, input, resize, clear, restart, and terminate terminals.  |
-| `review:write`          | Read review diff previews used to compose review feedback.               |
-| `access:read`           | Inspect pairing links and client sessions.                               |
-| `access:write`          | Create or revoke pairing links and client sessions.                      |
-| `relay:read`            | Inspect managed relay connectivity.                                      |
-| `relay:write`           | Link, configure, or unlink managed relay connectivity.                   |
-
-Pairing-link lists and access-stream snapshots and updates contain metadata only.
-The raw credential is returned only by the creation request, after the server checks
-`access:write` and the delegated scopes. Web and desktop clients keep that response
-in memory for sharing. They do not recover credentials from access read models.
-
-Ordinary pairing links grant the four client-operation scopes and read access to
-managed relay connectivity:
-`orchestration:read orchestration:operate terminal:operate review:write relay:read`.
-The desktop bootstrap credential and command-line administrative bootstrap
-credentials additionally grant `access:read access:write relay:write`.
-
-## Host file access
-
-Clients with `orchestration:read` can read files anywhere the environment's server account can
-read, following the environment-wide authorization model rather than introducing per-project
-filesystem permissions. `projects.readFile` accepts an absolute path and returns the text of that
-host file; only workspace-relative paths pass its root check, and `projects.writeFile` never
-accepts an absolute path. Clients use this to show files an agent wrote outside the workspace, such
-as a report in a temp directory, read-only.
-
-## Media preview access
-
-Clients with `orchestration:read` can request a `media-file` URL through `assets.createUrl` for
-supported images, videos, HTML, and PDF files anywhere the environment's server account can read.
-A thread ID supplies the workspace for relative paths; absolute paths refer to the environment
-host, not the client.
+Browser cookies, bearer tokens, and DPoP tokens adapt the same scoped session
+model. DPoP binds a token to a client's proof key; an invalid proof must fail
+rather than fall back to bearer authentication. The OAuth token-exchange
+vocabulary gives these grants a familiar meaning, but the environment does not
+implement a general-purpose OAuth authorization server.
 
-[`AssetAccess.ts`](../../apps/server/src/assets/AssetAccess.ts) resolves symlinks, requires a regular
-file, and validates the resolved file's literal extension. It opens the file and signs its canonical
-path and device/inode identity for one hour. The token grants access to that exact file, not adjacent
-files or its containing directory. Serving rechecks the canonical path, media type, and opened
-descriptor's identity, then streams full or partial responses from that descriptor. Replacing a
-file atomically requires a freshly signed URL; editing it in place does not. Because the token names
-one file, an HTML document served this way cannot load sibling assets; the directory-scoped
-`workspace-file` resource remains the route for HTML inside the workspace. Uploaded attachments keep
-their separate asset resource.
+Bearer and DPoP clients obtain short-lived WebSocket tickets through authenticated
+HTTP so long-lived tokens stay out of socket URLs. Browser sessions can
+authenticate the upgrade with their cookie. A successful handshake grants no
+extra authority: [every RPC declares a required
+scope](../../apps/server/src/auth/RpcAuthorization.ts).
 
+Desktop restarts forget the previous local bearer token, so its reusable
+bootstrap grant replaces earlier sessions for the same subject and method.
+Revocation and insertion share a [database
-Signed asset URLs are bearer credentials. Anyone who obtains a URL and can reach the environment
+transaction](../../apps/server/src/persistence/AuthSessions.ts); a failed
+replacement must leave the old credential usable. Pairing and browser sessions
+do not follow this replacement rule.
-can fetch that file until it expires. Clients should copy the authored reference, not the temporary
-URL. Responses use `nosniff`; SVG responses retain their restrictive sandbox policy. Video reads
-support byte ranges so playback does not require a complete download first.
 
-Host videos can change in place, so their responses use `private, no-store` and omit `ETag`
-and `Last-Modified`. File metadata cannot prove byte-for-byte identity for `If-Range`; advertising
-those validators would encourage native players to send conditional seeks that require a full
-response. Ordinary range requests receive partial responses. An explicitly supplied `If-Range`
-still falls back to a full response because no strong validator is available. Host image previews
-keep their private cache policy and weak metadata validators.
+## The environment is the filesystem boundary
 
+Projects are organizational boundaries, not filesystem sandboxes.
+`orchestration:read` permits reading files the server account can read, including
+absolute paths outside a project. This lets clients display artifacts that an
+agent writes in a temporary directory. Relative paths and writes still follow
+the [workspace path rules](../../apps/server/src/workspace/WorkspaceFileSystem.ts).
-The server serves media in place without importing it into attachment storage. Deletion makes
-future server reads fail, though an already loaded client or its cache can retain bytes. Native
-viewers may use temporary client-side files for display or explicit sharing; those are not durable
-environment copies.
 
+Signed asset URLs are bearer credentials. A URL for media on the host grants
+access to one canonical file and its device/inode identity, not its containing directory.
+[Asset access](../../apps/server/src/assets/AssetAccess.ts) rechecks the opened
+file's identity when serving it, so atomic replacement requires a new URL while
+editing the same file in place does not. An HTML file authorized this way cannot
-## Authentication Flows
+load sibling assets; directory-scoped workspace previews are a separate grant.
+Clients should share the authored file reference so they do not disclose the
+temporary URL's credential.
 
+Host videos can change in place. Their [HTTP
+responses](../../apps/server/src/http.ts) omit cache validators because file
+metadata cannot prove byte-for-byte identity for `If-Range`. Adding weak
+validators would turn native-player seeks into full downloads.
-### Browser Session
-
-`POST /api/auth/browser-session` consumes a one-time bootstrap credential and creates a
-browser session cookie. The cookie is an HTTP transport adapter for the same
-scoped session model; the response never exposes the session secret to browser
-JavaScript.
-
-### Bearer Access Token
-
-Non-browser clients use `POST /oauth/token` with an
-`application/x-www-form-urlencoded` body:
-
-```text
-grant_type=urn:ietf:params:oauth:grant-type:token-exchange
-subject_token=<bootstrap credential>
-subject_token_type=urn:t3:params:oauth:token-type:environment-bootstrap
-requested_token_type=urn:ietf:params:oauth:token-type:access_token
-scope=orchestration:read orchestration:operate terminal:operate review:write relay:read
-```
-
-Clients may additionally submit `client_label`, `client_device_type`, and
-`client_os` extension parameters so the authorized-clients UI can identify the
-device that established the session. These are presentation hints only; the
-environment derives transport metadata such as IP address and user agent from
-the request and does not use these fields for authorization.
-
-The response has the token-exchange shape:
-
-```json
-{
-  "access_token": "<opaque session token>",
-  "issued_token_type": "urn:ietf:params:oauth:token-type:access_token",
-  "token_type": "Bearer",
-  "expires_in": 2592000,
-  "scope": "orchestration:read orchestration:operate terminal:operate review:write relay:read"
-}
-```
-
-Sessions issued from a plain bearer exchange use the store's
-`DEFAULT_SESSION_TTL` of 30 days. The shorter one-hour `expires_in: 3600` applies
-only to DPoP-bound exchanges, where the token is additionally constrained by a
-proof key. See `SessionStore.ts` and `EnvironmentAuth.ts`.
-
-The reusable `desktop-bootstrap` grant replaces active sessions with the same
-subject and authentication method. Revocation and insertion share one database
-transaction, so a failed insertion preserves the previous credential. This also
-removes stale local desktop entries from earlier launches. Browser-cookie sessions
-and sessions issued through pairing links are not replaced.
-
-Requested scopes must be a subset of the one-time bootstrap credential grant.
-An ordinary paired client therefore cannot exchange its grant for
-`access:read`, `access:write`, or `relay:write`.
-
-### DPoP-Bound Access Token
-
-The same `/oauth/token` exchange supports proof-of-possession tokens. A client
-that sends a `DPoP` header has its proof verified by `verifyRequestDpopProof`;
-the resulting JWK thumbprint is stored on the session, which is then issued with
-method `dpop-access-token` and a one-hour TTL instead of the bearer default. An
-invalid proof gets a DPoP challenge header and a credential error rather than a
-bearer token. Newer servers include a safe `dpopFailureReason` category in that
-error. When an older server omits the category, clients mention clock skew as
-one possible cause rather than presenting it as confirmed.
-
-`dpop-access-token` is advertised alongside `browser-session-cookie` and
-`bearer-access-token` in the descriptor's `sessionMethods`
-(`EnvironmentAuthPolicy.ts`), so clients can discover support rather than
-assume it. Relay-brokered clients use this mode so that a leaked token cannot be
-replayed without the corresponding key.
-
-### WebSocket Ticket
-
-`POST /api/auth/websocket-ticket` accepts any authenticated session and returns
-a short-lived, single-purpose WebSocket ticket, issued through
-`EnvironmentAuth.issueWebSocketTicket` with a five-minute default TTL. The
-client presents its bearer or DPoP credential in headers to get the ticket, then
-appends only that ticket to the socket URL as `wsTicket`. This keeps long-lived
-tokens and browser cookies out of WebSocket URLs while letting the handshake
-authenticate.
-
-The ticket carries its session's scopes; each RPC method then enforces
-`orchestration:read`, `orchestration:operate`, `terminal:operate`,
-`review:write`, `relay:write`, or `access:read` as appropriate, through
-`RPC_REQUIRED_SCOPES` in `apps/server/src/auth/RpcAuthorization.ts`. Review feedback submission currently dispatches
-an orchestration operation, so clients performing it also need
-`orchestration:operate`. Creating a ticket is not authorization to call every
-RPC method.
-
-## Standards Alignment
-
-- Bearer access tokens are used through the `Authorization: Bearer` scheme from
-  RFC 6750.
-- The token endpoint profiles the request and response vocabulary from OAuth 2.0
-  Token Exchange (RFC 8693), including `subject_token`, `requested_token_type`,
-  `access_token`, `issued_token_type`, and `token_type`.
-- Scope values follow the OAuth 2.0 scope model from RFC 6749: space-delimited,
-  unordered capabilities with subset checking during exchange.
-
-This is intentionally not a general-purpose OAuth authorization server. The
-environment bootstrap token type is private, the bootstrap cookie and WebSocket
-connection-token routes are product-specific adapters, and the API returns its
-typed `HttpApi` errors rather than implementing every OAuth error response
-surface.
-
-## Upgrade Behavior
-
-Migration `031_AuthAuthorizationScopes` is a hard cutover from role-bearing auth
-records to scoped records. It deletes existing pairing links and sessions while
-leaving non-authentication environment state unchanged. Upgraded clients must
-pair again; old `owner` or `client` credentials are never silently mapped to new
-capabilities.
-
-## Relay Boundary
-
-Relay-managed tunnels use their own tokens and keys. The relay can reuse scope
-parsing and token-exchange conventions, but an environment access token is not a
-relay token and cannot be presented to the relay.
````

## docs/internals/glossary.md

Status: modified; +39; −203

```diff
@@ -1,211 +1,47 @@
 # Glossary
-
-> For maintainers. Using T3 Code? See [docs/user](../user/).
-
-This is a living glossary for T3 Code. It explains what common terms mean in this codebase.
-
-## Table of contents
-
-- [Project and workspace](#project-and-workspace)
-- [Thread timeline](#thread-timeline)
-- [Orchestration](#orchestration)
-- [Provider runtime](#provider-runtime)
-- [Checkpointing](#checkpointing)
-- [Appearance](#appearance)
-
-## Concepts
-
-### Project and workspace
-
-#### Project
-
-The top-level workspace record in the app. In [the orchestration contracts][1], a project has a `workspaceRoot` and a title. It does not contain threads: `OrchestrationProject` and `OrchestrationThread` are separate arrays on the read model, and a project can have zero threads. See [workspace-layout.md][2].
-
-#### Workspace root
-
-The root filesystem path for a project. In [the orchestration model][1], it is the base directory for branches and optional worktrees. See [workspace-layout.md][2].
-
-#### Worktree
-
-A Git worktree used as an isolated workspace for a thread. If a thread has a `worktreePath` in [the contracts][1], it runs there instead of in the main working tree. Git operations live behind the VCS driver contract in `apps/server/src/vcs/VcsDriver.ts`, implemented by [GitVcsDriverCore.ts][3].
-
-### Thread timeline
-
-#### Thread
-
-The main durable unit of conversation and workspace history. In [the orchestration contracts][1], a thread holds messages, activities, checkpoints, and session-related state. See [projector.ts][4].
-
-#### Turn
-
-A single user-to-assistant work cycle inside a thread. It starts with user input and ends when the session leaves `running` status, which [projector.ts][4] treats as the authoritative completion signal (`settledTurnStateForSessionStatus`). Checkpoint and diff work may settle afterward without changing when the turn ended. See [the contracts][1] and [ProviderRuntimeIngestion.ts][5].
-
-#### Activity
-
-A user-visible log item attached to a thread. In [the contracts][1], activities cover important non-message events like approvals, tool actions, and failures. They are projected into thread state in [projector.ts][4].
-
-### Orchestration
-
-Orchestration is the server-side domain layer that turns runtime activity into stable app state. The main entry point is [OrchestrationEngine.ts][7], with core logic in [decider.ts][8] and [projector.ts][4].
-
-#### Aggregate
-
-The domain object a command or event belongs to. In [the contracts][1], that is usually `project` or `thread`. See [decider.ts][8].
-
-#### Command
-
-A typed request to change domain state. In [the contracts][1], commands are validated in [commandInvariants.ts][9] and turned into events by [decider.ts][8].
-Examples include `thread.create`, `thread.turn.start`, and `thread.checkpoint.revert`.
-
-#### Domain Event
-
-A persisted fact that something already happened. In [the contracts][1], events are the source of truth, and [projector.ts][4] shows how they are applied.
-Examples include `thread.created`, `thread.message-sent`, and `thread.turn-diff-completed`.
-
-#### Decider
-
-The pure orchestration logic that turns commands plus current state into events. The core implementation is in [decider.ts][8], with preconditions in [commandInvariants.ts][9].
-
-#### Projection
-
-A read-optimized view derived from events. See [projector.ts][4], [ProjectionPipeline.ts][11], and [ProjectionSnapshotQuery.ts][10].
-
-#### Projector
-
-The logic that applies domain events to the read model or projection tables. See [projector.ts][4] and [ProjectionPipeline.ts][11].
-
-#### Read model
-
-The current materialized view of orchestration state. In [the contracts][1], it holds projects, threads, messages, activities, checkpoints, and session state. See [ProjectionSnapshotQuery.ts][10] and [OrchestrationEngine.ts][7].
-
-#### Reactor
-
-A side-effecting service that handles follow-up work after events or runtime signals. Examples include [CheckpointReactor.ts][6], [ProviderCommandReactor.ts][12], and [ProviderRuntimeIngestion.ts][5].
-
-#### Receipt
-
-A typed signal emitted when an async milestone completes, such as `checkpoint.baseline.captured`, `checkpoint.diff.finalized`, or `turn.processing.quiesced`. Receipts are a test-only mechanism: the production `RuntimeReceiptBusLive` publish is a no-op and only the test layer is PubSub-backed. Do not build production behavior on them. See [RuntimeReceiptBus.ts][13] and [CheckpointReactor.ts][6].
-
-#### Quiesced
-
-"Quiesced" means a turn has gone quiet and stable: follow-up work such as [CheckpointReactor.ts][6] has settled. It appears in [the receipt schema][13], so in practice it is something tests wait on rather than a production signal.
-
-### Provider runtime
-
-The live backend agent implementation and its event stream. The main service is [ProviderService.ts][14], the adapter contract is [ProviderAdapter.ts][15], and the overview is in [providers.md][16].
-
-#### Provider
-
-The backend agent runtime that actually performs work. Six drivers ship built in: Codex, Claude, Cursor, Grok, OpenCode, and Antigravity. See [ProviderService.ts][14], [ProviderAdapter.ts][15], and [CodexAdapter.ts][17] as a representative adapter.
-
-#### Session
-
-The live provider-backed runtime attached to a thread. Session shape is in [the orchestration contracts][1], and lifecycle is managed in [ProviderService.ts][14].
-
-#### Runtime mode
-
-The safety/access mode for a thread or session. [The contracts][1] define four values: `approval-required`, `auto-accept-edits`, `auto`, and `full-access`. See [permission modes][18].
-
-#### Interaction mode
-
-The agent interaction style for a thread. In [the contracts][1], the values are `default` and `plan`.
-
-#### Assistant delivery mode
-
-Controls how assistant text reaches the thread timeline. In [the contracts][1], `streaming` updates incrementally and `buffered` accumulates text. Buffered delivery is not held until the turn completes: it spills once accumulated text would exceed 24,000 characters, and flushes at approval and user-input boundaries. See [ProviderRuntimeIngestion.ts][5].
-
-#### Snapshot
-
-A point-in-time view of state. The word is used in multiple layers, including orchestration, provider, and checkpointing. See [ProjectionSnapshotQuery.ts][10], [ProviderAdapter.ts][15], and [CheckpointStore.ts][19].
-
-#### Usage limits
-
-The rolling subscription quota windows a provider reports for its signed-in account, such as Claude's five-hour and weekly windows or Codex's primary and secondary allowances. Each driver decides in its own `checkProvider` whether it has any and returns them on the snapshot as `usageLimits`; drivers with no notion of subscription usage leave the field absent. Adapters that receive rate-limit telemetry during a turn normalise it into a `ProviderUsageLimitsUpdate` at the boundary, and `ProviderUsageLimitsIngestion` folds it onto the owning instance's snapshot through `ServerProviderShape.applyUsageLimits`, so no central service needs to know a driver kind. See [providerUsageLimits.ts](../../packages/contracts/src/providerUsageLimits.ts) and [makeManagedServerProvider.ts](../../apps/server/src/provider/makeManagedServerProvider.ts).
 
-#### Usage limit source
+Terms whose meaning matters across T3 Code. Architecture and lifecycle constraints belong in the
+[overview](./overview.md), not in these definitions.
 
-A read-only quota feed outside this environment's provider CLIs, configured under `settings.usageLimitSources`. The only kind today is a CLIProxyAPI hub, whose `quota-scheduler/status` reports the windows of every pooled account. `UsageLimitSources` polls each source on the provider health interval and publishes `UsageLimitSourceSnapshot`s over the config stream as `usageLimitSourcesUpdated`, gated by a client capability flag the way environment themes are. The management key round-trips through the secret store with a redaction marker on disk. See [UsageLimitSources.ts](../../apps/server/src/usage/UsageLimitSources.ts).
+## Workspace and conversation
 
+| Term           | Meaning                                                                                           |
+| -------------- | ------------------------------------------------------------------------------------------------- |
+| Environment    | One running server and the machine, credentials, workspace access, and state it owns.             |
+| Client         | A web, desktop, or mobile UI connected to an environment. The desktop app can also host a server. |
+| Project        | An environment-local workspace record rooted at a directory.                                      |
+| Workspace root | The project's base filesystem directory on the environment.                                       |
+| Worktree       | A separate Git checkout a thread can use instead of the project's main checkout.                  |
+| Thread         | The durable conversation and work history for a project. It survives provider process exits.      |
+| Turn           | One user-to-agent work cycle. Provider work can finish before checkpoint and diff work settles.   |
+| Activity       | A non-message timeline item, such as a tool action, approval, or failure.                         |
+| T3 home        | The base data directory. Runtime state normally lives under its `userdata` directory.             |
-#### Model manifest
 
-The per-driver list of current model slugs that decides which models land in the model picker's legacy section. Bundled at `apps/server/src/provider/model-manifest.json` and refreshed at runtime from the same file on `main`, so classification updates ship as commits instead of releases. See the [provider architecture][16] model manifest section.
+## Orchestration
 
+| Term                    | Meaning                                                                                      |
+| ----------------------- | -------------------------------------------------------------------------------------------- |
+| Command                 | A request to change domain state. Accepting it does not mean its side effects have finished. |
+| Event                   | A persisted fact produced by a command.                                                      |
+| Decider                 | The pure logic that turns a command and current state into events.                           |
+| Projection / read model | A view of current state derived from persisted events.                                       |
+| Projector               | The logic that applies events to a read model.                                               |
+| Reactor                 | A worker that performs follow-up work in response to recorded intent or runtime signals.     |
+| Command receipt         | A durable record of a command's result, used to make retries idempotent.                     |
+| Runtime receipt         | A test-only signal that an asynchronous milestone completed.                                 |
+| Quiesced                | The relevant follow-up workers have finished, beyond the provider turn merely ending.        |
-### Checkpointing
 
-Checkpointing captures workspace state over time so the app can diff turns and restore earlier points. The main pieces are [CheckpointStore.ts][19], [CheckpointDiffQuery.ts][20], and [CheckpointReactor.ts][6].
-
-#### Checkpoint
-
-A saved snapshot of a thread workspace at a particular turn. In practice it is a hidden Git ref in [CheckpointStore.ts][19] plus a projected summary from [ProjectionCheckpoints.ts][21]. Capture and lifecycle work happen in [CheckpointReactor.ts][6].
-
-#### Checkpoint ref
-
-The durable identifier for a filesystem checkpoint, stored as a Git ref. It is typed in [the contracts][1], constructed in [Utils.ts][22], and used by [CheckpointStore.ts][19].
-
-#### Checkpoint baseline
-
-The starting checkpoint for diffing a thread timeline. This flow is surfaced through [RuntimeReceiptBus.ts][13], coordinated in [CheckpointReactor.ts][6], and supported by [Utils.ts][22].
-
-#### Checkpoint diff
+## Providers and checkpoints
 
+| Term                | Meaning                                                                                                      |
+| ------------------- | ------------------------------------------------------------------------------------------------------------ |
+| Provider            | The agent runtime T3 Code controls, such as Codex or Claude Code.                                            |
+| Driver              | The integration for a provider kind.                                                                         |
+| Provider instance   | One configured provider, with its own settings and lifecycle. Multiple instances can use the same driver.    |
+| Adapter             | The boundary translating a provider's native protocol into T3 Code operations and events.                    |
+| Session             | The provider runtime attached to a thread. A session can be stopped and resumed without deleting the thread. |
+| Runtime mode        | The thread's permission policy. See [permission modes](../user/permission-modes.md).                         |
+| Interaction mode    | How the agent approaches the task, such as planning. Separate from permission policy.                        |
+| Checkpoint          | A saved workspace state used for diffs and restore, stored as a hidden Git ref.                              |
+| Checkpoint baseline | The workspace state captured before the work being compared.                                                 |
-The difference between two checkpoints. [CheckpointDiffQuery.ts][20] reads full patches on demand. [CheckpointReactor.ts][6] uses NUL-delimited Git numstat output for automatic file summaries, parsed by [Diffs.ts][23].
+| Turn diff           | The workspace changes attributed to one turn.                                                                |
-
-#### Turn diff
-
-The file patch and changed-file summary for one turn. It is usually computed in [CheckpointDiffQuery.ts][20], represented in [the contracts][1], and recorded into thread state by [projector.ts][4].
-
-### Appearance
-
-#### Environment theme
-
-A theme an environment's machine publishes for clients to follow, one file per theme under `themes/` in that environment's state directory; the filename is the theme id. [environmentTheme.ts][25] watches the directory and streams the set over `subscribeServerConfig`; clients render each as a library card, generating a full palette when the file carries seed colors and using the palette directly when it is a standard exported theme file. A desktop that retints its apps when the system theme changes rewrites its file, so T3 Code follows along without a restart. See [environment-theme.md][26].
-
-#### Default theme
-
-The environment's theme, held in its `settings.json` as `defaultTheme` (with `defaultThemeSetAt`
-as the set-generation) and set with `t3 theme set <id>`. Web and desktop clients apply each set
-once — live when connected, on the next connect otherwise — so setting it switches them, while a
-theme a user picks in Settings afterwards sticks until the next set; mobile keeps its own
-appearance settings. Naming a published [environment theme](#environment-theme) is how a desktop
-ships T3 Code already matching it.
-
-## Practical Shortcuts
-
-- If you see `requested`, think "intent recorded".
-- If you see `completed`, think "result applied".
-- If you see `receipt`, think "async milestone signal, for tests".
-- If you see `checkpoint`, think "workspace snapshot for diff/restore".
-- If you see `quiesced`, think "all relevant follow-up work has gone idle".
-
-## Related Docs
-
-- [Architecture overview][24]
-- [Provider architecture][16]
-- [Permission modes][18]
-- [Workspace layout][2]
-
-[1]: ../../packages/contracts/src/orchestration.ts
-[2]: ./workspace-layout.md
-[3]: ../../apps/server/src/vcs/GitVcsDriverCore.ts
-[4]: ../../apps/server/src/orchestration/projector.ts
-[5]: ../../apps/server/src/orchestration/Layers/ProviderRuntimeIngestion.ts
-[6]: ../../apps/server/src/orchestration/Layers/CheckpointReactor.ts
-[7]: ../../apps/server/src/orchestration/Layers/OrchestrationEngine.ts
-[8]: ../../apps/server/src/orchestration/decider.ts
-[9]: ../../apps/server/src/orchestration/commandInvariants.ts
-[10]: ../../apps/server/src/orchestration/Layers/ProjectionSnapshotQuery.ts
-[11]: ../../apps/server/src/orchestration/Layers/ProjectionPipeline.ts
-[12]: ../../apps/server/src/orchestration/Layers/ProviderCommandReactor.ts
-[13]: ../../apps/server/src/orchestration/Services/RuntimeReceiptBus.ts
-[14]: ../../apps/server/src/provider/Layers/ProviderService.ts
-[15]: ../../apps/server/src/provider/Services/ProviderAdapter.ts
-[16]: ./providers.md
-[17]: ../../apps/server/src/provider/Layers/CodexAdapter.ts
-[18]: ../user/permission-modes.md
-[19]: ../../apps/server/src/checkpointing/CheckpointStore.ts
-[20]: ../../apps/server/src/checkpointing/CheckpointDiffQuery.ts
-[21]: ../../apps/server/src/persistence/Services/ProjectionCheckpoints.ts
-[22]: ../../apps/server/src/checkpointing/Utils.ts
-[23]: ../../apps/server/src/checkpointing/Diffs.ts
-[24]: ./overview.md
-[25]: ../../apps/server/src/environmentTheme.ts
-[26]: ../user/environment-theme.md
```

## docs/internals/mobile-development.md

Status: modified; +20; −45

```diff
@@ -1,50 +1,25 @@
 # Mobile development lifecycle
 
+The [connection runtime's HMR boundary](../../apps/mobile/src/lib/hot-swappable-atom-runtime.ts)
+keeps a stable atom runtime and replaces its Effect layer through a writable atom.
+It accepts the update only after installing the new layer. Otherwise importers
-Ordinary component changes use Metro Fast Refresh. The connection runtime uses
+retain the old behavior even though Metro reports a successful refresh.
-a stable development-only atom runtime whose writable layer atom receives the
-newly evaluated Effect layer. Its module accepts the update only after installing
-that layer. Existing subscribers observe the new context, and Effect releases the
-previous runtime scope without restarting React Navigation. Production uses an
-ordinary `Atom.runtime` without this hot-update boundary.
 
-Replacing the atom registry itself disposes its old nodes; replacing the managed
-runtime starts its asynchronous disposal. These modules use normal Metro update
+Do not reset the shared atom registry to refresh a connection-runtime edit. Reset
+removes listeners from mounted consumers that Metro has no reason to rerender.
+When the registry or managed-runtime module itself changes, normal Metro propagation
+disposes its old resources. This boundary does not make arbitrary module-level atom
+families safe to hot-swap. Production uses an ordinary atom runtime.
-propagation. The app does not call `DevSettings.reload` during their replacement.
-React can still reset state when its normal Fast Refresh rules require it.
 
-Do not reset the shared registry to clean up a connection-runtime edit: registry
-reset removes listeners from unedited mounted consumers. Do not self-accept a
-runtime module while leaving importers attached to its old implementation. A
-hot-update boundary must install fresh behavior through the existing reactive
-dependencies. These boundaries do not make every module-level atom family in the
-app hot-swappable; new runtime singletons still need explicit ownership. Editing
-the registry or managed runtime can still rebuild a much larger dependency graph
-than an ordinary component or connection-runtime edit.
+[Environment supervisor scopes](../../packages/client-runtime/src/connection/registry.ts)
+are children of the registry scope. The per-environment map supports targeted
+shutdown, but a supervisor created after its cleanup runs would escape it. A closed
+parent scope also closes late arrivals, preventing interrupted startup or runtime
+replacement from leaving a WebSocket alive outside the new registry.
 
+The [Uniwind patch](../../patches/uniwind@1.11.0.patch) still compiles CSS on Metro
-Environment supervisor scopes are children of the connection registry scope.
-The registry's per-environment map supports targeted shutdown, but it cannot be
-the sole owner: a supervisor created after that map's finalizer has run must
-still inherit the closed parent scope. Otherwise interrupted startup or runtime
-replacement can leave a session and WebSocket alive outside the current registry.
-
-The compact Home list owns its minute-based presentation clock in a focus effect.
-Blur clears the interval without a render-driving focus subscription; focus
-refreshes the clock immediately. Other prop or state changes can still render
-the hidden list. Exact snooze-expiry timers remain active, and the visible iPad
-sidebar keeps its own minute updates.
-
-Connection and runtime projections are shared per environment. Thread selection
-consumers should read those atoms instead of repeatedly parsing the same socket
-URL or constructing new connection objects during each render.
-
-The Uniwind dependency patch still recompiles CSS on Metro updates so newly used
-classes are discovered. It fingerprints the generated native stylesheet and
-theme list, then skips development-only global invalidation when that output is
-unchanged. A changed stylesheet or theme list still resets the style caches and
-notifies subscribers. The digest is committed only after initialization succeeds.
-Web and production retain their existing initialization behavior.
-
-After installing or changing the Uniwind patch, restart Metro once with
-`vp run dev:client:reset` from `apps/mobile`. pnpm gives patched packages new
-filesystem paths, and cached transforms can otherwise retain references to the
-previous package. Ordinary development starts should retain the transform cache.
+updates so newly used classes are discovered. It skips global style invalidation
+only when the generated stylesheet and theme list are unchanged. Skipping compilation
+would lose new classes; invalidating every consumer for unchanged output makes an
+ordinary component edit refresh the whole app. The fingerprint is recorded only
+after initialization succeeds.
```

## docs/internals/mobile-navigation.md

Status: modified; +44; −126

```diff
@@ -1,138 +1,56 @@
 # Mobile navigation
 
-The iOS Home and thread routes share the root native stack in
+Home and thread routes share the [root native stack](../../apps/mobile/src/Stack.tsx)
-[`Stack.tsx`](../../apps/mobile/src/Stack.tsx). Keeping them in one navigation
+so UIKit can animate the header within one navigation controller. The iPad sidebar
+has its own stack. Splitting Home and threads between controllers loses that
+continuous transition.
-controller lets UIKit animate the header between routes. The iPad sidebar owns
-a separate, single-screen stack; Android uses its own in-flow headers.
 
-Home and the iPad sidebar render their brand and connection status through
-`headerTitle`, with `Threads` retained as the route title. The editor-style native
-bar aligns that title on the leading side. Do not model the brand as a toolbar
-button: on iOS 26.5 UIKit morphs a background-free custom leading item's rectangle
-into the next screen's glass back button, even with distinct item identifiers.
-Using the title slot lets the brand and native back button animate separately.
+## UIKit constraints
 
+The [react-native-screens patch](../../patches/react-native-screens@4.26.2.patch)
+preserves behavior that is easy to break when changing native headers:
-The connection-status title has a maximum width based on its header's width and
-trailing actions. A long environment name or larger text must not push Settings
-into UIKit's overflow menu. Keep the full status as the accessibility label
-while visually truncating it. The iPad sidebar uses its pane width, not the full
-window width.
 
+- The brand belongs in `headerTitle`. On iOS 26.5, UIKit morphs a background-free
+  leading toolbar item's rectangle into the next screen's glass back button,
+  even when the items have different identifiers.
+- UIKit's scroll-edge fade does not recognize Fabric text views. An empty native
+  label supplies the custom title's geometry. Removing that apparently unused
+  view changes the fade.
+- Leading, trailing, and center item groups need independent caches. A button can
+  belong to only one group; rebuilding an unchanged group can pull its buttons
+  out of a group UIKit is still animating. Cache ownership also includes the
+  header config, whose event emitter changes on remount.
+- A horizontal ScrollView at its leading edge must yield to the full-screen back
-On iOS 26, UIKit does not recognize Fabric's custom text views when shaping the
-native scroll-edge fade. The screens patch gives custom title subviews an empty,
-non-interactive native label matching their bounds. This supplies the fade's
-geometry without drawing anything or turning the title back into a toolbar item.
-Check the top scroll fade as well as push/pop when changing these title views.
+  gesture. Upstream gives every horizontal ScrollView priority, so swiping back
+  on a code block or table can only bounce its content.
 
+The patch adds native header props that require code generation and a new binary.
+Patch rebuild guidance lives in the
+[mobile development README](../../apps/mobile/README.md#development).
-The react-native-screens patch caches leading, trailing, and center item groups
-independently. A button can belong to only one group: constructing another group
-with the same button removes it from the previous one. Unrelated menu updates
-must therefore preserve the other groups while UIKit may be animating them.
 
-Each cache includes the owning header config, its item values, and custom native
-item identities, so changed content or remounted event emitters still rebuild.
-
-Custom header identifiers require native code generation and a new mobile build;
-an over-the-air JavaScript update alone is insufficient. The Android view manager
-implements the generated identifier setter as a no-op because this behavior is
-specific to iOS 26 and later.
-
-On iOS 26 the full-screen back swipe is UIKit's `interactiveContentPopGestureRecognizer`,
-and react-native-screens makes it wait for any horizontally scrollable ScrollView to
-fail first. Upstream applies that to every horizontal ScrollView regardless of
-position, so a back swipe on a code block or table that is already at its leading
-edge only bounces. The patch narrows the rule to ScrollViews with content still
-hidden to the left; at the leading edge they yield to the pop gesture like plain
-text does.
-
-After changing a dependency patch, refresh CocoaPods before rebuilding an
-existing iOS project. pnpm installs each patch hash in a different directory;
-an old Pods project can keep compiling the previous directory even though Metro
-and `apps/mobile/node_modules` resolve to the new patch.
-
-`ControlPillMenu` resolves semantic icon colors through `withUniwind` and supplies them to every
-iOS `MenuView` action, including nested actions. The menu library's Fabric bridge converts a missing
-`imageColor` to transparent, so callers should use this wrapper instead of
-rendering `MenuView` directly. Explicit colors are preserved, and destructive
-actions default to the theme's danger foreground color. Native stack header menus
-use a separate implementation and do not need this workaround.
-
 ## Native media presentations
 
-`PresentationSource` in `NativePresentation` registers a thumbnail for AVKit,
-image zoom transitions, and UIKit's share sheet. Wrap the thumbnail as its single child
-and pass the stable identifier to the presentation. The registry keeps weak
-references to source views; recycled or compact composer thumbnails can register
-the same identifier. Identifiers must distinguish simultaneously visible attachments.
-The source registration does not own the preview. Android uses a regular view.
-
-On iOS, video previews mount `AVPlayerViewController` temporarily inside the
-registered source and enter full screen through AVKit. AVKit
-owns that zoom, its playback controls, Close button, and interactive dismissal.
+iOS delegates full-screen video to [AVKit](../../apps/mobile/modules/t3-native-controls/ios/T3NativeVideoPresentation.swift)
+and images and documents to [Quick Look](../../apps/mobile/modules/t3-native-controls/ios/T3NativeFilePresentation.swift).
+Each framework owns its controls and transition. A separate UIKit zoom transition
+prevents AVKit's native Close action from exiting full screen and interferes with
+Quick Look's interactive return to its thumbnail. The AVKit entry selector is
+guarded, with standard modal presentation as the fallback.
-Do not replace AVKit's transition with `preferredTransition`: in the iOS 27
-simulator, that leaves native Close unable to exit full screen. When the source is unavailable,
-the player uses a standard modal presentation. Programmatic entry uses the same
-guarded `enterFullScreenAnimated:completionHandler:` selector as Expo Video;
-if that selector is unavailable, the player also falls back to a standard modal.
 
+[Thumbnail registration](../../apps/mobile/modules/t3-native-controls/ios/T3NativePresentation.swift)
+holds weak view references for transition and share-sheet anchors. It does not own
+the preview; a source row can disappear while a native presentation is open.
+Identifiers must distinguish attachments that are visible at the same time.
+Native promises complete after dismissal so callers keep local file leases until
+the preview or share flow has finished.
-`FilePreviewModal` resolves image and PDF sources from a URI, a signed environment asset,
-or a retained composer file. On iOS, Quick Look owns image and document layout, controls,
-zooming, sharing, and interactive dismissal. Its delegate supplies the registered thumbnail
-and its bounds for Quick Look's source-view zoom. Do not layer `preferredTransition` or
-another image scroll view over that presentation: Quick Look coordinates its image gestures
-with the return to the thumbnail. Missing sources use the standard transition, and Reduce
-Motion disables animation. A pending programmatic Close waits until the current presentation
-or cancelled dismissal has settled before starting another transition.
 
+Treat a dismissal request as pending until UIKit finishes its current transition.
+Starting another dismissal while presentation or an interactive dismissal is
+settling can strand the controller. Video cleanup restores the previous audio
+configuration only if it still matches the preview's configuration. It must not
+deactivate the shared audio session, which may belong to another player or recorder.
-The shared native presenter copies original bytes into its own temporary directory and
-removes that copy after dismissal. Network downloads write to disk, and sharing never edits
-the source attachment. Draft images use their stored upload data rather than a potentially
-expired picker URI. No React Navigation route or custom transition animator is needed.
 
-The same viewer handles message images, markdown images, PDF attachments and links,
-composer thumbnails, and workspace image previews. The workspace PDF web preview has an
-Open PDF action for the native viewer. Android retains its image viewer and uses the
-system chooser for PDFs. Saving images on iOS uses the add-only photo-library permission.
+Video playback holds the first signed asset URL for the lifetime of the preview.
+Following credential refreshes reactively would restart playback. Quick Look copies
+original bytes into its own temporary directory so preview and sharing cannot
-
+mutate a draft or workspace file.
-Every full-screen video preview on iOS is AVKit, playing directly from the signed asset
-URL: sent attachments, composer drafts, and file links that point at a video. Inline
-markdown embeds and the workspace file screen play in place with the Expo Video player.
-AVKit handles buffering; the client does not download the entire file or show a separate
-opening overlay before presentation.
-The URL is captured once per preview so credential refresh does not restart playback.
-Media actions (copy path, open in file viewer, save or share) belong to the surface that
-opened the video, a long-press on the thumbnail, so the player carries no
-menu. AVKit has no retry; a failure alerts and closes, re-mints the URL in the background, and the next open plays from the fresh one.
-
-The native presentation promise completes after dismissal. Local draft previews
-hold their file lease until that promise settles. The iOS preview component requests
-native dismissal when its source screen unmounts. Playback pauses in the background.
-AVPlayer activates audio as playback starts. The presenter pauses and releases
-its own player on close, then restores the previous audio-session configuration
-if no other component changed it during playback. It does not deactivate the
-shared session, which may still serve another player or recorder. Android plays every
-video, attachments included, in its React Native modal with the Expo Video player.
-Composer drafts play and share from their local file; other videos stream from the
-signed URL and download only for save or share.
-
-`shareFileFromSource` uses the same source registration to anchor UIKit's activity
-controller. Its promise completes when the native share flow finishes, keeping
-the existing attachment lease and foreground handoff active for that duration.
-Android uses Expo Sharing. On iOS, received and draft video attachments expose
-Save or share through `VideoAttachmentMenu`. The attachment supplies the source
-identifier, and the native share presentation inherits its appearance. AVKit's
-iOS playback controls do not expose a public custom-share-action API.
-
-Video attachment thumbnails use Expo Video's native frame extraction and Expo Image.
-Received attachments use their signed asset URL; drafts retain and resolve their local file
-until extraction ends. Extraction is serial; temporary players never play or change audio settings. Leaving the screen cancels
-pending work; a 15-second limit prevents an unreachable source from holding up the queue.
-The client keeps at most 32 native images, each bounded to 480 pixels per side, keyed by
-environment and attachment identity rather than expiring URLs. Images still displayed keep
-their own references when evicted from that cache.
-
-The asset HTTP route supports single byte ranges for videos so iOS can read metadata and
-frames without first downloading the whole file. Normal downloads keep their full response;
-unsupported ranges and conditional `If-Range` requests also fall back to the full file.
-An older environment without range support may still show the play-card fallback. Thumbnail
-failure never disables playback or sharing.
```

## docs/internals/model-manifest.md

Status: modified; +19; −36

```diff
@@ -1,42 +1,25 @@
 # Model manifest
 
-`apps/server/src/provider/model-manifest.json` is bundled for offline startup and fetched from
-`main` at runtime. A remote fetch replaces the in-memory and on-disk cache only after generic
-catalog references and provider-owned adapter data validate. A failed or invalid fetch keeps the
+The [bundled manifest](../../apps/server/src/provider/model-manifest.json) allows
-last successful remote manifest. The bundle is used when no valid remote cache exists, or when
-the bundle's `updatedAt` is later than the cached manifest's, so a release that edits the manifest
-takes effect before the next successful fetch. Bump `updatedAt` whenever you edit the file.
+offline startup; fetching it from `main` lets model metadata change between
+releases. Failed fetches or invalid data preserve the last usable manifest.
+Remote data must pass both catalog-reference validation and the owning provider's
+adapter validation before replacing the cache.
 
-The top-level provider catalog is generic: models contain presentation metadata, aliases, status,
-an optional badge, and a reusable capability profile. The profile and model `adapter` fields are
-opaque until the owning provider validates them with its own allowlisted schema.
+A newer bundle outranks the cached remote manifest by `updatedAt`, so a release can
+correct model data before the next successful fetch. Bump `updatedAt` whenever the
+file changes. Fetch time cannot establish which copy contains the newer edit.
 
-Claude Code uses the manifest as its complete built-in model catalog. To add a Claude model that
-uses an existing profile, add one object to `providers.claudeAgent.models`. Do not add a test or
-change application code. Add or change a profile in the same JSON file only when the model exposes
-a capability combination that does not already exist.
+Generic catalog data describes presentation and capabilities. Each provider owns
+its adapter schema and dispatch mappings. Claude uses the manifest for its entire
+built-in catalog. Adding a model with an existing capability profile is a JSON
+edit; a new profile is needed only for a new capability combination. Codex still
+gets its model list from its app server.
 
+`currentModels.claudeAgent` is frozen for releases that predate catalog discovery.
+Do not extend it when adding Claude models. Codex uses `currentModels.codex` as a
-`currentModels.claudeAgent` is retained as a frozen compatibility field for releases that predate
-catalog discovery. New Claude models do not need to be added there. Codex still discovers models
-from its app server and uses `currentModels.codex` only as a legacy-classification overlay.
-
-Claude model entries support:
-
-- `aliases`, `status`, `badge`, and `profile` for client presentation and selection.
-- `adapter.claudeCode.minVersion` and `maxVersionExclusive` for installed-runtime compatibility.
-- Profile-level effort mappings, model suffixes, and context-window metadata for dispatch.
+legacy-classification overlay for discovered models.
 
+Model data is schema-validated configuration. Tests should cover resolver, cache,
+and adapter semantics with synthetic model names, so adding a model never requires
+tests that repeat the configuration.
-## Test policy
-
-Changing model data does not require tests. Do not add or update tests for a model slug, display
-name, alias, legacy status, version boundary, badge, or profile assignment. The bundled manifest is
-configuration and is validated by its schema when imported.
-
-Add tests only when implementation behavior changes:
-
-- Fetching, caching, fallback, or schema-version handling changes in the manifest service.
-- Provider-neutral profile resolution gains new semantics.
-- A provider adapter gains a new compatibility or dispatch mapping type.
-
-Resolver tests must use synthetic providers and model names so normal JSON edits never create test
-churn.
```

## docs/internals/overview.md

Status: modified; +50; −162

````diff
@@ -1,179 +1,67 @@
 # Architecture
-
-> For maintainers. Using T3 Code? See [docs/user](../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`][rpc]
-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`][ws] 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](./environment-auth.md).
-
-On the client, [`session.ts`][session] 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](./connection-runtime.md).
-
-## Orchestration is event-sourced
-
+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.
-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`][engine] 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`][decider]) 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`][projector], 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`][contracts]. 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`][projector]. 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`][settlement] 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. The sweep looks a branch up from the thread's worktree when it
-still exists, so it shares the per-cwd PR cache the sidebar polls instead of spending a second host
-request. 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.
+## Ownership boundaries
 
-At turn completion, `CheckpointReactor` refreshes PR discovery when the checkout matches the
-thread's non-default branch. `VcsStatusBroadcaster` requires loaded remote status and permission
-from background policy. `GitManager` retries only a successful "no PR" cache entry for the current
-branch, preserving known PRs and failure backoff without fetching remotes. Remote status reads
-that write the broadcaster cache share a lock per cwd, including the initial status load.
+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](./connection-runtime.md) and
+[remote environments](./remote.md).
 
+The [RPC contract](../../packages/contracts/src/rpc.ts) is the boundary between independently
+versioned clients and servers. Subscriptions send the state a client needs, so a client viewing one
-## Drainable workers
+thread does not pay for every thread's history. Authentication of a socket does not authorize every
+method on it. See [environment auth](./environment-auth.md).
 
+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](./providers.md).
-Follow-up work runs asynchronously in queue-backed workers built on [`DrainableWorker`][worker]:
-[`ProviderRuntimeIngestion`][ingest] normalizes provider runtime streams into orchestration commands,
-[`ProviderCommandReactor`][cmd] dispatches provider calls in response to intent events,
-[`CheckpointReactor`][checkpoint] captures and reverts workspace checkpoints, and
-[`ThreadSettlementReactor`][settlement] evaluates server-owned automatic settlement rules.
 
+## Durable intent and side effects
-`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.
 
+The event log is the source of truth for orchestration state. The
+[engine](../../apps/server/src/orchestration/Layers/OrchestrationEngine.ts) serializes commands;
+the [decider](../../apps/server/src/orchestration/decider.ts) produces events without performing
+provider or filesystem work. Events, persisted projections, and the accepted command receipt commit
-Runtime receipts are a test-only mechanism. `RuntimeReceiptBusLive` in
-[`RuntimeReceiptBus.ts`][receipts] publishes nothing; only the test layer is PubSub-backed. Do not
-build production behavior on receipts.
+in one database transaction. The in-memory state changes and subscribers receive events after that
+commit. This keeps command retries idempotent and prevents a persisted projection from getting
+ahead of the event log.
 
+Reactors perform side effects after intent has been recorded, then feed results back through
+commands. 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 the decider and the database
+transaction.
-## Provider drivers
 
+Persisted 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
-Six drivers ship built in, registered in [`builtInDrivers.ts`][drivers] 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
+what the newest client sends.
-which agent is behind them. See [providers.md](./providers.md).
 
-## Checkpointing
+## Turn completion and checkpoints
 
-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
+A turn ending and its follow-up work settling are separate milestones. The
+[projector](../../apps/server/src/orchestration/projector.ts) settles the turn from its session
+status. A late checkpoint or diff must not extend the recorded turn duration or keep the client
+showing provider work as active.
-[`VcsDriver.ts`](../../apps/server/src/vcs/VcsDriver.ts), implemented for Git in the same directory.
 
+[Checkpoints](../../apps/server/src/checkpointing/CheckpointStore.ts) use hidden Git refs to
+capture workspace state without adding commits to the user's branch. A revert must coordinate
-## Startup
+workspace state with the provider conversation. A provider that cannot roll back its conversation
+must reject that operation before changing the filesystem.
 
+## Waiting for asynchronous work
-[`serverRuntimeStartup.ts`][startup] 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
+Tests use [drainable workers](../../packages/shared/src/DrainableWorker.ts) to wait until both the
+queue and its current item have finished. An empty queue alone does not prove the worker is idle.
 
+Runtime receipts mark specific test milestones. Their
-- [Workspace layout](./workspace-layout.md), [Glossary](./glossary.md)
+[production layer](../../apps/server/src/orchestration/Layers/RuntimeReceiptBus.ts) is a no-op;
+production behavior must use persisted state and events. These test signals are separate from the
+durable command receipts that make dispatch idempotent.
-- [Mobile navigation headers](./mobile-navigation.md)
-- [Remote environments](./remote.md), [Server updates](./server-updates.md)
-- [Resource telemetry](./resource-telemetry.md)
-- [Product analytics](./product-analytics.md)
-- [Scripts](./scripts.md), [CI gates](./ci.md)
 
-[rpc]: ../../packages/contracts/src/rpc.ts
-[contracts]: ../../packages/contracts/src/orchestration.ts
-[ws]: ../../apps/server/src/ws.ts
-[session]: ../../packages/client-runtime/src/rpc/session.ts
-[startup]: ../../apps/server/src/serverRuntimeStartup.ts
-[engine]: ../../apps/server/src/orchestration/Layers/OrchestrationEngine.ts
+See the [glossary](./glossary.md) for shared terms and the
+[development runbook](../operations/development.md) for setup and checks.
-[decider]: ../../apps/server/src/orchestration/decider.ts
-[projector]: ../../apps/server/src/orchestration/projector.ts
-[worker]: ../../packages/shared/src/DrainableWorker.ts
-[ingest]: ../../apps/server/src/orchestration/Layers/ProviderRuntimeIngestion.ts
-[cmd]: ../../apps/server/src/orchestration/Layers/ProviderCommandReactor.ts
-[checkpoint]: ../../apps/server/src/orchestration/Layers/CheckpointReactor.ts
-[settlement]: ../../apps/server/src/orchestration/ThreadSettlementReactor.ts
-[receipts]: ../../apps/server/src/orchestration/Layers/RuntimeReceiptBus.ts
-[drivers]: ../../apps/server/src/provider/builtInDrivers.ts
````

## docs/internals/product-analytics.md

Status: modified; +37; −165

```diff
@@ -1,177 +1,49 @@
 # Product analytics
-
-T3 Code sends anonymous product events from the server to PostHog. The server
-uses the first available hashed Codex account ID, hashed Claude user ID, or
-installation-scoped anonymous ID as the distinct ID. It also keeps the
-telemetry opt-out, event buffer, and batch delivery. Clients do not load the
-PostHog browser SDK.
-
-## Client events
-
-These events use the metadata from the WebSocket connection that caused them.
-The metadata is not a person property or server-global current-client value.
-Two clients connected to one server can report different values at the same
-time.
-
-| Event                   | Description                                                                                                                                          |
-| ----------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------- |
-| `client.connected`      | The server accepted an authenticated WebSocket connection. Reconnects count again. Use this event for connection diagnostics, not active-use counts. |
-| `client.thread.started` | The server accepted a command that created a thread.                                                                                                 |
-| `client.turn.requested` | The server accepted a turn request. This is the standard active-use event.                                                                           |
-
-`provider.turn.sent` stays a provider execution event. It does not receive
-client metadata because a provider turn can continue after the requesting
-client disconnects.
-
-## Provider turn events
-
-`provider.turn.completed` records one anonymous event when a provider emits a
-canonical completed or aborted turn. The event reports normalized main-agent
-token counts when the provider supplies enough data.
-
-| Property              | Values and meaning                                                                                                              |
-| --------------------- | ------------------------------------------------------------------------------------------------------------------------------- |
-| `provider`            | Provider driver, such as `codex`, `claudeAgent`, or `opencode`. Use this as the harness breakdown.                              |
-| `model`               | Selected or provider-reported model when known. This is metadata, not proof that one model produced every token.                |
-| `effort`              | Selected or provider-reported reasoning effort when known.                                                                      |
-| `interactionMode`     | Turn interaction mode, `default` or `plan`, when known.                                                                         |
-| `runtimeMode`         | Session permission mode when known.                                                                                             |
-| `mixedModels`         | `true` when T3 Code observed `model.rerouted` during the turn. This does not split token counts by model.                       |
-| `durationMs`          | Time from the observed turn start or accepted send request to the terminal event. Omitted when start metadata is not available. |
-| `terminalStatus`      | `completed`, `failed`, `interrupted`, or `cancelled`.                                                                           |
-| `usageStatus`         | `complete`, `partial`, or `unavailable`.                                                                                        |
-| `usageScope`          | `main_agent`. Child-agent token use is not included.                                                                            |
-| `hasSubagents`        | Whether the adapter observed child agents during the turn. Omitted when the adapter does not provide a normalized usage report. |
-| `inputTokens`         | Total main-agent input tokens. This includes uncached input, cache reads, and cache creation or writes.                         |
-| `cachedInputTokens`   | Cache-read tokens. This is a subset of `inputTokens`.                                                                           |
-| `cacheCreationTokens` | Cache creation or write tokens. This is a subset of `inputTokens`.                                                              |
-| `outputTokens`        | Total main-agent output tokens, including reasoning tokens.                                                                     |
-| `reasoningTokens`     | Reasoning tokens when the provider reports them separately. This is a subset of `outputTokens`.                                 |
-
-`complete` means the provider supplied full input and output totals for the
-turn. `partial` means each included count is valid, but T3 Code did not observe
-the full turn. This can happen after an interruption, provider failure, or
-reconnect. `unavailable` means the provider did not supply trustworthy counts.
-Numeric properties are omitted when their values are unknown. They are not set
-to zero.
-
-Codex uses deltas from its cumulative token totals, and falls back to the
-newest response's `last` usage when no prior total exists or the total was
-reset. Claude uses the final result's per-turn `usage` value and does not use
-cumulative `modelUsage`.
-OpenCode sums unique main-session step totals. Cursor, Grok, and Antigravity
-usage stays `unavailable` until their ACP token fields and scope are verified.
-
-The completed event count does not have to match `provider.turn.sent`. For
-example, Claude can emit synthetic turns without a matching send request.
-Duplicate terminal events for the same provider instance, thread, and turn
-produce one analytics event.
-
-Collection is best effort and starts with the first server release that
-contains this event. T3 Code does not scan provider history or backfill older
-turns.
-
-## Recommended properties
-
-Client properties appear on the three client events when the connected client
-reports them. Older clients can omit every client property.
-
-| Property               | Values and meaning                                                                                                                                                                              |
-| ---------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
-| `surface`              | Product client: `web`, `desktop`, or `mobile`.                                                                                                                                                  |
-| `webDeployment`        | Web delivery: `hosted` for the hosted app or `server` for web files served by a T3 server. Web only. This does not describe connection distance.                                                |
-| `clientOs`             | `macOS`, `Windows`, `Linux`, `iOS`, `Android`, `ChromeOS`, `other`, or `unknown`.                                                                                                               |
-| `clientDeviceType`     | `desktop`, `phone`, `tablet`, or `unknown`. This is separate from `surface`.                                                                                                                    |
-| `clientBrowser`        | Normalized browser family. Web only. Browser detection is best effort.                                                                                                                          |
-| `clientAppVersion`     | Version of the connected client.                                                                                                                                                                |
-| `clientOsMajorVersion` | Client OS major version when the native client reports it. Initially mobile only.                                                                                                               |
-| `clientDeviceModel`    | Hardware model when the native client reports it. Initially mobile only. This is not a user-assigned device name.                                                                               |
-| `connectionMethod`     | `direct`, `ssh`, `relay`, or `unknown`. `direct` means that the client connected to the server endpoint without an SSH or relay connection. It does not mean both processes run on one machine. |
-
-Server properties appear on all events, including server boot and background
-events.
-
-| Property           | Values and meaning                                             |
-| ------------------ | -------------------------------------------------------------- |
-| `serverOs`         | Server process OS, normalized to the same names as `clientOs`. |
-| `serverArch`       | Server process architecture.                                   |
-| `serverWslDistro`  | WSL distribution from `WSL_DISTRO_NAME`, when present.         |
-| `serverAppVersion` | T3 server version.                                             |
-| `serverMode`       | Server runtime mode: `desktop` or `web`.                       |
-
-## Legacy properties
 
+The server owns PostHog delivery, opt-out, and identity for every connected client.
+[Identity selection](../../apps/server/src/telemetry/Identify.ts) hashes an available
+provider account ID, falling back to an installation-scoped ID. This identity can
+span several clients; it does not identify a browser session. Clients do not load
+the PostHog browser SDK. Client-use events require an authenticated connection, so
+visiting the hosted app without connecting does not count as product use.
-Existing property meanings do not change:
 
-- `clientType` describes how the server runs. It is `desktop-app` for a desktop
-  server and `cli-web-client` for a CLI web server. It does not describe the
-  connected client. Use `surface` and `webDeployment` for new reports.
-- `platform`, `arch`, `wsl`, and `t3CodeVersion` describe the server. Use the
-  new `server*` names for new reports.
-- `appVersion` describes the connected client. Use `clientAppVersion` for new
-  reports.
-- Mobile connection events keep `os`, `osMajorVersion`, and `deviceModel`.
-  Use the new `client*` names for new reports.
+## Attribution boundaries
 
+Client dimensions belong to the event's WebSocket connection. A server-global
+"current client" would misattribute simultaneous web, desktop, and mobile use.
+Provider execution has its own events because a turn can outlive the requesting
-## PostHog dashboard
+connection.
 
+Keep client and server dimensions separate. A desktop host can serve a phone or a
+remote browser, and a direct connection can cross a network. Older clients omit
+metadata. Missing client values must stay unknown rather than being backfilled
+from server properties. The legacy `clientType` property describes how the server
-Create one saved dashboard named `Client and platform usage`. Set
-`client.turn.requested` as the event for active-use reports. A user can appear
-in several client groups during one period, so do not add breakdown values to
+runs; use `surface` for the connected client.
-calculate a total.
 
+## Interpreting events
-Save these insights:
 
-| Insight                         | Configuration                                                                                                                                                                                                                                                                           |
-| ------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
-| Active users, daily             | Trends, `client.turn.requested`, unique users, daily interval.                                                                                                                                                                                                                          |
+Use `client.turn.requested` for active-use reports. `client.connected` counts
+reconnects, so network behavior can inflate it. One identity can appear in several
+client groups during a period; adding those groups double-counts users.
-| Active users, weekly            | Trends, `client.turn.requested`, unique users, weekly interval.                                                                                                                                                                                                                         |
-| Active users, monthly           | Trends, `client.turn.requested`, unique users, monthly interval.                                                                                                                                                                                                                        |
-| Client usage                    | Trends, `client.turn.requested`, unique users. Save four filtered series: `surface = desktop`, `surface = mobile`, `surface = web` and `webDeployment = hosted`, and `surface = web` and `webDeployment = server`. Name them Desktop, Native mobile, Hosted web, and Server-served web. |
-| Client OS, active users         | Trends, `client.turn.requested`, unique users, breakdown by `clientOs`.                                                                                                                                                                                                                 |
-| Client OS, turns                | Trends, `client.turn.requested`, total events, breakdown by `clientOs`.                                                                                                                                                                                                                 |
-| Client versus server OS         | Table, `client.turn.requested`, breakdown by `clientOs` and `serverOs`.                                                                                                                                                                                                                 |
-| Connection method, active users | Trends, `client.turn.requested`, unique users, breakdown by `connectionMethod`.                                                                                                                                                                                                         |
-| Connection method, turns        | Trends, `client.turn.requested`, total events, breakdown by `connectionMethod`.                                                                                                                                                                                                         |
-| Mobile devices                  | Table, `client.turn.requested`, filter `surface = mobile`, breakdown by `clientOs`, `clientOsMajorVersion`, and `clientDeviceType`.                                                                                                                                                     |
-| Client version adoption         | Trends, `client.turn.requested`, unique users, breakdown by `clientAppVersion`.                                                                                                                                                                                                         |
-| Server version adoption         | Trends, `client.turn.requested`, unique users, breakdown by `serverAppVersion`.                                                                                                                                                                                                         |
-| Missing metadata                | Table or SQL insight that shows the percentage of `client.turn.requested` events where each of `surface`, `clientOs`, `clientDeviceType`, `clientAppVersion`, and `connectionMethod` is absent. Track `webDeployment` and `clientBrowser` only within `surface = web`.                  |
 
-In PostHog Data management, use the event and property descriptions from this
+Provider send and completion counts need not match. Providers can emit synthetic
+turns without a send request. Collection is best effort, with no scan or backfill
+of provider history.
-document. Mark the recommended properties as verified. Keep `clientType`
-visible with its legacy description so old reports remain understandable.
 
+Token totals cover the main agent. Child agents and model rerouting prevent a turn
-Create a second saved dashboard named `Provider token efficiency`. Start each
-token insight with these filters:
-
+from representing one provider/model combination's full cost. For provider
+comparisons, require complete usage, no observed subagents, and no mixed models;
+compare matching model, effort, interaction mode, and terminal status. Aggregate
-- `usageStatus = complete`
-- `hasSubagents = false`
-- `mixedModels = false`
-
-Break down input tokens, output tokens, and their ratio by `provider`. Compare
-the same `model`, `effort`, `interactionMode`, and `terminalStatus`. Without
-these filters, model choice, reasoning level, plan mode, failures, and child
-agents can look like harness differences. Keep separate views for median turn
-usage and aggregate `sum(outputTokens) / sum(inputTokens)`. Do not average each
-turn's ratio because small-input turns can dominate that result.
-
-## Collection and release boundary
+output/input ratios should divide the summed totals. Averaging per-turn ratios
+lets small-input turns dominate.
 
+Unknown counts stay absent. Partial usage contains valid observed counts but
+cannot establish a whole-turn total. Keep these distinctions when changing token
-Client values are best effort. Invalid values are ignored and never reject a
+normalization or building reports.
-connection. Browser clients use user-agent data for broad OS, browser, phone,
-and tablet groups. They do not infer CPU architecture or an exact OS version
-from `navigator.platform`.
 
-This change does not collect URLs, authentication tokens, prompts, IP
-addresses, or user-assigned device names. The token event does not include
+## Collection boundary
-thread IDs, turn IDs, provider instance IDs, event IDs, request IDs, raw
-provider payloads, or child-agent output. It does not scan provider history on
-disk or start external work. It measures authenticated product use. It does
-not measure a person who visits the hosted app without connecting to a server.
 
-The new fields start with the first client and server release that contains
-this metadata path. Historical events cannot reliably identify the client OS,
-hosted web use, device type, or connection method when the old client did not
-send those fields. Reports must treat missing values as pre-release or older
+Keep analytics payloads to product metadata and normalized measurements. Do not
-client data instead of backfilling them from server fields.
+send prompts, authentication material, raw provider payloads, user-assigned device
+names, or conversation identifiers. Client metadata is best effort; invalid values
+must not reject a connection. PostHog person profiles remain disabled.
```

## docs/internals/providers.md

Status: modified; +68; −375

```diff
@@ -1,395 +1,88 @@
-# Provider architecture
-
-> For maintainers. Using T3 Code? See [docs/user](../user/).
-
-A provider is the agent runtime that does the actual work. T3 Code supports several, and the
-orchestration layer does not know which one is behind a thread.
-
-## Built-in drivers
-
-[`builtInDrivers.ts`][drivers] exports `BUILT_IN_DRIVERS` with six entries:
-
-| Driver kind   | Driver source                                 |
-| ------------- | --------------------------------------------- |
-| `codex`       | [`Drivers/CodexDriver.ts`][codex]             |
-| `claudeAgent` | [`Drivers/ClaudeDriver.ts`][claude]           |
-| `cursor`      | [`Drivers/CursorDriver.ts`][cursor]           |
-| `grok`        | [`Drivers/GrokDriver.ts`][grok]               |
+# Provider constraints
-| `opencode`    | [`Drivers/OpenCodeDriver.ts`][opencode]       |
-| `antigravity` | [`Drivers/AntigravityDriver.ts`][antigravity] |
 
-Each driver declares its `driverKind`, a `configSchema`, and a `create` function that builds an
-adapter in a child scope. Adapter implementations live beside them in
+Orchestration records intent and state without knowing which provider runs a thread. Provider
+protocols, account ownership, permissions, and capabilities belong at the
+[adapter boundary](../../apps/server/src/provider/Services/ProviderAdapter.ts). Normalize there
-`apps/server/src/provider/Layers/` (`CodexAdapter.ts`, `ClaudeAdapter.ts`, and so on) and conform to
-[`ProviderAdapter.ts`][adapter]. Read the driver plus its adapter to see how a specific agent's
-transport, config, and event shapes are mapped.
+instead of spreading provider checks through reactors and clients.
 
+A driver kind identifies an integration; an instance identifies one configuration and account
+lifecycle. Route work by instance, so two accounts using the same driver do not share mutable
+session or catalog state.
-## Runtime context
 
-Every adapter uses `apps/server/src/provider/RuntimeInstructions.ts` to identify T3 Code and
-the harness, and describe Markdown image/video embeds. Codex includes it in developer
-instructions; Claude appends it to its system preset; OpenCode sends it in each prompt's
-`system` field. Cursor, Grok, and Antigravity append a separate text block to ACP prompts,
-which have no system-message field. This does not change the stored user message.
+## Process and account isolation
 
-Per-turn context includes the current model when known. Codex includes reasoning effort;
-Grok includes it when explicitly selected for the turn. Claude's session-level context omits model and effort because they can
-change during a session. OpenCode variants are not assumed to be reasoning-effort levels.
+T3-managed OpenCode chat uses one server per thread. Its MCP registrations are directory-scoped, while
+T3's MCP connection is thread-scoped. Sharing a chat server between threads in one directory would
+let them replace each other's connection. Catalog and text-generation work can share the
+[instance-owned helper](../../apps/server/src/provider/OpenCodeServerOwner.ts), which closes
+after an idle period. External OpenCode servers remain externally owned and can require an
+external restart to pick up configuration changes.
 
-## Codex async questions
+OpenCode also stores persistent approval grants per directory. Automatic full-access replies use
+`once` so they cannot widen a supervised thread's permissions on a shared external server.
+See the [adapter](../../apps/server/src/provider/Layers/OpenCodeAdapter.ts).
 
-Codex 0.153 exposes `request_user_input_async` through `item/started` and `item/completed`
-notifications. The item has `type: "agentMessage"`, `delivery: "async"`, and a `questions` array.
-Each question has a `title` and an optional `options` array of strings. The tool returns `{"accepted":true}`
-without waiting. This is separate from the `item/tool/requestUserInput` server request.
-See the [Codex tool handler](https://github.com/openai/codex/blob/d979df154cf60e13eafb5453e75b6d84f21c67bf/codex-rs/core/src/tools/handlers/request_user_input_async.rs).
-
-The Codex adapter maps completed question items to `user-input.requested` with
-`responseMode: "message"` and stable request and event IDs. Questions use the existing web,
-desktop, and mobile panels. They stay pending while the turn runs and after it finishes.
-
-The engine reads the request's latest stored activity before deciding a reply. This works after
-startup, when the command snapshot has no activities, and after a resolution leaves the recent
-activity window. The query returns one activity, not the full thread history.
-
-For these requests, the decider saves the resolution and a user message in one transaction.
-The standard turn path delivers the message, including session resume and active-turn input.
-It does not send a JSON-RPC response to Codex. Other providers and blocking Codex questions
-keep their existing response paths.
-
-## Registry and routing
-
-Two registries separate configuration from live processes:
-
-- [`ProviderInstanceRegistry`][instances] keys configured instances by `ProviderInstanceId`. Creating
-  one looks up the driver by `driverKind`, decodes `entry.config` with that driver's schema, opens a
-  child scope, and calls `driver.create`.
-- [`ProviderAdapterRegistry`][registry] resolves an instance ID to its live adapter via
-  `getByInstance`.
-
-[`ProviderService`][service] sits on top. It combines the adapter registry with the provider session
-directory to route session and turn operations for a thread, so callers name a thread, not an agent.
-
-`ProviderService.sendTurn` expands [assistant citations](./assistant-citations.md) into quoted
-reference data before dispatching to any adapter. Bound user comments remain distinct from the quoted
-assistant text. Persisted messages keep their serialized links.
-
-Adding a driver means writing the driver plus adapter and adding it to `BUILT_IN_DRIVERS`. No
-orchestration, contract, or client change is required for the common case.
-
-### Grok health check
-
-`checkGrokProviderStatus` never opens an ACP session. It runs `grok --version`, then `grok models`
-for login state and model slugs, then a single ACP `initialize` and reads models from
-`_meta.modelState`. `authenticate` and `session/new` are skipped on purpose: `authenticate` can open
-a browser login and `session/new` boots every configured MCP server, both of which made background
-probes hang or surprise the user. A failed `initialize` degrades to `warning` with the CLI's model
-list instead of persisting `error` over a working install. The built-in `grok-build` slug is the
-CLI's product name, not an ACP model id. `applyGrokAcpModelSelection` treats it as "keep the
-session's current model" and never sends it in `session/set_model`.
-
-## Antigravity ownership and protocol
-
-[`AntigravityDriver`][antigravity] uses Google's official ACP executable. The instance config
-selects the ACP auth method: `oauth-personal` (default), `oauth-business`, `gemini-api-key`, or
-`agent-platform`. The two OAuth methods share the loopback sign-in flow below. The API key
-methods pass the configured key to the agent as `GEMINI_API_KEY` or `GOOGLE_API_KEY` and never
-open a browser. A GCP project and location are written to the profile's `settings.json` on each
-launch. The driver never reuses CLI credentials or ambient `GOOGLE_*` variables and never falls
-back to another method. Antigravity is disabled by default and supports multiple provider
+Antigravity separates account profiles per instance while sharing installed executables across the
+environment. It forces file-based credential storage because the native macOS keychain entry would
+otherwise be shared across instances. The launch environment removes ambient Google credentials,
+so an instance cannot silently use another account or billing project.
-instances. The open driver and instance identifiers require no database migration.
+See [profile isolation](../../apps/server/src/provider/antigravityAuthSupport.ts).
 
-### Runtime installation
+The [Antigravity installer](../../apps/server/src/provider/AntigravityInstallation.ts) outlives
+client connections and provider-instance rebuilds. Releases are immutable, with an atomic pointer
+selecting the version for new processes. Running processes hold leases on their version. Updates
+and removal must respect those leases instead of replacing executables under a running agent.
 
-[`AntigravityInstallation`][antigravity-installation] belongs to the environment, outside
-WebSocket and provider-instance scopes. Instances share an explicit download operation and the
-completed runtime. Client disconnects and instance rebuilds do not cancel installation.
+## Setup must not happen as a health-check side effect
 
-The fixed [release table][antigravity-release] contains official Google URLs, SHA-256 hashes,
-archive sizes, and the exact executable pair for each published host. Downloads stream to disk.
+Opening a provider session can start MCP servers, run hooks, or launch a login browser.
-Lazy `yauzl` entry streams extract only that pair, with member names, types, duplicates, and
-sizes checked. Validation runs ACP `initialize` in a temporary profile without authentication
-or a session. Progress updates are bounded, not sent for every network chunk.
+[Grok probes](../../apps/server/src/provider/Layers/GrokProvider.ts) avoid authentication and
+session creation for this reason. Antigravity likewise reserves authenticated catalog sessions for
+explicit setup or model refresh; background checks use initialization only.
 
-Complete releases live in immutable version directories under the T3 home
-`tools/antigravity-acp/<platform>-<arch>/versions`. An atomic `active.json` change selects the
-release for new processes. Each process holds a version lease until it exits. Updates do not
-replace running executables. Removal refuses active leases or explicit binary paths that still
+[Antigravity sign-in](../../apps/server/src/provider/AntigravityAuth.ts) belongs to the initiating
+T3 auth session. The client carries the return URL back to the environment because the provider's
+loopback listener may be on another machine. Forward only the callback for the owned pending flow;
+a successful callback HTTP request is not proof that provider authentication finished. The native
-reference the managed files. Failure or cancellation removes owned partial files, not the
+process owns token exchange and storage.
-previous release or account data.
 
-Resolution order is explicit `binaryPath`, active managed release, then the instance's `PATH`.
+Antigravity sign-out closes admission to new processes and stops existing processes before clearing account
+metadata. Otherwise a helper or resumed session could retain the old account. Cached model lists
+do not establish current access, and an authoritative empty catalog must clear the old list.
-An invalid explicit path fails without fallback. Manual installations are never changed by the
-installer. Every launch pins `ANTIGRAVITY_HARNESS_PATH` to the selected executable's sibling.
 
-### Google profiles and sign-in
+Antigravity text-generation helpers deny tool requests, but native hooks and MCP configuration can
+run before the prompt. They reject profiles with such configuration before launch. Prompt
+instructions and tool denial do not create a native sandbox.
+See [helper constraints](../../apps/server/src/textGeneration/AntigravityTextGeneration.ts).
 
-Each instance owns a stable profile at
-`<stateDir>/providers/antigravity/<sha256(instanceId)>`.
-[`antigravityAuthSupport.ts`][antigravity-auth-support] sets `GEMINI_HOME` to this directory and
-`AGY_ACP_FORCE_FILE_STORAGE=1` after merging instance environment variables. File storage avoids
-the official macOS keychain entry being shared across instances. Profile directories use mode
-`0700` on POSIX. This is file storage, not an encrypted keychain. Windows uses the host profile's
+## Protocol traps
-filesystem permissions.
 
+Codex async questions arrive as notifications and are answered with a new user message. There is
+no pending RPC response to send. Blocking questions still use the request/response path. The
-The launch environment removes API-key and cloud-billing variables, disables inherited
+[adapter](../../apps/server/src/provider/Layers/CodexAdapter.ts) distinguishes them; the
+[decider](../../apps/server/src/orchestration/decider.ts) records an async answer and its user
+message together.
-environment extension, sets `PYTHONUNBUFFERED=1`, and controls `BROWSER`. A tested Node or
-Electron-as-Node helper prevents the official agent from opening a browser on the environment.
-The same launch factory serves setup, health checks, chat, and text generation.
 
-The official agent prints a non-JSON OAuth line on stderr in version 1.1.1. Earlier versions
-print it on stdout. T3 accepts the exact native prefix on either stream and its browser-helper
-marker on stderr. Fragmented lines are joined and bounded. Other malformed protocol output
-remains fatal. Authorization URLs are validated before use. Other stderr is discarded because
-it can contain OAuth data. Normal work rejects an interactive login request with a
-sign-in-required error instead of waiting for consent. A rejected stderr callback fails pending
-ACP requests and closes the owned process.
+An async question can outlive the turn or a server restart. The engine reads that request's
+durable activity before resolving it because the in-memory command snapshot omits old activities.
+Do not infer that a request has disappeared merely because it is outside the recent window.
 
-[`AntigravityAuth`][antigravity-auth] owns each sign-in process and deadline in the instance
-scope. Only the initiating T3 auth session receives its URL and flow ID or can complete or
-cancel it. Other clients receive busy state without those values. Subscriptions follow
-controller replacement when settings rebuild an instance.
+Capabilities must describe what the provider can actually do. Antigravity can capture workspace
+checkpoints but cannot roll back its conversation. The [checkpoint boundary](./overview.md#turn-completion-and-checkpoints)
+therefore rejects revert before touching files. Native permission and question option IDs must
+also survive normalization; a display label is not necessarily a valid reply.
 
-For remote completion, the client sends the full return URL through the typed setup RPC.
+## Attachments and stored history
-The server validates the pending loopback origin, port, root path, and single matching state
-before forwarding once to the owned listener. It does not probe the listener or follow
-redirects. Google's process owns PKCE, token exchange, refresh, and storage. Callback HTTP
-success is not authentication success. The controller waits for authenticated session setup
-and catalog discovery. Cancellation closes the process instead of sending a synthetic denial.
 
-Auth RPCs `provider.auth.start`, `complete`, `cancel`, `logout`, and `subscribe` require
-`orchestration:operate`. Install `start`, `cancel`, and `remove` use that scope too.
-`provider.install.subscribe` and public provider snapshots require `orchestration:read`.
+Attachments live outside the project workspace. [ProviderService](../../apps/server/src/provider/Layers/ProviderService.ts)
+puts their environment-local paths in turn input and lets adapters choose native input formats.
+A path in the prompt does not grant filesystem access. Keep provider sandbox and approval rules
+in force; copying uploads into the project to bypass them changes that boundary.
-[`providerSetup.ts`][provider-setup] defines the operation IDs, states, and safe errors.
 
+File attachments introduced a replay compatibility limit. Image-only clients cannot decode
+file-bearing messages, and an image-only server can fail the entire environment's startup when
-Sign-out closes process admission for the instance, stops provider bindings through
-[`ProviderAuthService`][provider-auth-service], then stops owned startup and helper processes.
+replaying one such event. Rollouts and downgrades must account for persisted history as well as
+current client support.
-A fresh official process calls `initialize` and native `logout` without authenticating.
-Only then does the provider clear auth, models, commands, skills, and workspace metadata.
-Thread history and native session files remain. Settings sign-out and a text-only `/logout`
-use this same path. The command is handled before model prompting or title generation.
-Disabling an instance closes its processes but keeps credentials. Account replacement is
-explicit sign-out followed by sign-in.
 
+Model classification has its own [manifest constraints](./model-manifest.md). Assistant-reference
+handling is documented under [citations](./assistant-citations.md).
-### Sessions, models, and client capabilities
-
-[`AntigravityAdapter`][antigravity-adapter] owns one ACP process per active thread. It uses
-native `session/resume` without transcript replay and reapplies the persisted model and
-permission mode after new or resumed setup. An unavailable explicit model fails instead of
-accepting the native default. Steering cancels the previous prompt, waits for its result and
-event drain, then sends the replacement. Native background commands use T3's existing
-background-task state.
-
-The permission mapping is `approval-required` and `auto` to `default`, `auto-accept-edits` to
-`auto_edit`, and `full-access` to `yolo`. Native requests still need replies in `yolo`.
-`interaction_` requests are user questions, not approvals. T3 keeps opaque option IDs in
-`UserInputQuestion.options[].value` and sets `allowCustomAnswer=false`. Both clients preserve
-these values. Ordinary approval replies use only offered option IDs, including `allow_always`
-only when present. Existing providers keep their prior behavior when the optional fields are
-absent.
-
-`showInteractionModeToggle=false` keeps native `/plan` separate from T3 Plan mode.
-`supportsConversationRollback=false` hides unsupported client actions and makes checkpoint
-revert fail before filesystem changes. Checkpoint capture and diffs remain supported.
-
-Automatic status refreshes, reconnects, and workspace checks do not open catalog sessions.
-Health probes use `initialize` only. Disabled instances do not run background probes.
-An explicit `serverRefreshProviders` request with `refreshModels: true` calls the driver's
-optional `refreshModels` operation. Antigravity opens a short-lived catalog session under the
-instance's process admission guard, uses saved credentials, publishes models and commands,
-then closes the process. An interactive login request fails with sign-in required. Web's
-**Refresh provider status** and mobile's **Refresh models** actions request this operation.
-Account access starts unknown and becomes authenticated after successful session setup,
-including an explicit model refresh.
-The [provider snapshot][antigravity-provider] takes models and commands from setup and native
-updates. It preserves returned Gemini model IDs, labels, order, and thinking-level choices.
-ACP `config_option_update` notifications and `session/set_config_option` responses replace
-the instance's model catalog. Child session notifications do not change the root catalog.
-The registry treats a successful empty catalog as authoritative and clears cached metadata
-after sign-out. It must not retain a previous account's models. Cached models do not prove
-current access. The auth response does not supply an email, plan tier, or reliable quota.
-
-Some upstream failures arrive as assistant text followed by `end_turn`. Preserve that message
-without treating model-written text as a structured error or successful task completion.
-
-### Text generation
-
-[`AntigravityTextGeneration`][antigravity-text] implements titles, branch names, commit text,
-and PR text through the same instance and Google sign-in. Each helper uses a temporary empty
-workspace, no injected MCP servers, native `default` mode, and explicit denial of tools and
-questions. Output is bounded, parsed against the existing schemas, and sanitized. Cancellation,
-timeout, and sign-out close the process. Cleanup removes only that helper's verified temporary
-native session files.
-
-The official agent has no verified hard no-tools setting. Global hooks and MCP configuration
-can run before a prompt. Helpers check the profile's `config/hooks.json` and
-`config/mcp_config.json` before launch and reject nonempty, malformed, or oversized
-configuration. `supportsTextGeneration=false` keeps such an instance out of system-model
-pickers. Empty managed profiles are supported. Do not describe prompt-time denial as a native
-sandbox.
-
-## OpenCode server ownership and catalog
-
-Native OpenCode event logs skip text deltas and running tool snapshots. Pending, completed,
-and failed tool states remain, including final output and errors. This filter does not change
-events sent to clients or orchestration storage.
-
-Each OpenCode provider instance owns one lazy local server for catalog discovery and
-text-generation helpers through [`OpenCodeServerOwner.ts`][opencode-server-owner]. Concurrent
-borrowers share startup. The server closes 30 seconds after the last borrower releases it, or
-when the provider instance closes. A failed or exited process can be started again on the next
-use. An externally configured OpenCode server remains externally owned.
-
-The local server and its SDK clients use one resolved password. An explicit provider password
-overrides `OPENCODE_SERVER_PASSWORD` in the spawned environment. Without an explicit password,
-the client uses the password from the environment that the process inherits. External servers use
-only their explicit provider password and never inherit the host's local password.
-
-Every server connection must pass the authenticated `/global/health` check before inventory or
-session operations start. The response must contain a valid version at or above 1.14.19. Local
-owners cache this result for the lifetime of the spawned process. External actions check once when
-they create their server connection, not for each model or SDK request.
-
-Chat adapters keep their own server per thread. They register a thread-specific `t3-code` MCP
-connection, while OpenCode stores MCP connections by directory. Sharing these chat servers
-without changing MCP routing would let two threads in one directory replace each other's
-connection.
-
-Chat adapters send the runtime mode as a session ruleset, but upstream OpenCode evaluates
-doom-loop and subagent asks against the agent ruleset only. In full access the adapter answers
-those asks itself so the user never sees an approval they already granted. It replies `once`
-rather than `always` because OpenCode stores `always` grants per directory, and on a shared
-external server that would widen what a supervised thread in the same directory may do.
-
-OpenCode loads its catalog through the HTTP API when an enabled provider instance starts. The
-provider registry keeps the snapshot in memory and persists it in the existing per-instance cache.
-Each `subscribeServerConfig` connection refreshes all providers, so a client reconnect reloads the
-OpenCode catalog from the current helper. The `serverRefreshProviders` request also refreshes it.
-Periodic OpenCode probes remain disabled. OpenCode reads credentials for each inventory request,
-but its native configuration files can remain cached for the lifetime of the helper process. The
-helper closes 30 seconds after its last inventory or text-generation borrower releases it. A
-refresh after that idle period starts a new helper and reads file changes. Repeated refreshes and
-active text-generation work can extend process reuse. Changes to the provider configuration or
-environment replace the instance and start a new discovery. Changes to unrelated settings only
-update snapshot enrichment. Other providers retain their existing refresh policy.
-
-T3 Code does not own an external OpenCode process. Native configuration changes there can require
-an external reload or restart before T3 Code's next refresh sees them.
-
-The shared server's idle shutdown does not clear the catalog. Failed discovery keeps the last
-known models, slash commands, and skills through the registry's existing merge rules. A successful
-empty inventory is authoritative. Existing threads keep their explicit model identifier and
-options when catalog metadata is missing; the catalog is not permission to choose a different
-model for a thread.
-
-## Model manifest
-
-The model picker's legacy section is driven by `apps/server/src/provider/model-manifest.json`, which
-lists the current (non-legacy) model slugs per driver kind. The `ModelManifest` service
-(`apps/server/src/provider/ModelManifest.ts`) refreshes that data from the same file on `main` via
-raw.githubusercontent.com, so moving a model in or out of the legacy section is a commit, not a
-release. Preference order is remote fetch, then the on-disk copy of the last successful fetch (in
-the state directory), then the bundled copy. Fetches are TTL-gated, run concurrently with provider
-probes, respect the `enableProviderUpdateChecks` setting, and never fail a provider check. The
-Codex and Claude drivers apply the classification to every snapshot with `applyModelManifest`;
-driver kinds absent from the manifest have no legacy concept.
-
-## Attachment access
-
-The server stores uploaded attachments in its attachment directory, outside the project workspace.
-`ProviderService` adds the absolute path of each attachment to the turn text, then passes every
-attachment to the provider adapter. Each adapter decides what its provider ingests natively:
-
-- Codex, Claude, Cursor, and Grok send images as native image inputs and skip generic files. For
-  these providers, generic files reach the agent only as file paths in the turn text.
-- Antigravity sends BMP, JPEG, PNG, and WebP images and common audio formats as native blocks,
-  text files as embedded resources, and PDFs as resource links. Other files are rejected with an
-  error instead of being dropped. The session advertises the ACP client file system capability,
-  so workspace reads and writes come back through `fs/read_text_file` and `fs/write_text_file`
-  and are confined to the workspace and the attachments directory.
-- OpenCode sends PNG/JPEG/GIF/WebP images, text files, and PDFs up to 20 MB as native file parts
-  with their real mime type. Everything else (ZIP and other binaries, image formats model APIs
-  reject, oversized files) falls back to the file path in the turn text, like the other providers.
-- Antigravity sends BMP/JPEG/PNG/WebP images as native image blocks, UTF-8 text as embedded
-  resources, and PDFs as local resource links. Text is limited to 1 MiB per file, images to
-  10 MiB each, and all attachments to 50 MiB per turn. Unsupported formats or oversized inputs
-  fail explicitly. Native path permissions still apply to PDFs.
-
-Claude receives the attachment directory as an allowed additional directory. Codex keeps its
-configured sandbox policy, so access depends on that policy and the selected runtime mode. OpenCode
-allows all paths in full-access mode and requests approval for directories outside the workspace in
-restricted modes. Cursor and Grok use their own provider permission rules.
-
-The server does not copy attachments into a project or bypass provider approval rules. If an agent
-cannot read an attachment, the user must approve the access or select a runtime mode that permits it.
-
-Updated attachment schemas tolerate unknown attachment members, but old image-only clients still
-cannot decode messages that contain file attachments. Client file-picking rollouts must account for
-this limit.
-
-Do not run an old image-only server against state that contains file attachments. Replay decodes
-each persisted event before projection. A file-bearing event can make `ProjectionPipeline` bootstrap
-and `OrchestrationEngine` startup fail for the entire environment, not only the affected thread.
-
-## How provider work is requested
-
-Clients never call a provider directly. They dispatch orchestration commands over the RPC method
-`orchestration.dispatchCommand`, defined with the rest of the orchestration surface in
-[`orchestration.ts`][contracts]. The client-dispatchable provider-facing commands are
-`thread.turn.start`, `thread.turn.interrupt`, `thread.approval.respond`,
-`thread.user-input.respond`, `thread.checkpoint.revert`, and `thread.session.stop`, plus the mode
-setters `thread.runtime-mode.set` and `thread.interaction-mode.set`.
-
-The engine persists an event for the command, and a server-side reactor performs the provider call.
-Provider output comes back as internal commands such as `thread.message.assistant.delta` and
-`thread.session.set`, which clients observe through `orchestration.subscribeThread`. See
-[overview.md](./overview.md) for the command/event loop.
-
-## Server-side workers
-
-Provider work flows through three queue-backed workers. All three are built with
-`makeDrainableWorker` from [`DrainableWorker.ts`][worker] and expose `drain` for deterministic test
-synchronization.
-
-1. [`ProviderRuntimeIngestion`][ingest] consumes provider runtime streams and emits orchestration
-   commands.
-2. [`ProviderCommandReactor`][cmd] reacts to orchestration intent events and dispatches provider
-   calls.
-3. [`CheckpointReactor`][checkpoint] captures workspace checkpoints on turn start and completion, and
-   performs reverts.
-
-### Buffered assistant delivery
-
-A thread in `buffered` assistant delivery mode accumulates assistant text instead of streaming each
-delta. The buffer is not held until turn completion. In [`ProviderRuntimeIngestion`][ingest],
-`MAX_BUFFERED_ASSISTANT_CHARS` is 24,000: the append that would exceed it invalidates the buffer and
-spills the whole accumulated text as one delta. The buffer also flushes at interaction boundaries,
-when a request opens (approval) or user input is requested, via
-`flushBufferedAssistantMessagesForTurn`.
-
-[drivers]: ../../apps/server/src/provider/builtInDrivers.ts
-[codex]: ../../apps/server/src/provider/Drivers/CodexDriver.ts
-[claude]: ../../apps/server/src/provider/Drivers/ClaudeDriver.ts
-[cursor]: ../../apps/server/src/provider/Drivers/CursorDriver.ts
-[grok]: ../../apps/server/src/provider/Drivers/GrokDriver.ts
-[opencode]: ../../apps/server/src/provider/Drivers/OpenCodeDriver.ts
-[antigravity]: ../../apps/server/src/provider/Drivers/AntigravityDriver.ts
-[antigravity-adapter]: ../../apps/server/src/provider/Layers/AntigravityAdapter.ts
-[antigravity-provider]: ../../apps/server/src/provider/Layers/AntigravityProvider.ts
-[antigravity-installation]: ../../apps/server/src/provider/AntigravityInstallation.ts
-[antigravity-release]: ../../apps/server/src/provider/antigravityRelease.ts
-[antigravity-auth]: ../../apps/server/src/provider/AntigravityAuth.ts
-[antigravity-auth-support]: ../../apps/server/src/provider/antigravityAuthSupport.ts
-[antigravity-text]: ../../apps/server/src/textGeneration/AntigravityTextGeneration.ts
-[provider-auth-service]: ../../apps/server/src/provider/Layers/ProviderAuthService.ts
-[provider-setup]: ../../packages/contracts/src/providerSetup.ts
-[opencode-server-owner]: ../../apps/server/src/provider/OpenCodeServerOwner.ts
-[adapter]: ../../apps/server/src/provider/Services/ProviderAdapter.ts
-[instances]: ../../apps/server/src/provider/Services/ProviderInstanceRegistry.ts
-[registry]: ../../apps/server/src/provider/Services/ProviderAdapterRegistry.ts
-[service]: ../../apps/server/src/provider/Layers/ProviderService.ts
-[contracts]: ../../packages/contracts/src/orchestration.ts
-[worker]: ../../packages/shared/src/DrainableWorker.ts
-[ingest]: ../../apps/server/src/orchestration/Layers/ProviderRuntimeIngestion.ts
-[cmd]: ../../apps/server/src/orchestration/Layers/ProviderCommandReactor.ts
-[checkpoint]: ../../apps/server/src/orchestration/Layers/CheckpointReactor.ts
```

## docs/internals/remote.md

Status: modified; +47; −244

````diff
@@ -1,256 +1,59 @@
-# Remote Architecture
+# Remote architecture
 
-> For maintainers. Using T3 Code? See [docs/user](../user/).
-
-Remote environments are shipped, not planned. Direct, bearer-paired, relay-tunneled, Tailscale, and
+Each connection joins a client to one environment over HTTP and WebSocket. The
+environment owns providers, execution, files, and durable state. Direct access,
+Tailscale, SSH, and T3 Connect change how the client reaches that server; they do
-desktop-managed SSH access all exist today. This document describes the model they share and where
-each piece lives. For the user-facing setup guide see
-[remote access](../user/remote-access.md).
-
-## The model
-
-T3 has one runtime boundary: a client talks to a T3 server over HTTP and WebSocket, and the server
-owns orchestration, providers, terminals, git, and filesystem operations. Remoteness is expressed at
-the connection layer, never by splitting the runtime.
-
-```text
-┌──────────────────────────────────────────────┐
-│ Client (desktop / mobile / web)              │
-│  known environments, connection supervisor   │
-└───────────────┬──────────────────────────────┘
-                │ resolves one access endpoint
-┌───────────────▼──────────────────────────────┐
-│ Access method                                │
-│  direct ws/wss, relay tunnel,                │
-│  Tailscale serve, desktop-managed ssh        │
-└───────────────┬──────────────────────────────┘
-                │ connects to one T3 server
-┌───────────────▼──────────────────────────────┐
-│ Execution environment = one T3 server        │
-│  identity, providers, projects/threads,      │
-│  terminals, git, filesystem                  │
-└──────────────────────────────────────────────┘
-```
-
-### ExecutionEnvironment
-
-One running T3 server instance. It owns provider availability and auth, model availability, projects
-and threads, terminal processes, filesystem access, git operations, and server settings.
-
-It is identified by a stable `environmentId`, persisted by the server at `<stateDir>/environment-id`
-and generated on first start (`apps/server/src/environment/ServerEnvironment.ts`). Desktop, mobile,
-and web all reason about the same concept.
-
-Initialization publishes a complete ID atomically. Empty or whitespace-only ID files are repaired
-using a retained `<stateDir>/environment-id.recovery` file so concurrent and delayed repairs choose
-the same ID. Existing nonempty ID files remain authoritative.
-
-### Known environments and connection targets
-
-A saved client-side entry for an environment the client knows how to reach. It is not
-server-authored; it is local to a device or client profile. In the hosted web app these entries are
-browser-local. A hosted pairing URL can create one, but it does not give the hosted app a server-side
-control plane or a copy of session state.
-
-[`connection/model.ts`][model] defines four target tags, which are the real access taxonomy:
-
-| Target                    | Used for                                                                 |
-| ------------------------- | ------------------------------------------------------------------------ |
-| `PrimaryConnectionTarget` | The platform-managed local server (desktop backend, CLI-served web app). |
-| `BearerConnectionTarget`  | Any manually paired endpoint reached over direct HTTP/WebSocket.         |
-| `RelayConnectionTarget`   | Managed T3 Connect relay tunnels.                                        |
-| `SshConnectionTarget`     | Desktop-managed SSH environments.                                        |
-
-Bearer, relay, and SSH are persisted; primary is platform-managed. Note that Tailscale is not a
-separate target kind. A Tailscale URL is paired through the ordinary bearer path in
-[`onboarding.ts`][onboarding] (`preparePairingRegistration`), which accepts either a pairing URL or a
-host plus pairing code. Tailscale is an endpoint provider and transport, not a distinct runtime
-concept.
-
-### AdvertisedEndpoint
+not introduce another execution model. See
+[remote access](../user/remote-access.md) for setup.
 
-A server- or desktop-authored candidate endpoint for an environment: a concrete HTTP and WebSocket
-base URL pair, a default/available/unavailable marker, reachability hints (loopback, LAN, private,
-public, tunnel), and compatibility hints such as whether the hosted HTTPS app can use it.
+## Identity is independent of the route
 
+An environment keeps its ID across server restarts and endpoint changes. Saved
+connections are local to a client profile; the server's identity and state are
+not. A repository identity can correlate clones across environments, but never
+routes work between them. A project and its threads belong to one environment.
-Clients treat advertised endpoints as hints, not proof that a route works from the current device.
-The connection attempt decides.
 
+[Environment ID initialization](../../apps/server/src/environment/ServerEnvironment.ts)
+must publish a complete ID atomically. Repair of an empty ID file retains a
+recovery file so concurrent or delayed initializers choose the same winner.
-The UI shows one default endpoint in the network-access summary and keeps the rest behind an advanced
-list. `selectPairingEndpoint` in
+Removing that recovery state as ordinary temporary-file cleanup can change the
+identity underneath an already-running server.
-[`ConnectionsSettings.tsx`](../../apps/web/src/components/settings/ConnectionsSettings.tsx) excludes
-unavailable endpoints and then picks, in order:
 
+Advertised endpoints are reachability hints. Only the connecting device can
-1. the saved `defaultEndpointKey` override;
+prove that a route works. In particular, a host's loopback address refers to a
-2. the first endpoint marked `isDefault`;
-3. the first endpoint whose reachability is not `loopback`;
-4. the first endpoint compatible with the hosted HTTPS app;
-5. otherwise nothing.
+different machine when another device opens it. Endpoint selection must not
+silently fall back to loopback when a shareable endpoint is unavailable.
 
+## Hosted web is a client
-There is no unconditional loopback fallback. A loopback endpoint only wins through an explicit saved
-override or `isDefault`. Persist the override by stable endpoint kind rather than raw URL where
-possible, since LAN addresses change with networks; Tailscale endpoints use provider-specific stable
-keys (`tailscale-ip:`, `tailscale-magicdns:`).
 
+The hosted web app stores its connection catalog in the browser and connects
+directly to each environment. It does not proxy traffic or hold server-side
+pairing state. Hosting the UI over HTTPS therefore cannot make a plain HTTP LAN
+backend accessible from that browser context.
-### Endpoint providers
 
+A [hosted pairing URL](../../apps/web/src/hostedPairing.ts) identifies the backend
+in its query and carries the pairing secret in its fragment. Fragments stay out
-Endpoint providers contribute advertised endpoints without becoming part of the core environment
-model: core owns environments, pairing, and connection lifecycle, and providers return normalized
-`AdvertisedEndpoint` records.
+of requests to the hosted origin. The browser exchanges the secret with the
+environment and strips it from its history. Moving the token into a query
+parameter would disclose it to the wrong origin.
 
+## Access and process ownership are different
-Tailscale is the first provider, and T3 manages more than discovery. When `tailscaleServeEnabled` is
-set, the server acquires a Tailscale serve mapping for its actual listening port at startup with
-`ensureTailscaleServe` and releases it with `disableTailscaleServe` on scope close
-(`apps/server/src/server.ts`, using [`@t3tools/tailscale`](../../packages/tailscale/src/tailscale.ts)).
-Endpoint identifiers are synthesized in `apps/desktop/src/backend/tailscaleEndpointProvider.ts` with
-`private-network` reachability.
 
+Tailscale supplies an endpoint for ordinary pairing, so it needs no separate
+environment type. Authentication remains the environment's responsibility for
+every route. See [environment authentication](./environment-auth.md) and the
-### Hosted pairing request
+[T3 Connect trust boundary](./t3-connect.md).
 
-A hosted pairing request is a bootstrap URL for the static web app, not a transport:
+SSH can launch a server as well as forward a port. Desktop main owns that
+lifecycle because it can spawn SSH and handle authentication prompts. The
+renderer uses the forwarded endpoint through the shared connection runtime.
+[SSH cleanup](../../packages/ssh/src/tunnel.ts) stops a remote server only if the
+launcher owns it; a server it discovered already running must survive a client
+disconnect. Reconnection restores the forward before opening the application
+transport.
 
+Remote servers can outlive several client releases. Clients must use advertised
+capabilities and handle their absence, rather than assume their own version
+describes the server. Process replacement belongs to the launcher's
+[update protocol](./server-updates.md); the connection runtime handles the
+resulting disconnect.
-```text
-https://app.t3.codes/pair?host=https://backend.example.com:3773#token=PAIRCODE
-```
-
-The hosted app reads `host`, takes the token from the URL hash, exchanges it directly with that
-backend, strips the token from browser history, and saves the environment record locally. Helpers
-live in [`shared/remote.ts`](../../packages/shared/src/remote.ts) (`setPairingTokenOnUrl`,
-`getPairingTokenFromUrl`, `stripPairingTokenFromUrl`) and `apps/web/src/hostedPairing.ts`.
-
-Constraints:
-
-- the hosted app does not proxy HTTP or WebSocket traffic;
-- the backend must be directly reachable from the browser;
-- HTTPS pages can only reach HTTPS/WSS backends;
-- HTTP LAN endpoints keep using direct desktop or CLI pairing URLs;
-- the token belongs in the hash so it is never sent to the hosted app origin.
-
-### RepositoryIdentity and Project
-
-`RepositoryIdentity` is a best-effort logical repo grouping across environments, used for UI grouping
-and correlation only, never for routing. `Project` remains environment-local: a local clone and a
-remote clone are different projects that may share a `RepositoryIdentity`, and threads bind to one
-project in one environment.
-
-## Access methods
-
-Access answers one question: how does the client speak WebSocket to a T3 server? It does not answer
-how the server got started or who manages the process.
-
-### Direct WebSocket access
-
-`wss://t3.example.com` or `ws://10.0.0.15:3773`, paired as a bearer target. This is the base model.
-It works for desktop, mobile, and web with no client-side process management. Browser security rules
-are part of it: a hosted HTTPS client cannot connect to plain `ws://` or `http://` LAN backends.
-
-### Relay-tunneled access
-
-Managed T3 Connect relay tunnels use `RelayConnectionTarget` and are the answer when the host is
-behind NAT, inbound ports are unavailable, or mobile must reach a desktop-hosted environment. From
-the client's perspective this is still an ordinary WebSocket connection; the route is mediated. The
-relay Worker only brokers credentials and a managed endpoint; application traffic then flows over
-the provisioned Cloudflare tunnel hostname for the life of the connection, not through the relay
-Worker itself. See [t3-connect.md](./t3-connect.md).
-
-### Tailscale access
-
-A T3-managed `tailscale serve` mapping exposes the server on the tailnet over HTTPS, and the
-resulting private-network endpoints are advertised for pairing. Connection then follows the ordinary
-bearer path.
-
-### Desktop-managed SSH access
-
-SSH is an access and launch helper, not a separate environment type. `DesktopSshEnvironment`
-([apps/desktop/src/ssh/DesktopSshEnvironment.ts][sshenv]) exposes `discoverHosts`,
-`ensureEnvironment`, and `disconnectEnvironment`. It discovers targets from SSH config and known
-hosts, owns password/askpass prompts, and delegates lifecycle to `SshEnvironmentManager` in
-[packages/ssh/src/tunnel.ts][sshtunnel], which resolves the target, launches or reuses the remote T3
-server, opens a local tunnel, checks HTTP readiness, optionally issues a remote pairing token, and
-returns local HTTP/WS endpoints. Disconnect closes the tunnel and stops the remote server if the
-launcher started it; a server that was already running (marked `external`) is left running.
-
-The desktop main process owns this because it can spawn SSH, manage prompts, write launch scripts,
-and clean up forwards. The renderer connects through the forwarded URL like any other environment and
-needs no SSH-specific RPC path.
-
-Failure handling is explicit: SSH auth failure surfaces before an environment is saved, remote launch
-failure includes launcher output where available, forwarded-port failure leaves the environment
-disconnected rather than falling back to an unrelated endpoint, and reconnect restores the SSH bridge
-before reconnecting the WebSocket client.
-
-## Launch methods
-
-Launch answers a different question: how does a T3 server come to exist on the target machine? Keep
-it separate from access.
-
-- **Pre-existing server.** The operator already runs T3 and the client connects directly or through a
-  tunnel.
-- **Desktop-managed remote launch over SSH.** Desktop probes the machine, launches or reuses a remote
-  server, forwards a port, and the renderer connects normally. The saved environment records that it
-  came from SSH launch for reconnect and lifecycle UX only; that metadata never changes the protocol
-  or the identity model.
-- **Client-managed local publish.** A local server is published through the relay with
-  `t3 connect link`, exposing a desktop-hosted environment to mobile without router or firewall
-  changes.
-
-The same `ExecutionEnvironment` can be reached several of these ways. Only the launch and access
-paths differ.
-
-## Security model
-
-Some environments are reachable over untrusted networks, so remote-capable environments require
-explicit authentication, tunnel exposure never relies on obscurity, and saved endpoints carry enough
-auth metadata to reconnect safely.
-
-WebSocket authentication is a dedicated short-lived ticket, not a token in a query string. The client
-presents its long-lived bearer or DPoP credential in HTTP headers to
-`POST /api/auth/websocket-ticket` ([authorization/remote.ts][authremote]), and appends only the
-returned ticket as `wsTicket` on the socket URL. The server issues it through
-`EnvironmentAuth.issueWebSocketTicket`; tickets are tagged `kind: "websocket"` and default to a
-five-minute TTL (`DEFAULT_WEBSOCKET_TOKEN_TTL` in `apps/server/src/auth/SessionStore.ts`). The
-handshake verifies the ticket, and each RPC method still enforces its own scope. See
-[environment-auth.md](./environment-auth.md).
-
-Hosted pairing is a client-side convenience only. The hosted app must not receive pairing tokens
-through query parameters, must not store pairing state server-side, and must not imply that an HTTP
-backend is reachable from an HTTPS browser context.
-
-## Version coordination
-
-Remote environments stay online while clients move to newer releases. The environment descriptor
-carries the running server version and may advertise a safe replacement path, so the UI can show the
-right action without making the transport responsible for process management. The connection
-supervisor owns the resulting disconnect and reconnect like any other involuntary close. See
-[server-updates.md](./server-updates.md).
-
-## Machine identity
-
-Every environment is drawn with one of a fixed set of machine glyphs (`EnvironmentMachineKind` in
-contracts: server, cloud, desktop, laptop, mac-mini, mac-studio). Two inputs feed it, and the
-precedence lives in one helper, `resolveEnvironmentMachineKind`, so web and mobile cannot drift:
-
-1. `settings.environmentIcon`, a nullable server setting the user picks in Settings → Connections.
-   It is server state rather than client state so every device connecting to that server agrees.
-2. `environment.platform.machine`, detected once at startup by [ServerEnvironmentMachine.ts][machine]
-   (IOKit product name / `hw.model` on macOS, `/sys/class/dmi/id` on Linux with virtualization
-   markers mapped to "cloud"). Absent when there is no signal, so older servers, Windows hosts, and
-   containers decode without it.
-3. A generic server otherwise.
-
-Clients treat the field like any other capability: absent means "use the fallback", never "wait".
-
-## Future work
-
-These remain unbuilt and are listed to keep the model honest:
-
-- third-party tunnel products as additional endpoint providers;
-- a relay-hosted OAuth callback broker (see [t3-connect.md](./t3-connect.md));
-- richer multi-environment UI beyond the current connections list.
-
-[model]: ../../packages/client-runtime/src/connection/model.ts
-[onboarding]: ../../packages/client-runtime/src/connection/onboarding.ts
-[authremote]: ../../packages/client-runtime/src/authorization/remote.ts
-[sshenv]: ../../apps/desktop/src/ssh/DesktopSshEnvironment.ts
-[sshtunnel]: ../../packages/ssh/src/tunnel.ts
-[machine]: ../../apps/server/src/environment/ServerEnvironmentMachine.ts
````

## docs/internals/resource-telemetry.md

Status: modified; +42; −386

````diff
@@ -1,394 +1,50 @@
-# Resource telemetry architecture
-
-> For maintainers. Using T3 Code? See [docs/user](../user/).
-
-Status: implemented
-
-## Purpose
-
-Resource telemetry replaces recurring `ps`, PowerShell, `ioreg`, and `pmset`
-subprocess probes with two persistent, direct data sources:
-
-1. a standalone Rust resource-monitor executable that reads process counters
-   through operating-system APIs via `sysinfo`;
-2. Electron main-process APIs for Electron process metrics and host power state.
-
-The native monitor owns bounded in-memory history. The server only merges and
-summarizes that history when diagnostics requests it. Telemetry history is not
-persisted to disk or continuously copied into Node.
-
-## Why a standalone executable
-
-The monitor is intentionally not a Node native addon.
-
-- No N-API, `ffi-rs`, or dynamic-library ABI is loaded into the server process.
-- A monitor crash cannot corrupt the Node runtime.
-- The server can supervise, restart, version-check, and measure the monitor as a
-  normal child process.
-- The same protocol works for the desktop app and the published CLI.
-- Packaging is a single platform executable instead of an addon toolchain plus
-  Node/Electron ABI matrix.
-
-The cost is one persistent child process and NDJSON serialization. That is a
-better failure boundary than repeatedly spawning shell utilities or loading
-native code into Node.
-
-## Runtime topology
-
-### Desktop
-
-```text
-Electron main
-  ├─ powerMonitor
-  ├─ app.getAppMetrics() while diagnostics is open
-  ├─ inherited fd 4, telemetry NDJSON ─────────────┐
-  └─ inherited fd 5, demand-control NDJSON ◀──────┤
-                                                   ▼
-Node server ── stdin/stdout NDJSON ── Rust resource monitor
-     │
-     ├─ ResourceTelemetry Effect service
-     ├─ background power policy projection
-     └─ WebSocket RPC/subscription ── diagnostics UI
-```
-
-### Web, headless, and remote server
-
-Electron telemetry is unavailable. The native monitor still runs beside the
-server and tracks the server process tree. Power fields degrade to `unknown`
-instead of invoking platform shell commands.
-
-### WSL backend limitation
-
-Windows desktop packages currently ship the Windows resource-monitor executable.
-That executable cannot run inside the Linux WSL backend, so a WSL-only backend
-does not receive `resourceMonitorPath` and reports native process telemetry as
-unavailable. Electron host-power telemetry remains available over the inherited
-desktop pipe. Supporting native WSL process telemetry requires publishing a
-Linux sidecar for each supported architecture in the Windows artifact and
-converting its packaged path into the selected distro; the configuration
-deliberately does not pass the Windows `.exe` into WSL as a false fallback.
-
-## Native monitor
-
-The executable lives in `native/resource-monitor`.
-
-It receives schema-compatible commands on stdin and emits one JSON object per
-line on stdout:
-
-- `configure`
-- `setExternalProcesses`
-- `setSampleInterval`
-- `setStreaming`
-- `sampleNow`
-- `readHistory`
-- `shutdown`
-- `hello`
-- `snapshot`
-- `historyChunk`
-- `error`
-
-The protocol version is defined by
-`RESOURCE_MONITOR_PROTOCOL_VERSION` in
-`packages/contracts/src/resourceTelemetry.ts`.
-
-### Collection
-
-The monitor keeps one `sysinfo::System` instance and refreshes it at the
-power-adaptive interval selected by the server. It collects:
-
-- PID and parent PID;
-- process start time and run time;
-- process name and command line;
-- current and cumulative CPU usage;
-- resident and virtual memory;
-- cumulative process I/O counters.
-
-On Linux, task/thread enumeration is disabled. Command lines are refreshed with
-each sample so process replacements remain visible. Disabling task enumeration
-avoids walking every `/proc/<pid>/task/<tid>` directory on each refresh.
-
-### Process-tree selection
-
-Each sample scans the accessible process table, builds the PID/PPID graph, and
-retains:
-
-- the server process;
-- every descendant of the server, including provider-spawned grandchildren such
-  as shells, `node`, `tsgo`, language servers, and other tools;
-- Electron processes supplied as explicit external roots;
-- descendants of those Electron roots;
-- the resource monitor itself, because it is a server child.
-
-Process identity is `(pid, startTimeMs)`, not PID alone. Electron and native
-start times are matched with a two-second tolerance because native start times
-can have coarser platform resolution.
-
-The process list is emitted in depth-first tree order so renderer collapse and
-expansion preserves complete subtrees.
-
-### Native history and streaming
-
-Every native sample is appended to a one-hour in-memory ring bounded to 3,600
-snapshots, 20,000 retained process rows, and 64 MiB of retained history bytes.
-History stays in the sidecar until a `readHistory` request and is returned in
-bounded chunks. The first bound reached wins, so high process counts or large
-process names and command lines shorten the effective history window.
-
-Periodic snapshot streaming is disabled by default. The server enables it only
-while at least one diagnostics subscription is retained. `sampleNow` remains
-available for explicit refreshes and identity validation.
-
-The server adjusts native sampling without restarting the sidecar:
-
-- suspended, locked, low-power, or serious/critical thermal state: 15 seconds;
-- battery: 5 seconds;
-- normal AC: 5 seconds in the background and 1 second while live diagnostics is
-  open;
-- unknown or stale power: 5 seconds in the background and 1 second while live
-  diagnostics is open.
-
-### Sampling limits
-
-This is counter sampling, not syscall tracing.
-
-- A process that starts and exits entirely between samples may not be observed.
-- Cumulative CPU and I/O counters still provide accurate deltas for processes
-  that survive across samples.
-- Exact file paths, individual write syscalls, ETW events, eBPF events, and
-  Endpoint Security events are outside this implementation.
-
-Those deeper tracing systems can be added later as opt-in diagnostic modes
-without changing the public `ResourceTelemetry` model.
-
-## I/O semantics
-
-The monitor preserves platform semantics instead of presenting all counters as
-equivalent:
-
-- Unix-like platforms report storage I/O counters exposed by `sysinfo`.
-- Windows reports all process I/O bytes, not only disk bytes.
-- Operating-system caches can prevent logical application reads or writes from
-  appearing as physical storage bytes.
-
-The UI therefore labels these values as I/O reads and writes and exposes the
-per-process `ioSemantics` value.
-
-Group totals are observed deltas since telemetry startup. Per-process total
-columns are the operating system's cumulative counters for that process.
-
-## Electron telemetry
-
-Electron main owns `DesktopTelemetryPublisher`.
-
-Power events trigger an immediate snapshot. While diagnostics is closed, the
-server sends the configured active and idle host-power intervals to Electron
-(30 seconds and 2 minutes in the balanced profile). During those heartbeats
-Electron reads:
-
-- `powerMonitor.isOnBatteryPower()`;
-- `powerMonitor.getSystemIdleTime()`;
-- `powerMonitor.getSystemIdleState()`;
-- `powerMonitor.getCurrentThermalState()`.
-
-`app.getAppMetrics()` is only called while diagnostics demand is active. Its
-live cadence is 1 second on AC, 5 seconds on battery, and 15 seconds while
-locked, suspended, or thermally constrained.
-
-It also listens for:
-
-- lock and unlock;
-- suspend and resume;
-- AC and battery transitions;
-- thermal-state changes;
-- CPU speed-limit changes.
-
-Suspension stays latched across event-driven snapshots. An explicit resume
-clears it immediately; a periodic heartbeat also clears a stale latch so a
-missed resume event cannot leave telemetry permanently constrained.
-
-Electron does not expose a cross-platform low-power-mode getter, so that field
-remains `unknown`.
-
-The desktop backend is spawned with:
-
-- fd 3 for the existing bootstrap payload;
-- fd 4 for Electron-to-server telemetry NDJSON;
-- fd 5 for server-to-Electron diagnostics-demand NDJSON.
-
-These are private Electron-main/server pipes. They do not use the renderer
-WebSocket and are recreated for every backend restart.
-
-## Server Effect services
-
-The implementation is under `apps/server/src/resourceTelemetry`.
-
-### `ResourceMonitorBinary`
-
-Resolves an executable from:
-
-1. `T3CODE_RESOURCE_MONITOR_PATH`;
-2. desktop bootstrap configuration;
-3. bundled CLI resources;
-4. local Cargo build outputs.
-
-Unsupported platforms, missing binaries, and non-executable binaries use
-schema-backed tagged errors with descriptive messages.
-
-### `NativeTelemetryClient`
-
-Owns the resource-monitor process and protocol.
-
-- validates the hello/version handshake;
-- sends configuration and external process roots;
-- adapts the native interval from host power state;
-- enables streaming only for scoped live subscribers;
-- reads chunked native history on demand;
-- exposes `sampleNow`;
-- serializes commands;
-- supervises process exit and protocol failure;
-- restarts with bounded exponential backoff;
-- opens a circuit after repeated failures;
-- supports explicit retry;
-- publishes health changes immediately.
-
-Snapshot sequence numbers are scoped to a monitor generation. Server ingestion
-uses the monitor restart count as the generation key, so sequence reset after a
-restart cannot freeze telemetry.
-
-### `DesktopTelemetryReceiver`
-
-Reads fd 4, decodes schema-validated messages, stores the latest Electron
-snapshot, and publishes desktop health. It writes diagnostics demand to fd 5
-and gives the first sample a 90-second startup deadline. Once samples arrive,
-the stale deadline stays beyond the slower configured host-power interval with
-30 seconds of scheduling grace, so intentional 2–10 minute idle polling does
-not oscillate the policy between constrained and unconstrained states. Decode
-errors, protocol mismatch, control-write failure, stream failure, stale input,
-and normal stream closure are represented explicitly.
-
-### `ResourceTelemetry`
-
-Merges native and Electron data and owns public telemetry semantics.
-
-- calculates CPU and I/O rates from cumulative native counters;
-- preserves the last native rates during desktop-only updates;
-- classifies backend, Electron, and monitor processes;
-- computes process depth and child relationships;
-- tracks starts, exits, CPU time, and observed I/O;
-- projects power data;
-- acquires native streaming and Electron process metrics only for scoped live
-  subscribers;
-- queries and replays native history only when requested;
-- validates `(pid, startTimeMs)` before process signaling;
-- updates history health even when no further native sample arrives.
-
-Electron and monitor processes are visible but are not valid targets for the
-existing process-signal RPC.
-
-### History projection
-
-`ResourceTelemetryHistory` is a pure on-demand projection. It replays raw native
-snapshots to derive rates, lifecycle counters, buckets, and process summaries.
-Current Electron process metrics are intentionally excluded from historical
-replay so they cannot overwrite older native CPU or memory samples.
-
-### `ResourceAttribution`
-
-Tracks known logical application I/O separately from OS counters. Current
-integration points record successful writes for:
-
-- provider native and canonical event logs;
-- the local server trace sink.
-
-Entries contain component, operation, logical bytes, count, and elapsed time.
-Future persistence paths should call `ResourceAttribution.record` rather than
-adding diagnostics-specific counters.
-
-## Background policy integration
-
-`HostPowerMonitor` consumes `DesktopTelemetryReceiver` directly; observing host
-power does not retain live resource diagnostics or invoke shell probes.
-
-The monitor updates its latest timestamp on every Electron sample but only
-publishes semantic state changes. Increasing idle seconds alone does not cause a
-background-policy broadcast every second.
-
-## Public API and UI
-
-The WebSocket RPC surface provides:
-
-- current snapshot;
-- bounded history;
-- explicit monitor retry;
-- a live snapshot subscription.
+# Resource telemetry
 
+Process counters come from a [standalone Rust monitor](../../native/resource-monitor/src/main.rs)
+using `sysinfo`. Electron main supplies host power and Electron process metrics.
+Keeping native collection outside Node isolates collector crashes and avoids a
-The diagnostics page displays:
+Node/Electron addon ABI matrix. Desktop and CLI servers use the same child-process
+protocol. A missing or failed collector leaves the server running.
 
-- aggregate CPU, memory, I/O, and process counts;
+## Collection cost
-- backend, Electron, and monitor overhead groups;
-- power and thermal state;
-- collector health and restart information;
-- CPU and I/O history;
-- a collapsible live process tree;
-- safe process signaling for backend descendants;
-- instrumented logical application I/O.
 
+The native child owns sampling and bounded in-memory history. The server requests
+continuous snapshots only while diagnostics has live subscribers and fetches
+history on demand. Consuming host power for background scheduling must not retain
-Legacy process diagnostics RPCs are projected from the same service so they no
+live diagnostics. There is no telemetry database or recurring shell-probe fallback.
-longer start recurring process-table commands.
 
+History has independent bounds for age, snapshot count, process rows, and retained
+bytes. A count limit alone cannot bound memory when command lines vary in size.
-## Packaging
+Large process trees therefore shorten the available history window. Linux task
+enumeration is disabled because walking every `/proc/<pid>/task/<tid>` directory
+makes sampling itself expensive.
 
+Electron power updates travel over private inherited pipes, independent of the
-Desktop artifact builds compile the Rust target, stage it as
-`resources/resource-monitor/t3-resource-monitor[.exe]`, and pass its path to the
-backend bootstrap.
+renderer connection. Power events and slow heartbeats continue with diagnostics
+closed; `app.getAppMetrics()` runs only on live demand. The receiver's stale deadline
+must exceed the slowest configured heartbeat plus scheduling grace, or intentional
+idle polling makes background policy oscillate between constrained and
+unconstrained states. Headless servers leave unavailable power data unknown.
 
+## Measurement traps
-CLI release jobs upload each active platform monitor artifact and copy it into:
 
+- Process identity includes start time because operating systems reuse PIDs.
+  Electron and native start times have different precision, so merging allows a
+  small tolerance. Process signaling rechecks the native identity with a fresh
+  sample.
+- Snapshot sequence numbers belong to a monitor generation. Comparing them across
+  restarts would discard the new monitor's samples until its sequence caught up.
+- Sampling can miss a process that starts and exits between samples. Cumulative
+  counters still yield deltas for processes observed across samples.
+- Windows process I/O includes more than disk traffic. Unix counters report storage
+  I/O, which can differ from logical application reads and writes because of OS
+  caching. Keep instrumented logical I/O separate from these counters.
+- Group totals accumulate observed deltas since telemetry started. Per-process
+  cumulative counters cover the operating system's lifetime for that process.
-```text
-apps/server/dist/resource-monitor/<platform>-<arch>/
-```
-
-The published server package already includes `dist`, so those executables ship
-with the CLI. Missing platform artifacts degrade native telemetry to
-`unavailable`; the server continues running.
+- Historical replay uses native samples without current Electron CPU or memory
+  metrics. Merging the latest Electron values would overwrite the past.
 
+A WSL backend needs a Linux monitor even though Electron runs on Windows. Windows
+desktop packages currently supply only the Windows executable, so native process
+telemetry for the WSL backend is unavailable. The inherited Electron power feed
+still works.
-## Resource and failure behavior
-
-Steady state uses:
-
-- one native process;
-- power-adaptive native counter sampling with no periodic Node snapshot stream;
-- event-driven Electron power updates plus profile-driven heartbeats (30–60
-  seconds while active and 2–10 minutes while idle);
-- no `app.getAppMetrics()` calls while diagnostics is closed;
-- no telemetry database;
-- no recurring shell probes;
-- bounded PubSub queues and native ring history.
-
-The diagnostics page exposes the monitor's own process resource usage and
-collection duration so the observer's cost is measurable.
-
-Failures are isolated:
-
-- native failure does not stop the server;
-- Electron telemetry loss does not stop native telemetry;
-- schema/version errors are visible in health;
-- repeated native failures stop automatic restart churn until explicit retry;
-- server and desktop shutdown close their respective streams and child process
-  scopes.
-
-## Future integration points
-
-High-value follow-up work can use the existing service boundaries:
-
-- opt-in file-path attribution through platform-specific tracing;
-- process lifecycle events to reduce the chance of missing very short-lived
-  children;
-- additional `ResourceAttribution` instrumentation for databases, checkpoints,
-  caches, and file synchronization;
-- exported diagnostic bundles;
-- adaptive sample intervals based on diagnostics visibility and active work.
-
-These additions should preserve the current rules: direct platform APIs,
-schema-validated boundaries, explicit metric semantics, bounded retention, and
-no mandatory telemetry persistence.
````

## docs/internals/scripts.md

Status: deleted; +0; −215

````diff
@@ -1,215 +1,0 @@
-# Scripts
-
-> For maintainers. Using T3 Code? See [docs/user](../user/).
-
-## First checkout
-
-T3 Code uses [Vite+](https://viteplus.dev/guide/). Install the global `vp` command, install
-dependencies, then start the dev stack:
-
-```bash
-curl -fsSL https://vite.plus | bash   # Windows: irm https://vite.plus/ps1 | iex
-vp i
-vp run dev
-```
-
-Node 24 is required. Bun is not: the server picks Bun adapters when it detects Bun and falls back to
-Node otherwise, and nothing in contributor setup needs it.
-
-`vp run dev` prints a one-time pairing URL. Open it so the first browser navigation is
-authenticated.
-
-## Dev
-
-- `vp run dev`: Starts contracts, server, and web in watch mode.
-- `vp run dev --share`: Also publishes the web port over HTTPS on this machine's tailnet. The
-  startup pairing URL is built against the shared origin, and the mapping is removed on exit.
-  Shared runs default to Vite's bundled dev mode (`T3CODE_BUNDLED_DEV=1`): a remote browser pays a
-  network round trip per import level in unbundled dev, which turns a cold module graph into
-  minutes of waterfall. Set `T3CODE_BUNDLED_DEV=0` to opt a shared run back out.
-  The web entry loads the app with a dynamic import so React refresh starts before shared UI
-  chunks run. Keep app imports out of that entry. A static import can work on the first load
-  and then fail on reload after Vite splits code for lazy routes.
-  Bundled dev uses Tailwind's watched files to rebuild CSS. Its Vite-only hot-update hook is
-  disabled in this mode because Rolldown does not supply the Vite server or module graph.
-- `vp run dev --browser`: Auto-opens a browser. Off by default. The dev runner writes
-  `T3CODE_NO_BROWSER` itself from this flag, so setting `T3CODE_NO_BROWSER=0` in your environment has
-  no effect; use `--browser`.
-- `vp run dev:server`: Starts just the server. It runs on Node (`node --watch src/bin.ts`), so
-  without Bun present it selects `NodePtyAdapter` and `NodeHttpServer`.
-- `vp run dev:web`: Starts just the Vite dev server for the web app.
-- `vp run dev:desktop`: Starts the Electron shell against the dev server.
-- `vp run dev:marketing`: Starts the Astro marketing site.
-- Pass dev-runner flags directly after the root task name, for example:
-  `vp run dev --home-dir /tmp/t3code-dev`
-
-### Dev state directories
-
-- Dev commands run from a linked **git worktree** default to that worktree's gitignored `.t3`, even
-  when `T3CODE_HOME` is set, storing state in `<worktree>/.t3/userdata`. Pass `--home-dir <path>` to
-  choose another isolated directory explicitly. Submodules are not worktrees and keep the normal
-  precedence.
-- From the **main checkout**, dev commands implicitly use `~/.t3/dev`, keeping development state
-  separate from `~/.t3/userdata`. An explicit `--home-dir <path>` stores state under
-  `<path>/userdata`; the base directory remains available for caches, worktrees, and other shared
-  data.
-
-## Build, check, test
-
-- `vp run build`: Fans out over `apps/*`, `packages/*`, `oxlint-plugin-t3code`, and `scripts`.
-  Workspaces that define a build task run one: desktop, marketing, server (which depends on web), and
-  web. Shared packages are consumed and bundled transitively rather than built separately.
-- `vp run build:desktop`: Builds the desktop pipeline (desktop plus server).
-- `vp run start`: Runs the production server (serves the built web app as static files).
-- `vp check`: Vite+ format, lint, and type checks. This repo sets `typeCheck: false` in its lint
-  options, so workspace type checking runs separately.
-- `vp run typecheck`: Strict TypeScript checks for all packages.
-- `vp run test`: Runs workspace tests.
-- `vp run lint:mobile`: Mobile native static analysis (`scripts/mobile-native-static-check.ts`).
-- `node apps/server/scripts/t3-sqlite-state.ts <query|exec> --base-dir <path> ...`: Inspects or seeds
-  an isolated T3 SQLite database; writes create a private backup first.
-
-## Desktop artifacts
-
-- `vp run dist:desktop:artifact --platform <mac|linux|win> --target <target> --arch <arch>`: Builds a desktop artifact for a specific platform/target/arch.
-- `vp run dist:desktop:dmg`: Builds a shareable macOS `.dmg` into `./release`. Architecture defaults
-  to the host, so this produces an arm64 DMG on Apple Silicon. Use `dist:desktop:dmg:arm64` or
-  `dist:desktop:dmg:x64`, or pass `--arch <arm64|x64|universal>`, to force one.
-- `vp run dist:desktop:linux`: Builds a Linux AppImage into `./release`.
-- `vp run dist:desktop:win`: Builds a Windows NSIS installer into `./release`. `:arm64` and `:x64`
-  variants exist.
-
-### Linux AppImage prerequisites
-
-Linux AppImage packaging compiles the Rust resource monitor and the libsecret browser import
-helper. Install a Rust toolchain, the standard C/C++ build tools, libsecret development headers,
-pkg-config, and ImageMagick before running `vp run dist:desktop:linux`.
-
-Ubuntu and Debian:
-
-```bash
-sudo apt-get update
-sudo apt-get install cargo rustc build-essential libsecret-1-dev pkg-config imagemagick
-```
-
-Fedora:
-
-```bash
-sudo dnf install rust cargo gcc gcc-c++ make libsecret-devel pkgconf-pkg-config ImageMagick
-```
-
-Arch Linux:
-
-```bash
-sudo pacman -S rust base-devel libsecret pkgconf imagemagick
-```
-
-Linux desktop development also needs the C compiler, pkg-config, and libsecret headers. Its build
-and launch commands compile `native/browser-secret/main.c` into the gitignored native build
-directory. Releases place the executable in `resources/browser-secret`, outside `app.asar`.
-The helper performs a read-only Chromium schema lookup through libsecret and writes the exact
-secret bytes to its stdout pipe. Exit codes are 2 for a missing key, 3 for denied/locked access,
-and 4 for other keyring failures; the desktop importer preserves these distinctions.
-
-The artifact script checks these capabilities before starting the web and desktop builds. If
-anything is unavailable, it reports every failed check together and prints the Ubuntu/Debian
-installation command. The check compiles and links tiny temporary programs, so it also catches
-installed runtime X11 libraries that are missing their development headers or linker symlinks.
-
-### macOS DMG prerequisites
-
-Install the Xcode Command Line Tools and Rust before building a DMG:
-
-```bash
-xcode-select --install
-```
-
-Install Rust from [rustup.rs](https://rustup.rs). The artifact script checks Cargo, Clang, Make,
-`sips`, and `iconutil`. Universal builds additionally require `lipo`. It also verifies that Rust
-has every requested target; add missing targets with:
-
-```bash
-rustup target add aarch64-apple-darwin x86_64-apple-darwin
-```
-
-Unsigned local builds need no Apple credentials. Builds using `--signed` additionally require the
-certificate, provisioning profile, team ID, and notarization configuration described below.
-
-### Windows installer prerequisites
-
-Install Rust from [rustup.rs](https://rustup.rs), Python 3, and Visual Studio Build Tools. In the
-Visual Studio Installer, select **Desktop development with C++** and include:
-
-- MSVC x64/x86 build tools
-- A Windows 10 or Windows 11 SDK
-- MSVC Spectre-mitigated libraries
-
-ARM64 installers require the corresponding MSVC ARM64 build tools and Spectre-mitigated libraries
-instead of the x64/x86 components.
-
-Add the Rust target matching the installer architecture:
-
-```powershell
-rustup target add x86_64-pc-windows-msvc
-# For an arm64 installer:
-rustup target add aarch64-pc-windows-msvc
-```
-
-Windows supplies `tar.exe`; it is checked when `--wsl-prebuild` makes the artifact include the WSL
-runtime. NSIS is downloaded by electron-builder and does not need a separate installation.
-When `T3CODE_DESKTOP_REUSE_RESOURCE_MONITOR=true` points the build at an existing resource monitor,
-the artifact script skips the Rust and Visual Studio checks because it does not compile the monitor.
-Unsigned local builds need no Azure credentials. Builds using `--signed` additionally require the
-Azure Trusted Signing configuration described below.
-
-### Desktop `.dmg` packaging notes
-
-- Default build is unsigned/not notarized for local sharing.
-- The DMG build uses `assets/prod/black-macos-1024.png` as the production app icon source.
-- The DMG chrome follows the release channel: neutral for Latest and the Nightly sky artwork for
-  Nightly. Blueprint artwork remains exclusive to Dev builds. Packaging rasterizes the selected
-  SVG into standard and Retina PNGs inside the disposable staging directory.
-- The Finder window is 540×412 while its background is 540×380; the extra 32px accounts for the
-  title bar included in Finder's window bounds.
-- Desktop production windows load the bundled UI from the `t3code://app/` root URL (not a
-  `127.0.0.1` document URL, and not an explicit `index.html` path).
-- Desktop packaging includes `apps/server/dist` (the `t3` backend) and starts it on loopback with an
-  auth token for WebSocket/API traffic.
-- Your tester can still open it on macOS by right-clicking the app and choosing **Open** on first
-  launch.
-- To keep staging files for debugging package contents, run: `vp run dist:desktop:dmg --keep-stage`
-- To allow code-signing/notarization when configured in CI/secrets, add: `--signed`.
-- Signed macOS builds also require `T3CODE_APPLE_TEAM_ID` and
-  `T3CODE_MACOS_PROVISIONING_PROFILE`. The passkey RP domain is derived from
-  `T3CODE_CLERK_PUBLISHABLE_KEY` unless `T3CODE_CLERK_PASSKEY_RP_DOMAINS` overrides it.
-- Windows `--signed` uses Azure Trusted Signing and expects:
-  `AZURE_TRUSTED_SIGNING_ENDPOINT`, `AZURE_TRUSTED_SIGNING_ACCOUNT_NAME`,
-  `AZURE_TRUSTED_SIGNING_CERTIFICATE_PROFILE_NAME`, and `AZURE_TRUSTED_SIGNING_PUBLISHER_NAME`.
-- Azure authentication env vars are also required (for example service principal with secret):
-  `AZURE_TENANT_ID`, `AZURE_CLIENT_ID`, `AZURE_CLIENT_SECRET`.
-
-## Browser development
-
-`dev` and `dev:web` leave `VITE_HTTP_URL` and `VITE_WS_URL` unset so the browser resolves the backend
-from `window.location.origin`. Vite proxies `/api`, `/ws`, `/oauth`, and `/.well-known` to the
-server, allowing the same bundle to work from localhost or a tailnet hostname.
-
-## Running multiple dev instances
-
-Worktrees derive a preferred port offset from their path.
-
-- Default ports: server `13773`, web `5733`
-- Shifted ports: `base + offset`
-- Example: `T3CODE_DEV_INSTANCE=branch-a vp run dev:desktop`
-
-Offset resolution, in order:
-
-1. `T3CODE_PORT_OFFSET`, which must be a non-negative integer. Negative values are rejected.
-2. `T3CODE_DEV_INSTANCE`. An all-digit value is used directly as the offset; any other non-empty
-   value is hashed into one.
-3. The worktree path hash.
-
-Collision scanning depends on the mode. `dev:web` scans only the web port and shifts only the web
-offset. `dev:server` scans only the server port. `dev` and `dev:desktop` scan both and shift them
-together as one shared offset. Explicit server or dev-URL overrides remove the corresponding port
-from the availability check. Treat the `[dev-runner]` output as authoritative.
````

## docs/internals/server-updates.md

Status: modified; +45; −95

````diff
@@ -1,107 +1,57 @@
-# Server Update Architecture
-
-> For maintainers. Using T3 Code? See [docs/user](../user/).
-
-Remote server updates use one stable launcher selected by the platform service manager (systemd on
-Linux, launchd on macOS). Foreground CLI processes do not self-update, and a running server never
-edits its service definition or durable service state.
-
-## Ownership
-
-The service files under `<baseDir>/runtime` are:
-
-- `service-launcher.mjs`, the stable process selected by the service manager;
-- `service-state.json`, the launcher's durable selection state;
-- `versions/<version>`, immutable exact-version npm installs.
-
-The launcher is the only runtime writer of `service-state.json`. `t3 service install` and
-`t3 service update` may replace the launcher and state while the unit is stopped. Server children
-only communicate with the launcher over their inherited IPC channel.
-
-The state contains one active version and, at most, one update record:
-
-- `pending A → B` selects B as a retryable trial;
-- `committed A → B` selects B for ordinary restarts;
-- `rolled-back A → B` or `failed A → B` selects A;
-- invalid state fails closed so the service manager cannot guess at a runtime.
-
-Every write uses same-directory replacement plus file and directory fsync.
-
-## Remote Update
-
-1. The active server installs `t3@<target>` into a unique staging directory.
-2. The target runs `__service-preflight` and verifies that the stable launcher supports its update
-   protocol.
-3. The staging directory is renamed to its immutable version path only after preflight succeeds.
-4. The active child sends `request-update`. The launcher validates the child and target, writes
-   pending state, generates the update ID, then replies `update-accepted`.
-5. After a short response-flush grace period, the launcher stops the active child.
-6. With SQLite quiescent, the launcher snapshots the database, WAL, and shared-memory files.
-7. The launcher starts the target as a trial and gives it the pending update over IPC.
-8. The trial runs migrations, acquires dependencies, binds HTTP, starts every long-running root
-   fiber, and verifies that each root is parked at the activation gate.
-9. The trial sends `prepared`. The launcher durably commits B, deletes the snapshot, then replies
-   `committed`.
-10. The child opens the existing activation gate, accepts commands, and publishes lifecycle ready
+# Server updates
-    with the terminal update outcome.
 
+A [stable launcher](../../apps/server/src/serviceLauncher.ts) owns the runtime
+selected by systemd or launchd. It is the only runtime writer of durable service
+state. Server children request updates over inherited IPC; they never rewrite
+their service definition or select their own replacement. Local service commands
+may replace the launcher and state while the service is stopped. Foreground CLI
+processes do not self-update.
-Post-commit startup does not call service `start`, `initialize`, `connect`, `load`, or `acquire`
-operations. It only opens prepared gates and publishes prepared lifecycle state.
 
+Exact-version installs keep restarts independent of npm cache eviction or a moving
+release tag. Installation and preflight happen in staging before publishing an
+immutable runtime. Preflight checks the launcher protocol because a target that
-The launcher serializes child exits, IPC messages, and timers. A trial must report prepared within
-120 seconds. If the trial exits or times out before prepared, the launcher stops it, restores the
-snapshot, records rollback, and starts A. A durable restore marker makes an interrupted restore
-resume before either version can boot. After commit, B is active and the service manager's normal
-restart policy applies.
-
-## Database Rollback
-
-The launcher snapshots `state.sqlite`, `state.sqlite-wal`, and `state.sqlite-shm` after the old
-server stops and before the trial starts. This makes trial migrations and writes reversible without
-requiring down migrations. The snapshot is retained across launcher restarts and is removed only
-after commit or after both restore and the terminal rollback state are durable.
+needs new rollback guarantees cannot safely run under an older launcher. Upgrading
+that launcher requires a local service update.
 
+## Commit boundary
-The protocol version is part of the safety boundary. A target that requires database snapshots is
-blocked when the installed launcher is too old. Upgrade the launcher once with:
 
-```sh
-npx t3@<version> service update
-```
+The launcher durably records the pending update before acknowledging it, then
+stops the old child and starts the target as a trial. Service-state writes use
+same-directory replacement with file and directory fsync. Invalid state stops
+startup rather than guessing which runtime to boot.
 
+The trial must finish migrations, acquire dependencies, bind HTTP, and park every
+long-running root at the activation gate before reporting `prepared`. The launcher
+then commits the target version durably and replies `committed`. Only then may the
+child release its gates, accept commands, and publish ready. Keep fallible startup
+acquisitions before this boundary. A listener alone does not prove the runtime is
+ready to commit.
-The local command stops the unit, selects the new launcher and exact runtime, then restarts the
-service. Later releases, including releases with migrations, can use the remote trial path.
 
+A failed or timed-out trial returns to the old version. After commit, the target
-Snapshots briefly require enough free disk for another copy of the SQLite files. Attachments and
+is authoritative and the service manager's ordinary restart policy applies.
-other files under the state directory are outside this rollback boundary.
 
-## Client Correlation
+## Database rollback
 
+After the old child exits, the launcher snapshots SQLite's main file, WAL, and
-The update acknowledgement includes the launcher-generated update ID. After reconnecting, clients
-wait for a lifecycle ready event carrying that same ID. `committed` completes the operation only
-when the ready server is the target version. `rolled-back` and `failed` end it immediately with the
-recorded reason. Older servers without an ID retain version-only reconnect behavior.
+shared-memory file. This makes trial migrations reversible without down
+migrations. The snapshot is made once per update and survives launcher restarts;
+replacing it during a retry could capture changes from the failed trial.
 
-## Capability and Compatibility
+Rollback stops the trial before restoring. A durable restore marker makes an
+interrupted restore finish before either version boots. Keep the snapshot until
+commit, or until both restoration and the terminal rollback state are durable.
+Attachments and other files outside SQLite are outside this rollback boundary.
 
-The existing additive RPC and lifecycle schemas remain compatible with older clients. New servers
-advertise remote self-update only when they have valid launcher context and a live IPC channel.
-Desktop-managed servers advertise `desktopAppUpdate` when the desktop telemetry control fd is
-attached. The progress RPC asks the desktop app to check and download, then returns a preparation
-token while the backend is still connected. Only after the client receives that result does it send
-`server.commitDesktopUpdate`. A successful commit closes the connection and must reconnect at the
+## Client acknowledgement
-prepared version. The desktop app and bundled server versions stay equal because
-`scripts/update-release-package-versions.ts` bumps them together. If install fails, the desktop keeps its windows, restarts stopped backends, and
-replays the failure for the same token. This two-phase handoff prevents backend shutdown from
-dropping the only successful RPC result. Desktop servers without the capability direct the user to
-update the desktop app locally. Other process shapes provide a manual command; the old detached
-foreground respawn path no longer exists.
 
+An accepted update is still pending. Clients correlate the launcher's update ID
+with the ready event after reconnecting, then check the outcome and target version.
+A reconnect alone cannot distinguish successful replacement from rollback. Older
+servers without an update ID retain version-only correlation.
-## Source Map
 
+Desktop updates have a separate two-phase handoff because installing the app stops
+its bundled backend. Preparation returns a token while the connection is alive;
+the client commits that token only after receiving it. Otherwise backend shutdown
+could lose the only successful RPC result. The client must then observe the
+prepared version after reconnecting. If installation fails, desktop restarts the
-- Launcher and state machine: `apps/server/src/serviceLauncher.ts`
-- IPC and durable state types: `apps/server/src/cloud/serviceProtocol.ts`
-- Child IPC adapter: `apps/server/src/cloud/serviceLauncherClient.ts`
-- Staging and preflight: `apps/server/src/cloud/pinnedRuntime.ts` and `servicePreflight.ts`
-- Service installation: `apps/server/src/cloud/bootService.ts`
-- Activation boundary: `apps/server/src/serverRuntimeStartup.ts` and `serverActivation.ts`
-- Client outcome correlation: `packages/client-runtime/src/state/server.ts`
+stopped backends and replays the failure for the same token.
````

[Next page](https://git.cafe/pingdotgg/t3code/pull/9755/diffs?after=ImRvY3MvaW50ZXJuYWxzL3NlcnZlci11cGRhdGVzLm1kIg&expectedVersion=1&format=markdown)
